Skip to content

The warehouse processed the event. Why did it arrive again?

One order, five failure boundaries · Part 3 of 5 · 7 min read

Read the worked explanation ↓

Interview question. A warehouse consumer reads OrderCreated, reserves stock in its relational database, and then commits its broker offset. It crashes after the reservation commits but before the offset is saved. The event is delivered again. Where should progress be recorded, and how do you avoid reserving a second lamp? What changes if the reservation is performed by an external provider API?

Assume stable event IDs and a database that supports atomic transactions and unique constraints. The broker and warehouse database do not share one transaction.

Think first

A warehouse consumer commits a stock reservation, crashes before saving its Kafka offset, and receives the same event again. Design the local database boundary and describe the external-provider version.

Take a few minutes to form an approach, then compare it with the worked explanation.

Reveal the worked explanation

Choose which failure you want

A consumer needs two things to happen. It must perform the business effect and it must record that the message is finished. Putting them in either order still leaves a crash window.

Order of operationsCrash between themWhat recovery sees
Commit offset, then reserve stockThe event may be skippedMissing reservation
Reserve stock, then commit offsetThe event is read againPossible duplicate reservation

For an ordinary at-least-once consumer, I prefer to complete the business transaction before advancing the offset. The second failure is manageable when the handler recognizes work it already completed. The first risks silently losing work.

Say Kafka delivered offset 420. The warehouse reserves stock and commits, but its saved restart position is still 420 rather than 421. After a crash, another consumer resumes at 420. Kafka is behaving as expected. Its committed offset records consumer progress, not what happened inside the warehouse database.

Store proof of the effect with the effect

I would keep a processing receipt in the warehouse database. Its unique identity is scoped to the tenant, the logical handler, and the event ID:

tenant_id + handler_name + event_id

The handler is a stable purpose such as inventory-reservation, not the current worker's hostname. Analytics and inventory may both process the same event, so they should not suppress each other. Within one warehouse transaction, try to claim the receipt, conditionally reserve stock, and record the outcome. A repeated delivery finds the committed receipt and returns without reserving again.

The receipt must not be committed in advance of the business effect. Otherwise a crash could leave proof of completion with nothing completed. The database's uniqueness constraint also matters when two workers briefly overlap during a rebalance. A process-local set of seen IDs cannot arbitrate that race.

An insufficient-stock decision can be a terminal outcome too. Store it so redelivery does not unexpectedly change yesterday's rejection into today's reservation. A transient database error is different. Roll back and allow recovery to retry it.

There is a second invariant that the receipt does not cover. Two different orders can both race to reserve the last available unit. Deduplicating each event does nothing to prevent overselling across orders. The inventory decrement still needs a conditional update or another transactionally enforced allocation rule.

For additional protection, the fulfillment record should have a stable business identity such as (tenant_id, order_id, fulfillment_version). That catches a producer that mistakenly creates two event IDs for the same fulfillment intent. The event receipt and business uniqueness rule protect different boundaries.

Committing offsets safely

After the database transaction resolves, the consumer may save its broker progress. If it crashes first, delivery repeats and the receipt makes the replay harmless. This gives at most one committed effect per retained identity, provided the local transaction and its records survive. Eventual completion still depends on retries, capacity and a recoverable broker history.

If you add parallel processing within a partition, the offset question becomes more interesting. Record 420 finishes. Record 421 is slow. Record 422 finishes. Saving 423 as the restart position would skip 421 after a crash. Advance only past the contiguous prefix of durably completed records, taking account of the actual records delivered rather than assuming all integer offsets contain application data.

You can avoid that complexity initially by processing one partition serially. Or you can store the processed position together with the database effects and explicitly manage ownership when partitions move between workers. Neither choice magically deduplicates the same business event when it appears under another offset or in another cluster.

The interviewer changes one sentence

The warehouse now calls a third-party fulfillment API. Your database stores only workflow state.

Our local transaction cannot roll back the provider's reservation. The provider may commit it, the response may disappear, and our transaction may then abort. Treating the call as a normal function inside a database transaction conceals that difference.

I would first persist a durable local fulfillment operation with a stable provider operation ID. A worker invokes the provider and stores the result. If the call times out, the operation becomes UNKNOWN, not FAILED. Recovery looks up the provider's outcome or retries with the same provider identity, but only if the provider's contract supports that safely.

The consumer can advance its broker offset after it has durably transferred responsibility to that local workflow, even if the provider has not yet finished. The workflow must have its own recovery worker. The user-visible status remains pending or unknown until the provider result is established.

If the provider offers no stable idempotency identity and no reliable lookup for an uncertain call, you cannot guarantee both that the reservation will eventually happen and that it will never happen twice. Another retry risks duplication. Refusing to retry risks omission. That limitation comes from the unknown external outcome, not the queue configuration.

The same reasoning applies to webhook receivers and payment callbacks. The transport acknowledgment is a progress signal. The business outcome needs its own durable record.

