GH GambleHub

Idempotence and keys

What is idempotency

Idempotency is a property of an operation in which repeating with the same identifier does not change the final effect. In distributed systems, this is the main way to make the result equivalent to "exactly one processing," despite retrays, duplicate messages, and timeouts.

Key idea: each potentially repeatable operation should be marked with a key by which the system recognizes "this has already been done" and applies the result no more than once.

Where it matters

Payments and balances: write-offs/credits by 'operation _ id'.
Reservations/quotas/limits: same slot/resource.
Webhooks/notifications: repeated delivery should not duplicate the effect.
Import/Migration - Re-run files/packages.
Stream processing: duplicates from broker/CDC.

Types of keys and their scope

1. Operation key - ID of the specific attempt of the business transaction

Examples: 'idempotency _ key' (HTTP), 'operation _ id' (RPC).
Scope: service/aggregate; is stored in a deduplication table.

2. Event key - unique identifier of the event/message

Examples: 'event _ id' (UUID), '(producer_id, sequence)'.
Area: consumer/consumer group; protects projections.

3. Business key - natural domain key

Examples: 'payment _ id', 'invoice _ number', '(user_id, day)'.
Area: aggregate; used in uniqueness/version checks.

💡 Often used together: 'operation _ id' protects the command, 'event _ id' - delivery, 'business key' - invariants of the aggregate.

TTL and Retention Policy

TTL keys ≥ a possible redo window: log retention + network/process delays.
For critical domains (payments) TTL - days/weeks; for telemetry - hours.
Clean dedup tables with background jobs; for audit - archive.

Key stores (deduplication)

Transactional database (recommended): reliable upsert/unique indexes, joint transaction with effect.
KV/Redis: fast, convenient for a short TTL, but without a joint transaction with OLTP - careful.
State store stream processor: locally + changelog in broker; good at Flink/KStreams.

Diagram (option in DB):
  • idempotency_keys

`consumer_id` (или `service`), `op_id` (PK на пару), `applied_at`, `ttl_expires_at`, `result_hash`/`response_status` (опц.) .

Indexes: '(consumer_id, op_id)' - unique.

Basic implementation techniques

1) Effect + Progress Transaction

Record result and capture read/position progress in one transaction.

pseudo begin tx if not exists(select 1 from idempotency_keys where consumer=:c and op_id=:id) then
-- apply effect atomically (upsert/merge/increment)
apply_effect(...)
insert into idempotency_keys(consumer, op_id, applied_at)
values(:c,:id, now)
end if
-- record reading progress (offset/position)
upsert offsets set pos=:pos where consumer=:c commit

2) Optimistic Concurrency (unit version)

Protects against double effect when racing:
sql update account set balance = balance +:delta,
version = version + 1 where id=:account_id and version=:expected_version;
-- if 0 rows are updated → retry/conflict

3) Idempotent sinks (upsert/merge)

Accrue Once:
sql insert into bonuses(user_id, op_id, amount)
values(:u,:op,:amt)
on conflict (user_id, op_id) do nothing;

Idempotency in protocols

HTTP/REST

'Idempotency-Key: <uuid'hash>' header.
The server stores the key record and returns the same response again (or code '409 '/' 422' in case of invariant conflict).
For "insecure" POST, 'Idempotency-Key' + stable timeout/retray policy is required.

gRPC/RPC

Metadata 'idempotency _ key', 'request _ id' + deadline.
Server implementation - as in REST: a table of deduplication in a transaction.

Brokers/Streaming (Kafka/NATS/Pulsar)

Producer: stable 'event _ id '/idempotent producer (where supported).
Consumer: dedup by '(consumer_id, event_id)' and/or by business version of the aggregate.
Separate DLQ for non-idempotent/corrupt messages.

Webhooks and external partners

Demand 'Idempotency-Key '/' event _ id' in the contract; re-delivery must be safe.
Store 'notification _ id' and sending statuses; at retray - do not duplicate.

Key design

Determinism: Retrays must send the same key (generate in advance on the client/orchestrator).
Scope: Form 'op _ id' as' service: aggregate: id: purpose '.
Collisions: use UUIDv7/ULID or hash from business parameters (with salt if necessary).
Hierarchy: the general 'operation _ id' at the → front is translated into all suboperations (idempotent chain).

