Lesson 2 · Hexagonal — the shape of a v2 module
Ports & adapters
The single idea the whole architecture turns on — an interface the core owns, and a concrete implementation at the edge that the core never names.
Your win: point at any repository in your services and say which file is the port and which is the adapter, and explain why the arrow between them points the way it does.
Where the name comes from
Alistair Cockburn named "hexagonal architecture" (a.k.a. ports & adapters) to solve one problem: let an application be driven equally by users, other programs, automated tests, or batch scripts — and be "developed and tested in isolation from its eventual run-time devices and databases."1 The hexagon is just a box with several sides; each side is a port, and each port has one or more adapters plugged into it.
| Term | What it is | In this repo |
|---|---|---|
| Port | an interface the core owns — "here's what I need from the outside" | repo_port.go, core/port/…, storage.go |
| Adapter | a concrete implementation that talks to a real technology | infrastructure/repositories/, infrastructure/postgres/ |
A port is an interface — in your own code
Here is a real port from spike. Notice what it mentions and what it doesn't:
type EmailRepo interface {
UpsertEmail(ctx context.Context, db database.Ext, e *model.Email) error
// ... more methods
}
It takes a *model.Email and a database.Ext — and nothing else.
No pgx, no SQL, no connection pool. That's the port: a promise about behaviour, stated
entirely in the vocabulary of the domain. The command handler (Lesson 6) depends on
this.
An adapter is the implementation — at the edge
type EmailRepo struct{} // the adapter — same name, different package
func (e *EmailRepo) UpsertEmail(ctx context.Context, db database.Ext, ent *model.Email) error {
// builds an INSERT ... ON CONFLICT string, binds $1,$2,… , calls db.Exec — the pgx lives HERE
}
The adapter is where all the database detail lives — the SQL, the placeholders, the driver. The handler never sees any of it; it only ever holds the interface. Swap this adapter for an in-memory fake in a unit test and the handler can't tell the difference — which is the entire point Cockburn was making.
The arrow: dependency inversion
Why is this more than "use an interface"? Because of which way the dependency points:
Both the core and the adapter depend on the port. The core does not
depend on the adapter — it doesn't import the repositories package, doesn't know
postgres exists. That inversion (the concrete depends on the abstract, never the reverse) is what
makes the database a detail you can replace, mock, or defer.
database.Ext —
never pgx or pgtype. If you open a storage.go /
repo_port.go and see a driver type in a method signature, the abstraction has
already leaked. database.Ext is deliberately generic: it accepts both a real pool
and a transaction handle — which is how a command can run several writes in one
transaction (Lesson 6) without the port knowing about transactions at all.2
Same shape, every service
Once you see it in spike, you see it everywhere. conversationmgmt spells the two sides out in the folder names:
| Service | Port (interface) | Adapter (impl) |
|---|---|---|
| spike | infrastructure/repo_port.go:11 | infrastructure/repositories/email.go:19 |
| conversationmgmt | core/port/repository/conversation.go:10 | infrastructure/postgres/conversation.go:23 |
| eureka v2 | repository/storage.go:11 | repository/postgres/…_postgres.go:22 |
conversationmgmt even splits it physically: core/port/ holds every interface,
infrastructure/ holds every implementation. The word core is the
hexagon's inside; infrastructure is the plugged-in edge. (Whether that boundary
actually holds in conversationmgmt is Lesson 4 — hold that thought.)
Hexagonal Architecture — the original
Cockburn's own page. Short, and the source of the vocabulary. Read the intent paragraph and the "driven / driving" distinction.
→ alistair.cockburn.us — Hexagonal Architecture
→ in-repo internal/spike/modules/email/infrastructure/repo_port.go,
.../repositories/email.go
Check yourself (from memory)
Q1. In ports & adapters, what is a port?
Q2. Which way does the dependency point?
Q3. You open a storage.go and a method takes a pgtype.Text. What is that?
database.Ext. A
pgtype in the interface couples the core to the driver — the leak we watch for.
repo_port.go / core/port/ / storage.go). Adapter
= concrete impl at the edge, where pgx/SQL lives (infrastructure/repositories /
postgres). Dependency inversion: core → port ← adapter; the core
NEVER imports the adapter → DB is swappable/mockable/testable in isolation (Cockburn's whole
point). Purity rule: a port references only domain types + database.Ext,
never pgx/pgtype. database.Ext accepts a pool OR a tx handle → transaction injection
with no coupling. A driver type in a port signature = a leak.model vs dto). Ask me if the
dependency arrow still feels backwards — it's the crux of everything.
1. Alistair Cockburn, Hexagonal Architecture.
2. In-repo: internal/spike/modules/email/infrastructure/repo_port.go:11, .../repositories/email.go:19, .claude/rules/eureka-v2-conventions.md (storage.go purity).