Hexagonal Architecture
Hexagonal Architecture (Ports and Adapters) puts domain at the center.
Introduction
Hexagonal Architecture (Ports and Adapters) puts domain at the center. Ports are interfaces the domain defines; Adapters are infrastructure implementations. HTTP, CLI, Postgres, Kafka, Stripe — all adapters. Domain never imports frameworks.
The story
A logistics rewrite replaced Spring-coupled code with Hexagonal layout. Switching hot routes from PostgreSQL to DynamoDB took 2 weeks instead of projected 6 months — domain unchanged; only persistence adapter swapped.
The business problem
Framework-coupled domain blocks testing, migration, and multi-channel delivery:
- Vendor lock-in: domain imports Spring, Stripe, Mongoose directly.
- Untestable core: can't run business logic without infrastructure.
- Channel duplication: adding gRPC/CLI requires rewriting HTTP-tied code.
- Migration cost: DB swap touches every service file.
The problem teams faced
Hexagonal fits when:
- Domain must outlive frameworks and databases.
- Multiple delivery mechanisms (HTTP, queue, CLI) share same use cases.
- Tests need domain logic without Spring/container boot.
- Infrastructure churn expected (DB migration, vendor swap).
Understanding the topic
Intent: Isolate domain via ports (interfaces) and adapters (implementations).
- Domain core — entities, use cases, port interfaces.
- Primary/driving adapters — HTTP controller, CLI, message consumer.
- Secondary/driven adapters — repository, payment gateway, email sender.
- Composition root — wires adapters to ports at startup.
Internal architecture
Hexagonal module layout:
domain/entities/Order.tsports/OrderRepository.ts ← interface owned by domainusecases/PlaceOrder.tsadapters/in/HttpOrderController.ts ← drivingout/PostgresOrderRepository.ts ← drivenmain.ts ← composition root wires in→usecase→out
Visual explanation
Three diagrams cover hex shape, port direction, and adapter swap:
Informative example
Before / after — Mongoose in service to port/adapter:
// ❌ Domain coupled to Mongooseclass OrderService {async place(cart: Cart) {return OrderModel.create({ items: cart.items }) // Mongoose in domain}}// ✅ Port in domain, adapter in infrastructure// domain/ports/OrderRepository.tsinterface OrderRepository { save(order: Order): Promise<void> }// domain/usecases/PlaceOrder.tsclass PlaceOrder {constructor(private orders: OrderRepository) {}async execute(cart: Cart) { await this.orders.save(Order.fromCart(cart)) }}// adapters/out/MongoOrderRepository.tsclass MongoOrderRepository implements OrderRepository { /* Mongoose here */ }
Execution workflow
Define domain ports
Interfaces in domain package — OrderRepository, PaymentGateway.
Real-world use
Alistair Cockburn's original pattern; adopted in DDD codebases, NestJS with domain modules, Go hexagonal layouts, many bank rewrites.
Production case study
Logistics Postgres → DynamoDB:
- Scenario: Hot routes needed DynamoDB; domain Spring-coupled.
- Decision: Hexagonal rewrite — ports for persistence.
- Outcome: 2 weeks adapter swap vs 6 month projected rewrite.
Trade-offs
- Pro: domain testable; infra swappable; multi-channel reuse.
- Con: more packages and wiring; learning curve for team.
- Con: overkill when app is thin CRUD with stable stack.
Decision framework
- Use when infrastructure churn or multi-channel delivery expected.
- Use when domain rules are valuable enough to protect from frameworks.
- Avoid on throwaway MVPs with single HTTP + single DB forever.
Best practices
- Ports live in domain package — infrastructure implements, never defines.
- Driving adapters map transport DTOs — don't leak HTTP types into domain.
- Fake in-memory adapters for domain unit tests.
Anti-patterns to avoid
- Port interface defined in infrastructure layer — inverts dependency.
- Anemic hexagon — ports/adapters but no domain rules in core.
- Same class as port and adapter — missing seam.
Common mistakes
- Leaking DTO/ORM types through port interfaces.
- Composition root scattered — multiple places wire adapters.
Debugging tips
- Bug in domain or adapter? — if domain tests pass, bug is in adapter mapping.
Optimization strategies
- Start with one bounded context hexagonal — don't boil the ocean.
Common misconceptions
- Hexagonal ≠ microservices. In-process pattern; can be monolith.
- Same as Clean Architecture — convergent ideas; Clean adds concentric layer names.
Advanced interview questions
Interview Prep
Practice concise answers, then expand each card for the explanation.
1BeginnerQuestionPort vs Adapter?+
Answer
Follow-up
2IntermediateQuestionDriving vs driven adapter?+
Answer
Follow-up
3AdvancedQuestionHexagonal vs Layered?+
Answer
Follow-up
Summary
You can layout ports/adapters, keep domain framework-free, and swap persistence in weeks not months. Tell the DynamoDB 2-week story — if you can do that, you own Hexagonal.