Outbox under crash injection
Save an order, announce it, and let the process die in between. A dual write loses events or invents them; an outbox loses nothing and sends some twice.
What you’re seeing
Every design meets the same crashes. At a 20% chance, 101 of the 500 orders died before their commit, which harms nobody: nothing saved, nothing announced, and a client that heard nothing and can try again.
The dual writes break between their two writes. Committing first, 86 orders were saved and never announced, and nothing downstream will ever act on them. Publishing first, 86 orders were announced and never saved, and the consumer acted on every one: shipped, and never ordered. There is no safe order for two writes to two systems.
The outbox, and its price
Writing the event to an outbox table in the order’s own transaction makes the two writes one: in the recorded run nothing was lost and nothing invented. The price is at least once. The relay died 114 times between publishing an event and marking it, and each time the event went out again; the consumer died 128 times between acting and moving on, and each time the message came again.
Deduplicating by message id, the consumer still acted on each order once. Without it, it acted 242 times too many.
Where it meets my work
Event-driven pipelines on Kafka are part of my work at Picsart, and Kafka carried event-driven processes at Worldline. Wherever a service writes to its database and announces it on a topic, this is the choice underneath: a dual write that can lose an event or invent one, or an outbox that loses nothing and may send some twice. The idempotency experiment shows the other side: a client that retries, and a server that recognises the retry.
One PostgreSQL plays the service, the broker and the consumer, but they share no transaction: the broker is written on a connection of its own, as a real one would be reached. Orders are placed one after another, then the relay runs, then the consumer, rather than all at once; a crash is a process dying between two statements, not a lost network packet or a full disk.
The code behind it
The files that do the work, by their place in this site’s repository.
- api/src/main/java/com/aghazaryan/api/lab/outbox/
Producer.javaThe three ways to save and announce an order, and where each can dieBacklog.javaThe relay’s and the consumer’s loop: start again from what was committedConsumer.javaActs, then moves on, in two transactions; deduplicates by message idCrashes.javaWhere processes die: the same crashes for every design, from one seed
- api/src/main/resources/db/migration/
V4__outbox.sqlOrders, the outbox, a broker that shares no transaction, the consumer