All entries
April 18, 20268 min read

Event Sourcing an XP Ledger in .NET

Why an immutable event log beat a mutable points column for Sigma's gamification engine — and what it cost to get there.

.NETEvent SourcingDDDPostgreSQL

When I designed Sigma's gamification engine, the obvious schema was a users.xp integer column. Increment on every finished page, decrement never. Simple, fast, wrong.

The problem with a mutable counter

A single integer answers exactly one question: how much XP does this user have right now? It cannot answer any of the questions that showed up two sprints later:

  • How much XP did this user earn this week?
  • Which activity actually drives engagement — reading streaks or quizzes?
  • A user reports their XP "reset". What happened?

Every one of those needs history, and a counter has none.

The ledger model

Instead, every XP-affecting action becomes an immutable event appended to PostgreSQL:

public sealed record XpEarned(
    Guid UserId,
    int Amount,
    XpSource Source,
    DateTimeOffset OccurredAt) : IDomainEvent;

The current balance is a projection — a read model rebuilt from the stream. Balances become derived data, and derived data can always be regenerated when the rules change.

What it cost

Honesty section. Event sourcing is not free:

  1. Projections need maintenance. Every new read shape is a new projector plus a rebuild strategy.
  2. Idempotency is on you. At-least-once delivery means every projector must tolerate replays.
  3. Onboarding is slower. Every new contributor asks where the UPDATE statement is.

For a points ledger — financial-shaped data with audit requirements — the trade was worth it. For the user profile table next door, it absolutely was not. Event sourcing is a scalpel, not a default.

Building something similar?

I take on a small number of freelance projects each quarter.

Get in touch