About meMy ProjectsDico the agentMy MusicReading list
Real-Time Crash Game – Case study

Real-Time Crash Game

A real-time multiplayer crash game where every cent a bet moves is posted to a double-entry ledger and every crash point can be verified by the player.

  • NestJS
  • RabbitMQ
  • PostgreSQL
  • Redis
  • WebSockets

Challenge

Pay every player exactly once and be able to prove it. Rounds and balances live in separate services and databases, RabbitMQ can redeliver any message, and players have to trust that the crash point was not chosen after the bets were placed.

Approach

Games and Wallets each own a PostgreSQL database and talk only through events: a bet starts PENDING and joins the round after Wallets debits it. Every balance change is a balanced double-entry transaction against the house account, keyed so a redelivered event never pays twice, and hashed into a per-wallet SHA-256 chain the player can check on the wallet statement. Each round publishes SHA-256(seed) before bets open and reveals the seed after the crash. Optimistic locking on rounds, bets and wallets replaces distributed locks, and a stake debited after its bet was cancelled is refunded by a compensating event.

Outcome

A reconciliation job checks every 5 minutes that the ledger sums to zero, that each transaction balances and that every wallet chain is intact, and Grafana shows the result next to the house balance. Eleven ADRs record each consistency decision with its trade-offs.

Architecture

  1. WebSockets
  2. choreographed Saga

Design properties

Tap a part of the diagram to see its source.

Decisions and trade-offs

  1. Optimistic locking on rounds, bets and wallets instead of locks

    The same bet can be changed at once by confirmation, timeout, manual and automatic cash-out and the crash. Pessimistic or distributed locks would add latency and contention points.

    CostEvery write path has to decide what a conflict means: retry, accept the current state or fail.

  2. Only the player's balance is stored; the house balance is a sum of entries

    A stored house balance would be one row that every bet in every round fights over.

    CostTwo or three extra inserts for every money movement.

  3. Ledger idempotency keys behind the message inbox

    The inbox deletes processed rows after 30 days, so on its own it could not stop a late redelivery from paying twice.

  4. A SHA-256 hash chain per wallet

    Append-only triggers stop the application from editing the ledger, but not someone with direct database access. The chain makes any edit to a recorded transaction detectable.

    CostSomeone with full database access can still rewrite a whole chain and its head; only a digest kept outside the database reveals that.

  5. Provably fair crash points from a pre-generated seed chain

    Players need a guarantee that the server does not pick the crash point after seeing the bets, and can verify any round without trusting it.

    CostThe operator knows future results; removing that needs a client seed, which is not implemented.

  6. Auto cash-out targets in a Redis sorted set, paid by BullMQ

    Every 100 ms tick finds the targets it reached without scanning the round's bets, and queue delay never changes the amount paid, because the payout uses the target multiplier.

    CostRedis becomes a dependency of Games: if it goes down, auto cash-out stops while manual cash-out keeps working.

Key evidence

Tamper-evident double-entry ledger and provably fair crash points

DoneInternet