UX and Product Aspects

A repeated key request must return the same result (including body/status) or an explicit "already executed."

Show the user the statuses "operation is being processed/completed" instead of trying again "for good luck."

For long operations - polling by key ('GET/operations/{ op _ id}').

Observability

Log 'op _ id', 'event _ id', 'trace _ id', outcome: 'APPLIED '/' ALREADY _ APPLIED'.
Metrics: repetition rate, dedup table size, transaction time, version conflicts, DLQ rate.
Trace: the key must pass through the command → event → projection → external call.

Safety and compliance

Do not store PII in keys; key - identifier, not payload.
Encrypt sensitive fields in deduplication records with long TTL.
Retention policy: TTL and archives; right to be forgotten - through crypto-erasure of responses/metadata (if they contain PII).

Testing

1. Duplicates: run one message/request 2-5 times - effect exactly one.
2. Drop between steps: before/after recording the effect, before/after fixing the offset.
3. Consumer restart/rebalance: no dual use.
4. Competition: parallel queries with one'op _ id '→ one effect, the second -' ALREADY _ APPLIED/409 '.
5. Long-lived keys: Checks for TTL expiration and retries after recovery.

Anti-patterns

Random new key for each retray: the system does not recognize replays.
Two separate commits: first the effect, then the offset - the fall between them duplicates the effect.
Trusting only the broker: no deduplication in the bruise/aggregate.
Missing aggregate version: repeated event changes state a second time.
Fat keys: the key includes business fields/PII → leaks and complex indexes.
No repeatable responses: Client cannot safely retract.

Examples

Payment POST

Customer: 'POST/payments' + 'Idempotency-Key: k-789'.
Server: transaction - creates a'payment' and an entry in 'idempotency _ keys'.
Redo: returns same '201 '/body; in case of invariant conflict - '409'.

Bonus accrual (sink)

sql insert into credits(user_id, op_id, amount, created_at)
values(:u,:op,:amt, now)
on conflict (user_id, op_id) do nothing;

Projection from events

The consumer stores' seen (event_id) 'and' version'of the unit; repeat - ignore/idempotent upsert.
Read progress is captured in the same transaction as the projection update.

Production checklist

  • All unsafe operations have an idempotent key and its scope defined.
  • There are deduplication tables with TTLs and unique indexes.
  • The effect and progress of reading are committed atomically.
  • Optimistic competition (version/sequence) is included in the write model.
  • API contracts capture 'Idempotency-Key '/' operation _ id' and repetition behavior.
  • Metrics and logs contain 'op _ id '/' event _ id '/' trace _ id'.
  • Tests for duplicates, falls and races - in CI.
  • TTL/Archive policy and PII security are followed.

FAQ

How is' Idempotency-Key'different from' Request-Id '?
'Request-Id '- trace; it can be changed on retrays. 'Idempotency-Key' is the semantic identifier of the operation, which is required to be the same during repetitions.

Is it possible to do idempotence without a database?
For a short window - yes (Redis/in-process cache), but without a joint transaction, the risk of duplicates increases. In critical domains, it is better in one database transaction.

What to do with external partners?

Negotiate keys and repeatable responses. If the partner does not support - wrap the call in your idempotent layer and store "already applied."

How to choose TTL?
Sum the maximum delays: log retention + net/rebalance worst-case + buffer. Add stock (× 2).

Total

Idempotency is a discipline of keys, transactions, and versions. Stable operation identifiers + atomic fixation of effect and reading progress + idempotent sinks/projections give "exactly one effect" without transport-level magic. Make keys deterministic, TTL realistic and tests malicious. Then the retrays and duplicates will become routine, not incidents.

Contact

Get in Touch

Reach out with any questions or support needs.We are always ready to help!

Start Integration

Email is required. Telegram or WhatsApp — optional.

Your Name optional
Email optional
Subject optional
Message optional
Telegram optional
@
If you include Telegram — we will reply there as well, in addition to Email.
WhatsApp optional
Format: +country code and number (e.g., +380XXXXXXXXX).

By clicking this button, you agree to data processing.