cswork / libraryOpen practice workspace

Articles

Transactional Outbox Interview Questions: Relay Failures, Ordering, and Deduplication

Practice transactional outbox interviews with relay crash timelines, atomic inbox deduplication, aggregate ordering, CDC trade-offs, and safe replay retention.

The order committed, but the event never reached the broker. Moving the publish before the commit creates the opposite failure: consumers can hear about an order that later rolls back. Changing the order of those calls does not make the database and broker commit atomically.

A transactional outbox stores the business change and the intent to publish in one database transaction. A relay publishes committed events afterward. That closes the producer's dual-write gap, but relay retries can duplicate events, concurrent relays can reorder them, and consumers still need a durable recovery contract.

Evidence boundary: official vendor documentation and the original pattern reference support the mechanism. The crash fixture, ordering counterexample, and recommendations below are original exercises. No verified candidate reports establish them as a particular company's interview questions.

Which writes become atomic?

Documented pattern: save the business state and outbox entry within one transaction, then have a separate process publish the event. AWS's transactional outbox guidance explains the dual-write problem and warns that duplicate delivery still needs handling.

For an order example, the database transaction inserts order O-17 and event E-17. Either both commit or neither does. The relay sees committed publication intent; it does not need to reconstruct it from an unreliable application log.

The boundary matters. If the order is in one database and the outbox is in another without a shared atomic transaction, calling the second table “outbox” has not solved the original problem. Likewise, committing the order and then inserting an outbox row in a later transaction leaves a crash window.

Keep the event payload tied to the committed transition. If the relay reads only an order ID and later reconstructs the payload from mutable current state, it may publish a different historical meaning from the event intended at commit time. Store the required immutable data or explicitly define the event as a notification to retrieve current state.

The broader message queue interview guide covers delivery terminology. Here, trace the database and relay boundaries instead of repeating generic queue guarantees.

What belongs in the outbox record?

Original schema exercise: give each event a stable event ID, aggregate ID, aggregate version, event type, schema version, payload, and creation time. A polling relay may also need attempt information, claim ownership, lease expiry, and publication state. A CDC-based design can use a different operational schema.

The fields answer different questions. event_id identifies the event across retries. aggregate_id identifies the business entity whose events may require ordering. aggregate_version expresses a sequence under an explicitly enforced concurrency rule. schema_version identifies how to interpret the payload.

Do not generate a new event ID every time the relay retries. That turns repeated delivery of one event into several apparently independent events. Also keep transport delivery identifiers separate from the durable application event ID; their scopes and lifetimes may differ.

If two legitimate events happen to have identical payloads, they can still be different events. Deduplicating solely by payload hash can suppress a valid repeated business action. Decide what counts as the same business event before choosing its deduplication key.

The relay crashed after the broker confirmed: now what?

Consider this sequence:

  1. The relay reads committed E-17 as pending.
  2. It publishes E-17 and receives the required broker confirmation.
  3. It crashes before committing the outbox's sent marker.
  4. After recovery, E-17 is still pending and is published again.

Pattern fact: this duplicate window is a known consequence of the outbox relay, described in the transactional outbox pattern reference. Marking sent before publication trades the duplicate risk for a loss window.

Do not say the first copy could not have reached a consumer. The broker may already have delivered it. Official distinction: RabbitMQ publisher confirms and consumer acknowledgements cover different interactions and are independent of each other. See RabbitMQ's acknowledgement documentation.

A missing confirmation is ambiguous too: the broker may have accepted the event while the confirmation was lost. Retrying with the same event ID makes that uncertainty recoverable when downstream processing is idempotent.

State the broker acceptance condition your design relies on, including durability and routing configuration. “The publish method returned” is not universally equivalent to durable acceptance. The guarantee depends on the client API and broker configuration being used.

Where must consumer deduplication commit?

Original consumer design: use an inbox table with a uniqueness constraint on (consumer_name, event_id). Commit insertion into that table and the corresponding database effect in the same transaction. Acknowledge the broker only after that commit succeeds.

Conceptually:

BEGIN
  insert inbox(consumer_name, event_id)
  if that exact key already exists:
      perform no new business effect
  otherwise:
      apply the event's database effect
COMMIT
acknowledge this delivery

Use a conflict-aware insert for the intended unique key. Do not catch every database exception and treat it as a duplicate: a timeout, permission error, or unrelated constraint failure is not evidence that processing already succeeded.

The ordering of durable actions creates three important cases. A crash before commit leaves neither inbox record nor effect. A crash after commit but before acknowledgement leaves both; redelivery should observe the inbox entry and skip the effect. Acknowledging before commit can lose the work if the transaction later fails.

The consumer name scopes the deduplication record. Two independent projections may each need to process E-17 once. A single global “processed” bit can incorrectly prevent the second projection from doing its legitimate work.

This protects effects in the same transaction boundary. Sending email or charging an external provider cannot be made atomic merely by placing an inbox insert beside an HTTP call. Use an appropriate downstream idempotency contract, another durable intent, and reconciliation for ambiguous external outcomes.

Reproduce the crash window with a small fixture

