# Storage Model (/docs/architecture/storage-model)



The infrastructure layer's Postgres implementation follows a few firm
conventions: **type-safe queries via sqlc**, a **CQRS split** between reads and
writes, **narrow interface projections** so each caller sees only what it needs,
and **backward-compatible migrations**. This page explains how they fit together.

## sqlc, not raw SQL [#sqlc-not-raw-sql]

New queries are written as SQL and compiled to type-safe Go by
[sqlc](https://sqlc.dev). The generated `Querier` interface (package
`sqlcstorage`) is the low-level data-access surface; every column type, argument,
and result struct is generated from the schema, so a query that doesn't match
the schema fails at generation time rather than at runtime.

```yaml
# sqlc.yaml (excerpt)
sql:
  - engine: "postgresql"
    schema: "infrastructure/storage/postgres/sqlc/schema.sql"
    queries: "infrastructure/storage/postgres/sqlc/queries"
    gen:
      go:
        package: "sqlcstorage"
        sql_package: "pgx/v5"
        emit_interface: true
```

sqlc runs on the `pgx/v5` driver and connection pool. Type overrides map Postgres
types to the Go types the domain expects (for example, `timestamptz` → `time.Time`,
and non-nullable UUID primary keys → plain `string`).

<Callout type="info" title="When sqlc can't express it">
  A few queries need runtime-dynamic SQL — `WHERE` clauses built from
  whitelisted sort fields, PL/pgSQL function calls, or session-level `set_config`
  for row-level security. Those go through an in-repo parameterized SQL builder
  (`infrastructure/storage/postgres/builders/`, `$N` args, whitelisted fields)
  rather than string concatenation. Everything else is sqlc.
</Callout>

## The CQRS Manager [#the-cqrs-manager]

On top of the generated `Querier`, Ductor composes a **CQRS** surface that
separates reads from writes:

* **`QueryStore`** composes 30-plus domain-specific **Reader** interfaces —
  everything a read-only consumer needs.
* **`CommandStore`** composes all the **Writer** interfaces plus transactions.
* **`Manager`** is the union of `QueryStore` and `CommandStore`. There is a
  single implementation, in
  `infrastructure/storage/postgres/cqrs/store/manager.go`.

Domain-scoped repositories embed a shared `DBHandle` (the querier, the connection
pool, and an optional transaction handle) so they can all participate in the same
transaction when needed and share cache-invalidation hooks.

## Project from broad to narrow [#project-from-broad-to-narrow]

The `Manager` is deliberately *broad* — it can do everything. But application
services don't take the `Manager`; they take a **narrow local interface** that
lists only the methods that service uses. An adapter projects the broad manager
down to the narrow contract:

```mermaid
flowchart LR
  M["cqrs.Manager"] -->|adapter| R["RoutingStorageCQRS<br/>(what routing needs)"]
  M -->|adapter| C["ClaimStorageCQRS<br/>(what claims need)"]
  M -->|adapter| W["WorkflowOpsStore<br/>(what workflow ops need)"]
```

This keeps mock surfaces small in tests and makes each call site's real data
dependencies legible — you can read a service's interface and know exactly which
tables it touches. New code is encouraged to prefer these narrow per-aggregate
interfaces over the composite. See
[Layered Architecture](/docs/architecture/layered-architecture#narrow-interfaces-over-fat-ones).

## Migrations are backward-compatible [#migrations-are-backward-compatible]

Schema changes are ordered goose migrations under
`infrastructure/storage/postgres/cqrs/migrations/`, applied with the `migrate`
subcommand (`ductor migrate`). The firm rule: **migrations must be
backward-compatible within a release** — no breaking column removes that would
strand a running older binary mid-deploy. Add columns and backfill; don't drop
what the currently-deployed code still reads.

State-machine columns (run status, attempt status, timer status, idempotency
status) are mirrored by Go constants that are documented as the single source of
truth for the corresponding SQL `CHECK` constraint literals — so the allowed
values in code and in the database can't silently drift apart.

## Contract harness tests [#contract-harness-tests]

A storage convention worth calling out: new storage implementations must pass a
**contract harness** — a reusable test suite
(`<component>_contract_harness_test.go`) that any implementation of an interface
runs against. The same behavioral tests exercise the Postgres implementation and
any in-memory fake, so a fake used in unit tests is guaranteed to behave like the
real store. Integration coverage uses
[testcontainers](https://golang.testcontainers.org/) to spin up real Postgres and
Redis.

## Where to go next [#where-to-go-next]

* [Data Flow](/docs/architecture/data-flow) — what each table stores and who writes it.
* [Layered Architecture](/docs/architecture/layered-architecture) — where storage sits in the dependency graph.
* [Events & Event Sourcing](/docs/concepts/events) — the event-sourced aggregates alongside the relational tables.
