Lesson 15 · Senior v2 architecture

Transactions, `database.Ext`, and cross-module writes

Why `database.Ext` is more than a signature detail, and how the repo composes writes safely across repositories and modules.

Your win: explain why `database.Ext` exists, how transaction injection works in v2, and why cross-module writes still go through repository interfaces instead of direct mutating sqlc calls.

In plain English Plain English: `database.Ext` is the seam that lets the same repository code run with either a normal DB handle or a transaction handle.

The problem: one business action often means several writes

A useful write-side handler rarely updates only one thing. It may touch several tables, maybe even several module-owned repos, and still need to succeed or fail as one unit. That is where transaction-aware architecture matters.

The rule to memorise Reads may stay direct where the rules allow, but writes must stay on the guarded repository path so invariants and cross-cutting behavior are not bypassed.

Why `database.Ext` matters

The interface is not an incidental abstraction. It is what allows repo methods to accept either a pool or a tx handle through one signature. That is how `ExecInTx` or `ExecInTxV2` can thread one transaction through several writes without special-case repository APIs.

Anchor — where this matters here Spike's command handlers show the multi-write transaction shape. Eureka's read/write asymmetry shows the stronger architectural rule: direct sqlc reads are allowed, but mutating writes from usecases are not.
Common mistake Treating `database.Ext` as just a convenience type alias instead of seeing it as the seam that makes transaction composition possible across the repo layer.
Read this next

Eureka read/write rules + usecase conventions

The loaded CLAUDE and rules files are the highest-trust explanation of why the read/write split exists in this codebase.

internal/eureka/CLAUDE.md
.claude/rules/eureka-v2-conventions.md

Check yourself (from memory)

Q1. The main purpose of `database.Ext` is to…

That is the transaction seam the architecture relies on.
Why are cross-module writes still forced through repository interfaces?
recall, then click to reveal
Because the write path protects invariants, event hooks, and observability behavior, so usecases must not bypass it with direct mutating queries.
Want me to trace one repo write flow through `ExecInTx` step by step? Ask me.

Sources. Internal CLAUDE and rules docs.