Skip to content

Dependency Inversion

Definition

Dependency Inversion Definition


Violation Example

// BAD: High-level depends directly on low-level
public class OrderService {
    // Direct dependency on concrete implementation
    private MySQLDatabase database = new MySQLDatabase();
    private SmtpEmailSender emailSender = new SmtpEmailSender();
    private StripePayment payment = new StripePayment();

    public void createOrder(Order order) {
        // Tightly coupled to MySQL
        database.query("INSERT INTO orders...");

        // Tightly coupled to Stripe
        payment.charge(order.getTotal());

        // Tightly coupled to SMTP
        emailSender.send(order.getCustomerEmail(), "Order confirmed");
    }
}

// Problems:
// - Can't test without real MySQL, Stripe, SMTP
// - Can't switch to PostgreSQL without changing OrderService
// - Can't switch to SendGrid without changing OrderService
// - High-level business logic depends on low-level details

Proper Implementation

// GOOD: Both depend on abstractions

// Abstractions (interfaces owned by high-level module)
public interface OrderRepository {
    void save(Order order);
    Optional<Order> findById(OrderId id);
}

public interface PaymentGateway {
    PaymentResult charge(Money amount, PaymentDetails details);
}

public interface NotificationService {
    void sendOrderConfirmation(Order order);
}

// High-level module depends on abstractions
public class OrderService {
    private final OrderRepository orderRepo;
    private final PaymentGateway payment;
    private final NotificationService notifications;

    // Dependencies injected
    public OrderService(OrderRepository orderRepo,
                       PaymentGateway payment,
                       NotificationService notifications) {
        this.orderRepo = orderRepo;
        this.payment = payment;
        this.notifications = notifications;
    }

    public void createOrder(Order order) {
        PaymentResult result = payment.charge(
            order.getTotal(),
            order.getPaymentDetails()
        );

        if (result.isSuccessful()) {
            orderRepo.save(order);
            notifications.sendOrderConfirmation(order);
        }
    }
}

// Low-level modules implement abstractions
public class MySQLOrderRepository implements OrderRepository {
    public void save(Order order) { /* MySQL specific */ }
    public Optional<Order> findById(OrderId id) { /* MySQL specific */ }
}

public class StripePaymentGateway implements PaymentGateway {
    public PaymentResult charge(Money amount, PaymentDetails details) {
        // Stripe API calls
    }
}

public class EmailNotificationService implements NotificationService {
    public void sendOrderConfirmation(Order order) {
        // SMTP/SendGrid implementation
    }
}

Interface Ownership

Interface Ownership


Dependency Injection

// Dependency Injection is the mechanism to achieve DIP

// 1. CONSTRUCTOR INJECTION (Preferred)
public class OrderService {
    private final OrderRepository repo;

    public OrderService(OrderRepository repo) {
        this.repo = repo;
    }
}

// 2. SETTER INJECTION
public class OrderService {
    private OrderRepository repo;

    public void setRepository(OrderRepository repo) {
        this.repo = repo;
    }
}

// 3. INTERFACE INJECTION
public interface RepositoryAware {
    void setRepository(OrderRepository repo);
}

// WIRING (using Spring)
@Configuration
public class AppConfig {
    @Bean
    public OrderRepository orderRepository() {
        return new MySQLOrderRepository(dataSource());
    }

    @Bean
    public OrderService orderService() {
        return new OrderService(orderRepository());
    }
}

// OR using annotations
@Service
public class OrderService {
    private final OrderRepository repo;

    @Autowired  // Spring injects the implementation
    public OrderService(OrderRepository repo) {
        this.repo = repo;
    }
}

Testing Benefits

// DIP enables easy testing with mocks

public class OrderServiceTest {

    @Test
    void shouldSaveOrderAfterSuccessfulPayment() {
        // Arrange - use test doubles
        OrderRepository mockRepo = mock(OrderRepository.class);
        PaymentGateway mockPayment = mock(PaymentGateway.class);
        NotificationService mockNotify = mock(NotificationService.class);

        when(mockPayment.charge(any(), any()))
            .thenReturn(PaymentResult.success());

        OrderService service = new OrderService(
            mockRepo, mockPayment, mockNotify
        );

        // Act
        Order order = new Order(/* ... */);
        service.createOrder(order);

        // Assert
        verify(mockRepo).save(order);
        verify(mockNotify).sendOrderConfirmation(order);
    }

    @Test
    void shouldNotSaveOrderWhenPaymentFails() {
        // Arrange
        OrderRepository mockRepo = mock(OrderRepository.class);
        PaymentGateway mockPayment = mock(PaymentGateway.class);

        when(mockPayment.charge(any(), any()))
            .thenReturn(PaymentResult.failed("Declined"));

        OrderService service = new OrderService(
            mockRepo, mockPayment, mock(NotificationService.class)
        );

        // Act
        service.createOrder(new Order(/* ... */));

        // Assert - order not saved
        verify(mockRepo, never()).save(any());
    }
}

Architectural Application

Clean Architecture & DIP


Tips & Tricks

DIP Tips