Flow
Decisions

0023 — One sqlc package per domain, and no joins between domains

A service with several domains generates one sqlc package for each, which turns a cross-domain join into a compile error instead of a review comment.

Decided. A service with more than one domain generates a separate sqlc package for each domain: its own queries: file, its own out: directory, and a package named <domain>db. They all share one schema:, which is the whole service's tables.

SQL may not join tables from two different domains. To read across domains, the handler calls each domain and combines the answers. To write across domains, pass a pgx.Tx into the other domain's Go function, or send an event through the outbox.

Some background. sqlc reads our SQL files and writes Go functions from them. Which tables a generated package knows about depends on which SQL files we point it at. So splitting the SQL files splits what each package can say.

This builds on 0015, which gave every domain its own table prefix and its own outbox but did nothing to stop a query joining across them.

Alternatives

One sqlc package for the whole service. The simplest configuration, and what we did before.

Rejected because every domain's generated code can then reach every table. The boundary holds only while everyone remembers it is there. In a service with a dozen tables per domain, somebody will forget.

A separate Postgres schema per domain, so a cross-domain query needs a qualified name and feels awkward to write.

Rejected in 0015 already, for the tooling problems it caused. Awkward is also not the same as impossible.

Catch it in code review. Rejected because nothing goes wrong at the time. The problem shows up when you try to move the domain out, which is the most expensive moment to find it.

Why

  • The compiler enforces it. conversationdb.Queries has no method that returns a project row, so a cross-domain join does not build. That is stronger than a review comment.
  • It makes splitting a service cheap later. Every place this rule forces a seam is a place a service boundary could run. Workspace and intelligence were one service in an earlier draft, and the domains inside each will face the same question.
  • Handlers are already where things get assembled. 0019 put composition there. This makes it the only place it can happen.

Consequences

  • sqlc.yaml gets one block per domain instead of one per service. The blocks repeat most of the same settings, and that repetition grows with every domain added. It is in FRICTION.md and is an early requirement for the generator.
  • No foreign keys between domains. A column pointing at another domain's row is a plain uuid, checked at write time by calling that domain's Go function. For those columns the database no longer guarantees the reference is valid. The application does.
  • Reads that used to be one join are now several calls. When that gets too slow, the fix is a table inside the owning domain kept up to date by a trigger. Not a join, and not a second datastore, per 0002.
  • Services with one domain are unaffected and keep a single block.

Source: docs/decisions/0023-per-domain-sqlc-packages.md.

On this page