Event sourcing is altijd een lastige verkoop geweest. De pitch klinkt geweldig — sla elke verandering op als een onveranderbaar event, leid state af door ze opnieuw af te spelen, krijg een perfecte audit trail en time travel gratis. De realiteit is minder rooskleurig: schema-versioning, eventual consistency, projecties die onderhoud vragen, en een team dat anders moet denken over elke schrijfactie.
Voor de meeste teams was het antwoord “interessant, maar ons saaie CRUD-model voldoet prima.” Dat was meestal de juiste keuze.
Ik denk dat die rekensom verschuift.
Een korte opfrisser
In een CRUD-systeem sla je de huidige state op. Als een order verstuurd wordt, update je een rij. De vorige state is weg.
In event sourcing sla je de feiten op: OrderCreated, ItemAdded, OrderShipped. De state is wat je krijgt als je die events op volgorde afspeelt. Wil je een ander beeld van dezelfde data? Bouw een nieuwe projectie — een read model gevoed door dezelfde event stream — en speel de geschiedenis er doorheen.
Je wint een compleet causaal logboek. Je betaalt met meer bewegende delen.
Hoe ziet het eruit in C#?
Een minimale schets, zodat de rest van de post iets concreets heeft om naar te verwijzen:
// Events — feiten, in de verleden tijd, immutable
public abstract record OrderEvent;
public record OrderCreated(Guid OrderId, Guid CustomerId, DateTime CreatedAt) : OrderEvent;
public record ItemAdded(Guid OrderId, string Sku, int Quantity, decimal UnitPrice) : OrderEvent;
public record OrderShipped(Guid OrderId, DateTime ShippedAt, string Carrier) : OrderEvent;
// Aggregate — state afgeleid door events te vouwen
public class Order
{
public Guid Id { get; private set; }
public List<OrderLine> Lines { get; } = new();
public bool IsShipped { get; private set; }
public static Order Replay(IEnumerable<OrderEvent> history)
{
var order = new Order();
foreach (var @event in history) order.Apply(@event);
return order;
}
private void Apply(OrderEvent @event)
{
switch (@event)
{
case OrderCreated e: Id = e.OrderId; break;
case ItemAdded e: Lines.Add(new OrderLine(e.Sku, e.Quantity, e.UnitPrice)); break;
case OrderShipped: IsShipped = true; break;
}
}
}
public record OrderLine(string Sku, int Quantity, decimal UnitPrice);
Dit is de vorm. Elk aggregate in het systeem heeft dezelfde scaffolding nodig. Elk nieuw event vraagt om een Apply-clausule. Elke projectie consumeert dezelfde stream en bouwt zijn eigen read model. Vermenigvuldig dit met een paar dozijn aggregates en je ziet waarom teams afhaken.
Wat AI echt verandert
De pijnpunten van event sourcing blijken precies het soort werk waar Claude Code goed in is.
Het domein modelleren
Beslissen wat een event is, is het lastigste deel. Claude Code is hier een serieus nuttige sparringpartner — jij beschrijft het domein, het stelt events voor, jij duwt terug. De output heeft een duidelijke vorm (verleden tijd, één feit per event) die voor beide kanten makkelijk te beredeneren is.
Ik plak meestal een paar user stories in de chat en vraag “welke events heb ik hier nodig?”. De eerste lijst is zelden de uiteindelijke, maar het gesprek komt sneller op gang dan op een whiteboard.
Boilerplate
Zodra je een patroon hebt gekozen, is het volgende aggregate vooral typewerk. Eén stevig voorbeeld zoals Order in je codebase, en je kunt Claude Code vragen “voeg een Shipment aggregate toe volgens hetzelfde patroon, met events ShipmentDispatched, ShipmentDelivered, ShipmentReturned” — en je krijgt een werkende scaffold terug, inclusief de Apply-switch en een stub repository. Minuten, geen uren.
Projecties
Een nieuw read model betekende vroeger een aparte week werk — schema ontwerpen, projector schrijven, geschiedenis afspelen, valideren. Nu beschrijf ik de projectie in gewone taal, wijs ik Claude Code naar de event types, en laat het de projector schetsen:
public class CustomerOrderSummaryProjection
{
public void Project(OrderEvent @event, CustomerOrderSummary summary)
{
switch (@event)
{
case OrderCreated e:
summary.TotalOrders++;
summary.LastOrderAt = e.CreatedAt;
break;
case ItemAdded e:
summary.LifetimeValue += e.Quantity * e.UnitPrice;
break;
case OrderShipped e:
summary.ShippedOrders++;
break;
}
}
}
Speel de replay af, kijk of de cijfers kloppen voor een steekproef van klanten die je kent. Kloppen ze niet, dan vertelt de event stream je precies waar de projectie de mist in ging.
Tests als specificatie
Event sourcing past perfect bij het given/when/then-format: gegeven deze events, wanneer dit command binnenkomt, verwacht deze nieuwe events. Dat is bijna pseudocode — en een uitstekend doelwit voor AI-gegenereerde tests.
[Fact]
public void Een_verstuurde_order_kan_niet_opnieuw_verstuurd_worden()
{
// Given
var orderId = Guid.NewGuid();
var history = new OrderEvent[]
{
new OrderCreated(orderId, CustomerId: Guid.NewGuid(), DateTime.UtcNow),
new ItemAdded(orderId, Sku: "ABC-123", Quantity: 2, UnitPrice: 19.99m),
new OrderShipped(orderId, DateTime.UtcNow, Carrier: "PostNL"),
};
var order = Order.Replay(history);
// When / Then
var act = () => order.Ship(carrier: "DHL");
act.Should().Throw<InvalidOperationException>()
.WithMessage("*al verstuurd*");
}
Ik vraag Claude Code om “de negatieve cases die ik waarschijnlijk vergeet” en krijg elke keer een lijst die het doornemen waard is.
Debuggen
Productiebug? Dump de event stream van het betrokken aggregate, geef het aan Claude Code, vraag wat er is gebeurd. De volledige causale keten ligt er — geen “ik denk dat dat veld er voor die release van dinsdag anders uitzag.”
Waar het me daadwerkelijk hielp
Een concreet voorbeeld. Ik werk aan een klein factuur-zijproject — events voor InvoiceIssued, PaymentReceived, InvoiceCancelled, RefundIssued. Een nieuwe projectie toevoegen (achterstallige facturen per klant, met aging buckets) was vroeger een half dagje werk. Vorige week beschreef ik het in drie zinnen aan Claude Code, het schetste de projector en een replay-test, ik paste twee veldnamen aan, en het draaide groen tegen enkele duizenden events aan geschiedenis.
Dat is geen magie. Het is gewoon dat de delen die ik altijd vervelend vond — de mechanische vertaling van event types naar projectiecode — precies de delen zijn waar AI het beste in is.
Wanneer het nog steeds niet past
Niets van dit alles redt event sourcing voor echt simpele domeinen. Als je app een formulier over een tabel is, blijf bij CRUD. Events plus AI toevoegen om modern aan te voelen is een prima manier om complexiteit op te leveren waar niemand om vroeg.
Er is ook een categorie pijn die AI niet wegneemt. Schema-evolutie vraagt nog steeds om nadenken — als een event van vorm verandert, moet jij beslissen of je oude events upcast, ze versioneert, of de stream herschrijft. Claude Code kan de upcaster schrijven, maar het kan die ontwerpkeuze niet voor je maken. Hetzelfde geldt voor eventual consistency: een echte constraint, geen boilerplate-probleem.
Maar als je ooit naar event sourcing hebt gekeken en bent weggelopen omdat de kosten-batenanalyse niet helemaal klopte — het is een tweede blik waard. De kostenkant is verschoven.
Probeer het zelf
Pak het kleinste aggregate in je huidige codebase. Eentje met drie of vier state-overgangen, niets bijzonders. Open Claude Code in dat project en probeer dit:
claude
Vervolgens:
“Schets hoe dit eruit zou zien als een event-sourced aggregate. Events als C# records, een
Apply-methode, en een given/when/then-test voor de belangrijkste happy path.”
Lees wat eruit komt. Niet om te shippen — gewoon om de vorm te zien. Als het resultaat je doet denken “dat is minder werk dan ik verwachtte,” heb je het antwoord op de vraag die deze post eigenlijk stelde.