Local experiment: a SQLite fixture created an orders table, an outbox, an inbox, and a projection counter. A Python list represented broker acceptance so the crash schedule could be controlled. This is a transaction-state exercise, not a test of a real broker's durability.

The producer test raised an exception after both inserts but before commit. Both rows were absent afterward. The successful transaction then committed order O-17 and event E-17 together.

The relay appended E-17 to the simulated broker and raised an exception before marking it sent. On the next relay run, the simulated broker contained E-17 twice. This deliberately reproduces the duplicate window rather than trying to prevent it with a fortunate schedule.

The consumer used this core SQLite operation inside the same transaction as its projection update:

INSERT OR IGNORE INTO inbox(consumer, event_id)
VALUES ('projection-v1', 'E-17');

In the fixture's constrained schema, an inserted-row count of zero means that exact inbox key already exists. Production code should use a database-specific conflict target and validate all other errors rather than broadly ignoring constraints.

Observed results: a crash before the consumer commit rolled back both inbox insertion and projection increment. A subsequent delivery committed the increment. Redelivery after that commit found the existing inbox entry, leaving the final projection count at one despite two accepted copies of the event.

To extend this test, inject an error after the business update but before commit, and verify that both changes roll back. Then model a lost acknowledgement by redelivering after a successful commit. Assertions should inspect durable rows and effects, not just whether the handler returned without an exception.

Why can two relays break aggregate ordering?

Original counterexample: order O-17 has committed events version 1 and version 2. Relay A claims version 1 and pauses. Relay B skips A's locked row, claims version 2, and publishes immediately. A then resumes and publishes version 1.

Both workers avoided claiming the same row, but publication order is now 2, 1. An ORDER BY in each polling query does not prevent this interleaving.

Official fact: PostgreSQL documents that SKIP LOCKED skips rows that cannot be locked immediately and can support queue-like consumers; it provides an inconsistent view of the data. It is a work-distribution tool, not a per-aggregate sequencing guarantee. See PostgreSQL's SELECT locking clause.

Design inference: choose an ordering mechanism independently. Serialize relay work per aggregate, claim only its earliest outstanding event, or give consumers a defined strategy for detecting gaps and handling out-of-order arrivals. Preserve parallelism across unrelated aggregates where possible.

A broker key cannot repair publication order that was already reversed. Putting both events in the same partition can preserve their arrival order while preserving the wrong business sequence. Assign aggregate versions under a concurrency rule such as a locked aggregate update; do not assume timestamps or globally allocated IDs represent commit order.

If a consumer sees version 2 while expecting 1, decide whether to buffer, retry, rebuild from authoritative state, or reject for investigation. Simply keeping the largest version is safe only when the event semantics support replacing older state; it can lose necessary incremental effects.

What changes when the relay uses CDC?

Official implementation example: Debezium's outbox event router uses the event id as a header and, by default, aggregateid as the emitted message key. Its documentation connects the key to ordering within Kafka partitions. See the Outbox Event Router reference.

CDC changes how committed outbox records are discovered and routed. It does not remove the need to define stable event identity, connector recovery, consumer deduplication, or external side-effect handling. Nor does selecting an aggregate key prove that every producer and consumer preserves the required business order.

A polling implementation needs a recovery policy for abandoned claims. A CDC implementation needs a recovery policy for connector offsets and retained database logs. Compare those operational dependencies, latency requirements, and ownership costs instead of describing one as automatically “exactly once.”

When can you delete outbox and inbox records?

Editorial recovery rule: cleanup must follow the maximum supported replay horizon and the durable recovery evidence. A sent marker alone is not proof that every downstream effect completed, while retaining every row forever creates its own storage and operational burden.

Define what replay means: relay retry, broker redelivery, dead-letter replay, connector restart, or rebuilding a projection from an archive. If inbox deduplication expires before old events can reappear, a previously applied event can become new again to that consumer.

For a fresh projection rebuild, use an intentional consumer identity or rebuild namespace. Reusing the live consumer's inbox may skip all historical work; casually clearing it may duplicate live effects. Separate reconstructible projections from irreversible external actions.

Monitor oldest unpublished event age, expired claims, publication failures, duplicate detections, aggregate gaps, and replay outcomes. Event age is often more actionable than row count: ten stuck payment events can matter more than thousands of harmless recent events.

Practice relay and recovery reasoning

Name the commit boundary and recovery evidence before proposing a queue product.

Practice questionWhat to rehearse
Unify Notification Services Without Duplicate DeliveryDefine durable identity across repeated delivery.
Design an Ordering Service That Prevents the Last Item from Being OversoldCommit business state and publication intent together.
Deduplicate and Order Batch and Streaming LogsDistinguish duplicate detection from ordering.
Design a bank account ledgerIdentify effects that must commit in one transaction.
Design an Idempotent Payment Processing PlatformHandle external side effects and ambiguous timeouts.

Choose one Backend Engineer question and place a crash between every durable step. For each crash, state whether recovery can lose work, repeat work, or reorder work, then identify the mechanism that handles that specific outcome.

Sources and Further Reading

Checked September 21, 2026. The local fixture tests SQLite transactions and a simulated broker. Broker durability, CDC recovery, and ordering guarantees require verification against the deployed products and configuration.