This warehouse keeps generic processing receipts for 48 hours. An operator now proposes replaying 90 days of order events to rebuild a dashboard. Will the old receipt table protect the warehouse from those events?

Go deeper

The complications that show up at scale

Parallel workers and committed positions

Offsets identify positions inside a partition. In Kafka, the committed offset is the next position to resume, not the last business operation known to be successful. If messages finish out of order, record completion and advance only across a fully resolved prefix of the records actually delivered. Logs can contain offset gaps, so the tracker must be based on consumed records, not an assumption that every integer corresponds to an application message.

Rebalancing creates another race. The old worker may still be completing local work when its partition is assigned to a replacement. The receipt's unique constraint and business identity protect against overlapping effects in the shared database. If you store partition position inside that database, you also need a safe ownership or fencing protocol before accepting a position update from an old worker.

Kafka transactions can coordinate reading offsets and producing Kafka records as part of a Kafka transaction. They do not make a separate relational database or external HTTP provider an automatic participant. Never infer end-to-end exactly-once effects from a producer setting.

Dedupe identity versus business identity

Suppose a producer accidentally publishes OrderCreated twice under IDs e901 and e902. A receipt keyed by event ID allows each one to be processed once, but it sees two distinct events. If the desired rule is one reservation per order and fulfillment generation, enforce that as a separate business key in the warehouse transaction.

Conversely, different logical handlers may legitimately consume one event. Keeping the handler in the receipt scope prevents an analytics consumer's receipt from suppressing fulfillment.

External providers and the unknown state

The safe recovery design depends on the provider's contract. A stable idempotency key or reliable status lookup can let the local workflow establish whether a timed-out call succeeded. The key should be generated when the business operation is accepted, not anew on each worker attempt. Store it durably before making the call.

If the provider cannot answer and does not deduplicate requests, there is no client-only algorithm that guarantees both eventual success and no duplicated physical effect after an ambiguous timeout. The product needs an explicit pending or unknown state, a reconciliation process, and perhaps a manual intervention policy for truly irreversible work.

Check your reasoning

  • Identifies lost work from offset-first and duplicate effects from effect-first
  • Commits an event processing receipt and stock reservation together under a unique key
  • Separately prevents overselling across different order IDs
  • Tracks only contiguous completed progress under parallelism
  • Uses a durable external operation and an UNKNOWN state when calling an outside provider

Common mistake: Exactly-once Kafka publishing does not make external SQL and HTTP effects part of one transaction.

Record 420 and 422 have completed database effects, but 421 is still running. What is safe to save as the next Kafka restart position?

Read the answer explanation

421, provided 420 has durably completed and 421 has not — The committed restart position must not skip unresolved work. With record 420 completed and 421 still pending, position 421 is the first unresolved delivered record.

Apply it elsewhere

A billing consumer applies a credit to a customer account and sends a webhook to the customer. It crashes after committing the credit but before the webhook returns. Explain which effects can be handled atomically and which need separate recovery.

Compare your approach

Use one database transaction for the account credit, durable receipt, and outbox obligation to send the webhook. Send the webhook asynchronously with a stable delivery ID, acknowledge only according to the destination contract, and expect redelivery. External webhook delivery cannot be rolled back by the account transaction.

One more constraint

The same order appears with two distinct event IDs. Will a receipt keyed only by event ID prevent two reservations?

Quick revision

The answer in an interview

I would not commit the broker offset before the warehouse's business effect. That can lose work if a crash happens between them. Instead I would make repeating the handler safe. The warehouse transaction atomically commits a processing receipt with its inventory reservation, using a unique scope of tenant, logical handler and event ID.

When a crash occurs after the database commit but before the offset commit, the replacement reads the event again, finds the receipt, and does not reserve twice. Inventory also needs a separate business constraint so two different orders cannot oversell the same unit. Under parallel processing, only durably completed contiguous progress can be committed.

For an external fulfillment API, a local transaction is insufficient. Persist a fulfillment operation first, carry one stable provider operation ID, and represent an uncertain provider call as UNKNOWN until it can be reconciled. The broker offset can advance once the durable local workflow owns recovery, if that workflow is independently serviced.

Trap: Kafka transactions do not automatically roll back external HTTP side effects.

References

  • Design: Message Delivery Semantics · Apache Kafka 4.1 documentation

    Kafka transaction boundaries, cooperation from external output systems, and storing output with offsets.

  • KafkaConsumer · Apache Kafka 4.1 client API documentation

    Per-partition positions, committed offsets, consumer groups, and offset gaps. Version is pinned for reproducibility, not claimed to be latest.

  • Pattern: Idempotent Consumer · Chris Richardson, Microservices.io

    Processing identity committed in the same transaction as the local business effect.

  • Idempotent requests · Stripe API documentation

    Parameter comparison, replayed results, and behavior after key pruning. This is a Stripe-specific example, not a universal API contract.