Hexagonal architecture
Organise by feature, then by direction of dependency — never by technical layer, and never a layered tree under new names.
Verified 2026-07-31 · Language-independent. Enforcement recipes per stack are on the stack pages.
The standing default, in every language and on both sides of the stack. Do not
propose a layered controllers/ services/ models/ utils tree, and do not quietly
reintroduce one under new names.
The shape
<feature>/
core/ the decision-making centre — no framework imports, no I/O
model/ entities and value objects that own their rules
usecase/ one class per decision; a single entry point
port/ interfaces the core needs, declared by the core, plus their vocabulary
inbound/ adapters that drive the core: http, cli, queue consumer, mcp, cron, ui
outbound/ adapters the core drives: db, http client, cache, broker
api/ what other features may import — and nothing elseDependencies point inward, always. core imports nothing from inbound or
outbound. Adapters import core. Features import each other only through api/.
Why this and not layers
- Layers group by mechanism; features group by reason to change. A feature change in a layered tree touches five directories and reviews as five unrelated diffs.
- The core becomes testable without infrastructure. Rules live where a plain unit test can exercise them; nothing forces a database to assert a state transition. That is a payoff of the structure, not a separate discipline — see testing.
- Boundaries make extraction cheap. A feature with a real
api/is a service-in-waiting; a layered app is not, at any size. - The dependency rule is mechanically checkable — ArchUnit,
dependency-cruiser, eslint boundaries,
import-linter— which is rule 3 of the standing default, and the reason it survives. Where the language has no such tool, build one: see Go's checked-file approach.
Rules that keep it honest
- No package named for a mechanism —
service,util,helper,manager,dto,mapper,repository(as a package). They attract everything and mean nothing. - A port is declared by the core, in the core's vocabulary, and implemented outside. The reverse — the core importing the driver's types — is the most common way this collapses.
- The type lives with its owner. A use case owns its input and output; a port owns what it accepts and returns.
- A use case exists when there is a decision. A pure lookup is a port method. Wrapping every read in a class is ceremony, not architecture.
- State the operation's shape as a type, not a marker. An annotation is ignored by the compiler; an interface is not. Full rules: the operation.
- The core declares meaning; the platform recognises mechanism. When infrastructure refuses a write, the core says what that constraint means — it never catches the driver's exception or matches its message.
- Conversion belongs to the outer type, not to a mapper layer.
- Shared code is a feature too, or it is platform — never a
common/dumping ground with no owner.
The frontend applies the same rule
Feature folders (features/checkout/{ui,model,api,lib}), not components/ hooks/ services/. Route files stay thin adapters over a feature's core.
Server-state clients (TanStack Query and friends) belong to the feature's api/,
not to a global service singleton.
Full layout: feature structure.
Traps
- Hexagonal per project instead of per feature → one giant hexagon, and boundaries that do not help. Feature first, hexagon inside it.
- Ports with one implementation, invented for symmetry → declare a port when the core genuinely needs to state a need, not to satisfy a diagram.
- Anaemic core — records with getters and all the rules in the use case. If the model cannot refuse an illegal transition, the architecture is decorative.
- An
api/that re-exports everything → the boundary is gone. Export the contract only. - An architecture rule that selects nothing. Rules written against a method shape stop matching the moment you change that shape, and a rule matching nothing reports green forever. Prefer a checker that fails on an empty selection — otherwise moving a parameter can silently retire a security rule and nothing goes red.