Het snapshot-mechanisme
Waarom snapshots
Een betrouwbaar register dient de resultaten van queries herhaalbaar te maken. Een afnemer moet, nadat deze op tijdstip A een gegeven heeft opgevraagd, op tijdstip B nog steeds de vraag kunnen stellen wat dit gegeven op tijdstip A was. Een betrouwbaar register dient dit te kunnen nadat:
- Er nieuwe gevolgen zijn toegevoegd aan de registratie.
- Een gegeven hersteld is doordat het geldigheidstijdvak is veranderd vanwege een foutieve registratie.
- Een softwarebug in de opvraging is hersteld waardoor de initiële opvraging fout was.
Criterium 1 en 2 introduceren bitemporaliteit in de registratie. Criterium 3 vereist dat er mechanismen zijn die het mogelijk maken softwareaanpassingen te doen met behoud van de eerdere gegevensrepresentatie.
Inherente beperkingen
Elke registratie loopt tegen inherente beperkingen aan. Zowel geldigheids- als gevolgtijdstippen hebben te maken met eventual consistency: er zit altijd tijd tussen het begin van een registratie en het einde daarvan, en er zit altijd tijd tussen een opvraging en de zichtbaarheid van deze gegevens voor de afnemer. Dit betekent dat een afnemer altijd in potentie naar verouderde informatie kijkt.
Daarnaast heeft een registratie meestal niet de volledige controle over de aflevering van informatie. Entiteiten verlaten op een bepaald moment de registratie om elders te worden gepresenteerd — in een browser, een API-response, of een ander systeem dat de registratie niet kent.
Herhaalbaarheid van opvraging is dus hoogstens te behalen tot op het niveau van de gegevensrepresentatie net voordat deze het systeem verlaat.
Wat is een snapshot
Een snapshot 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 snapshot:
| Veld | Betekenis |
|---|---|
aggregateId | Welke entiteit dit snapshot beschrijft |
gevolgId | Welk gevolg dit snapshot heeft veroorzaakt |
payload | De inhoud van het snapshot (geserialiseerd), van type payloadType |
metadata | Aanvullende informatie over de herkomst (zie Snapshot metadata) |
vorigSnapshotIndex | Verwijzing naar de voorgaande snapshot (voor herleidbaarheid) |
De drie tijdsassen
Een snapshot kent drie tijdsassen:
- Geldigheidstijdstip — wanneer de toestand geldig is geworden in de werkelijkheid
- Gevolgtijdstip — wanneer het gevolg in de registratie is vastgelegd
- Projectietijdstip — wanneer het snapshot zichtbaar is geworden
Door deze drie assen te combineren in een query is tijdreizen mogelijk: "hoe zag dit gegeven eruit op tijdstip X, zoals vastgelegd op tijdstip Y, zichtbaar in de projectie op tijdstip Z?"
De snapshot-operaties
Het toevoegen van een nieuwe snapshot aan de snapshot store kan op verschillende manieren, en is afhankelijk van de situatie:
- Bestaat de aggregate al of is deze nieuw in de store?
- Wordt er een nieuw geldigheidsmoment toegevoegd aan de store, of wordt een bestaand geldigheidsmoment bovenschreven?
- Wordt er bij de bovenschrijving foute informatie hersteld of niet?
Mutaties in detail
In de basis kent de snapshotstore twee typen operaties:
Het initiële snapshot:
Deze flow wordt gebruikt bij de insert operatie.
Een nieuw snapshot op basis van een gevolg dat werkt op het voorgaande snapshot:
Deze flow wordt gebruikt bij de append, amend, en upend operaties.
De append betreft een 'reguliere' mutatie: bij de append wordt een geheel nieuw geldigheidstijdstip toegevoegd aan de snapshot store.
Er zijn situaties waarbij informatie over het verleden beschikbaar komt en met terugwerkende kracht actief moet worden. In dat geval wordt een snapshot toegevoegd op een geldigheidstijdstip dat al aanwezig was in de registratie. Deze operatie noemen we bovenschrijven, om het te onderscheiden van overschrijven.
Bovenschrijven kan op twee verschillende manieren, met als verschil het snapshot dat als uitgangspunt genomen wordt. Bij upend is gebleken dat het bestaande snapshot foutieve informatie bevat: een nieuwe snapshot dient aangemaakt te worden op basis van het voorgaande snapshot. Het nieuwe snapshot dient opnieuw aangemaakt te worden op dezelfde manier als de oude, maar met een verbetering uit het gevolg.
Bij amend was de reeds bestaande snapshot op zich niet fout, maar wordt er (achteraf) informatie toegevoegd (of verwijderd). Bij deze operatie dient de reeds bestaande snaphot als uitgangspunt genomen te worden.
In overzicht:
| Term | Betekenis |
|---|---|
| insert | Eerste punt van een nieuwe entiteit — altijd linksonder in de grafiek |
| append | Nieuw tijdvak aan het einde van de geldigheidstijdlijn — altijd rechtsboven |
| amend | Bovenschrijft een snapshot met als bron de oorspronkelijke snaphot |
| upend | Bovenschrijft een snapshot met als bron het voorgaande snaphot |
De snapshotstore
Snapshots worden opgeslagen in een append-only snapshotstore: eenmaal opgeslagen worden snapshots 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 SnapshotRepository-interface biedt methoden voor het opslaan en opvragen van snapshots over de drie tijdsassen. Door geldigheidstijdstip, gevolgtijdstip en projectietijdstip te combineren in een query is het mogelijk precies te vragen naar hoe een gegeven eruitzag op een bepaald moment in de werkelijkheid, zoals dat op een bepaald moment was vastgelegd, en zoals dat zichtbaar was in de projectie op een bepaald moment.
Dit maakt het mogelijk om de herhaalbaarheid van queries te garanderen tot op het niveau beschreven in de inherente beperkingen hierboven.
Snapshot metadata
Elke snapshot bevat metadata die vastlegt welk gevolg hem 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:
SnapshotMetadata(WozObjectGeregistreerd::class)
Dit legt vast dat dit snapshot is aangemaakt als reactie op een WozObjectGeregistreerd-gevolg.
Bij herstel via subgevolgen
Bij herstel via subgevolgen bevat één herstelgevolg meerdere subgevolgen die elk hun eigen snapshots produceren. Om elke snapshot uniek herleidbaar te maken naar zijn herkomst binnen dat herstelgevolg, worden twee extra velden gevuld:
SnapshotMetadata(
BelangNaarGecorrigeerd::class, // het omsluitende herstelgevolg
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 (herstel)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 herstel achteraf precies te reconstrueren hoe het geweest had moeten zijn.