HabitEngine is a backend that turns completed habits into a spendable currency — "momentum" — shared across every game that plugs into it. Users earn it by keeping up with habits they set for themselves; they spend it inside connected games like Idle Miner and Tower Defense. The product pitch is simple. The engineering underneath it turned out not to be, for one specific reason: the moment momentum is shared across multiple apps, a wallet balance stops being a UI nicety and starts being a correctness problem with real stakes. If two games can plausibly spend from the same balance at the same time, you now have a distributed systems problem that looks a lot like a ledger, not a habit tracker.
We wanted to talk about how we've approached that, because it's shaped most of the interesting decisions in the system.
Before any wallet or momentum code existed, we wrote down the concurrency model as an ADR — not as documentation after the fact, but as a forward commitment for the phase of work that would implement it. The core rule: every mutation, earn or spend, follows a read-validate-write cycle guarded by the wallet document's ETag. If the write loses a race — someone else wrote first — the whole cycle retries from the read, never just reapplies the same delta blindly, since the balance may have moved underneath it. For spends specifically, the negative-balance check happens after the re-read, under the same ETag guard as the write, not before it. Checking before the read is exactly what allows the actual race: two simultaneous spends can both read the same starting balance, both pass validation independently, and only the guarded write order stops the second one from silently succeeding too.
Writing that down before the implementation existed forced a decision that's easy to get wrong under deadline pressure: retries are bounded by a time budget, not an attempt count, because contention is bursty, not attempt-shaped. A fixed count either retries too fast under real contention or wastes time when there's none. It's a small design choice, but it's the kind that's much easier to get right on paper than to retrofit once a "just add a retry loop" fix is already live.
Even a correctly-modeled wallet operation can be undone by an assumption one layer down. We run on Azure Functions, backed by Cosmos DB configured for session consistency — which guarantees a client reads its own writes, but only within the same client session. Cosmos clients are registered as singletons per Function App instance, and Azure Functions scales out aggressively under a burst of concurrent traffic — which is exactly what a load test throwing fifty simultaneous requests at one wallet does. Spin up enough fresh instances fast enough, and a wallet write on one instance has no guarantee of being visible yet to a read on another, because there's no shared session token between them. The result looked exactly like a bug in the wallet logic — "no wallet found" on a user that had just been created — but it wasn't; it was a correctness assumption that only broke under genuine concurrent load, which is precisely why we run load tests before anything reaches users instead of trusting that manual testing would ever stumble into it. The fix ended up being one line — moving the whole account to strong consistency, which a single-region deployment can afford without the latency cost that makes strong consistency expensive in a multi-region setup — but finding it required actually generating the kind of concurrent traffic pattern real usage would.
The wallet ADR is one of several. We keep architecture decisions as living documents — not just what we decided, but the context that led there and the consequences we're accepting, including the ones we haven't resolved yet. A promotion-strategy ADR that says "development deploys immediately, staging deploys automatically once tests pass, production requires a human approval and a passing load test" is more useful to the next person touching the pipeline than the pipeline configuration alone, because it says why the gates exist, not just that they do. When we hit a GitHub Actions quirk where a required-reviewer gate silently wasn't enforcing — the approval step just wasn't configured on the environment, a setting with no YAML equivalent at all — that went back into the same document once resolved, not into a separate incident log no one reads again.
None of this is exotic. It's mostly discipline: write the correctness model down before building it, load-test before trusting it, and keep the reasoning next to the decision instead of letting it live only in whoever happened to be around when it was made. For a system where the thing being moved around is something users earned and expect to still be there, that discipline is the actual product, even if no player ever sees it directly.