← Back to work

Backend engineer (solo build) · 2026

WagerLedger

An event-sourced player wallet and betting ledger where every balance change is an append-only event, projected to a SQL read model and fanned out over messaging.

Models a financial ledger as an append-only event stream on KurrentDB, enforces responsible-gaming rules in the domain, and serves balances and statements from a SQL Server projection behind an ASP.NET Core API.

Source ↗
Cover image for WagerLedger

The problem

A player wallet is a financial ledger, and a ledger should never lose history. Storing only the current balance throws away the one thing an auditor, a dispute, or a bug investigation actually needs: how the balance got there. I wanted to build the wallet the way the domain really works, where the events are the source of truth and the balance is just a view derived from them. WagerLedger is that build: an event-sourced wallet and betting ledger I wrote solo, end to end.

Approach

Every command (deposit, withdraw, place bet, settle bet) loads the wallet by replaying its event stream, checks the domain rules, and appends new events. Nothing mutates a balance in place. The write side is decoupled from the read side: a projector turns the same events into a SQL read model the API queries, and the events are also published over messaging so independent consumers can react without coupling to the core. A responsible-gaming deposit limit lives in the domain itself, rebuilt from events so it holds even for a reconstituted wallet.

Commands fork from the API: load and replay against KurrentDB on the write side, publish over RabbitMQ, and query the SQL Server read model the projector builds.
Commands fork from the API: load and replay against KurrentDB on the write side, publish over RabbitMQ, and query the SQL Server read model the projector builds.

Architecture

How I built it

I worked test-first in small, self-contained commits, so the git history reads like a delivery log. The suite pairs fast unit tests for the domain and application with integration tests that run against real KurrentDB, RabbitMQ, and SQL Server containers and skip cleanly when the infrastructure is not up. One shared store contract runs against both an in-memory adapter and the KurrentDB adapter, so both are held to identical behaviour. I verified the full flow by running the built container against the live stack and watching a deposit travel from the API through the event store and messaging into the read model.

What I'd do differently

The read model projects from the message bus, which is at-least-once; a production version would dedupe by event id, or project directly from the event store with stored checkpoints so it can rebuild deterministically. The internals also assume a trusted caller, so real authentication and per-player authorization are the natural next step.