Systemen bouwen met event sourcing is prachtig
De afgelopen maanden heb ik event sourcing opnieuw ontdekt, en hoe fijn het is om systemen te bouwen die gebruikmaken van deze aanpak. Ik heb hier eerder over geschreven, in 2019, maar sindsdien heb ik andere technieken en benaderingen geleerd. De eenvoud van de code die je schrijft met event sourcing is iets waar ik echt van geniet. Het stelt me in staat om systemen te bouwen die flexibel en goed onderhoudbaar zijn.
Een kleine waarschuwing: ik wilde eigenlijk een kort artikel schrijven, maar ik werd te enthousiast en het is wat langer geworden dan bedoeld. Toch is het doel hetzelfde gebleven: ik wil de voordelen van event sourcing benadrukken, en laten zien hoe jij dit ook in je eigen projecten kunt inzetten. Voel je vrij om stukken over te slaan, maar ik raad aan het hele artikel te lezen voor het volledige plaatje.
Het “probleem” met CRUD
Ik heb expres deze subtitel gekozen, omdat ik een probleem wil aanwijzen, niet omdat CRUD slecht is.
Na een paar jaar werken aan een CRUD-app loop je onvermijdelijk tegen beslissingen aan die je in het verleden hebt genomen. Dat is het moment waarop je ziet hoe rommelig alle bedrijfsregels en toenemende complexiteit kunnen worden.
Een veld updaten in een database is makkelijk, totdat je merkt dat de data niet meer logisch is. Je moet dan achterhalen wat er veranderd is, waarom dat gebeurd is en wanneer. Traditionele CRUD biedt geen goede manier om deze veranderingen te volgen, dus debugging kost veel tijd en energie.
Natuurlijk zijn er manieren om dit op te lossen, daar kom ik later op terug.
CRUD is te technisch
Idealiter is code niets meer dan een representatie van hoe het bedrijf werkt. Maar CRUD is te technisch voor niet-technische mensen.
Stel je voor: je hebt een e-commerce bestelling die je wilt annuleren. Iemand zegt: “Verwijder deze bestelling gewoon.” Een junior developer hoort dit en verwijdert letterlijk de bestelling. Opdracht geslaagd, toch?
Maar na wat meer ervaring besef je dat dit niet is wat het bedrijf bedoelde. Ze wilden de bestelling niet meer zien in hun lijst met te verwerken bestellingen. De bestelling hoefde niet meer afgehandeld te worden, en de klant wilde het product of de dienst niet meer.
Als je de bestelling echt verwijdert, verlies je alle context. Je weet niet meer wie de bestelling plaatste, wanneer, of waarom het geannuleerd werd. Over een jaar weet niemand meer waarom het er niet meer is.
De business bedoelt het goed, maar spreekt jouw taal niet. En dat hoeven ze ook niet. Jij als developer moet hun taal spreken. Geen technische termen, geen jargon, gewoon normale woorden.
Daarom gebruiken teams steeds vaker technieken zoals DDD (Domain-Driven Design). DDD draait om het begrijpen van de business en het bouwen van software die daarop aansluit.
Iedereen begrijpt “Bestelling geannuleerd” in de context van het bedrijf. Je hoeft niet uit te leggen wat “Een bestelling verwijderen” betekent — zoiets gebeurt niet in de echte wereld. Je kunt de tijd niet terugdraaien, maar je kunt wel aangeven dat er geen actie meer nodig is.
Ik hoor je denken: klinkt goed, maar hoe werkt dat dan in de praktijk? Waarom zou dit niet gewoon met CRUD kunnen?
Hoe verschilt event sourcing van CRUD?
Als je bekend bent met event sourcing, ken je dit voorbeeld misschien al: In een CRUD-applicatie Create, Read, Update en Delete je data in een database. Dat is prima voor veel toepassingen. Totdat iemand je vraagt: “Wie heeft deze bestelling geannuleerd?”
In een CRUD-app kun je dat niet beantwoorden, tenzij je daar vooraf aan gedacht hebt.
Dus voeg je een audit trail toe. Super! Maar dat lost het alleen op voor toekomstige bestellingen, niet voor bestaande. Je kunt alleen zeggen: “Vanaf nu houden we bij wie een bestelling annuleert.”
Event sourcing lost dit fundamenteel op. Je maakt geen directe wijzigingen meer in de database, maar je houdt bij wat er is gebeurd: gebeurtenissen.
OrderPlacedOrderCancelledOrderRefunded
Dit zijn feitelijke gebeurtenissen. Ze bevatten metadata: wie het deed, waar, wanneer, etc.
Read models
Maar wat doe je met die lijst van gebeurtenissen? Je collega’s of klanten willen geen lijst met events. Ze willen weten: “Wie annuleerde deze bestelling?”
Je hebt dus een representatie nodig van de huidige status van je entiteit, zoals een bestelling. Je bereikt dat door gebeurtenissen toe te passen op een leeg startobject. Bijvoorbeeld:
{
"order_id": "12345",
"is_cancelled": false,
"cancelled_by": null
}
Na het toepassen van een OrderCancelled event met:
{
"order_id": "12345",
"employee_id": "67890"
}
krijg je:
{
"order_id": "12345",
"is_cancelled": true,
"cancelled_by": "67890"
}
Je past events toe op een Aggregate, daar gaan we nu naar kijken.
Businessregels
Klinkt ingewikkeld? Misschien. Maar als je ooit te maken hebt gehad met veranderende bedrijfsregels, dan weet je hoe snel dingen complex worden.
In event sourcing gebruik je commands, zoals CancelOrderCommand. Deze valideert de regels. Als alles klopt, genereer je het OrderCancelled event.
Alle regels zitten netjes bij elkaar in de Aggregate. De events zelf zijn slechts vastgelegde feiten.
Hier een vereenvoudigd voorbeeld in Go:
type OrderAggregate struct {
Events []Event
ID string
isCancelled bool
}
func (o *OrderAggregate) HandleCommand(ctx context.Context, command Command) error {
switch cmd := command.(type) {
case *CancelOrderCommand:
if o.isCancelled {
return fmt.Errorf("order %s is already cancelled", o.ID)
}
o.Apply(OrderCancelled{
ID: cmd.ID,
CancelledBy: cmd.AdminID,
})
}
return nil
}
func (o *OrderAggregate) Apply(event Event) {
o.Events = append(o.Events, event)
switch e := event.(type) {
case *OrderCancelled:
o.isCancelled = true
}
}
Na het toepassen kun je het event opslaan in je event store. Die events zijn nu feiten, en blijven altijd waar.
Read models, projectors en live modellen
Je hebt projected read models (zoals database-rijen) en live read models (in-memory gegenereerd door alle events toe te passen).
Met projectors kun je snel nieuwe features toevoegen op basis van bestaande events. Bijvoorbeeld rapporten:
- Hoeveel bestellingen werden vorige maand geannuleerd?
- Hoeveel bestellingen zijn geplaatst in december?
- Hoeveel bestellingen zijn geannuleerd én later hersteld?
Je kunt dit met CRUD doen, maar het is lastiger, foutgevoeliger en moeilijker aanpasbaar.
Data bekijken op een specifiek moment in de tijd
Live read models geven je toegang tot de status van de data op een bepaald moment. Bijvoorbeeld: wat was de prijs van een product toen de klant vorige week belde?
In event sourcing zie je de vraag “Wat vond je van deze site?” beantwoord met “Ja”. Als later de vraag wordt aangepast naar “Haat je deze site?”, weet je nog steeds dat de gebruiker “Ja” antwoordde op de oorspronkelijke vraag. Dat kan niet met CRUD.
Wat met performance?
Zal dit niet traag zijn? Niet als je het slim aanpakt. Je streams moeten klein blijven. In Omoik leerde ik dit de harde manier.
Ik begon met alle events in één stream: traag. Nu heeft elke enquête en elke inzending z’n eigen stream (max ~20 events). Supersnel!
Doordat het event ID globaal wordt bijgehouden, weet ik altijd wat de staat van de enquête was op het moment van inzenden.
Codewijzigingen breken het verleden niet
Je kunt je hele applicatie herschrijven, maar de events blijven gelijk. Ze zijn immutable. Dat maakt refactors veilig.
Je past een projector aan, replayt de events, en voilà: alles is weer correct.
Conclusie
Ik kan nog uren doorgaan, maar de boodschap is simpel: event sourcing is krachtig. Het maakt je systemen flexibeler, beter onderhoudbaar en toekomstbestendig.
Gebruik je het nog niet? Geef het een kans. Het is even wennen, maar daarna wil je niet meer terug.
Gepubliceerd op: June 20th, 2025Ik maak je bedrijf efficiënter met software die écht bij je past.
Klaar om je bedrijf efficiënter te maken? Laten we kennismaken.