Writing a store

The store SPI, the store contract, and why behaviour changes belong in the contract first.

The store SPI, IdempotencyStore in idempotency-core, is five methods, and three of them carry the protocol: tryAcquire, complete and release. The other two are mechanical - extendLock is the engine’s heartbeat and purgeExpired is the garbage collection.

Everything difficult about idempotency that is not the engine’s job lives behind the first of the three: all blocking, waiting, and stale-lease stealing happens inside tryAcquire.

That concentration is deliberate. A store waits however its backend allows - the three shipped stores each poll, at an interval set for that backend - instead of the engine polling on one schedule it would have to tune for every backend.

The contract is the specification

IdempotencyStoreContract in idempotency-test is the single source of truth for store behaviour.

  1. Implement the SPI against your backend.

  2. Extend the contract and implement store().

    class MyStoreTest extends IdempotencyStoreContract {
        @Override
        protected IdempotencyStore store() {
            return new MyIdempotencyStore(...);
        }
    }
    Java
  3. Pass all of it. Not most of it - a store whose lease stealing is almost right is worse than one that does not support the feature, because the difference only appears under concurrency in production.

  4. Override completeInTransaction and supportsTransactionalCompletion(), and extend TransactionalStoreContract too, if and only if your store can complete inside a caller’s transaction. Both default to declining, which is a supported answer: complete always means a write of its own, and the engine calls completeInTransaction only for joined completion.

Behaviour changes belong in the contract first

This is the rule that keeps three backends from becoming three subtly different libraries.

If your store cannot honour a behaviour - joined completion is the usual one - say so rather than approximating it. The engine fails the context at startup when something asks for what the store does not provide, which is a better outcome than an approximation that holds until it does not.

Before you write one

The three shipped stores cover a relational database, Redis, and a single JVM. A new backend is worth writing when you run a durable store that is none of those and would rather not add one. It is not worth writing to get a slightly different schema out of the JDBC store - constructing that store by hand against a second DataSource is the cheaper answer.