Architectural Styles Deep Dive · 7 of 8

Hexagonal & Clean — Keep the Domain in the Middle

Hexagonal architecture (Alistair Cockburn, 2005) — also called Ports & Adapters and, in Robert Martin's variant, Clean Architecture — puts your business domain in the center and pushes everything else to the edges. The domain depends on nothing concrete. HTTP, databases, message brokers, file systems are all adapters that plug into ports the domain owns. Swap any one of them without touching business logic.

Ports & AdaptersDomain-CentricDependency InversionTestable
← Back to Architecture
Quick Facts

The Shape

Basic Concepts

  • Domain (the hexagon's interior): entities, value objects, domain services. Pure business logic. No imports from frameworks, no @Entity, no SQL.
  • Ports: interfaces the domain owns. Driving ports are how the outside world calls in (use-case interfaces). Driven ports are how the domain calls out (repository, email, payment).
  • Adapters: the concrete implementations. Driving adapters are HTTP controllers, CLI handlers, message consumers. Driven adapters are Postgres repos, Stripe clients, SMTP senders.
  • The dependency rule: dependencies point inward only. Adapters know about ports; ports know about the domain; the domain knows about nothing outside itself.
  • Clean Architecture (Uncle Bob's variant) draws the same idea as concentric circles: Entities → Use Cases → Interface Adapters → Frameworks. Same dependency rule.
Why It Wins

What You Buy

Infrastructure Becomes Pluggable

Postgres → DynamoDB? Swap one adapter. REST → gRPC? Add a new driving adapter, leave the old one. Stripe → Adyen? New payment adapter. The use cases don't change because they never knew which one was wired in.

Tests Run Without the World

Unit tests for use cases inject fake adapters — in-memory repository, fake email sender. No DB container, no network, no flaky integration. Domain logic tests run in milliseconds.

The Domain Lives Longer Than the Stack

Frameworks come and go (Struts → Spring MVC → WebFlux → who's next). When the domain is framework-free, you survive each migration with a stable core and a rewritten outer ring.

Forces Honest Modeling

Without an ORM in the room, you can't blur "domain" with "row in the orders table." Entities have to express invariants in code; the persistence shape is a separate problem solved by an adapter.

Mechanics

How to Build It

Source Layout

Typical packages:

  • domain/ — entities, value objects, domain services. No third-party imports.
  • application/ — use cases (one class per business operation), driving and driven port interfaces.
  • adapters/in/ — HTTP controllers, message consumers, CLI commands.
  • adapters/out/ — DB repositories, external API clients, email senders.
  • config/ — composition root: wires concrete adapters to ports.
Driving vs Driven Ports

Driving (inbound): the use-case interface. PlaceOrderUseCase.execute(command). The HTTP controller calls this; so could a Kafka consumer or a CLI command. The domain doesn't know which.

Driven (outbound): what the use case needs from the world. OrderRepository.save(order), PaymentGateway.charge(amount). The use case calls the interface; an adapter implements it.

Domain Stays Pure

No @Entity, no @JsonProperty, no DB session, no HTTP request object. If your domain class needs a JPA annotation, you've leaked persistence into the domain. Use a separate persistence model in the adapter; map between the two at the boundary.

The Composition Root

One place — startup, the DI container's config — wires concrete adapters to port interfaces. PlaceOrderUseCase(new PostgresOrderRepository(), new StripePaymentGateway(), ...). Everything else only sees interfaces.

Mapping at the Boundary

Adapters translate between the outside world's shape and the domain's. HTTP request → command DTO → use case input. Domain output → response DTO → JSON. JPA entity → domain entity in the repository adapter. This mapping is the price; it's what keeps the domain pure.

Where It Hurts

The Tax

Boilerplate

Adding a field can mean: domain entity, JPA entity, mapper, request DTO, response DTO, port interface, adapter implementation. For a CRUD app where the domain has no real logic, this overhead doesn't pay back. Hexagonal is for rich domains.

Two Models for One Concept

The domain entity and the persistence entity are usually distinct classes. Engineers new to the codebase often try to "simplify" by merging them — and quietly destroy the boundary. Documentation and code review have to defend it.

Easy to Over-apply

Not every service deserves hexagonal. A glue service that calls one third-party API and writes to one queue doesn't need a domain layer; it is an adapter. Forcing the structure adds friction with no benefit.

Cousins

Hexagonal, Clean, Onion — Same Idea

NameAuthorTwist
Hexagonal / Ports & AdaptersCockburn (2005)The original. Hexagon is metaphor only — could have any number of sides.
Onion ArchitectureJeffrey Palermo (2008)Concentric rings; emphasizes domain-centric layering.
Clean ArchitectureRobert C. Martin (2012)Adds the explicit Use Case layer between Entities and Adapters.

All three share the dependency rule: source code dependencies point inward. Pick whichever vocabulary your team already knows; the substance is the same.

Decision

When to Pick Hexagonal

Worth the boilerplate when:

  • The domain is genuinely rich — non-trivial rules, invariants, calculations.
  • The system will outlive its current infrastructure choices (5+ years).
  • You'll have multiple driving adapters: web + mobile API + admin CLI + scheduled jobs all calling the same use cases.
  • You have engineers experienced enough to maintain the boundary under deadline pressure.

Skip it for thin CRUD apps, glue services, prototypes, and anything where the domain logic is essentially "save this and notify that."

Continue

Other Architectural Styles