Outbox, Kafka, idempotent consumers, a DLT and a waiting room for BoxOffice
order calls notification over HTTP before answering. The e-mail provider takes 50–500 ms and sometimes fails. At 10:00 the stadium show goes on sale. What can go wrong?
order
notification
fan → gateway → order → POST /notifications → SMTP → … → 201
POST /notifications
201
The request ends after the e-mail.
fan → waiting room → order → orders + outbox (1 tx) → 201
orders
outbox
relay → Kafka order-events → notification → Mailpit
order-events
failures → order-events-dlt
order-events-dlt
kafka
apache/kafka:4.3.1
kafka-init
kafka-ui
mailpit
kafka:9092
localhost:9094
--reset-offsets
INSERT order; COMMIT; then kafka.send(...) — a crash in between stores an order without its event. Reversed, you announce an order that does not exist.
INSERT order; COMMIT;
kafka.send(...)
acks=all
SELECT id, aggregate_id, event_type, payload::text AS payload FROM outbox WHERE published_at IS NULL ORDER BY created_at LIMIT 100 FOR UPDATE SKIP LOCKED
UPDATE ... published_at
SKIP LOCKED
Alternative: CDC (Debezium reads the WAL) — lower delay, more infrastructure.
INSERT INTO orders (...) VALUES (...) ON CONFLICT (hold_id) DO NOTHING
hold_id
UNIQUE
200
Idempotency is a design decision at every hop: API, relay, consumer.
INSERT INTO processed_events (event_id) ... ON CONFLICT DO NOTHING
E-mail sent, commit fails → a second e-mail on redelivery. External effects need the provider's idempotency key (eventId).
eventId
DefaultErrorHandler
ExponentialBackOffWithMaxRetries(3)
DeadLetterPublishingRecoverer
JacksonException
IllegalArgumentException
Non-blocking alternative: @RetryableTopic (retry topics) — no blocking, but no ordering.
@RetryableTopic
2 M fans, 60 k seats, an inventory that serves a few hundred holds per second. Who gets in first?
INCR
ZPOPMIN
POST /orders
X-Admission-Ticket
wait = position / rate
NOTIFY_MODE
k6 in open model (constant-arrival-rate); lag from kafka-consumer-groups.sh; delays from SQL.
constant-arrival-rate
kafka-consumer-groups.sh
Concurrency 1, 50 ms per e-mail, 100 orders/s for 60 s. Lag at the end? Time to drain?
event-fanout
async-queue
spike
Form a hypothesis in the simulator, then check it with k6 on your own services.
Outline: (1) why the e-mail must leave the request path; (2) Kafka in KRaft mode; (3) the dual-write problem and the outbox; (4) idempotent consumer; (5) retries and DLT; (6) waiting room; (7) what we measure.