awesome-everything RU
↑ Back to the climb

Queues, Streams, Eventing

Outbox pattern: code and schema reading

Crux Read real SQL and handler snippets — an outbox schema, a relay claim query, and a dual-write bug — predict the behaviour, and pick the highest-leverage fix.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at senior altitude — in orbit
◷ 14 min

The schema, the relay query, and the handler are where outbox correctness is won or lost. Read each snippet, predict how it behaves under a crash or under load, then choose the fix a senior engineer would make first.

Goal

Practise the loop you run in every outbox review: read the SQL and the handler, find the gap a crash exposes, and reach for the fix that closes it without inventing a distributed transaction.

Snippet 1 — the handler

async function placeOrder(db, broker, order) {
  await db.query("INSERT INTO orders (id, status) VALUES ($1, 'placed')", [order.id]);
  await broker.publish("OrderPlaced", order);   // separate system, separate call
}
Quiz

This handler is the dual write the unit exists to kill. Where is the gap, and what is the minimal fix?

Snippet 2 — the outbox schema

CREATE TABLE outbox (
  id          uuid PRIMARY KEY,
  aggregate   text        NOT NULL,
  event_type  text        NOT NULL,
  payload     jsonb       NOT NULL,
  created_at  timestamptz NOT NULL DEFAULT now(),
  sent_at     timestamptz                          -- NULL until the relay marks it sent
);
CREATE INDEX outbox_unsent ON outbox (created_at) WHERE sent_at IS NULL;
Quiz

Why is the partial index `WHERE sent_at IS NULL` the right shape for the relay's workload?

Snippet 3 — the relay claim query

BEGIN;
SELECT id, event_type, payload
  FROM outbox
 WHERE sent_at IS NULL
 ORDER BY created_at
 LIMIT 100
 FOR UPDATE SKIP LOCKED;
-- publish each row to the broker, then:
UPDATE outbox SET sent_at = now() WHERE id = ANY($claimed_ids);
COMMIT;
Quiz

Three relay replicas run this exact query concurrently. What does FOR UPDATE SKIP LOCKED buy you, and what guarantee still does NOT hold?

Snippet 4 — the consumer

async function onOrderPlaced(db, event) {
  // event.id is the stable outbox row id, carried through the broker
  await db.query("INSERT INTO orders_processed (event_id) VALUES ($1)", [event.id]);
  await chargeCard(event.order);
}
Quiz

Delivery is at-least-once, so this consumer can receive the same event twice. What is wrong, and how do you make it idempotent?

Recap

Every outbox review reads the same way in code: the bare handler shows the dual-write gap a crash exposes; the schema’s partial index keeps the relay’s unsent-row poll cheap as the table grows; the claim query’s FOR UPDATE SKIP LOCKED lets replicas scale out without double-publishing, yet the publish-then-mark gap keeps delivery at-least-once; and the consumer closes the loop with a unique event id plus ON CONFLICT DO NOTHING so a replay charges the card exactly once. Find the gap, make the intent durable, claim rows disjointly, dedupe downstream.

Continue the climb ↑Outbox pattern: build a crash-safe event pipeline
shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.