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

ModuleVerdictWhy
notification system_notificationcopy thisstrict CQRS, one handler per file, shared by gRPC + Kafka
spike email✅ copyclean CQRS + hexagonal, wiring at the edge
eureka assessment✅ copy (sqlc)the fullest sqlc + read/write asymmetry (Lesson 11)
notification announcement⚠️ carefulrich domain, but pgtype leaks into usecases (Lesson 3)
conversationmgmt conversation⚠️ carefulcore imports infrastructure — the DI leak (Lesson 4)
notification tagmgmt · any v1❌ don't copylegacy 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 queriesa handful of dynamic-filter queries is easy to maintain by hand
a team wants a mockable, traced query layer generated for freethe extra codegen step + a custom WASM plugin isn't worth it
The honest answer for an interview "Why isn't sqlc everywhere?" → "It's a trade-off, not an oversight. sqlc's compile-time safety pays off most in a query-heavy service like our LMS; simpler CRUD services get more value from hand-rolled dynamic SQL than from adding a codegen step. Even the sqlc service is a hybrid — generated queries where the safety helps, dynamic SQL elsewhere." Naming a trade-off as a trade-off is what seniority sounds like.

The whole course, one view

PartLessonsThe one thing
1 · Hexagonal1 map · 2 ports/adapters · 3 domain/dto · 4 the leakthe core owns interfaces (ports); adapters implement them; the arrow only points inward
2 · CQRS5 Fowler · 6 commands · 7 the asymmetry · 8 wiringsplit write handlers from read handlers; no bus; read direct, write through a port
3 · sqlc9 config · 10 querier · 11 end-to-end · 12 thiswrite SQL → generate a typed, mockable querier; wrap it in an adapter (via a constructor)
How this connects to your other courses The read/write asymmetry rests on transactions (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

🎯 Six answers you now own
Read this next (and keep)

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?

Deliberate gradual migration: new features go into v2 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?

Compile-time safety earns its codegen cost in complex, read-heavy queries (the LMS); simpler CRUD services do fine with hand-rolled dynamic SQL.
Recall: the whole v2 picture — migration, ladder, when sqlc pays, the arc.
four things, then reveal
Migration: incremental — v1 maintained-not-grown, new features in v2 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).
That's the course — all twelve lessons. You can now read any v2 module cold, name every layer and its leaks, and explain hexagonal, CQRS, and sqlc from both the canon and your own code. The one thing left is a retention check: ask me to run a cold mock interview across the whole course and I'll write your first real learning records. Ask me anything — that's what I'm here for.

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).