Home/Learn/Low Level Design/Dependency Inversion Principle

Dependency Inversion Principle

Intermediate
SOLID Principles in Practice

High-level modules should not depend on low-level modules; both should depend on abstractions — and abstractions should not depend on details.

Overview

DIP (Robert Martin) inverts the traditional dependency direction. Traditionally, a high-level OrderService depends on a low-level MySqlOrderRepository — changes to MySQL affect OrderService. With DIP, both depend on an OrderRepository interface. The concrete MySqlOrderRepository implements the interface. High-level policy (OrderService) is now insulated from low-level details (MySQL specifics). Dependency Injection is the mechanism that applies DIP at runtime. Spring's IoC container is the industrial-strength DIP enabler — it wires abstractions to implementations based on configuration.

DIP Applied to Repository Layer

Without DIP, OrderService constructs MySqlOrderRepository directly — changing to PostgreSQL requires modifying OrderService. With DIP, OrderService depends on an OrderRepository interface; the concrete implementation is injected.

Java — DIP with Repository interface and Spring DI
// ❌ DIP violation: high-level depends on low-level concrete class
public class OrderService {
    // Directly coupled to MySQL implementation
    private final MySqlOrderRepository repo = new MySqlOrderRepository();

    public void placeOrder(Order order) {
        repo.save(order); // tied to MySQL — cannot test without DB
    }
}

// ✅ DIP applied: both depend on abstraction
// 1. Define the abstraction (interface owned by the HIGH-LEVEL module)
public interface OrderRepository {
    void save(Order order);
    Optional<Order> findById(String id);
    List<Order> findByUserId(String userId);
}

// 2. Low-level module implements the abstraction
public class MySqlOrderRepository implements OrderRepository {
    private final JdbcTemplate jdbc;
    public MySqlOrderRepository(JdbcTemplate jdbc) { this.jdbc = jdbc; }

    @Override public void save(Order order) {
        jdbc.update("INSERT INTO orders (id, user_id, total) VALUES (?, ?, ?)",
                    order.getId(), order.getUserId(), order.getTotal());
    }

    @Override public Optional<Order> findById(String id) {
        return jdbc.query("SELECT * FROM orders WHERE id = ?",
                          ORDER_MAPPER, id).stream().findFirst();
    }

    @Override public List<Order> findByUserId(String userId) {
        return jdbc.query("SELECT * FROM orders WHERE user_id = ?", ORDER_MAPPER, userId);
    }
}

// 3. High-level module depends ONLY on the interface
@Service
public class OrderService {
    private final OrderRepository orderRepo;    // abstraction, not concrete
    private final PaymentGateway  payment;

    public OrderService(OrderRepository orderRepo, PaymentGateway payment) {
        this.orderRepo = orderRepo;
        this.payment   = payment;
    }

    public Order placeOrder(Cart cart) {
        Order order = Order.from(cart);
        payment.charge(order.getUserId(), order.getTotal());
        orderRepo.save(order);
        return order;
    }
}

// 4. Spring wires them (DI applies DIP at runtime)
@Configuration
public class DataConfig {
    @Bean
    public OrderRepository orderRepository(JdbcTemplate jdbc) {
        return new MySqlOrderRepository(jdbc);
        // Swap to PostgreSQL: return new PostgresOrderRepository(jdbc);
        // Swap to MongoDB: return new MongoOrderRepository(mongoTemplate);
    }
}

// 5. In tests: inject an in-memory fake — zero database needed
class OrderServiceTest {
    @Test void placeOrder_savesOrder() {
        List<Order> savedOrders = new ArrayList<>();
        OrderRepository fakeRepo = new OrderRepository() {
            @Override public void save(Order o) { savedOrders.add(o); }
            @Override public Optional<Order> findById(String id) { return Optional.empty(); }
            @Override public List<Order> findByUserId(String uid) { return List.of(); }
        };
        OrderService sut = new OrderService(fakeRepo, new FakePaymentGateway());
        sut.placeOrder(Cart.withItems(List.of(new Item("Book", 499.0))));
        assertEquals(1, savedOrders.size());
    }
}

Key Points to Remember

  • 1High-level modules must not depend on low-level modules — both depend on interfaces.
  • 2The interface is "owned" by the high-level module — low-level modules implement it.
  • 3DIP makes the high-level policy stable and immune to changes in low-level details.
  • 4Dependency Injection is the runtime mechanism that wires abstractions to implementations.
  • 5Without DIP, switching from MySQL to MongoDB requires modifying every high-level class that directly imports MySQL types.

Interview Questions

Sign in to ask Aria
1

What is the difference between Dependency Inversion and Dependency Injection?

MediumAmazon
2

Who should "own" the abstraction interface — the high-level or low-level module?

HardGoogle
3

How does Spring's IoC container apply DIP?

MediumAtlassian
4

How does DIP improve testability?

EasyMicrosoft

Ask Aria about Dependency Inversion Principle

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…