Core Concept

Idempotency & Exactly-Once Effects

Networks retry; brokers redeliver. Idempotency keys, dedup stores, and idempotent handlers turn at-least-once delivery into safe, effectively-once side effects — the foundation of payments, sagas, and reliable event publishing.


1. What It Is

Networks retry — so your API will see duplicate requests. We design for at-least-once delivery and make handlers idempotent so duplicates produce the same effect as one call.

What:

A design property where executing the same operation multiple times produces the same outcome as executing it once — enforced via client-supplied idempotency keys, server-side dedup stores, and naturally idempotent operations.

Primary purpose:

Make retries, duplicate deliveries, and crash recovery safe without requiring perfect exactly-once network delivery (which does not exist at scale).

Usually used for:

Payment charges, inventory reservations, webhook processing, saga compensations, CDC sink writes, and any mutating API behind an unreliable client or message broker.

2. Core Mental Model

Separate delivery guarantee (how many times a message arrives) from effect guarantee (how many times the world changes). Interview answer: "We use at-least-once delivery with idempotent handlers — that gives us effectively-once side effects."

🔑 Idempotency key

Client generates a unique key per logical operation (UUID). Server stores key → result mapping before executing side effects.

📦 Dedup store

Redis, DB unique index, or in-process cache that answers "have we already processed this key?" in O(1).

♻️ Natural idempotency

PUT with full resource state, DELETE, SET key=value, UPSERT ON CONFLICT — safe to repeat without extra keys.

In the room

Exactly-once end-to-end is marketing — say at-least-once delivery plus idempotent consumers. Idempotency keys on POST, dedup tables, or natural keys (payment_id) are the concrete tools. Connect to outbox and saga compensations.

3. Why It Matters in HLD

Distributed systems duplicate messages — idempotency keys and dedup make retries safe. Three lenses:

Needed When:

Money moves, inventory decrements, tickets are issued, or any workflow spans multiple services with retries and async handoffs.

Avoids:

Double charges, duplicate bookings, inflated counters, repeated webhook side effects, and saga compensation storms.

Optimizes For:

Resilient client UX (safe retries), broker simplicity (at-least-once is enough), and operational recovery without manual reconciliation.

4. Architecture & Data Flow

Walk payment retry as interview steps. Step 1 — Client sends: POST with Idempotency-Key UUID. Step 2 — Server check: lookup key in idempotency store. Step 3 — First seen: process payment, store result keyed by idempotency key. Step 4 — Duplicate: return cached result without re-charging. Step 5 — TTL: expire keys after 24h; state at-least-once + idempotent consumer model.

Loading...

Idempotent handler skeleton

PYTHON
def charge_payment(idempotency_key: str, amount: int):
    cached = dedup_store.get(idempotency_key)
    if cached:
        return cached  # duplicate request — return same response

    if not dedup_store.setnx(idempotency_key, "processing", ttl=86400):
        return wait_for_inflight(idempotency_key)  # concurrent duplicate

    try:
        result = payment_processor.charge(amount)
        dedup_store.set(idempotency_key, result, ttl=86400)
        return result
    except Exception:
        dedup_store.delete(idempotency_key)  # allow retry on failure
        raise

In the room

Say "at-least-once delivery with idempotent consumers" — exactly-once end-to-end is marketing; interviewers want honest semantics plus dedup design.

5. Key Characteristics

At-most-once, at-least-once, exactly-once effects — delivery semantics we compare:

  • Delivery vs effect guarantees — what the transport promises vs what your system must enforce:
GuaranteeMechanicTradeoff
At-most-once
  • Fire-and-forget
  • no retry on failure
Messages or requests may be lost — acceptable for metrics, not money
At-least-once
  • Retry until ACK
  • consumer may see duplicates
No loss, but handlers must be idempotent or deduped
Exactly-once (effect)At-least-once transport + idempotent sink or transactional commit
  • Achievable for side effects
  • true end-to-end EOS is expensive
  • Dedup store options — pick by durability and retention requirements:
StoreLookupBest For
Redis SETNX + TTLO(1) in-memoryShort-lived API idempotency (24h payment keys)
DB unique index on idempotency_keyDurable, queryable audit trailLedger-grade dedup with compliance retention
Bloom filter (probabilistic)
  • Fixed memory
  • false positives only
High-volume stream dedup where rare double-process is tolerable
  • Idempotency key rules: client-generated UUID v4 or deterministic hash of (user_id + operation + payload fingerprint); scope keys per tenant; TTL ≥ max client retry window (typically 24h for payments).
  • Concurrent duplicates: two requests with the same key in flight — use SETNX "processing" state or DB unique constraint to serialize; second waiter polls or blocks briefly.

6. Strategic Tradeoffs

Safe retries trade storage for dedup records and design discipline — we state both:

BenefitCost
Safe retries — clients, gateways, and brokers can replay without double-charging or double-bookingStorage overhead — every mutating endpoint needs a dedup record with TTL or compaction policy
Simpler distributed design — accept at-least-once delivery instead of chasing impossible global 2PCHandler discipline — every side effect (DB write, charge, email) must be designed idempotent from day one

