Synchronisatie van collecties
In gedistribueerde systemen hebben consumers vaak een actuele, lokale en vooral
consistente kopie nodig van een dynamische collectie binnen een REST API,
bijvoorbeeld /publicaties. Daarmee kunnen zij data snel bevragen, lokaal
verrijken of koppelen.
Het snapshots-en-delta's-patroon richt zich op one-way state synchronization: het in één richting synchroniseren van de actuele toestand van een collectie. Snapshots bieden een startpunt; delta's houden die toestand daarna efficiënt bij.
De belangrijkste garantie van het patroon is sequentiële consistentie: de lokale kopie doorloopt exact dezelfde volgorde van atomaire toestandsveranderingen als de bron. Hierdoor is de lokale kopie — weliswaar met een tijdsvertraging — te allen tijde consistent met de (historische) toestand van de bron.
Wanneer toe te passen
Het uitgangspunt blijft dat data bij de bron wordt bevraagd. Dit patroon is niet bedoeld als generieke datareplicatie of als vervanging van data bij de bron. Ook is het niet bedoeld voor bidirectionele synchronisatie, conflictresolutie of volledige historische replay op basis van events.
Het patroon past vooral bij read-optimized use cases waarin een consumer een lokale, consistente leesweergave nodig heeft en het telkens rechtstreeks bevragen van de bron of bronnen onwenselijk is. Dat kan bijvoorbeeld spelen bij eisen aan schaalbaarheid, beschikbaarheid, netwerklast, volledigheid of controleerbaarheid.
Vrijwel altijd gaat het daarbij om metadata in plaats van om de volledige bronregistratie: een index, verwijzingslaag of publicatieoverzicht waarmee consumers de juiste brondata kunnen vinden, controleren of raadplegen. In die zin kan zo'n gesynchroniseerde collectie de rol van een directory service vervullen. Voorbeelden zijn discovery en routering in decentrale stelsels, het opbouwen van een aantoonbaar volledige publieke index voor grote metadata-collecties zoals openbare publicaties, en het overkoepelend reconstrueren van de samenhang van items binnen een keten.
Toepassing van dit patroon vraagt daarom om een expliciete architectonische afweging. De keuze voor synchronisatie moet worden onderbouwd vanuit de use case, de doelbinding en de garanties die daarvoor nodig zijn, zoals schaalbaarheid, beschikbaarheid, controleerbaarheid en consistente verwerking. Het patroon is minder geschikt voor deelverzamelingen die per consumer of per request met parameters worden samengesteld.
Het snapshots-en-delta's-patroon
Het snapshots-en-delta's-patroon maakt synchronisatie betrouwbaar door twee parallelle stromen te combineren: een laagfrequente stroom van snapshots en een hoogfrequente stroom van delta's. De ene stroom biedt een veilig startpunt, de andere stroom zorgt ervoor dat de lokale kopie actueel blijft.
Snapshots en delta's krijgen daarom een positie in dezelfde reeks. In dit artikel noemen we die positie een state-id. Een snapshot bevat de toestand tot en met het bijbehorende state-id; een delta beschrijft de stap van het vorige state-id naar een volgende state-id.
In onderstaande tabel wordt snapshot 42 als instappunt gebruikt:
| Snapshots | Delta's | Actie | State-id lokale kopie |
|---|---|---|---|
| 10 | ouder snapshot | ||
| 17 | al in nieuwer snapshot | ||
| 38 | al in nieuwer snapshot | ||
| 42 | al in 42 | ||
| 42 | snapshot laden | 42 | |
| 57 | delta toepassen | 57 | |
| 63 | delta toepassen | 63 | |
| 71 | delta toepassen | 71 |
- Delta's tot en met
42zijn al insnapshot 42opgenomen. - Na het laden van
snapshot 42gaat de consumer verder metdelta 57. Delta 57moet aansluiten op state-id42.- Daarna past de consumer volgende delta's in volgorde toe.
- De consumer bewaart steeds de laatst succesvol verwerkte
state-id.
Snapshots
Een snapshot is een volledige, consistente weergave van de collectie op één specifiek moment. Daarmee kan een consumer starten zonder de volledige wijzigingsgeschiedenis te kennen. Dat is nodig bij de eerste start, maar ook bij herstel na verlies van lokale status, verlopen retentie of een breuk in de delta-keten.
Een snapshot is pas bruikbaar als duidelijk is bij welke positie in de reeks het hoort. Die positie vormt de verticale lijn in het patroon: alles tot en met dat punt zit al in het snapshot, alles daarna moet via delta's worden verwerkt.
Delta's
Delta's beschrijven de stap van een bekende toestand naar de daaropvolgende toestand. Conceptueel is een delta vergelijkbaar met een atomaire 'commit' of database-transactie: het bevat een set mutaties die de collectie in één stap van de ene consistente toestand naar de volgende brengt. Om de lokale kopie consistent te houden, moet de delta-keten aaneengesloten zijn: een consumer kan zijn toestand alleen veilig doorschuiven als een nieuwe delta exact aansluit op de positie van de laatst succesvol verwerkte snapshot of delta. Functioneel betekent dit dat een delta ook de voorgaande positie in de reeks meedraagt, zodat een consumer kan controleren dat die aansluiting klopt.
Delta's vormen de reguliere route om de lokale kopie continu actueel te houden zonder telkens de volledige dataset opnieuw op te hoeven vragen. Ze bevatten niet alleen de notificatie dát er iets is veranderd, maar dragen ook direct de inhoud van die wijziging met zich mee.
State-ids
Een state-id identificeert een specifieke, stabiele toestand van de collectie:
het is de identifier van een positie in de reeks. Conceptueel lijkt dat op een
ETag, maar een state-id heeft een
sterkere semantiek: het markeert het resultaat van een atomaire overgang. Een
delta beschrijft precies de stap van één state-id naar het volgende —
daartussenin is de collectie consistent. Een ETag hoort bij een representatie
en biedt die atomiciteitsgarantie niet per se.
De provider kiest de concrete vorm van het state-id, bijvoorbeeld een oplopend transactienummer, een hash of een UUID. De enige eis is dat een state-id uniek is binnen de collectie. De volgorde van wijzigingen ligt niet in de state-ids zelf, maar in de verwijzing die elke delta naar de voorafgaande state-id bevat — state-ids hoeven dus niet gesorteerd of oplopend te zijn.
REST API's
Hieronder werken we het snapshots-en-delta's patroon uit voor REST API's. Het patroon zelf — snapshots, delta's, state-ids — is leidend; de URL-structuur is hier een mogelijke opzet voor. Wie de aanbeveling volgt, maakt zijn API direct bruikbaar voor consumers die het patroon kennen en respecteert hierin zoveel mogelijk de standaarden.
Resourcemodel
Het patroon voegt twee sub-resources toe aan een (eventueel bestaande) collectie:
/publicaties → de collectie zelf (ongewijzigd)
/publicaties/_snapshots → lijst van beschikbare snapshots
/publicaties/_deltas → lijst of stroom van delta's (polling of SSE);
geen individuele endpoints per delta
De inhoud van een snapshot is geen vaste extra sub-resource in dit model. De
snapshotlijst verwijst ernaar via href; die kan naar een technisch pad binnen
dezelfde API wijzen, zoals /publicaties/_snapshots/42, maar ook naar een
externe locatie zoals een CDN.
Snapshots ophalen
De provider publiceert een lijst van beschikbare snapshots, gesorteerd op reekspositie (oudste eerst). De consumer kiest het laatste (meest recente) item in die lijst als startpunt:
GET /publicaties/_snapshots
→ 200 OK
{
"items": [
{
"id": 10,
"href": "/publicaties/_snapshots/10",
"total": 800
},
{
"id": 42,
"href": "/publicaties/_snapshots/42",
"total": 850
}
]
}
Een snapshot is een statische collectie: nadat het snapshot is gemaakt, verandert het niet meer. Daardoor kan de provider die inhoud op verschillende manieren aanbieden, bijvoorbeeld met paginering, vaste chunks of bestanden. Snapshot-chunks zijn statische bestanden en kunnen potentieel groot zijn. Ze lenen zich daardoor voor distributie via een CDN, wat een API gateway kan ontlasten. Voor het patroon is het van belang dat alle delen samen dezelfde snapshot-toestand representeren. De provider dient te garanderen dat vanaf elk aangeboden snapshot de aansluitende delta-keten beschikbaar is. Vervolgens haalt de consumer de inhoud op via de bijbehorende link.
De provider houdt snapshots lang genoeg beschikbaar om ze volledig te
downloaden; verloopt een snapshot toch tussentijds — kenbaar via 410 Gone op
een latere chunk — dan herhaalt de consumer het proces met het meest recente
beschikbare snapshot.
Delta's ophalen
Individuele delta's worden niet als afzonderlijke REST-resources (zoals
GET /publicaties/_deltas/57) aangeboden. Hun waarde zit in de aaneengesloten
chronologische reeks; een losse delta bevragen dient geen synchronisatiedoel en
zou leiden tot een overload aan afzonderlijke HTTP-requests (chatty API).
Daarom ontsluit de provider delta's alleen als gecombineerde stroom of batch.
Hieronder werken we daarvoor polling en SSE uit.
Structuur van delta's
Een delta is de concrete schakel tussen de garanties hierboven en de implementatie hieronder: de consumer kan alleen veilig doorschuiven als elke delta expliciet aangeeft op welke vorige toestand hij aansluit.
{
"id": 57,
"prev_id": 42,
"operations": [
{
"type": "update",
"resource_id": "item-abc",
"resource": {
"id": "item-abc",
"name": "Resource ABC - Gewijzigd",
"status": "actief"
}
}
]
}
Een delta bevat altijd een array van operaties (operations), ook als er maar
één wijziging is. Zo kan de provider meerdere samenhangende wijzigingen in één
keer laten toepassen. Elke operatie heeft minimaal een type, bijvoorbeeld
create, update of delete. Bij een delete-operatie ontbreekt het
resource-veld bewust (tombstone).
In de aanbevolen vorm bevat resource steeds de volledige resulterende weergave
van het record (Event-Carried State Transfer). Dat is de voorkeursvorm: de
consumer hoeft geen vorige toestand op te halen om de wijziging te begrijpen, en
retries blijven idempotent. Het veld resource_id staat ook buiten het
resource-object, zodat de getroffen resource ook bij een delete-operatie
eenduidig identificeerbaar blijft.
Alleen als resources extreem groot zijn en bandbreedte de doorslag geeft, kan de provider in plaats van de volledige resource ook een JSON Merge Patch (RFC 7396) of JSON Patch (RFC 6902) meesturen. Dat is een uitzondering op de voorkeursvorm en maakt de consumer-logica complexer, omdat patching afhankelijk is van de lokale uitgangstoestand en van paden en schemastructuur.
Delta's kunnen desgewenst in een
CloudEvents-envelop worden verpakt. Dit is goed
mogelijk, mits de delta-semantiek behouden blijft: één event dient nog steeds
één delta te representeren, inclusief de atomaire set wijzigingen in
operations.
Polling
De consumer vraagt periodiek nieuwe delta's op via het state-id:
GET /publicaties/_deltas?after=42&limit=10
→ 200 OK
{
"items": [
{
"id": 57,
"prev_id": 42,
"operations": [
{
"type": "update",
"resource_id": "item-abc",
"resource": { "id": "item-abc", "name": "Resource ABC - Gewijzigd" }
}
]
}
]
}
Dit gedraagt zich als cursor-based paginering, maar gebruikt expliciet
after=<state-id> om de volgende stap in de delta-keten op te vragen. Via
limit blijft de responsgrootte beheersbaar. De consumer verwerkt delta's in
volgorde, zet het eigen actuele state-id naar het id van de laatste verwerkte
delta en vraagt daarna de volgende pagina op met after=<nieuw_state_id>. Een
lege items-lijst betekent dat de consumer actueel is en na het polling-interval
opnieuw kan opvragen.
Ontvangt de consumer een delta waarvan prev_id niet aansluit bij het huidige
state-id, dan is er sprake van een hiaat in de keten en dient de consumer
opnieuw te beginnen vanaf een snapshot.
Als het gevraagde state-id niet meer bekend is bij de provider, antwoordt die
met 410 Gone:
GET /publicaties/_deltas?after=99
→ 410 Gone
Ook in dat geval dient de consumer opnieuw te beginnen vanaf een snapshot. Voor polling is dat het algemene herstelpad: bij een gat in de keten of een onbekend state-id opnieuw beginnen vanaf een snapshot.
Streaming (SSE)
De consumer opent een langdurige verbinding; de provider pusht delta's zodra ze
beschikbaar zijn. De consumer stuurt Last-Event-ID mee als state-id — zowel
bij de initiële verbinding als bij herverbinding na een onderbreking:
GET /publicaties/_deltas
Accept: text/event-stream
Last-Event-ID: 42
→ 200 OK (text/event-stream)
id: 57
data: {"id": 57, "prev_id": 42, "operations": [{"type": "update", "resource_id": "item-abc", ...}]}
id: 63
data: {"id": 63, "prev_id": 57, "operations": [{"type": "delete", "resource_id": "item-xyz"}]}
De consumer valideert bij elke ontvangen delta dat prev_id overeenkomt met het
huidige state-id. Een mismatch signaleert een hiaat en leidt tot hetzelfde
herstelpad als bij polling. Een open SSE-verbinding kan na de opzet geen
410 Gone meer ontvangen; verloopt het state-id tijdens de sessie, dan sluit de
provider de verbinding. Bij herverbinding stuurt de consumer opnieuw
Last-Event-ID; als dat state-id inmiddels niet meer bekend is, antwoordt de
provider alsnog met 410 Gone.
Retentie van snapshots en delta's
Een cruciale verantwoordelijkheid van de provider is de overlap tussen snapshot-retentie en delta-retentie. Het downloaden van een groot snapshot kost tijd. Als een consumer pas daarna overschakelt op delta's, mogen de delta's die in de tussentijd zijn ontstaan niet al zijn opgeruimd. De retentie van delta's moet daarom ruimschoots langer zijn dan de langst plausibele download- en verwerkingstijd van een snapshot. Anders gezegd: voor elk snapshot dat de provider aanbiedt, moet de aansluitende delta-keten vanaf het state-id van dat snapshot nog beschikbaar zijn.
De ontvangst van delta's kan vooruitlopen op het laden van een snapshot: terwijl het gekozen snapshot nog wordt gedownload, kan een consumer nieuwe delta's alvast ontvangen en tijdelijk bufferen. Zodra het snapshot is geladen, verwijdert de consumer alle gebufferde delta's tot en met de snapshotpositie en verwerkt deze alleen de delta's die daarop aansluiten. Dat kan de hersteltijd verkorten, maar is niet nodig als de provider de genoemde overlap garandeert.
Is een gevraagde positie toch verlopen, dan maakt de provider dat expliciet met
410 Gone: bij polling op het delta-endpoint, bij SSE tijdens de herverbinding,
of bij het alsnog opvragen van een verlopen snapshot-deel. De consumer kan
daaruit afleiden dat opnieuw een snapshot moet worden opgehaald.
Gerelateerde patronen
- Voor het betrouwbaar genereren en publiceren van delta's kan een provider het Transactionele outbox-patroon toepassen.
- Voor een bredere introductie op event-driven communicatiepatronen, zie Event Driven Architecture.