Concept · Architecture Patterns

Hexagonal & Clean Architecture

01

Why this matters

You wrote your business logic in a controller class that calls Postgres directly via an ORM and HTTP-responds with JSON. Year 2: you need to support gRPC. Year 3: you need to use DynamoDB instead of Postgres. Year 4: the same logic runs as a Lambda function. Each migration is a 6-month rewrite because business logic, framework concerns, and infrastructure are tangled together.

Hexagonal architecture (Alistair Cockburn, 2005) and its sibling Clean architecture (Bob Martin, 2012) are the discipline of separating core business logic from delivery mechanism and infrastructure. Same code runs as REST, gRPC, CLI, queue consumer, or test harness — with no changes to the business logic.

02

Ports and adapters

Hexagonal's metaphor: imagine your application as a hexagon. Inside the hexagon is the business logic (the "core"). Around the edges are ports — interfaces the core defines for what it needs from the outside. Outside the hexagon are adapters — concrete implementations of those ports.

  • Driving ports (left side): the world calls into the core. HTTP controller, gRPC handler, CLI command, message-queue consumer — all are adapters that translate external requests into calls on driving ports.
  • Driven ports (right side): the core calls out to the world. Repository for storage, email sender, payment gateway client — adapters implement these ports.

The core knows nothing about HTTP, JSON, SQL, or the cloud. It works in pure domain language. Swap any adapter without changing core code.

Ports & Adapters Topology SVG
Domain Core pure business logic no framework, no I/O HTTP controller gRPC handler CLI command Test harness DRIVING ADAPTERS Postgres repo Email gateway Stripe client In-memory mock DRIVEN ADAPTERS
03

Clean Architecture — Bob Martin's variant

Same idea, expressed as concentric layers:

  1. Entities — the most stable rules. Business invariants. Plain objects.
  2. Use cases — application-specific orchestration. "Place an order" workflow.
  3. Interface adapters — controllers, presenters, repositories. Translate between use cases and frameworks.
  4. Frameworks & drivers — Spring, Postgres, AWS SDK. The outermost shell.

The dependency rule: source code dependencies only point inward. Outer layers know about inner; inner layers know nothing about outer. Entities don't import anything framework-related. Use cases import entities but not Postgres.

04

When it's worth it

Worth it when:

  • Business logic is genuinely complex (insurance pricing, banking rules, scheduling).
  • You'll outlive multiple framework choices (5+ year systems).
  • You need fast unit tests that don't touch I/O. Hexagonal makes the core trivially testable.
  • You have ports you actually want to swap (multiple persistence backends, multiple delivery channels).

Overkill when:

  • CRUD app with thin business logic — you'd be building scaffolding for nothing.
  • Throwaway prototype — speed of iteration beats long-term flexibility.
  • Small team where one developer wears all hats — the architectural overhead exceeds the value.
Common failure mode

Teams adopt clean architecture as boilerplate, end up with 4-layer indirection on every CRUD endpoint. The pattern only pays off when the inner core is genuinely complex. For thin domains, you've added ceremony with no benefit.

05

Deep dive — testability is the killer feature

The single best argument for hexagonal architecture: your domain core can be tested without spinning up Postgres, Kafka, Redis, or any framework. Tests run in milliseconds, don't flake, can be exhaustive.

Pattern:

  • Define a OrderRepository port (interface).
  • Production has PostgresOrderRepository implementation.
  • Tests use InMemoryOrderRepository.
  • Domain logic — "OrderPlacementService" — accepts a repo via constructor injection.

Result: you can run 10,000 tests of order placement logic in 2 seconds. CI feedback loop tightens dramatically. Bugs caught at the unit-test level, not in slow integration tests.

This is also the foundation of TDD — you can't TDD a system where every method does I/O. You can TDD a system whose core is pure functions over domain types.

Interview answer

"We use ports and adapters. Domain core has no framework or I/O dependencies. Postgres, REST, Kafka, Stripe are all adapters around defined ports. This makes the core unit-testable in milliseconds and lets us swap any infra concern (e.g., DynamoDB instead of Postgres) without touching domain logic."

06

Real-world

Banks & insurance

Complex domain → hexagonal pays off

Pricing engines, underwriting rules, compliance checks — pure domain code separated from frameworks. Replaces decade-old mainframes with clean Java/Kotlin services.

DDD-heavy startups (modern Java/Kotlin)

Hexagonal is the default

Spring Boot communities adopt hexagonal as best practice. Reference templates from the Spring team itself.

Functional-language shops

Naturally hexagonal

Clojure, F#, Haskell make pure domain logic + IO at edges idiomatic. Hexagonal is just "how you write code."

Anti-pattern at small startups

Common mis-application

10-person team adopting 4-layer hexagonal for a CRUD app drowns in indirection. Match architectural cost to actual complexity.

07

Used in problems

Payment gateway naturally maps to hexagonal — domain (charge, refund, dispute) cleanly separated from PSP adapters (Stripe, Adyen, Braintree). E-commerce platforms with complex pricing/promotion rules benefit from clean architecture in the pricing core.

Next up