hexagonal-architecture · git:20260718.ea9bc00 · 2026-07-18 · sha256 7525e7fba7e0cd97
hexagonal-architecture git:20260718.ea9bc00A
Immutable. This exact content is served forever at /api/v1/blob/7525e7fba7e0cd97.
--- name: hexagonal-architecture description: Isolate domain logic behind ports it owns while frameworks, databases, and transports live in adapters that plug into those ports. Use when business rules keep getting entangled with the web framework or database and you want the core testable and swappable in isolation. --- # Hexagonal architecture Also called ports and adapters. The idea is a hard rule: the domain depends on nothing, and everything depends on the domain. When the HTTP handler and the SQL query bleed into the business rules, you cannot test the rules without a server and a database, and you cannot change either without risking the rules. ## Method 1. **Define ports as interfaces the domain owns.** A port is a Python Protocol, a Java interface, a Go interface declared inside the domain package. The domain names the shape it needs, `PaymentGateway.charge(amount)`, and never imports the thing that fulfills it. 2. **Keep the domain import-clean.** No `import requests`, no ORM base class, no framework annotation inside the core. Grep the domain package for your web and database libraries: a single hit is a leak to fix, not to tolerate. 3. **Split ports into driving and driven.** Driving (primary) ports are how the outside calls in: a use-case interface a REST controller invokes. Driven (secondary) ports are how the domain calls out: repositories, gateways, clocks. Naming the two directions keeps the wiring legible. 4. **Put every concrete detail in an adapter.** The REST adapter drives a primary port; the Postgres adapter implements a secondary port; a Stripe adapter implements the payment port. Each adapter is replaceable without the domain noticing. 5. **Wire everything at a composition root.** One place, usually `main`, that constructs adapters and injects them into the domain. The domain receives its dependencies; it never reaches out and builds them. 6. **Test the core with fakes.** Swap in an in-memory repository and a stub gateway to test business rules with no network and no I/O. If a domain test needs a running database, a boundary has been crossed. 7. **Translate at the adapter edge.** Map request payloads and database rows to domain models inside the adapter, so external formats never reach the core. ## Checks - Do the domain tests run with no database, no HTTP server, and no clock skew? - Could you replace Postgres with an in-memory store by writing one adapter and changing one line at the composition root? - Does any domain file import a framework, driver, or transport library? ## Boundaries Domain-driven design supplies the model that lives inside the hexagon: this skill governs the boundary around it, not the modeling within. For a thin CRUD service with no real business rules, the indirection buys little worth its cost.