0008-conformance-tested-storage.md 4.2 KB

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.