Appearance
Dependency Inversion Principle (DIP)
SOLID Series — This article is Part 5 of 5. See also: SRP · OCP · LSP · ISP
Introduction
The Dependency Inversion Principle, the fifth of Robert C. Martin's SOLID principles, states two things: high-level modules should not depend on low-level modules — both should depend on abstractions; and abstractions should not depend on details — details should depend on abstractions. DIP reverses the traditional dependency direction so that business logic is insulated from infrastructure concerns, making systems far more flexible, testable, and maintainable.
Core Concept
In a naive layered architecture, the business logic layer directly instantiates database connections, file writers, and email clients. When the database changes from MySQL to PostgreSQL, or the log target changes from a file to a cloud service, the business logic class must be modified. DIP eliminates this coupling by inserting an abstraction layer — an interface — between the high-level policy and the low-level mechanism. Both layers then depend on the interface, and the direction of source-code dependency is inverted relative to the direction of data flow.
The key phrase is "inversion": in the traditional model, high-level modules control low-level modules. With DIP, high-level modules define the interface that low-level modules must satisfy. The dependency arrow points toward the high-level abstraction.
Anti-Pattern — OrderService with Concrete Dependencies
The most common DIP violation is a service class that news its own infrastructure dependencies.
java
// ANTI-PATTERN: OrderService directly depends on concrete low-level classes
public class OrderService {
// Hard-coded concrete dependency — cannot be swapped without modifying OrderService
private MySQLDatabase database = new MySQLDatabase(
"jdbc:mysql://localhost:3306/orders", "root", "password");
// Another concrete dependency — impossible to replace with CloudLogger in production
private FileLogger logger = new FileLogger("/var/log/orders.log");
public Order placeOrder(Cart cart, Customer customer) {
logger.log("Placing order for customer: " + customer.getId());
Order order = new Order(cart, customer);
order.setStatus(OrderStatus.PENDING);
order.setCreatedAt(LocalDateTime.now());
try {
database.save("orders", order); // tightly coupled to MySQLDatabase API
database.save("order_items", cart.getItems()); // schema details leak into service
logger.log("Order " + order.getId() + " saved successfully");
} catch (SQLException e) {
logger.logError("Failed to save order: " + e.getMessage());
throw new OrderPersistenceException("Order could not be saved", e);
}
return order;
}
public Optional<Order> findOrder(Long orderId) {
try {
return database.findById("orders", orderId, Order.class);
} catch (SQLException e) {
logger.logError("Error fetching order " + orderId + ": " + e.getMessage());
return Optional.empty();
}
}
}Problems with this design:
OrderServicecannot be unit-tested without a running MySQL instance.- Switching from MySQL to PostgreSQL requires modifying
OrderService. - Switching to a cloud logging service requires modifying
OrderService. - The test environment must mirror production infrastructure exactly.
MySQLDatabaseis directly referenced — the import alone creates a compile-time dependency.
Correct Implementation — Abstractions and Constructor Injection
Define interfaces that represent what the high-level module needs. Low-level modules implement those interfaces.
java
// CORRECT: Database abstraction — belongs to the domain/application layer
public interface Database {
<T> T save(T entity);
<T> Optional<T> findById(Long id, Class<T> type);
<T> List<T> findAll(Class<T> type);
void delete(Long id, Class<?> type);
}
// CORRECT: Logger abstraction — belongs to the application layer
public interface Logger {
void log(String message);
void logError(String message);
void logError(String message, Throwable cause);
}java
// CORRECT: MySQL implementation — lives in the infrastructure layer
public class MySQLDatabase implements Database {
private final String jdbcUrl;
private final String username;
private final String password;
public MySQLDatabase(String jdbcUrl, String username, String password) {
this.jdbcUrl = jdbcUrl;
this.username = username;
this.password = password;
}
@Override
public <T> T save(T entity) {
// MySQL-specific JDBC or Hibernate implementation
System.out.println("[MySQL] Saving entity: " + entity.getClass().getSimpleName());
return entity; // simplified
}
@Override
public <T> Optional<T> findById(Long id, Class<T> type) {
System.out.println("[MySQL] Finding " + type.getSimpleName() + " with id: " + id);
return Optional.empty(); // simplified
}
@Override
public <T> List<T> findAll(Class<T> type) {
System.out.println("[MySQL] Fetching all " + type.getSimpleName());
return List.of();
}
@Override
public void delete(Long id, Class<?> type) {
System.out.println("[MySQL] Deleting " + type.getSimpleName() + " with id: " + id);
}
}
// CORRECT: PostgreSQL implementation — can replace MySQL without touching OrderService
public class PostgreSQLDatabase implements Database {
private final DataSource dataSource;
public PostgreSQLDatabase(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public <T> T save(T entity) {
System.out.println("[PostgreSQL] Saving entity: " + entity.getClass().getSimpleName());
return entity;
}
@Override
public <T> Optional<T> findById(Long id, Class<T> type) {
System.out.println("[PostgreSQL] Finding " + type.getSimpleName() + " with id: " + id);
return Optional.empty();
}
@Override
public <T> List<T> findAll(Class<T> type) {
return List.of();
}
@Override
public void delete(Long id, Class<?> type) {
System.out.println("[PostgreSQL] Deleting " + type.getSimpleName() + " with id: " + id);
}
}java
// CORRECT: File logger — implements the abstraction
public class FileLogger implements Logger {
private final String logFilePath;
public FileLogger(String logFilePath) {
this.logFilePath = logFilePath;
}
@Override
public void log(String message) {
System.out.println("[FILE LOG] " + LocalDateTime.now() + " INFO: " + message);
// Real implementation: write to logFilePath via BufferedWriter
}
@Override
public void logError(String message) {
System.err.println("[FILE LOG] " + LocalDateTime.now() + " ERROR: " + message);
}
@Override
public void logError(String message, Throwable cause) {
System.err.println("[FILE LOG] " + LocalDateTime.now() + " ERROR: " + message + " | " + cause.getMessage());
}
}
// CORRECT: Cloud logger — swap in without touching any business logic
public class CloudLogger implements Logger {
private final CloudLoggingClient client;
private final String serviceName;
public CloudLogger(CloudLoggingClient client, String serviceName) {
this.client = client;
this.serviceName = serviceName;
}
@Override
public void log(String message) {
client.send(LogEntry.info(serviceName, message));
}
@Override
public void logError(String message) {
client.send(LogEntry.error(serviceName, message));
}
@Override
public void logError(String message, Throwable cause) {
client.send(LogEntry.error(serviceName, message, cause));
}
}java
// CORRECT: OrderService depends only on abstractions, received via constructor injection
public class OrderService {
private final Database database;
private final Logger logger;
// Constructor injection — dependencies are explicit and substitutable
public OrderService(Database database, Logger logger) {
this.database = database;
this.logger = logger;
}
public Order placeOrder(Cart cart, Customer customer) {
logger.log("Placing order for customer: " + customer.getId());
Order order = new Order(cart, customer);
order.setStatus(OrderStatus.PENDING);
order.setCreatedAt(LocalDateTime.now());
Order saved = database.save(order);
logger.log("Order " + saved.getId() + " placed successfully");
return saved;
}
public Optional<Order> findOrder(Long orderId) {
logger.log("Looking up order: " + orderId);
return database.findById(orderId, Order.class);
}
public void cancelOrder(Long orderId) {
logger.log("Cancelling order: " + orderId);
database.delete(orderId, Order.class);
logger.log("Order " + orderId + " cancelled");
}
}Spring Boot — Dependency Injection in Practice
Spring's IoC container implements DIP at the framework level. You declare your abstractions, annotate your implementations, and let Spring wire everything together.
java
// Spring Boot: interface lives in the service layer
public interface OrderRepository {
Order save(Order order);
Optional<Order> findById(Long id);
List<Order> findAll();
void deleteById(Long id);
}
// Infrastructure: JPA implementation — Spring manages instantiation
@Repository
public class JpaOrderRepository implements OrderRepository {
@PersistenceContext
private EntityManager entityManager;
@Override
@Transactional
public Order save(Order order) {
entityManager.persist(order);
return order;
}
@Override
public Optional<Order> findById(Long id) {
return Optional.ofNullable(entityManager.find(Order.class, id));
}
@Override
public List<Order> findAll() {
return entityManager.createQuery("SELECT o FROM Order o", Order.class).getResultList();
}
@Override
@Transactional
public void deleteById(Long id) {
Optional.ofNullable(entityManager.find(Order.class, id)).ifPresent(entityManager::remove);
}
}
// Application service — depends on interface, not JpaOrderRepository
@Service
public class OrderManagementService {
private final OrderRepository orderRepository;
private final Logger logger;
// Spring injects JpaOrderRepository here automatically
public OrderManagementService(OrderRepository orderRepository, Logger logger) {
this.orderRepository = orderRepository;
this.logger = logger;
}
public Order createOrder(OrderRequest request) {
logger.log("Creating order for: " + request.getCustomerId());
Order order = Order.from(request);
return orderRepository.save(order);
}
public List<Order> getAllOrders() {
return orderRepository.findAll();
}
}
// Spring configuration — wires Logger implementation
@Configuration
public class InfrastructureConfig {
@Bean
public Logger logger(@Value("${log.file.path:/var/log/app.log}") String logPath) {
return new FileLogger(logPath);
// Switch to: return new CloudLogger(cloudClient, "order-service");
// without changing OrderManagementService at all
}
}Diagrams
Abstraction Layer Architecture
Spring IoC Container
Module Dependency Graph — Before and After DIP
DIP Compliance Flowchart
Class Diagram — Complete DIP Architecture
Unit Test — Testing With Mock Dependencies
The ultimate payoff of DIP is testability. Because OrderService depends on interfaces, tests can inject mock implementations without any real database or file system.
java
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private Database database;
@Mock
private Logger logger;
@InjectMocks
private OrderService orderService;
@Test
void placeOrder_savesOrderAndLogsSuccess() {
// Arrange
Cart cart = new Cart(List.of(new CartItem("Widget", 2, 9.99)));
Customer customer = new Customer(42L, "Alice");
Order expectedOrder = new Order(cart, customer);
expectedOrder.setId(100L);
when(database.save(any(Order.class))).thenReturn(expectedOrder);
// Act
Order result = orderService.placeOrder(cart, customer);
// Assert
assertThat(result.getId()).isEqualTo(100L);
verify(database).save(any(Order.class));
verify(logger).log(contains("customer: 42"));
verify(logger).log(contains("Order 100"));
// No MySQL server, no file system — pure logic test
}
@Test
void findOrder_delegatesToDatabase() {
Long orderId = 55L;
Order order = new Order();
order.setId(orderId);
when(database.findById(orderId, Order.class)).thenReturn(Optional.of(order));
Optional<Order> result = orderService.findOrder(orderId);
assertThat(result).isPresent();
assertThat(result.get().getId()).isEqualTo(orderId);
verify(logger).log(contains("55"));
}
@Test
void cancelOrder_deletesFromDatabaseAndLogs() {
Long orderId = 77L;
doNothing().when(database).delete(orderId, Order.class);
orderService.cancelOrder(orderId);
verify(database).delete(orderId, Order.class);
verify(logger, times(2)).log(anyString());
}
}These tests run in milliseconds with no external dependencies. Switching the production implementation from MySQLDatabase to PostgreSQLDatabase requires zero test changes.
Best Practices
- Constructor injection over field injection — Constructor injection makes dependencies explicit, required, and visible. Field injection with
@Autowiredhides dependencies and makes the class harder to instantiate in tests. Always prefer@Autowiredon the constructor or, better yet, omit it entirely and let Spring detect the single constructor. - Depend on interfaces, not classes — Import only the interface type in your high-level module. If your
importstatements include concrete infrastructure classes (MySQLDatabase,FileLogger), that is a DIP violation in the source code. - Let the IoC container manage lifecycle — Spring, Guice, and CDI are DIP implementations at the framework level. Trust the container to wire dependencies; do not
newinfrastructure objects inside services. - Place interfaces in the application layer — The
DatabaseandLoggerinterfaces belong to your domain/application layer, not the infrastructure layer. This ensures the dependency arrows point inward (toward the domain), which is the architectural inversion DIP describes. - One interface per capability — Combining DIP with ISP means each injected interface should represent only the capability the class actually needs. An
OrderServicethat only reads orders should receiveReadableOrderRepository, not the fullOrderRepository. - Test at the seam — The constructor of a DIP-compliant service is the "seam" where implementations are swapped. Always write tests that inject mock implementations via the constructor to verify the service's logic in isolation.
- Avoid service locators — The Service Locator pattern (
ServiceLocator.get(Database.class)) is a DIP anti-pattern in disguise. It hides dependencies inside method bodies, making them invisible to callers and impossible to inject in tests.
Related Concepts
Other SOLID Principles:
- SRP — Single Responsibility Principle: DIP separates responsibilities across the dependency boundary — business logic is isolated from infrastructure.
- OCP — Open/Closed Principle: DIP enables OCP. Because
OrderServicedepends onDatabase, swappingMySQLDatabaseforPostgreSQLDatabaseis an extension, not a modification. - LSP — Liskov Substitution Principle: DIP works only if LSP holds — every
Databaseimplementation must be a valid substitute for theDatabaseinterface. - ISP — Interface Segregation Principle: Combine with ISP to inject the narrowest possible interface. Prefer
ReadableRepositoryoverFullRepositorywhen only reads are needed.
Further Reading:
- Inversion of Control (IoC) containers: Spring Framework, Google Guice, Jakarta CDI.
- Clean Architecture by Robert C. Martin: the Dependency Rule and how it maps to DIP.
- Hexagonal Architecture (Ports and Adapters): a structural pattern that enforces DIP at the module level.
- REST API Design: applying DIP to HTTP client abstractions in API integrations.