Loading content...
We've established that in Hexagonal Architecture, the domain sits at the center and ports define contracts. We've seen how adapters implement those contracts. But there's a fundamental question we haven't fully answered: Why don't domain objects just directly use the database or call the payment API?
The answer is Dependency Inversion—the 'D' in the SOLID principles, and the most misunderstood principle in software architecture.
Dependency Inversion isn't just about using interfaces. It's about who defines the interface and who implements it. In Hexagonal Architecture, the domain defines interfaces for what it needs, and infrastructure adapts to those interfaces. This inversion of the traditional dependency direction is what makes the architecture powerful.
By the end of this page, you will understand the Dependency Inversion Principle deeply, how it enables Hexagonal Architecture, and why it creates systems that are genuinely flexible and testable. You'll see concrete examples of dependency direction and learn to recognize when dependencies are flowing the wrong way.
Before we discuss the solution, let's deeply understand the problem. In traditional layered architecture, dependencies flow downward:
Presentation Layer → Business Layer → Data Access Layer → Database
This seems logical: the UI depends on business logic, which depends on data access, which depends on the database. But this creates a devastating problem: the business logic is coupled to the database.
Why is this coupling devastating?
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
// ❌ TRADITIONAL: Business logic depends on data access implementation package com.example.business; // PROBLEM: Business layer imports data layer concrete classesimport com.example.data.JdbcOrderDao;import com.example.data.JdbcCustomerDao;import java.sql.SQLException; public class OrderService { // PROBLEM: Depends on concrete JDBC implementation private final JdbcOrderDao orderDao; private final JdbcCustomerDao customerDao; public OrderService() { // PROBLEM: Creates its own dependencies this.orderDao = new JdbcOrderDao(); this.customerDao = new JdbcCustomerDao(); } public void submitOrder(String customerId, List<OrderItem> items) { try { // PROBLEM: Business logic mixed with SQL details Customer customer = customerDao.findById(customerId); if (customer == null) { throw new CustomerNotFoundException(customerId); } Order order = new Order(customer, items); order.validate(); // Actual business logic // PROBLEM: Business logic knows about database transactions orderDao.beginTransaction(); try { orderDao.save(order); customerDao.updateOrderCount(customerId); orderDao.commitTransaction(); } catch (SQLException e) { orderDao.rollbackTransaction(); throw new OrderPersistenceException(e); } } catch (SQLException e) { // PROBLEM: Business layer handles SQL exceptions throw new ServiceException("Database error", e); } }} /* * Dependency Graph (wrong direction): * * OrderService * ↓ depends on * JdbcOrderDao (concrete) * ↓ depends on * java.sql (JDBC) * ↓ depends on * MySQL Driver * * To test OrderService, you need a MySQL database running. * To change to PostgreSQL, you modify OrderService. * Business logic is not isolated. */When business logic depends on data access implementation, every layer above is transitively coupled to the database. The presentation layer depends on business, which depends on data access, which depends on the database. A change in your database schema can ripple all the way to your UI code.
The Dependency Inversion Principle (DIP) has two parts, both of which are essential:
Part 1: High-level modules should not depend on low-level modules. Both should depend on abstractions.
Part 2: Abstractions should not depend on details. Details should depend on abstractions.
Let's unpack this carefully:
High-level modules = Business logic, domain rules, use cases (the "what") Low-level modules = Infrastructure, databases, frameworks (the "how") Abstractions = Interfaces, abstract classes, protocols Details = Concrete implementations
The critical insight is in the second part: Abstractions should not depend on details. This means the interface should be defined by whoever needs it (the high-level module), not by whoever implements it (the low-level module).
In Hexagonal Architecture terms:
OrderRepository interface based on what the domain needsThe domain doesn't conform to what the database can do. The database adapter conforms to what the domain needs.
The Inversion Explained:
In the traditional model:
JdbcRepositoryWith Dependency Inversion:
OrderRepository (reflecting what the domain needs)OrderRepositoryThis inversion of control over the interface definition is the key insight. The interface moves from the low-level module to the high-level module, and the dependency arrow for the implementation reverses.
Let's see dependency inversion in action with a complete example. We'll transform the coupled code from earlier into properly inverted architecture.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
// ============================================// STEP 1: Define interface in the DOMAIN layer// ============================================package com.example.domain.ports; // This interface is defined by the DOMAIN, not the data layer// It uses DOMAIN types, not database typespublic interface OrderRepository { Order save(Order order); Optional<Order> findById(OrderId orderId); List<Order> findByCustomer(CustomerId customerId); OrderId nextId();} // Notice:// - Defined in domain.ports package (domain owns it)// - Uses domain types: Order, OrderId, CustomerId// - No SQL, no JDBC, no database concepts // ============================================// STEP 2: Domain service depends on the INTERFACE// ============================================package com.example.domain.services; import com.example.domain.ports.OrderRepository; // DOMAIN interfaceimport com.example.domain.ports.PaymentGateway; // DOMAIN interfaceimport com.example.domain.model.*; // DOMAIN entities public class OrderService { // Depends on ABSTRACTIONS defined by the domain private final OrderRepository orderRepository; private final PaymentGateway paymentGateway; private final CustomerRepository customerRepository; // Dependencies INJECTED, not created public OrderService( OrderRepository orderRepository, PaymentGateway paymentGateway, CustomerRepository customerRepository ) { this.orderRepository = orderRepository; this.paymentGateway = paymentGateway; this.customerRepository = customerRepository; } public OrderResult submitOrder(SubmitOrderCommand command) { // Pure business logic - no infrastructure concerns Customer customer = customerRepository.findById(command.customerId()) .orElseThrow(() -> new CustomerNotFoundException(command.customerId())); Order order = Order.create(orderRepository.nextId(), customer); command.items().forEach(item -> order.addItem(item)); // Business validation order.validate(); // Business rule: check payment PaymentResult payment = paymentGateway.authorize( order.calculateTotal(), command.paymentMethod() ); if (payment instanceof PaymentResult.Declined declined) { return new OrderResult.PaymentFailed(declined.reason()); } // Save the order Order saved = orderRepository.save(order); return new OrderResult.Success(saved.getId()); }} // ============================================// STEP 3: Infrastructure ADAPTS to the domain interface// ============================================package com.example.infrastructure.persistence; import com.example.domain.ports.OrderRepository; // DOMAIN interfaceimport com.example.domain.model.*; // DOMAIN entitiesimport javax.persistence.*; // Infrastructure class IMPLEMENTS domain interfacepublic class JpaOrderAdapter implements OrderRepository { private final EntityManager em; private final OrderMapper mapper; public JpaOrderAdapter(EntityManager em, OrderMapper mapper) { this.em = em; this.mapper = mapper; } @Override public Order save(Order order) { // Translate domain to JPA, persist, translate back OrderEntity entity = mapper.toEntity(order); em.persist(entity); return mapper.toDomain(entity); } @Override public Optional<Order> findById(OrderId orderId) { OrderEntity entity = em.find(OrderEntity.class, orderId.value()); return Optional.ofNullable(entity).map(mapper::toDomain); } @Override public List<Order> findByCustomer(CustomerId customerId) { return em.createQuery( "SELECT o FROM OrderEntity o WHERE o.customerId = :cid", OrderEntity.class) .setParameter("cid", customerId.value()) .getResultStream() .map(mapper::toDomain) .toList(); } @Override public OrderId nextId() { return new OrderId(UUID.randomUUID().toString()); }} /* * Dependency Graph (CORRECT direction): * * OrderService * ↓ depends on * OrderRepository (interface, in domain) * ↑ implements * JpaOrderAdapter (infrastructure) * ↓ depends on * JPA/Hibernate/Database * * Key observations: * 1. OrderService only knows about OrderRepository (domain interface) * 2. JpaOrderAdapter depends on domain types (implements OrderRepository) * 3. The arrow from JpaOrderAdapter POINTS TOWARD the domain * 4. To test OrderService, mock OrderRepository - no database needed * 5. To switch to MongoDB, create MongoOrderAdapter - OrderService unchanged */The key visual test: draw your dependency arrows. In correct Hexagonal Architecture, all arrows from infrastructure should point toward the domain. The domain has no outgoing arrows to infrastructure. If you see an arrow from domain to infrastructure, the dependencies are inverted (wrong).
Dependency Inversion is a principle. Dependency Injection (DI) is a technique that enables it. DI means providing dependencies to an object rather than having the object create them itself.
Why DI is Essential:
Without DI, even if you define interfaces correctly, your objects might still create their own dependencies, defeating the purpose:
// BAD: Dependencies are inverted, but not injected
public class OrderService {
private final OrderRepository repository = new JpaOrderAdapter(); // Still coupled!
}
With DI, dependencies are provided from outside:
// GOOD: Dependencies are inverted AND injected
public class OrderService {
private final OrderRepository repository; // Just the interface
public OrderService(OrderRepository repository) { // Injected
this.repository = repository;
}
}
DI Patterns:
Constructor Injection (preferred) Dependencies are provided through the constructor. The object can't exist without its dependencies, making requirements explicit.
Method Injection Dependencies are provided per-method call. Useful when a dependency varies by invocation.
Property/Setter Injection Dependencies are set after construction. Allows for optional dependencies but can leave objects in invalid states.
Interface Injection The dependency provides an injector method that will inject the dependency. Less common in modern practice.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
// ============================================// CONSTRUCTOR INJECTION (preferred)// ============================================public class OrderService { private final OrderRepository orderRepository; private final PaymentGateway paymentGateway; // All dependencies declared explicitly, required for construction public OrderService( OrderRepository orderRepository, PaymentGateway paymentGateway ) { this.orderRepository = Objects.requireNonNull(orderRepository); this.paymentGateway = Objects.requireNonNull(paymentGateway); } // Service methods use the injected dependencies public OrderResult submitOrder(SubmitOrderCommand command) { // Use orderRepository, paymentGateway... }} // Benefits of constructor injection:// 1. Dependencies are obvious (in the constructor signature)// 2. Object is always in a valid state after construction// 3. Dependencies can be final (immutable)// 4. Easy to see when a class has too many dependencies // ============================================// DI CONTAINER CONFIGURATION (Spring example)// ============================================@Configurationpublic class ApplicationConfig { @Bean public OrderRepository orderRepository( EntityManager entityManager, OrderMapper mapper ) { return new JpaOrderAdapter(entityManager, mapper); } @Bean public PaymentGateway paymentGateway(StripeConfig config) { return new StripePaymentAdapter(config); } @Bean public OrderService orderService( OrderRepository orderRepository, PaymentGateway paymentGateway ) { return new OrderService(orderRepository, paymentGateway); }} // In production:// - Spring creates adapters with real infrastructure// - OrderService receives real implementations // In tests:class OrderServiceTest { @Test void testOrderSubmission() { // Create mocks for testing OrderRepository mockRepo = mock(OrderRepository.class); PaymentGateway mockPayment = mock(PaymentGateway.class); // Inject mocks into service OrderService service = new OrderService(mockRepo, mockPayment); // Test business logic without real infrastructure when(mockPayment.authorize(any(), any())) .thenReturn(new PaymentResult.Authorized(...)); OrderResult result = service.submitOrder(command); assertInstanceOf(OrderResult.Success.class, result); }}You don't need a DI container/framework to practice Dependency Injection. Manual DI (writing your own factory/composition code) is perfectly valid and sometimes clearer. DI containers (Spring, Guice, Dagger, tsyringe) automate the wiring but add complexity. Choose based on project size and team preference.
When dependencies are properly inverted, several powerful benefits emerge. These aren't theoretical—they translate directly into development velocity, system stability, and team productivity.
| Aspect | Without DIP | With DIP |
|---|---|---|
| Unit test speed | 10-30 seconds (database needed) | 10-100 milliseconds |
| Change database | Modify business layer | Create new adapter |
| Add new client (CLI) | May require business changes | New driving adapter only |
| Debug business bug | Unclear, mixed with SQL | Isolated domain code |
| New team member | Must understand full stack | Can focus on domain OR infra |
| Technology upgrade | Risky, affects everywhere | Isolated to adapters |
Each benefit compounds the others. Fast tests mean developers run them constantly. Clear boundaries mean bugs are found in isolation. Technology flexibility means you can adopt better tools as they emerge. Over a system's lifetime, these benefits multiply.
Even in codebases that claim to follow Hexagonal Architecture, dependency inversion is frequently violated. Here are the most common mistakes and how to recognize them:
Violation 1: Framework Annotations in Domain
When domain entities have framework annotations (@Entity, @JsonProperty, @Column), the domain depends on infrastructure. The entity becomes impossible to use without that framework.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
// ❌ VIOLATION: Domain entity with infrastructure annotationspackage com.example.domain.model; import javax.persistence.*; // JPA in domain!import com.fasterxml.jackson.*; // Jackson in domain! @Entity // Domain now depends on JPA@Table(name = "orders")public class Order { @Id @GeneratedValue(strategy = GenerationType.UUID) private String id; // Not even a domain ID type @Column(name = "cust_id", nullable = false) private String customerId; // Database column names in domain @JsonProperty("order_status") // Serialization in domain @Enumerated(EnumType.STRING) private OrderStatus status; // This "domain" entity can't exist without JPA and Jackson} // ✅ CORRECT: Pure domain entity with separate persistence mappingpackage com.example.domain.model; // No framework imports - pure Java/domainpublic class Order { private final OrderId id; private final CustomerId customerId; private OrderStatus status; // ... pure business logic} // Separate JPA entity in infrastructurepackage com.example.infrastructure.persistence; @Entity@Table(name = "orders")class OrderEntity { @Id private String id; @Column(name = "cust_id") private String customerId; @Enumerated(EnumType.STRING) private String status;} // Mapper translates between them@Componentclass OrderMapper { Order toDomain(OrderEntity entity) { /* ... */ } OrderEntity toEntity(Order order) { /* ... */ }}Violation 2: Leaky Port Interfaces
When port interfaces expose infrastructure concepts, they're not true abstractions. The domain becomes coupled to infrastructure through the interface.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
// ❌ VIOLATION: Port interface leaks database conceptspublic interface OrderRepository { // Leaks SQL query capability into domain List<Order> findByQuery(String sql); // Leaks database pagination into domain Page<Order> findAll(Pageable pageable); // Spring Page/Pageable // Leaks transaction management into domain void beginTransaction(); void commit(); void rollback(); // Leaks database-specific feature void saveWithOptimisticLock(Order order, long expectedVersion);} // ✅ CORRECT: Port interface uses only domain conceptspublic interface OrderRepository { Order save(Order order); Optional<Order> findById(OrderId orderId); // Domain-meaningful query, not generic SQL List<Order> findPendingOrdersForCustomer(CustomerId customerId); // Domain-driven pagination OrderPage findRecentOrders(int pageNumber, int pageSize); // Domain-specific result type record OrderPage(List<Order> orders, int totalPages, boolean hasNext) {}} // Implementation translates to Spring Page internally@Repositorypublic class JpaOrderAdapter implements OrderRepository { @Override public OrderPage findRecentOrders(int pageNumber, int pageSize) { Page<OrderEntity> page = jpaRepo.findAll( PageRequest.of(pageNumber, pageSize, Sort.by("createdAt").descending()) ); return new OrderPage( page.getContent().stream().map(mapper::toDomain).toList(), page.getTotalPages(), page.hasNext() ); }}Violation 3: Domain Services Importing Infrastructure
If a domain service imports anything from infrastructure packages, dependency inversion is violated.
java.sql.*, org.springframework.*, com.mongodb.*, etc.Long, Integer) instead of domain ID typesSQLException, HttpClientException)A simple test: look at the import statements in your domain classes. If you see framework or infrastructure imports, dependencies are flowing the wrong way. Domain code should only import from the domain package (and standard library).
Let's see a complete example of properly inverted architecture, showing how the domain defines contracts and infrastructure adapts to them.
Package Structure:
src/
├── domain/
│ ├── model/
│ │ ├── Order.java # Entity
│ │ ├── OrderId.java # Value Object
│ │ ├── Money.java # Value Object
│ │ └── OrderStatus.java # Enum
│ └── ports/
│ ├── input/
│ │ └── SubmitOrderUseCase.java # Input Port
│ └── output/
│ ├── OrderRepository.java # Output Port
│ ├── PaymentGateway.java # Output Port
│ └── NotificationService.java # Output Port
│
├── application/
│ └── services/
│ └── OrderService.java # Implements SubmitOrderUseCase
│
└── infrastructure/
├── adapters/
│ ├── input/
│ │ └── rest/
│ │ └── OrderController.java # Driving Adapter
│ └── output/
│ ├── persistence/
│ │ └── JpaOrderAdapter.java # Driven Adapter
│ ├── payment/
│ │ └── StripePaymentAdapter.java # Driven Adapter
│ └── notification/
│ └── SmtpNotificationAdapter.java # Driven Adapter
└── config/
└── DependencyConfig.java # DI wiring
Key Observations:
We've explored Dependency Inversion—the principle that makes Hexagonal Architecture possible. Let's consolidate the key concepts:
Module Complete:
You've now learned the complete Hexagonal Architecture pattern:
Together, these create applications that are testable, flexible, and maintainable. The initial investment in structure pays dividends throughout the system's lifetime.
Congratulations! You've mastered Hexagonal Architecture (Ports and Adapters). You understand how to isolate your domain, define meaningful contracts, implement adapters, and ensure dependencies flow correctly. This architecture pattern, when applied consistently, creates systems that stand the test of time and change.