# Store Domain Transaction API

Date: 2026-05-09
Status: current

## Purpose

Store domain packages that have migrated to this pattern expose a non-transactional `Store` API and a transaction-bound `TxStore` API. The pattern lets callers compose multiple domain writes in one transaction without adding SQL or transaction lifecycle control to upper layers.

## SQL Ownership

Production SQL belongs in `internal/store/{domain}`, `internal/tidb`, migrations, tests, and guardrails. API handlers, services, coordinators, and other upper layers call domain methods instead of writing SQL directly.

`internal/store/access` remains a restricted authorization and read-model exception. It must not become a generic SQL helper package.

## API Pattern

A migrated package defines:

```go
type Store struct {
    db coredb.DB
}

type TxStore struct {
    tx coredb.Tx
}

func (store *Store) InTx(tx coredb.Tx) TxStore {
    return TxStore{tx: tx}
}
```

`Store.InTx(tx)` returns an exported concrete `TxStore` by value. `Store` owns `coredb.DB`; `TxStore` owns `coredb.Tx`. `TxStore` must not hold `coredb.DB` or `*Store`.

`Store.InTx(tx)` requires a non-nil `Store` and a non-nil `coredb.Tx`, but it does not validate either value and does not return an error. Store construction and dependency tests own that validation.

## Method Shape

For each exported domain operation, migrated packages provide same-named methods on `Store` and `TxStore`:

```go
func (store *Store) Foo(ctx context.Context, input Input) (Output, error) {
    var out Output
    err := coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
        var err error
        out, err = store.InTx(tx).Foo(ctx, input)
        return err
    })
    return out, err
}

func (txs TxStore) Foo(ctx context.Context, input Input) (Output, error) {
    // validation, normalization, SQL, callbacks, and domain behavior
}
```

No-return methods use the simpler wrapper:

```go
func (store *Store) Foo(ctx context.Context, input Input) error {
    return coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
        return store.InTx(tx).Foo(ctx, input)
    })
}
```

`Store` exported methods only start a transaction and delegate to the same-named `TxStore` method. Real behavior, validation, normalization, SQL, and callbacks live in `TxStore` methods or private helpers called by `TxStore`.

`TxStore` methods may call other `TxStore` methods. `Store` exported methods must not call other `Store` exported methods, because that can create nested independent transactions. `TxStore` methods must not call `Store` methods and must not begin, commit, roll back, or call `coredb.WithTx`.

`Validate()` stays on `Store` when a package has dependency validation; it is not paired on `TxStore`.

## Cross-Domain Callbacks

Any cross-domain DB-writing callback used by a migrated domain must receive `coredb.Tx`, and function field names should include `InTx`. Callback dependencies should be narrow functions rather than broad injected domain stores.

If an operation needs multiple external DB-writing callbacks or nontrivial workflow policy, move orchestration to an approved `internal/service/*` package and keep domain stores focused on persistence and domain-local invariants.

## Upper-Layer Boundaries

Coordinators and services may own transaction lifecycle and call `store.InTx(tx).Method(...)`. They must not write SQL directly. API handlers should not manage cross-domain transactions directly; they should depend on non-transactional ports or service ports.

Services should depend on narrow domain ports and a narrow transaction runner or `coredb.TxBeginner`, not on `internal/store.Graph`.

## Migration Bridges

Non-migrated packages may temporarily expose bridge functions or methods named `...InTx(ctx, tx coredb.Tx, ...)`. These bridges must not begin, commit, or roll back transactions. They are removed when the package adopts `Store` / `TxStore`.

Migrated packages must not add exported `FooInTx` methods; transaction context is expressed by `Store.InTx(tx).Foo(...)`.

## Performance Escape Hatch

The default migrated shape is `Store -> coredb.WithTx -> TxStore`. If profiling proves explicit transactions are a bottleneck for a single-SQL method with no DB callback and no multi-step consistency requirement, a later PR may introduce a private helper shared by `Store` and `TxStore`.

That helper must stay unexported and must not become a public `Queryer` or `Execer` API.

## Enforcement

`guardrails/store/check-store-txstore-pattern.sh` enforces the structural
`Store` / `TxStore` pattern for the packages listed in
`guardrails/store/sql-ownership-inventory.json`.

The broader SQL ownership inventory and ratchet live in
`specs/storage/sql-ownership-inventory.md` and
`guardrails/store/check-store-sql-ownership.sh`. Migration bridges are
temporary and are manually reviewed until their owning package migrates.
