What they're testing
Whether you recognise this as unsolvable head-on, and whether you know the standard pattern for it.
The short answer~30 seconds
You can't commit two systems atomically without a distributed transaction, and those are expensive and brittle. The outbox pattern: in the SAME transaction as the business change, insert a row into an outbox table. A separate process reads that table, publishes to the broker, and marks it sent. If that process dies mid-way it republishes — so the consumer must tolerate duplicates, which is at-least-once plus an idempotent consumer.
The long answer
The reason is that writing to two places has four possible orderings and three of them are wrong. Write DB then publish: the message is lost if the process dies just after commit. Publish then write DB: the write can fail and you've announced an event that never happened. Wrapping both in try/catch helps nothing, because the process can die in between. The outbox reduces the problem to one transaction on one system, and that's the whole trick.
There are two ways to read the outbox. Polling: a job scans unsent rows every few hundred milliseconds — simple, easy to operate, a few hundred ms of latency. Change data capture: read the WAL directly with Debezium — lower latency and no query load, at the cost of another piece of infrastructure to run. For most systems, polling with FOR UPDATE SKIP LOCKED is enough and far less work.
The obligatory caveat: an outbox gives at-least-once, NOT exactly-once. Consumers will see duplicates, sooner or later. So every consumer must be idempotent — record processed message ids, or design the operation so repetition changes nothing. Answering "outbox" without mentioning this means you've read about it rather than run it.
One last operational detail: ordering. If the business needs events for one entity to arrive in order, you must partition by entity id and process each partition serially; publishing the whole outbox in parallel will reorder them. This is routinely discovered late, when an updated event arrives before its created.
BEGIN;
UPDATE orders SET status = 'paid' WHERE id = $1;
INSERT INTO outbox (aggregate_id, type, payload)
VALUES ($1, 'OrderPaid', $2); -- cùng transaction, cùng số phận
COMMIT;
-- Tiến trình đẩy, chạy song song nhiều worker mà không giẫm chân nhau:
SELECT * FROM outbox WHERE sent_at IS NULL
ORDER BY id LIMIT 100 FOR UPDATE SKIP LOCKED;What they'll ask next
?What about two-phase commit?
It solves it in theory and is rarely used in practice: it requires XA support on every participant, holds resources locked across both phases, and if the coordinator dies mid-way participants hang in doubt. Kafka and most modern brokers don't support XA, so the question usually closes itself there.
?Doesn't the outbox table grow unbounded?
It does, without housekeeping. Delete sent rows past a retention window in batches, or partition by day and drop whole partitions — far cheaper than DELETE and it leaves no dead tuples.
These lose points
- "Publish inside
@Transactionaland let Spring handle it." Spring can't commit a broker together with a database. - Promising exactly-once without idempotent consumers. There's no exactly-once delivery, only effectively-once processing.