Lesson 12 · sqlc — typed SQL + the synthesis
The whole picture
Where v2 came from, which modules to copy and which to avoid, when sqlc actually pays — and the twelve-lesson course in one view you can carry into an interview.
Your win: place any v2 module on the quality ladder, argue when sqlc is worth the codegen, and recite the architecture end to end from memory.
Why two generations exist — the migration
You've seen v1 and v2 side by side in every service. That's not indecision — it's a deliberate,
incremental migration. The legacy layer (entities/ repositories/ services/) is
maintained but not grown; every new feature goes into modules/, one bounded
context at a time.1 You don't rewrite a running service in
one heroic PR; you add v2 modules alongside v1 and let the old code shrink by attrition. Both eureka
and notification are explicitly "two code generations." That coexistence is the strategy.
The module-quality ladder — copy the good ones
Because the migration is gradual, module quality is uneven — even inside one service. Knowing which to imitate is half of working in v2 well:2
| Module | Verdict | Why |
|---|---|---|
notification system_notification | ✅ copy this | strict CQRS, one handler per file, shared by gRPC + Kafka |
spike email | ✅ copy | clean CQRS + hexagonal, wiring at the edge |
eureka assessment | ✅ copy (sqlc) | the fullest sqlc + read/write asymmetry (Lesson 11) |
notification announcement | ⚠️ careful | rich domain, but pgtype leaks into usecases (Lesson 3) |
conversationmgmt conversation | ⚠️ careful | core imports infrastructure — the DI leak (Lesson 4) |
notification tagmgmt · any v1 | ❌ don't copy | legacy flat layer — inline SQL, inline interfaces |
The load-bearing conventions aren't in a readme — they're in .claude/rules/*.md
(storage.go purity, querier injection, error translation). When a readme and a rule disagree, the
rule wins.
When is sqlc actually worth it?
sqlc is eureka-only, and even there it's a hybrid (Lesson 10). Is that a gap to fix, or a reasonable call? Mostly the latter:
| sqlc earns its keep when… | hand-rolled SQL is fine when… |
|---|---|
| queries are complex & read-heavy (eureka's LMS: joins, filters, reports) | writes are mostly CRUD-ish upserts (spike emails, notifications) |
| compile-time column/param safety pays off across many queries | a handful of dynamic-filter queries is easy to maintain by hand |
| a team wants a mockable, traced query layer generated for free | the extra codegen step + a custom WASM plugin isn't worth it |
The whole course, one view
| Part | Lessons | The one thing |
|---|---|---|
| 1 · Hexagonal | 1 map · 2 ports/adapters · 3 domain/dto · 4 the leak | the core owns interfaces (ports); adapters implement them; the arrow only points inward |
| 2 · CQRS | 5 Fowler · 6 commands · 7 the asymmetry · 8 wiring | split write handlers from read handlers; no bus; read direct, write through a port |
| 3 · sqlc | 9 config · 10 querier · 11 end-to-end · 12 this | write SQL → generate a typed, mockable querier; wrap it in an adapter (via a constructor) |
ExecInTx + a
tx-or-pool database.Ext) — your Postgres course. Ports exist
so you can inject mocks — your Testing course (mock the port, test the core). And the
handlers here are the same transport-agnostic ones reused across gRPC, Kafka, and
NATS — your NATS and eventing courses. v2 architecture is the frame the whole platform
hangs on; this course is where the pieces meet.
Interview lightning round
- Hexagonal? Core defines ports; infra provides adapters; the core never imports infra.
- CQRS? Separate write & read handlers; no bus here; Fowler: use it sparingly.
- Reads vs writes? Read via the querier directly; write only through a repo port.
- sqlc? Annotated SQL → type-safe, mockable, traced Go; compile-time column safety.
- Keeping the DB out of the domain? Ports take domain types + a generic DB handle;
pgtypeabove the adapter is a leak. - Seen it done wrong? A "hexagonal" module whose core constructor imported postgres — interfaces that bought nothing.
The reference shelf
Everything compressed for reuse — skim before an interview.
→ Cheat sheet ·
Glossary ·
Repo v2 map
→ external: Cockburn ·
Fowler ·
sqlc ·
Three Dots Labs
Check yourself (from memory)
Q1. Why do v1 and v2 code coexist in a service?
modules/; the legacy layer is maintained but not grown. Coexistence is the strategy.
Q2. Which module is the one to copy for CQRS?
system_notification is the "✅ canonical — copy this"
module: one handler per file, shared by gRPC and Kafka. Avoid tagmgmt and v1.
Q3. Best reason sqlc is eureka-only?
modules/, one bounded context at a time; coexistence is the strategy.
Ladder: copy system_notification/spike email/eureka
assessment; be careful with announcement (pgtype leak) &
conversation (DI leak); avoid tagmgmt/v1. Real spec = .claude/rules/*.md.
When sqlc pays: complex read-heavy queries + compile-time safety; simpler CRUD is
fine hand-rolled — even eureka is hybrid. It's a trade-off, not an oversight. The arc:
hexagonal (ports/adapters, arrow inward) → CQRS (write/read handlers, no bus, read-direct/write-via-port)
→ sqlc (annotated SQL → typed mockable querier in an adapter). Ties to Postgres (tx), Testing
(mock the port), NATS (shared handlers).1. In-repo: internal/eureka/CLAUDE.md, internal/notification/CLAUDE.md ("Two code generations").
2. In-repo: internal/notification/CLAUDE.md (module-quality table); .claude/rules/eureka-v2-conventions.md; Lessons 3 & 4 (the leaks).