Skip to content

How the port works ​

The engine does arithmetic. Deciding which wallets are candidates, walking them in order, converting at rates, rounding, running the overdraft pass, computing window balances — none of that needs to know what a database is.

So it doesn't. The engine receives a store and talks to it through sixteen methods. Everything else is pure Python.

      your application
             │
             ▼
   allowances.engine          peek · fulfill · attach · unfulfill · plan_switch
             │
     ┌───────┴────────┐
     ▼                ▼
 Store port    DefinitionProvider
     │                │
     ▼                ▼
 DjangoStore     SettingsProvider
 MemoryStore     DictProvider
 (yours)         (yours)

Two seams, not one. The store is where state lives; the provider is where definitions come from. They vary independently — a Django store with definitions from a remote service is a perfectly sensible combination.

What a store owes the engine ​

The full interface is in the API reference. Three obligations are worth stating separately, because they are the ones that are easy to get subtly wrong:

Ordering. Wallets come back oldest-first, by (created_at, id). The engine treats that order as the tie-breaker within a fulfillment step, so an unstable order means non-deterministic draws — the same request charging different wallets on different runs.

Atomicity. unit_of_work() makes everything inside it commit together or not at all, and it must nest: the engine opens one, and a caller may already have opened another around it.

Locking. lock=True must stop a concurrent fulfillment from seeing the same rows until the block ends. Without it, two simultaneous calls can each read the same balance and each spend it. A backend that can't lock should say so in its documentation rather than pretend.

Why bother ​

Testing. The engine's behaviour is arithmetic, and proving arithmetic doesn't need Postgres. This library's own engine tests run against a dict in well under a second, and again against Django to prove the two agree.

It keeps the design honest. Every time the engine reaches for something only an ORM could do, the in-memory backend stops working. That is a useful alarm.

It leaves the door open. A Redis backend for hot-path quota, a store fronting a remote entitlement service, a read-only store over an existing billing schema — none of those require touching the engine. Whether Redis is actually a good idea is a separate question, and the honest answer is "partly".

Backends that ship ​

DjangoRelational, transactional, locks properly. The production one.
In-memoryA dict. For tests and for proving the port holds.

And writing your own if neither fits.

Partial backends ​

Not every store can do everything. An append-only or cache-shaped backend may have no archive to move wallets into, and no way to answer a scan like records_with_unattached_transactions.

Raise UnsupportedOperationError rather than silently doing nothing. A no-op archive looks exactly like a working one until the table is enormous.

MIT licensed.