Idempotency bij API-koppelingen.
Waarom dubbele webhooks, retries en netwerkstoringen je data niet mogen vervuilen. Over idempotency keys, deduplicatie en robuuste integraties.
Wat is idempotency?
Idempotency is een eigenschap van een operatie die garandeert dat het resultaat hetzelfde blijft, ongeacht hoe vaak je de operatie uitvoert. Eenmaal uitvoeren of tien keer uitvoeren levert exact dezelfde eindtoestand op. Het is een begrip uit de wiskunde dat in software-architectuur onmisbaar is geworden.
Een alledaags voorbeeld: je drukt op de liftknop. De lift komt. Je drukt nogmaals op de knop omdat je ongeduldig bent. De lift komt niet twee keer en er worden geen twee liften gestuurd. Het systeem herkent dat het verzoek al is verwerkt en negeert de herhaling. Dat is idempotency in de praktijk.
Bij API-koppelingen betekent dit dat een verzoek veilig herhaald kan worden zonder ongewenste neveneffecten. Een betaling wordt eenmaal verwerkt. Een order wordt eenmaal aangemaakt. Een factuur wordt eenmaal verstuurd. Ongeacht of het verzoek een keer of vijf keer binnenkomt.
Waarom het misgaat
In een perfecte wereld komt elk verzoek precies een keer aan en wordt het precies een keer verwerkt. De werkelijkheid is minder elegant. Netwerken zijn onbetrouwbaar, servers gaan tijdelijk offline en externe systemen sturen dubbele berichten.
Webhooks die dubbel afvuren. Betaalproviders als Stripe en Mollie sturen webhook-notificaties wanneer een betaling slaagt. Maar als jouw server het ontvangstbewijs niet snel genoeg terugstuurt, stuurt de provider het bericht opnieuw. Soms twee keer. Soms vijf keer. Zonder idempotency verwerk je die betaling vijf keer.
Netwerktimeouts die retries veroorzaken. Je applicatie stuurt een verzoek naar een externe API om een factuur aan te maken. Het verzoek komt aan, de factuur wordt aangemaakt, maar het antwoord raakt verloren door een netwerkstoring. Je applicatie denkt dat het is mislukt en stuurt het verzoek opnieuw. Resultaat: twee identieke facturen.
Race conditions bij gelijktijdige verzoeken. Twee serverprocessen ontvangen dezelfde webhook-notificatie op vrijwel hetzelfde moment. Beide controleren of de order al is verwerkt, beide zien dat dit nog niet het geval is, en beide gaan aan de slag. Resultaat: een dubbele verwerking die geen van beide processen heeft gedetecteerd.
De gevolgen zijn concreet: dubbele betalingen die je moet terugboeken, dubbele orders die je magazijn verstoren, dubbele e-mails die je klanten irriteren. Het zijn geen theoretische problemen. Ze komen voor bij elke integratie die lang genoeg draait.
Het is niet de vraag of een webhook dubbel binnenkomt. Het is de vraag of je systeem er klaar voor is wanneer het gebeurt.
Idempotency keys
De meest directe oplossing is het gebruik van idempotency keys. Het concept is eenvoudig: elke unieke operatie krijgt een unieke sleutel mee. Voordat het systeem de operatie uitvoert, controleert het of die sleutel al eerder is verwerkt. Zo ja, dan wordt het oorspronkelijke resultaat teruggegeven zonder de operatie opnieuw uit te voeren.
Stripe heeft dit patroon gepopulariseerd in hun betaal-API. Bij elk verzoek stuur je een uniek gegenereerde sleutel mee. Stripe slaat deze sleutel op samen met het resultaat. Als hetzelfde verzoek nogmaals binnenkomt met dezelfde sleutel, stuurt Stripe het opgeslagen resultaat terug zonder de betaling opnieuw te verwerken.
In de praktijk bouw je dit in als een laag die binnenkomende verzoeken onderschept. Het systeem controleert of de meegestuurde sleutel al eerder is verwerkt en blokkeert dubbele verwerking. Als de sleutel nieuw is, wordt het verzoek normaal verwerkt en wordt de sleutel samen met het resultaat opgeslagen. Bij een herhaald verzoek wordt het opgeslagen resultaat direct teruggegeven.
Belangrijk is dat de sleutel altijd door de client wordt aangemaakt, niet door de server. De client is de enige die weet of een verzoek een retry is van een eerder verzoek of een nieuw, onafhankelijk verzoek. Gebruik hiervoor een UUID of een combinatie van relevante business-identifiers.
Webhook deduplicatie
Bij inkomende webhooks heb je geen controle over de verzender. Je kunt niet afdwingen dat Stripe of Mollie een idempotency key meestuurt. Wat je wel kunt doen, is elke ontvangen webhook registreren en dubbele leveringen detecteren.
De aanpak is rechttoe rechtaan. Elke webhook van een betrouwbare provider bevat een uniek event-ID. Bewaar dit ID in een dedicated tabel zodra je de webhook begint te verwerken. Controleer voor verwerking of het ID al bestaat. Bestaat het? Dan is de webhook al verwerkt en kun je direct een successtatus teruggeven zonder verdere actie.
De valkuil zit in de timing. Tussen het moment van controleren en het moment van opslaan kan een tweede, identiek verzoek binnenkomen dat dezelfde controle passeert. Dit is de eerder genoemde race condition. De oplossing is een unique constraint op de database-kolom die het event-ID bevat. Als twee processen tegelijkertijd proberen hetzelfde ID op te slaan, faalt een van beide op de database-constraint. Dat is geen bug; dat is je vangnet dat werkt.
Combineer dit met een transactie die het opslaan van het event-ID en de daadwerkelijke verwerking omvat. Faalt de verwerking? Dan wordt het event-ID ook teruggedraaid en kan de volgende retry het opnieuw oppakken.
Patronen voor robuuste integraties
Idempotency is een onderdeel van een breder denkkader rond betrouwbare integraties. Er zijn meerdere patronen die elkaar versterken.
At-least-once vs exactly-once
De meeste message-systemen en webhook-providers garanderen at-least-once delivery: het bericht komt minstens een keer aan, maar mogelijk vaker. Exactly-once delivery is in gedistribueerde systemen vrijwel onmogelijk te garanderen. Accepteer dat berichten dubbel kunnen komen en bouw je verwerking daar omheen.
Natuurlijk idempotente operaties
Sommige operaties zijn van nature idempotent. Een upsert (insert-or-update) op basis van een extern ID is inherent veilig: als het record al bestaat, wordt het bijgewerkt in plaats van gedupliceerd. State machines die alleen geldige transities toestaan, voorkomen dat een order twee keer wordt gemarkeerd als betaald. Ontwerp je datamodel zo dat operaties waar mogelijk van nature idempotent zijn.
Database-transacties
Wikkel gerelateerde schrijfoperaties in een database-transactie. Als een van de stappen faalt, worden alle stappen teruggedraaid. Dit voorkomt inconsistente tussentoestanden waarbij een order half is verwerkt: de betaling is geregistreerd maar de voorraad niet is bijgewerkt.
Dead letter queues
Niet elke verwerking slaagt bij de eerste poging. Een dead letter queue vangt berichten op die na meerdere pogingen nog steeds falen. In plaats van het bericht te verliezen, wordt het apart opgeslagen voor handmatige inspectie of latere verwerking. Dit geeft je de zekerheid dat geen enkel event stilletjes verdwijnt.
Betrouwbare software gaat niet over het voorkomen van fouten. Het gaat over het bouwen van systemen die graceful omgaan met de fouten die onvermijdelijk komen.
Conclusie
Elke externe integratie verdient idempotency-denken. Het maakt niet uit of je werkt met Stripe-webhooks, Mollie-callbacks, een ERP-koppeling of een eigen API die door derden wordt aangeroepen. De vraag is altijd: wat gebeurt er als dit verzoek twee keer binnenkomt?
Idempotency is geen luxe voor complexe systemen. Het is een basisvereiste voor elke koppeling die in productie draait. Het verschil tussen een koppeling die maanden probleemloos draait en een koppeling die om de zoveel tijd handmatig gerepareerd moet worden, zit vrijwel altijd in hoe goed dubbele verwerking is afgevangen.
Begin bij de basis: sla verwerkte event-ID's op, gebruik database-constraints als vangnet en wikkel kritieke operaties in transacties. Bouw van daaruit verder met idempotency keys voor uitgaande verzoeken en maak je operaties waar mogelijk van nature idempotent. Het is geen kwestie van fouten voorkomen, maar van er op een voorspelbare manier mee omgaan.
/Gerelateerde artikelen
API-koppelingen bouwen
Systemen met elkaar laten praten via APIs.
Een eigen API bouwen
Waarom je product een API nodig heeft en hoe je die opzet.
Event-driven architectuur
Wanneer queues en events beter zijn dan synchrone API-calls.