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.

Runs on
The JVM behind this site
Evidence for
Event-driven pipelines on Kafka at Picsart and Worldline
Loading the experiment…This experiment runs in JavaScript, which is turned off. The notes below describe what it shows.

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 die
    • Backlog.javaThe relay’s and the consumer’s loop: start again from what was committed
    • Consumer.javaActs, then moves on, in two transactions; deduplicates by message id
    • Crashes.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