Conformance-tested storage with an in-memory reference
- Status: accepted
- Authors: Cesar Rodas
- Date: 2026-06-29
- Targeted modules:
kuatia-storage (store_tests!, InMemoryStore), kuatia-storage-sql
- Associated tickets/PRs: N/A
Context and Problem Statement
Store is a trait with several sub-traits and multiple backends (an
in-memory store and a SQL store over SQLite/PostgreSQL), and, since
ADR-0003, the saga's correctness depends on every backend behaving
identically, down to the affected-row counts each primitive returns.
How do we guarantee that two independent Store implementations have
exactly the same observable semantics, and keep them in lock-step as the
trait evolves?
Decision Drivers
- Semantic equivalence: InMemory and SQL must return the same counts
and state transitions, or the saga's count interpretation (ADR-0003)
breaks on one backend.
- One source of truth for behavior: the contract should be
executable, not just prose.
- Cheap to extend: adding a backend, or a sub-trait method, should
make the obligations obvious.
- Fast feedback: most semantics should be testable without a
database.
Considered Options
Option 1: Bespoke tests per backend
Each Store impl has its own hand-written test suite.
Pros:
- Good, because each suite can exploit backend specifics.
Cons:
- Bad, because the two suites drift: a behavior tested for one backend
may be untested (and divergent) for the other.
- Bad, because there is no single, enforceable definition of "correct
Store behavior."
Option 2: Trait documentation only (plus mocks)
Specify semantics in doc comments; let callers mock the store.
Pros:
- Good, because it is low-effort up front.
Cons:
- Bad, because prose is not executable, so nothing prevents a backend
from violating it.
- Bad, because mocks encode an assumed contract, which can itself be
wrong.
Option 3: A shared conformance suite + an in-memory reference
A store_tests! macro generates one suite of async tests; every
backend (InMemoryStore, SqlStore) runs the same suite via its own
factory. InMemoryStore doubles as the executable reference for the
intended semantics. The convention is that every Store sub-trait
method has a conformance test, so new methods force new tests.
Pros:
- Good, because both backends are held to the identical, executable
contract, including the affected-row counts ADR-0003 relies on.
- Good, because
InMemoryStore is a fast, dependency-free reference for
the semantics and a ready test double for higher layers.
- Good, because adding a backend is "run the macro with your factory,"
and adding a sub-trait method is incomplete until its conformance test
exists.
Cons:
- Bad, because the macro suite must stay backend-agnostic (no
backend-specific assertions), so a few backend-specific behaviors
still need separate tests.
- Bad, because the in-memory reference must be maintained to match the
SQL backend exactly, a second implementation to keep honest (which is
also the point).
Decision Outcome
Chosen option: Option 3, a shared store_tests! conformance suite
with InMemoryStore as the executable reference, because it is the
only option that enforces semantic equivalence across backends (a hard
requirement once the saga interprets counts, ADR-0003), gives a fast
dependency-free reference/double, and makes the obligations for new
backends and new methods explicit.
Positive Consequences
- Both
InMemoryStore and the SQL backend pass the same suite; a
divergence in counts or transitions fails the build.
- New
Store sub-trait methods come with conformance tests by
convention.
- Higher layers (ledger, saga) test against the fast in-memory store.
Negative Consequences
- The conformance suite must remain backend-neutral; genuinely
backend-specific behavior needs its own tests.
- The in-memory reference is a second implementation that must track the
SQL one.
Links
- Underpins ADR-0003 (equivalent
count semantics across backends).
- Background:
crates/kuatia-storage/src/store_tests.rs and the backend
test harnesses.