Chasing true end-to-end exactly-once (Kafka transactional EOS + DB 2PC) adds latency and operational complexity. In most interviews, at-least-once + idempotent sink is the pragmatic answer unless the prompt explicitly demands ledger-grade EOS.

7. Failure / Bottleneck Awareness

Duplicate charges, lost updates, and idempotency store races — we volunteer mitigations:

💸 The Orphan Charge (Timeout After Success)

Problem: Server charges the card, crashes before caching the idempotency result. Client retries with the same key — without dedup, user is charged twice.

Mitigation: Write idempotency record to durable store before calling the payment processor, or use processor-native idempotency keys (Stripe, Adyen) that dedupe on their side.

🔒 Stuck "Processing" State

Problem: Handler dies after SETNX "processing" but before completion. Retries block forever waiting for a result that will never arrive.

Mitigation: TTL on processing markers (5–30 min), background sweeper job to reconcile in-flight keys against processor state, or lease-based locks with heartbeat.

📈 Dedup Store Hot Keys

Problem: Flash sale concentrates millions of distinct idempotency keys on one Redis shard during checkout burst.

Mitigation: Shard dedup store by key hash, cap key cardinality per user, or use local in-process dedup for ultra-short windows plus DB unique index for durability.

8. Common HLD Usage

Payments, webhooks, and queue consumers require idempotency in HLD discussions:

Production SystemPatternRationale
Stripe / Payment APIsIdempotency-Key header on POST
  • Mobile clients retry on timeout
  • gateway returns cached charge response for duplicate keys within 24h window.
Kafka consumer pipelinesUpsert by primary key + offset checkpoint
  • Consumer redelivery after crash replays events
  • INSERT ON CONFLICT or SET by doc_id makes reprocessing safe.
Temporal / workflow enginesActivity idempotency key = hash(workflow_id, activity_id, attempt)
  • Activities run at-least-once
  • downstream APIs receive deterministic keys so replays return cached results.

9. Decision Signals

Add idempotency keys when POST operations must survive client retries or message redelivery:

🎯 Design for idempotency when:
  • POST creates side effects — charges, bookings, inventory holds — not just cache writes.
  • Retries are expected — mobile networks, gateway timeouts, circuit-breaker half-open probes.
  • Message consumers ACK after processing — crash between process and ACK redelivers the message.
  • Saga compensations run multiple times — compensating actions must be safe to repeat (concept #11).
⏭️ Skip explicit dedup when:
  • Operation is naturally idempotent (PUT full state, DELETE, SET).
  • Duplicate processing is harmless (append-only analytics event with downstream dedup by event_id).
  • At-most-once is acceptable (fire-and-forget metrics).

11. Deep Dive (Optional)

Transactional Outbox + Idempotent Consumers

The transactional outbox pattern (see concept #30) writes business state and an outbox event row in one local DB transaction. A CDC relay (Debezium) or polling publisher delivers events to Kafka at least once. Downstream consumers achieve effectively-once processing by:

  1. Storing processed event_id in a dedup table (or Redis SET with TTL).
  2. Skipping handler logic if event_id already exists.
  3. Using idempotent sink writes: INSERT ON CONFLICT DO UPDATE, Elasticsearch upsert by doc_id, Redis SET.

This stacks cleanly with concept #11: saga orchestrators emit compensating events through the same outbox, and compensations carry their own idempotency keys so rollback retries do not double-refund.

Idempotency Key Storage Schema

SQL
CREATE TABLE idempotency_keys (
  key           VARCHAR(64) PRIMARY KEY,
  request_hash  VARCHAR(64) NOT NULL,  -- reject key reuse with different body
  status        ENUM('processing','completed','failed'),
  response_body JSONB,
  created_at    TIMESTAMPTZ DEFAULT now(),
  expires_at    TIMESTAMPTZ NOT NULL
);
CREATE INDEX idx_idempotency_expires ON idempotency_keys(expires_at);

Reject requests where the same key arrives with a different payload hash (HTTP 422). Background job deletes expired rows. For high QPS, front Redis with SETEX idempotency:{key} and async flush to Postgres for audit.

Non-Idempotent Operations Made Safe

  • Counter increment → store "applied event_ids" set; only increment if event_id not seen.
  • Send email → dedup by (template_id, recipient, idempotency_key); email provider may also dedupe.
  • Reserve seat → conditional UPDATE WHERE seats_available > 0 + unique (show_id, seat_id) constraint; retry returns existing reservation for same key.
  • Append to event log → event sourcing (problem #34 Event Sourcing System) uses deterministic event IDs so replays are no-ops.

Kafka Exactly-Once Processing (When to Mention)

Kafka's transactional producer (enable.idempotence=true) + transactional consume-process-produce gives EOS within the Kafka cluster. External side effects (DB, HTTP) still need idempotent handlers. Interview framing: "EOS in Kafka prevents duplicate writes to output topics; we still idempotency-key external API calls."

💬Review

Help Us Improve

How helpful was this walkthrough?

Click a star to rate. We actively use this feedback to refine and update our system design content.

Placeholder
Optional but highly appreciated!

Discussion

Share your thoughts, ask questions, or help others.

Loading comments...