Het outbox pattern voorkomt dat een koppeling berichten verliest wanneer een applicatie tegelijk haar database bijwerkt en een bericht verstuurt. Je schrijft het bericht in dezelfde databasetransactie weg in een outboxtabel. Een apart proces leest die tabel en publiceert de berichten naar de berichtenbus. Zo gaat een bericht alleen de deur uit als de wijziging echt is opgeslagen.
Het probleem speelt bij elke koppeling die een ander systeem op de hoogte houdt. Een order krijgt de status verzonden en het facturatiesysteem moet dat weten. Een klant wijzigt zijn adres en het CRM moet mee. Zolang alles werkt, merkt niemand iets. Na een korte storing blijkt dan dat een factuur nooit is gemaakt of twee keer is verstuurd.
Het probleem van de dual write
Een dual write is een handeling waarbij een applicatie naar twee systemen schrijft die geen gezamenlijke transactie kennen, zoals een database en een berichtenbus. De AWS Prescriptive Guidance over het transactional outbox pattern noemt de twee manieren waarop dat misgaat:
- De database slaagt, het bericht mislukt. Het ontvangende systeem hoort nooit van de wijziging en de gegevens lopen uit elkaar.
- Het bericht gaat weg, de database mislukt. Het ontvangende systeem verwerkt een wijziging die niet bestaat, bijvoorbeeld een betaling voor een boeking die is teruggedraaid.
De voor de hand liggende oplossingen werken niet. Eerst het bericht versturen en dan committen geeft het tweede probleem. Eerst committen en dan versturen geeft het eerste, want het proces kan precies tussen die twee stappen crashen. Een gedistribueerde transactie met two phase commit (2PC) over database en broker valt ook af. Volgens Chris Richardson van microservices.io ondersteunen de database of de broker dat lang niet altijd. En als ze het wel doen, koppel je de service ongewenst aan allebei.
Hoe meer systemen je koppelt, hoe vaker dit voorkomt. Het Connectivity Benchmark Report 2026 van MuleSoft, gebaseerd op 1.050 IT verantwoordelijken in negen landen waaronder Nederland, telt gemiddeld 957 applicaties per organisatie. Daarvan is 27% met elkaar gekoppeld. Elke nieuwe koppeling die gebeurtenissen doorgeeft, is een plek waar een dual write kan ontstaan.
Hoe het transactional outbox pattern werkt
Het outbox pattern maakt van twee schrijfacties er één. De applicatie schrijft de zakelijke wijziging en het bericht in dezelfde lokale transactie naar haar eigen database. Het bericht komt in een aparte tabel, de outbox. Slaagt de transactie, dan staan beide er. Mislukt ze, dan staat geen van beide er.
Daarna neemt een tweede onderdeel het over: de message relay. Die leest nieuwe rijen uit de outbox, publiceert ze naar de broker en markeert ze als verstuurd. Crasht de relay, dan pakt hij na een herstart de onverstuurde rijen op. Zo raakt geen bericht meer kwijt. Het kan wel twee keer aankomen, en daar komen we zo op terug.
In pseudocode ziet de schrijfkant er zo uit:
BEGIN TRANSACTION
UPDATE orders SET status = 'shipped' WHERE id = :order_id
INSERT INTO outbox (id, aggregate_type, aggregate_id, event_type, payload, created_at)
VALUES (:new_uuid, 'order', :order_id, 'OrderShipped', :json, now())
COMMIT
De relay doet in de eenvoudigste vorm dit:
elke paar seconden:
rows = SELECT * FROM outbox WHERE published_at IS NULL ORDER BY created_at LIMIT 100
voor elke row in rows:
broker.publish(topic = row.aggregate_type, key = row.aggregate_id, message_id = row.id, body = row.payload)
UPDATE outbox SET published_at = now() WHERE id = row.id
Het bericht krijgt een vaste id die meereist naar de ontvanger, voor deduplicatie. De aggregate_id, hier het ordernummer, gebruik je als sleutel om de volgorde te bewaken.
Polling publisher of change data capture
Voor de relay zijn twee varianten gangbaar. De polling publisher vraagt de outbox periodiek op, zoals in de pseudocode hierboven. Bij transaction log tailing, ook bekend als change data capture (CDC), leest een tool het transactielog van de database mee, zoals de binlog van MySQL of de WAL van PostgreSQL.
| Polling publisher | Change data capture | |
|---|---|---|
| Werkt met | Elke SQL database | Databases met een leesbaar transactielog |
| Extra onderdelen | Een achtergrondproces in je eigen code | Een CDC platform zoals Debezium, vaak met Kafka |
| Vertraging | Afhankelijk van het pollinginterval | Kort, de relay volgt het log |
| Volgorde | Lastig bij gelijktijdige transacties | Volgt de commitvolgorde van het log |
Debezium heeft voor dit patroon een kant en klare outbox event router. Die verwacht standaard een outboxtabel met de kolommen id, aggregatetype, aggregateid, type en payload. De waarde van aggregatetype bepaalt het Kafka topic, en aggregateid wordt de sleutel van het bericht.
Onze vuistregel: begin met een polling publisher als je één database en een bescheiden aantal berichten hebt. Kies CDC als je al een streamingplatform draait, veel berichten verwerkt of meerdere services op dezelfde manier wilt ontsluiten.
Idempotente ontvangers en deduplicatie
Het outbox pattern garandeert dat een bericht minstens één keer aankomt, en na een storing soms vaker. Crasht de relay nadat hij een bericht heeft gepubliceerd maar voordat hij de rij als verstuurd heeft gemarkeerd, dan publiceert hij het na de herstart opnieuw. De ontvanger moet daar tegen kunnen.
Het bijbehorende patroon heet de idempotent consumer. De ontvanger slaat de id van elk verwerkt bericht op in een tabel met processed messages, in dezelfde transactie als de verwerking zelf. De combinatie van ontvanger en bericht id is de primaire sleutel. Komt hetzelfde bericht nog een keer binnen, dan faalt het invoegen, rolt de transactie terug en wordt het bericht genegeerd.
Brokers bieden daarnaast ingebouwde deduplicatie:
- Amazon SQS FIFO negeert een bericht met dezelfde deduplicatie id binnen een venster van 5 minuten.
- Azure Service Bus houdt de MessageId bij gedurende een instelbaar venster van standaard 10 minuten, met een minimum van 20 seconden en een maximum van 7 dagen. De basic tier ondersteunt dit niet.
- Kafka met Debezium geeft de outbox id mee in de berichtheaders, zodat de ontvanger hem kan controleren.
Die vensters zijn eindig. Een relay die na een storing van een uur opnieuw begint, valt buiten een venster van 5 minuten. Deduplicatie in de broker is daarom een extra vangnet. De idempotente ontvanger blijft nodig.
Volgorde bewaken
Voor veel koppelingen maakt de volgorde uit. Een bericht OrderGeannuleerd dat vóór OrderAangemaakt binnenkomt, levert bij de ontvanger een fout of een verkeerde status op. Meestal volstaat volgorde per object: alle berichten over order 4711 komen in de juiste volgorde aan.
Daarvoor gebruik je de aggregate_id als sleutel. In Kafka komen berichten met dezelfde sleutel in dezelfde partitie, en binnen een partitie blijft de volgorde behouden. Gunnar Morling van Debezium beschrijft precies dit mechanisme. Bij SQS FIFO heet het equivalent de message group id, bij Service Bus de session id.
Bij een polling publisher zit er een valkuil in de outbox zelf. Een volgnummer of tijdstempel wordt uitgedeeld bij het invoegen, terwijl de commit later komt. Twee gelijktijdige transacties kunnen daardoor in omgekeerde volgorde zichtbaar worden. Een relay die onthoudt tot welk nummer hij gelezen heeft, slaat dan rijen over. Werk daarom met een kolom als published_at en lees alles wat nog niet verstuurd is. Laat per sleutel één relay tegelijk publiceren.
Opruimen en monitoren
Een outbox groeit bij elk bericht en heeft dus een opruimbeleid nodig. Er zijn drie gangbare aanpakken:
- Verwijderen na publicatie. De relay verwijdert de rij zodra de broker het bericht heeft bevestigd.
- Direct verwijderen bij CDC. Omdat Debezium het log leest, kun je de rij in dezelfde transactie invoegen en weer verwijderen. De outboxtabel blijft dan altijd leeg, terwijl het bericht via het log toch wordt verstuurd.
- Bewaartermijn. Verstuurde rijen blijven een tijd staan voor analyse en worden daarna opgeschoond. Microsoft Learn raadt in zijn Cosmos DB voorbeeld een bewaartermijn van meerdere dagen aan, zoals 10 dagen, zodat een haperende relay tijd heeft om bij te komen.
Een outbox faalt stil. De applicatie blijft werken terwijl de berichten zich opstapelen. Meet daarom minstens deze signalen:
- De leeftijd van het oudste onverstuurde bericht.
- Het aantal onverstuurde rijen in de outbox.
- Bij CDC: de achterstand van het replicatieslot en de omvang van het transactielog.
- Het aantal berichten in de dead letter queue van de ontvanger.
Het derde punt vraagt aandacht bij PostgreSQL. De documentatie waarschuwt dat een replicatieslot zoveel WAL kan vasthouden dat de schijf volloopt. Stilstaande CDC kan zo je hoofddatabase raken. Met de instelling max_slot_wal_keep_size begrens je dat.
Wanneer je het outbox pattern niet nodig hebt
Het patroon voegt een tabel, een achtergrondproces en monitoring toe. In deze gevallen kun je het overslaan:
- Er is geen tweede schrijfactie. Een synchrone API koppeling waarbij de aanroeper het antwoord afwacht en bij een fout opnieuw probeert, heeft geen outbox nodig. De ontvanger moet dan wel idempotent zijn.
- Een gemist bericht is geen probleem. Een melding om een cache te verversen of een statistiek bij te werken mag af en toe ontbreken.
- De broker is de bron. Wie event sourcing toepast, slaat gebeurtenissen op als primaire gegevens. Microservices.io noemt dat als alternatief voor een outbox.
- Je kunt de database niet aanpassen. Bij een standaardpakket, zoals bij veel ERP koppelingen, zijn CDC op de bestaande tabellen of de webhooks van de leverancier vaak de betere route.
Voor koppelingen tussen meerdere systemen kan ook een integratielaag de outbox en de verdeling van berichten centraal regelen. Lees daarover meer in ons artikel wat is middleware.
Isatis bouwt al ruim dertig jaar koppelingen, onder meer rond onderhoudssoftware (MRO) voor de luchtvaart, een platform voor subsidieadministratie en RFID in de supply chain. Met 30+ engineers in Nijmegen en Sarajevo, meer dan 100 projecten en certificering volgens ISO 9001 en ISO 27001 weten we waar berichten onderweg zoekraken. Op onze pagina over software integratie lees je hoe we koppelingen opzetten, en bij API ontwikkeling hoe we de interfaces daaromheen bouwen.
Veelgestelde vragen
Wat is het outbox pattern?
Het outbox pattern is een ontwerppatroon waarbij een applicatie een uitgaand bericht in dezelfde databasetransactie opslaat als de wijziging waar het over gaat. Een apart proces publiceert de berichten daarna naar een berichtenbus. Zo verstuur je alleen berichten over wijzigingen die echt zijn opgeslagen, en gaat er geen bericht verloren.
Voorkomt het outbox pattern dubbele berichten?
Nee. Het patroon garandeert dat een bericht minstens één keer aankomt, en na een storing soms vaker. Daarom moet de ontvanger idempotent zijn. Die slaat de id van elk verwerkt bericht op en negeert een bericht dat hij al kent. Deduplicatie in de broker is een extra vangnet met een beperkt tijdvenster.
Wat is het verschil tussen polling en change data capture?
Een polling publisher vraagt de outboxtabel periodiek op en werkt met elke SQL database. Change data capture leest het transactielog van de database mee, bijvoorbeeld met Debezium. CDC heeft minder vertraging en volgt de commitvolgorde, maar vraagt een extra platform en databasespecifieke configuratie.
Heb ik het outbox pattern nodig voor een REST API koppeling?
Alleen als je systeem na een eigen wijziging een ander systeem moet informeren en die melding niet mag ontbreken. Bij een synchrone aanroep waarbij de aanroeper op het antwoord wacht en bij een fout opnieuw probeert, volstaat een idempotente ontvanger.





