Day 127 — Hexagonal Architecture (Ports & Adapters)¶
Month 5 · Week 3 · ⬅ Day 126 · Day 128 ➡ · Journal index
🎯 Learning Objective¶
Structure a service so the business core has zero infrastructure imports, by defining ports (interfaces) the core owns and adapters that implement them — making dependencies point inward.
📚 Topics¶
- Hexagonal / ports & adapters; the dependency rule (arrows point toward the core)
- Inbound (driving) vs outbound (driven) ports; where to define the interface
- The composition root; compile-time interface assertions
📖 Reading / Sources¶
- Alistair Cockburn — Hexagonal Architecture (original)
- Go blog — Organizing a Go module
- Effective Go — Interfaces
- Accept interfaces, return structs (Go proverb context)
📝 Notes¶
- The one rule: dependencies point inward. The domain core (entities + use cases) imports nothing from the outside; adapters import the core. Invert the dependency with an interface → [[dependency-inversion]].
- A port is an interface the core declares. Inbound/driving ports are the use cases the core exposes (called by HTTP/CLI/gRPC adapters). Outbound/driven ports are what the core requires (a
Repository, aMailer) → [[ports]]. - Define the interface next to its consumer, not next to the implementation. In Go the consumer package owns the port; the adapter package merely satisfies it. This is the opposite of Java's "interface lives with the impl" → [[accept-interfaces]].
- Keep ports small — one or two methods. Go interfaces are satisfied structurally, so a narrow port is easy to fake and easy to swap → [[interface-segregation]].
- The composition root (usually
main) is the only place that names concrete adapters and wires them into the core. Swapping memory→Postgres is a one-line change there → [[composition-root]]. - Return domain sentinels (
ErrNotFound) from ports so callers branch on meaning, not on adapter-specific text; wrap with%wsoerrors.Isstill matches → [[error-wrapping]]. - A
var _ Port = (*Adapter)(nil)line is a free compile-time contract check — grow the interface and the adapter fails to build.
💻 Code Examples¶
// OUTBOUND PORT — declared in the core, next to the service that needs it.
type AccountRepository interface {
Find(id string) (*Account, error) // returns ErrNotFound when absent
Save(a *Account) error
}
// CORE service depends on the PORT, injected via its constructor.
type Ledger struct{ repo AccountRepository }
func NewLedger(repo AccountRepository) *Ledger { return &Ledger{repo: repo} }
// ADAPTER (outside the hexagon) satisfies the port; imports the core, not vice-versa.
var _ AccountRepository = (*memRepo)(nil)
Full runnable model (stdlib only):
examples/month-05/hexagonal/main.go· Run:go run ./examples/month-05/hexagonal
🏋️ Exercises / Practice¶
| Exercise | Status | Link |
|---|---|---|
| Repository port + in-memory adapter + service | ✅ | exercises/month-05/week-3/ports |
🐛 Mistakes Made¶
- First put the
Repositoryinterface in the adapter package → created an import cycle when the core needed it. Moved the interface to the core (its consumer). - Returned the stored
*Accountpointer directly from the in-memory adapter; a caller mutated it and corrupted state. Now I return a copy.
❓ Open Questions¶
- Where exactly does validation live — entity constructor, service, or both? (Leaning: invariants in the entity, orchestration rules in the service.)
🧠 Active Recall (answer without looking)¶
- Q: In Go, which package should declare a repository interface — the one that uses it or the one that implements it?
A
The package that *uses* it (the consumer/core). Go interfaces are satisfied implicitly, so the core declares the port it needs and the adapter just implements it — keeping the dependency arrow pointing inward, with no import cycle.- Q: What does "dependencies point inward" mean for imports?
A
The domain core imports nothing from infrastructure; adapters (DB, HTTP, cache) import the core. The core is dependency-free and unit-testable; you swap adapters at the composition root without touching it.🪶 Feynman Reflection¶
Picture the business logic in the middle of a hexagon. Each edge is a hole — a port — shaped like an interface. Outside the wall, adapters plug into those holes: one for the database, one for HTTP, one for tests. The core never knows what's plugged in; it only knows the hole's shape. So I can yank the Postgres adapter out and push a fake in, and the core can't tell the difference — which is exactly why it's testable.
🕳️ Knowledge Gaps¶
- Modeling transactions across two repository calls without leaking SQL into the core (unit-of-work pattern).
✅ Summary¶
I can lay out a service as a dependency-free core with small ports it owns, plug adapters in at a composition root, and prove the contract with a compile-time assertion.
⏭️ Next Steps / Prep for Tomorrow¶
- Day 128: wire that object graph by hand — manual dependency injection.
| Time spent | Difficulty | Confidence |
|---|---|---|
| 90 min | 🟦🟦⬜⬜⬜ | 🟦🟦🟦⬜⬜ |
Suggested commit: feat(examples): hexagonal ports & adapters core + adapter (day 127)