The problem
In a layered app the business logic imports the database driver. Testing a pricing rule then needs a live DB, and switching from REST to a queue consumer means touching the core. The core should not know how it's called or where data lives.
How it works
- Ports are interfaces owned by the core. Inbound ports are use cases. Outbound ports are what the core needs, such as a repository or a publisher.
- Adapters implement the ports for a specific technology.
- Dependencies always point inward. The core imports nothing from the adapters.
// Port (owned by the core)
export interface ReleaseRepository {
findById(id: string): Promise<Release | null>;
save(r: Release): Promise<void>;
}
// Adapter (infrastructure)
export class MongoReleaseRepository implements ReleaseRepository { /* driver code */ }
// Test adapter
export class InMemoryReleaseRepository implements ReleaseRepository { /* Map-based */ }- Rich business rules that must be unit-tested fast
- The same use case has several entry points (API, worker, CLI)
- You expect infrastructure to change (DB, broker, vendor APIs)
- Thin CRUD services — the extra interfaces are just ceremony
- Small scripts and prototypes
Trade-offs
- ✅ The core is testable with in-memory adapters, so tests run in milliseconds.
- ✅ Infrastructure can be swapped without touching business code.
- ❌ More files and indirection. Over-abstracted, it becomes "interface for everything".
- ❌ Needs discipline in code review. Imports leak inward easily.
In one line
Hexagonal means the domain owns interfaces for everything it needs, and infrastructure plugs in as adapters. I use it where the rules are valuable enough to deserve fast, DB-free tests.
Sources & further learning
Videos, courses, docs and books I recommend for this topic.
Related topics
Clean Architecture
Concentric rings — entities, use cases, interface adapters, frameworks — with one rule: source-code dependencies only point inward.
Layered (N-Tier) Architecture
Split an application into horizontal layers — presentation, application, domain, data — where each layer only calls the one below it.
SOLID Principles
Five object-oriented design principles that keep code open to change — single responsibility, open/closed, Liskov substitution, interface segregation, dependency inversion.