Skip to content

ADR 0005 — Storage-agnostic via the LedgerBackend protocol

Status: Accepted

Context

The same inventory needs to run as a throwaway in-memory object in a test, a single SQLite file on a laptop, git-friendly JSON in a repo, a Snowflake schema in production, and a thin client against a remote HTTP service. Coupling the SDK to any one of these would force a rewrite to change storage and make testing slow.

Decision

All persistence sits behind a single @runtime_checkable LedgerBackend protocol. The Ledger SDK is written against the protocol only; the backend is a constructor argument (Ledger.from_sqlite(...), Ledger.from_snowflake(...), Ledger(JsonFileLedgerBackend(...)), Ledger(HttpLedgerBackend(...))). Third parties can add backends (e.g. Postgres) by implementing the protocol and registering an entry point — no core change.

Consequences

Positive

  • Choosing storage is a one-line decision that never leaks into application code.
  • Tests run in-memory and fast; the same code path is exercised against every backend.
  • Backends are an open extension point, not a closed enum.

Negative (accepted)

  • The protocol is a contract: adding a method means implementing it across every backend (and any third-party one), so the surface must evolve deliberately. The HTTP backend in particular can't always reconstruct server-side state locally and falls back to caches.
  • The lowest-common-denominator protocol can't expose every backend's native superpowers.

Alternatives considered

  • Hard-code one backend (rejected): forces a rewrite to change storage and makes tests depend on infrastructure.
  • An ORM abstraction (rejected): heavier, leakier, and a poor fit for the append-only event-log and the non-SQL backends (JSON files, HTTP).

See Choosing a backend.