De bitemporele payloadstore
Dit is een uitwerking van een mogelijke variant van implementatie van bitemporaliteit in projecties.
Eerder hebben we elementen van bitemporele projecties uitgelegd; de temporaliteit in projecties, en de standaardoperaties die gebruikt kunnen worden bij het bijwerken van projecties. In dit hoofdstuk beschrijven we een implementatie van deze patronen: de bitemporele opslag van payloads. Deze implementatie kun je terugvinden in Gitlab. Let op: het package wordt hernoemd naar payloadstorage; de link verwijst voorlopig nog naar de oude locatie snapshot.
Waarom bitemporele opslag van payloads
De handreiking beschrijft de herhaalbare vraag als kerneis van een betrouwbaar register: een afnemer moet op een later tijdstip nog steeds kunnen opvragen wat een gegeven op een eerder tijdstip was. Die eis, inclusief de drie criteria en de inherente beperkingen, vereist een opslagmechanisme dat twee temporele assen expliciet bijhoudt.
De bitemporele payloadstore is zo'n mechanisme. Elk payload record combineert een geldigheidstijdstip (domeintijd) met een gevolgtijdstip en een projectietijdstip (beide systeemtijd). Daarmee kan de store de drie criteria uit de handreiking stuk voor stuk adresseren: nieuwe gevolgen, gecorrigeerde geldigheid en herstelde softwarefouten resulteren elk in nieuwe payload records die naast de bestaande worden geplaatst, zonder de bestaande te muteren.
Wat is een payload record
Een payload record legt de toestand van een (in een projectie gekozen) verzameling gegevens vast op een specifiek projectietijdstip, als resultaat van een specifiek gevolg.
De kernvelden van een payload record:
| Veld | Betekenis |
|---|---|
entityId | Welke entiteit dit payload record beschrijft |
gevolgId | Welk gevolg dit payload record heeft veroorzaakt |
payload | De inhoud van het payload record (geserialiseerd), van type payloadType |
metadata | Aanvullende informatie over de herkomst (zie Payload metadata) |
predecessor | Verwijzing naar het voorgaande payload record (voor herleidbaarheid) |
Tijdstippen en bitemporaliteit
Het systeem is bitemporeel: er zijn twee temporele assen, geen drie, ondanks de drie tijdstippen die verderop volgen. De eerste as is de domeintijd. Voor toestandsprojecties — het perspectief dat de payloadstore hanteert — manifesteert die zich als geldigheidstijd: wanneer een toestand geldig is (geworden) in de werkelijkheid. De tweede as is de systeemtijd: wanneer het systeem iets heeft vastgelegd — een klokmoment zonder domeininhoud.
Systeemtijd manifesteert zich op twee plekken in de CQRS-architectuur, analoog aan een commit-timestamp versus een replication-timestamp in een gedistribueerde database. Aan de journaalkant is dat het effectTimestamp (gevolgtijdstip) — het moment waarop het gevolg in het gevolgenjournaal is vastgelegd. Aan de projectiekant is dat het projectionTimestamp (projectietijdstip) — het moment waarop het payload record zichtbaar is geworden in de payloadstore. Tussen deze twee momenten zit verwerkingstijd: bij synchrone verwerking verwaarloosbaar, bij asynchrone of gedistribueerde architectuur kan dat significant zijn.
Elk payload record draagt daarmee drie tijdstippen (validFromTimestamp, effectTimestamp, projectionTimestamp). Dat zijn drie velden over twee temporele assen, want het derde tijdstip is een tweede manifestatie van systeemtijd, geen onafhankelijke dimensie.
Door deze drie tijdstippen te combineren in een bevraging is tijdreizen mogelijk: hoe zag dit gegeven eruit op geldigheidstijdstip X, zoals vastgelegd bij gevolg Y, zichtbaar in de projectie op tijdstip Z?
De payloadstore
Payload records worden opgeslagen in een append-only payloadstore: eenmaal opgeslagen worden payload records nooit aangepast of verwijderd, alleen toegevoegd. Dit sluit aan bij het principe van Event Sourcing, waarbij de volledige geschiedenis van een registratie behouden blijft en volledig herleidbaar is.
De PayloadRecordRepository-interface biedt methoden voor het opslaan en opvragen van payload records over de twee temporele assen. Voor het opslaan van nieuwe payload records zijn de eerder beschreven standaardoperaties geïmplementeerd.
Dit maakt het eerder beschreven tijdreizen mogelijk, en daarmee de herhaalbaarheid van bevragingen tot op het niveau beschreven in de inherente beperkingen hierboven.
Payload metadata
Elk payload record bevat metadata die vastlegt welk gevolg het heeft veroorzaakt. Dit maakt het mogelijk om achteraf precies te reconstrueren hoe de registratie tot stand is gekomen.
Het basisgeval
In het basisgeval — bij alle reguliere gevolgen — bevat de metadata alleen het gevolg-type:
PayloadMetadata(WozObjectGeregistreerd::class)
Dit legt vast dat dit payload record is aangemaakt als reactie op een WozObjectGeregistreerd-gevolg.
Bij correctie
Bij correcties (voor de gevolgrepresentatie van correcties, zie hier) bevat één correctiegevolg meerdere subgevolgen die elk hun eigen payload records produceren. Om elk payload record uniek herleidbaar te maken naar zijn herkomst binnen dat correctiegevolg, worden twee extra velden gevuld:
PayloadMetadata(
BelangNaarGecorrigeerd::class, // het omsluitende correctiegevolg
BelangOvergedragen::class, // het subgevolg zoals het had moeten zijn
2, // de positie van dit subgevolg in de reeks
)
De drie velden:
| Veld | Betekenis |
|---|---|
gevolgType | Het type van het (correctie)gevolg dat de verwerking heeft geïnitieerd |
subgevolgType | Het type van het subgevolg zoals het eigenlijk had moeten zijn |
subgevolgIndex | De positie van het subgevolg in de reeks (0-gebaseerd) |
Dit maakt het mogelijk om bij correctie achteraf precies te reconstrueren hoe het geweest had moeten zijn.