Low-Level Design (Object-Oriented Design) Module 5 – Modelling lifecycles, time, money and rules
IDs, idempotency keys and deduplication
Choose IDs that sort, read well or stay unguessable, make retried requests safe with idempotency keys, and drop duplicate events in bounded memory.
What you will learn
- Choose random, time-ordered or readable IDs for each identifier a design needs
- Generate codes that people type, with a uniqueness check and a secure source when they open something
- Implement idempotency keys that return the stored reply when a request is retried
- Detect a key reused for a different request, or still running, and answer each case
- Handle duplicate events by event ID or by sender and sequence number, in bounded memory
Before you start
What you’ll learn · Before you start
What you will learn
- Choose random, time-ordered or readable IDs for each identifier a design needs
- Generate codes that people type, with a uniqueness check and a secure source when they open something
- Implement idempotency keys that return the stored reply when a request is retried
- Detect a key reused for a different request, or still running, and answer each case
- Handle duplicate events by event ID or by sender and sequence number, in bounded memory
Before you start
On this page
IDs, idempotency keys and deduplication answer one question in three places: how does a design recognise the same thing when it arrives again? An ID names one object for its whole life, and its format decides whether IDs sort by creation time, can be read out over a phone or can be guessed. An idempotency key names one attempt at an operation: the client sends it with a request and again with every retry, and the server answers a retry with the stored reply instead of doing the work twice. Deduplication drops an event that was already handled, by remembering recent event IDs or the last sequence number of each sender, for as long as copies can still arrive.
Why a design meets the same request twice
Networks and people repeat themselves. A phone that sends a payment and hears nothing for 30 seconds cannot tell
whether the request was lost or only its reply, so it sends the request again. A message broker that loses an
acknowledgement delivers the same event a second time, and people press Pay twice. Retrying is the only safe move
for a client that does not know the outcome, so the design has to make the repeat harmless.
capture_twice.py sends one card payment twice, because the first reply was lost:
"""One card payment, captured by a client whose first reply was lost, so it sends the same request again.
The trusting service charges every request it receives. The keyed service files each request under the
idempotency key the client sent with it: a retry with the same key gets the stored reply, and a request
that reuses a key for a different payment is refused. Amounts are integer minor units (paise)."""
class Gateway:
"""Stands in for the card network: every call moves money."""
def __init__(self):
self.charges = []
def charge(self, card, amount):
self.charges.append((card, amount))
return f"ch_{len(self.charges)}"
class KeyReused(Exception):
"""The key was first used for a different request."""
class TrustingCapture:
def __init__(self, gateway):
self.gateway = gateway
def capture(self, key, card, amount): # the key is ignored
return {"charge": self.gateway.charge(card, amount), "amount": amount}
class KeyedCapture:
def __init__(self, gateway):
self.gateway = gateway
self.replies = {} # idempotency key -> (the request it was first used for, the reply)
def capture(self, key, card, amount):
request = (card, amount)
if key in self.replies:
first, reply = self.replies[key]
if first != request:
raise KeyReused(f"key {key} was first used for amount {first[1]}")
return reply # a retry: the stored reply, and no second charge
reply = {"charge": self.gateway.charge(card, amount), "amount": amount}
self.replies[key] = (request, reply)
return reply
for service in (TrustingCapture(Gateway()), KeyedCapture(Gateway())):
first = service.capture("k-7f3a", "card-4242", 49_900) # this reply is lost on the way back
retry = service.capture("k-7f3a", "card-4242", 49_900) # so the client sends the request again
taken = sum(amount for _, amount in service.gateway.charges)
print(f"{type(service).__name__}: first {first['charge']}, retry {retry['charge']}, "
f"{len(service.gateway.charges)} charge(s), {taken} paise taken")
try:
service.capture("k-7f3a", "card-4242", 59_900) # same key, different amount
except KeyReused as refused:
print("Reused key with another amount: refused,", refused) Output
TrustingCapture: first ch_1, retry ch_2, 2 charge(s), 99800 paise taken KeyedCapture: first ch_1, retry ch_1, 1 charge(s), 49900 paise taken Reused key with another amount: refused, key k-7f3a was first used for amount 49900
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 capture_twice.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The trusting service charged the customer twice. The keyed service charged once, answered the retry with the reply it had stored, and refused a request that reused the key for another amount, because one key names one request. Idempotency keys only work when the service looks the key up before it does any work:
One request, one key, three paths
Text description of the diagram
A client sends a request together with an idempotency key, and sends the same key again whenever it retries after a timeout.
The service looks up the pair of client and key in its key store before doing any work, and takes one of three paths:
- Not found: a new key. The service claims it, does the work once (for example, charges the card and credits the wallet) and saves the reply under the key.
- Found, with the same request fingerprint: a retry. The service returns the saved reply, and nothing runs again.
- Found, with a different fingerprint: the key is being reused for another request. The service refuses it, because a key names exactly one request.
A design that stays correct under repeats rests on a few choices, and each one has a plain job:
- Random IDs (UUID version 4) carry 122 random bits, so any process can make one alone, and they reveal nothing.
- Time-ordered IDs (UUID version 7) start with the creation time in milliseconds, so new IDs sort after old ones.
- Readable codes use a short alphabet without look-alike characters, and each new code is checked for uniqueness.
- Access codes, the ones that open something, come from a secure random generator.
- Request fingerprints show a server that a known key has arrived with a different request.
- Key claims record a key before the work starts, so a second copy in flight finds it.
- Event-ID windows remember recent event IDs and drop copies that arrive within that time.
- Sequence windows keep the highest sequence number of each sender, plus a short record below it.
Random or time-ordered IDs: what an index sees, measured Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- Codes people read and type, with collision numbers
- Codes that open something
- Idempotency keys: claim, work, save
- Two copies in flight, reproduced
- What a reply is: fingerprints, scope and failures
- Crashes between the writes, at every point
- How long to keep keys
- Deduplicating events in bounded memory
- Practise: wallet top-ups that are safe to retry
- Wallet top-ups that are safe to retry
- Check yourself
- Interview questions
- When would you choose a version 7 UUID over a version 4 UUID, and when would you not?
- A wallet app retries a top-up when the network times out. Support finds that about 3 in 10,000 top-ups are credited twice. Design the fix in the wallet service's classes, and say how you would test it.
- Design the pickup codes for a network of 40,000 parcel lockers: what a code looks like, how it is generated, how long it lives and what limits wrong attempts. Today the codes are six digits from the random module and never expire.
- A consumer drops duplicate order events by keeping their IDs in memory for 10 minutes. After an outage, the broker redelivered two days of events and some customers were charged twice. What went wrong, and how do you redesign deduplication so that an outage cannot do this again?
- Key takeaways
5 runnable examples (JavaScript, Python) · 1 exercise with sample tests · 3 interview answers · about 28 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Read a complete sample first: Low-level design: parking lot (the model)
References
- RFC 9562: Universally Unique IDentifiers (UUIDs) (Internet Engineering Task Force (IETF))
- uuid: UUID objects according to RFC 9562 (uuid4, uuid7) (Python Software Foundation)
- secrets: generate secure random numbers for managing secrets (Python Software Foundation)
- random: generate pseudo-random numbers (Python Software Foundation)
- Java SE 25 API: java.security.SecureRandom (Oracle)
- The Idempotency-Key HTTP Header Field (Internet-Draft of the HTTPAPI working group) (Internet Engineering Task Force (IETF))
- API reference: idempotent requests (Stripe)
- collections: OrderedDict objects (Python Software Foundation)
- RFC 9293: Transmission Control Protocol (TCP), sequence numbers (Internet Engineering Task Force (IETF))
- RFC 4303: IP Encapsulating Security Payload (ESP), sequence number verification (Internet Engineering Task Force (IETF))
- Apache Kafka documentation: design, message delivery semantics (The Apache Software Foundation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress