Appearance
Dependency Inversion & Ports/Adapters
How to build flexible, testable systems by inverting dependencies and creating pluggable architectures
The Dependency Rule
The Dependency Rule is the fundamental principle that makes Clean Architecture work:
Source code dependencies must point only inward, toward higher-level policies.
This simple rule has profound implications:
- Business logic never depends on technical details
- Core domain code remains stable and portable
- Technical implementations can be swapped without affecting business rules
- Testing becomes trivial when there are no external dependencies
But how do we achieve this when our use cases need databases, our domain events need message queues, and our entities need to be saved somewhere?
The answer: Dependency Inversion.
Understanding Dependency Inversion
The Problem
In traditional layered architectures, high-level components depend on low-level components:
Controller → Service → Repository → DatabaseThis creates rigid systems where business logic is coupled to technical details. Want to switch from PostgreSQL to MongoDB? Rewrite your services. Want to test business logic? Set up a test database.
The Solution
Dependency Inversion flips this relationship. Instead of high-level components depending on low-level ones, both depend on abstractions:
Controller → IService ← ServiceImpl
Service → IRepository ← RepositoryImplThe high-level component defines the interface it needs, and the low-level component implements that interface. The dependency arrow has been inverted.
Key Principles
1. High-level modules should not depend on low-level modules
Your business logic (high-level) shouldn't know about databases, web frameworks, or external services (low-level).
2. Both should depend on abstractions
Define interfaces that represent capabilities, not implementations.
3. Abstractions should not depend on details
Interfaces should be defined in terms of business needs, not technical capabilities.
4. Details should depend on abstractions
Implementations conform to interfaces, not the other way around.
Ports and Adapters
The Ports and Adapters pattern (also called Hexagonal Architecture) is a practical application of dependency inversion that creates a pluggable architecture.
Ports: The Interfaces
A Port is an interface that defines a boundary between your application and the outside world. Ports are defined by the application based on what it needs, not what the infrastructure provides.
Types of Ports:
Inbound Ports (Driving/Primary)
- Define how the outside world can interact with your application
- Implemented by use cases
- Called by external actors (users, other systems)
- Examples:
CreateOrderUseCase,ProcessPaymentUseCase
Outbound Ports (Driven/Secondary)
- Define what your application needs from the outside world
- Defined as interfaces in the domain or use case layer
- Implemented by infrastructure
- Examples:
OrderRepository,PaymentGateway,NotificationService
Adapters: The Implementations
An Adapter is a concrete implementation that connects your application to the external world through a port.
Types of Adapters:
Inbound Adapters (Driving/Primary)
- Translate external requests into application calls
- Implement delivery mechanisms
- Examples: REST controllers, GraphQL resolvers, CLI commands, Message queue consumers
Outbound Adapters (Driven/Secondary)
- Implement outbound port interfaces
- Handle technical details of external interactions
- Examples: SQL repositories, HTTP API clients, SMTP email senders, File system accessors
How It Works in Practice
Step 1: Define the Port (Interface)
The use case layer defines what it needs:
typescript
// Defined in the use case layer
interface OrderRepository {
save(order: Order): Promise<void>
findById(id: OrderId): Promise<Order | null>
findByCustomer(customerId: CustomerId): Promise<Order[]>
}Step 2: Use the Port
The use case depends only on the interface:
typescript
class CreateOrderUseCase {
constructor(private orderRepository: OrderRepository) {}
async execute(request: CreateOrderRequest): Promise<Order> {
const order = Order.create(request)
await this.orderRepository.save(order)
return order
}
}Step 3: Implement the Adapter
The infrastructure layer provides the implementation:
typescript
// In the infrastructure layer
class PostgresOrderRepository implements OrderRepository {
async save(order: Order): Promise<void> {
// PostgreSQL-specific implementation
}
// ... other methods
}Step 4: Wire It Together
The application's composition root connects ports to adapters:
typescript
// At application startup
const orderRepository = new PostgresOrderRepository(dbConnection)
const createOrderUseCase = new CreateOrderUseCase(orderRepository)Benefits of This Approach
Testability
Replace real implementations with test doubles:
typescript
const mockRepository = new InMemoryOrderRepository()
const useCase = new CreateOrderUseCase(mockRepository)
// Test pure business logic without databaseFlexibility
Swap implementations without changing business logic:
typescript
// Switch from PostgreSQL to MongoDB
const orderRepository = new MongoOrderRepository(mongoClient)
// Use case remains unchangedParallel Development
Teams can work independently:
- Domain experts build use cases against interfaces
- Infrastructure teams implement adapters
- UI teams build against use case interfaces
Clear Boundaries
The architecture makes boundaries explicit:
- Ports define the contract
- Adapters handle the technical details
- Business logic remains pure
Common Patterns
Repository Pattern
Abstracts data persistence, allowing business logic to work with domain objects without knowing storage details.
Gateway Pattern
Abstracts external service calls, hiding HTTP requests, authentication, and error handling from business logic.
Presenter Pattern
Abstracts output formatting, allowing use cases to return raw data that different adapters can format appropriately.
Event Publisher Pattern
Abstracts event publishing mechanisms, letting domain code emit events without knowing about message queues or event buses.
Anti-Patterns to Avoid
Leaky Abstractions
Don't let implementation details leak through interfaces:
typescript
// Bad: SQL concepts in interface
interface UserRepository {
executeSQL(query: string): Promise<any>
}
// Good: Business operations
interface UserRepository {
findByEmail(email: Email): Promise<User | null>
}Interface Segregation Violation
Don't force clients to depend on methods they don't use:
typescript
// Bad: One massive interface
interface Repository<T> {
create(), update(), delete(), find(),
search(), count(), exists(), bulk()...
}
// Good: Focused interfaces
interface SaveUser {
save(user: User): Promise<void>
}Wrong Layer Definition
Interfaces should be defined where they're used, not where they're implemented:
typescript
// Bad: Interface defined in infrastructure
// infrastructure/IDatabase.ts
// Good: Interface defined in use case layer
// use-cases/ports/OrderRepository.tsPractical Guidelines
Start Simple
Don't create interfaces for everything upfront. Start with concrete implementations and extract interfaces when you need flexibility or testing.
Name Interfaces After Capabilities
Name interfaces after what they do, not how they're implemented:
- Good:
SaveOrder,NotifyUser,ValidatePayment - Bad:
IPostgresDB,ISMTPService,IStripeAPI
Keep Interfaces Stable
Interfaces should change less frequently than implementations. Design them around stable business concepts, not volatile technical details.
Use Dependency Injection
Whether through a framework or manual wiring, use dependency injection to connect ports and adapters without creating coupling.
Applied In
See how these principles are implemented in practice:
- Clean Architecture in NestJS Modules - Dependency injection and layer boundaries
- Repository Pattern in NestJS - Ports and adapters for data access
- Modular Monolith in NestJS - Module boundaries and dependency management
- Acceptance Testing Guidelines - Test doubles and adapter patterns in testing
- React Design Guidelines (coming soon) - Props as ports, components as adapters
Related Concepts
- Clean Architecture - The architectural context for dependency inversion
- Use Cases - Primary consumers of ports and adapters
- SOLID Principles (coming soon) - Dependency Inversion Principle in detail
Further Reading
- Hexagonal Architecture - Original article by Alistair Cockburn
- Ports and Adapters Pattern - Detailed explanation by Herberto Graça
- Clean Architecture by Robert C. Martin - Chapters 11-12 on DIP and Boundaries
- Dependency Inversion Principle - SOLID principle explanation