cconnected.
Understand the pattern. Find your own way.Find your starting point
Your learning feed/Idea 189 of 218
Ideas this builds on
Low-level design189 / 218

Make failure and retries part of the design

Picture

Two threads both see the same free spot. Separately checking and claiming lets both succeed. A retry adds another problem: even a single caller might repeat an operation after losing the first response.

Predict

What two protections are needed for competing callers and repeated requests?

Walk through the small example

An atomic check-and-claim boundary protects availability. A stable request ID with remembered input and result makes retries idempotent and detects conflicting key reuse.

Mental model and complete Python solution

The lock guards availability and result records together. A repeated key with the same spot replays its earlier result without allocating again. The same key with a different spot raises, because it no longer identifies the same request. This example records both successful and failed claims; a new attempt after availability changes needs a new request ID. The lock protects only threads sharing this object, and records are lost on process exit.

What the function promises: ClaimStore receives a set of initially available spot IDs. claim(request_id, spot) returns whether that operation reserved the spot. Calls may run from multiple threads in one process. No release or persistent storage is included.

from threading import Lock
from concurrent.futures import ThreadPoolExecutor

class ClaimStore:
    def __init__(self, spots):
        self._available = set(spots)
        self._results = {}
        self._lock = Lock()

    def claim(self, request_id, spot):
        if not request_id:
            raise ValueError("A stable request ID is required")
        with self._lock:
            if request_id in self._results:
                previous_spot, result = self._results[request_id]
                if previous_spot != spot:
                    raise ValueError("Request ID reused with different input")
                return result
            result = spot in self._available
            if result:
                self._available.remove(spot)
            self._results[request_id] = (spot, result)
            return result

store = ClaimStore({1})
assert store.claim("request-a", 1)
assert store.claim("request-a", 1)
assert not store.claim("request-b", 1)
try:
    store.claim("request-a", 2)
except ValueError:
    pass
else:
    raise AssertionError("Conflicting request reuse was accepted")
racing = ClaimStore({7})
with ThreadPoolExecutor(max_workers=4) as pool:
    results = list(pool.map(lambda i: racing.claim(str(i), 7), range(12)))
assert sum(results) == 1

Work and storage: Expected O(1) lookup and update inside a serialized critical section. Memory grows with spots and remembered request IDs; an expiry policy must be consistent with the promised retry window.

When to recognize it: Checking availability and claiming it must be one atomic operation; a retry needs a stable request identity.

Check before applying it: A Python lock protects one process only. Multiple servers need shared transactional storage; crash recovery needs durable idempotency records.

Change one rule

Two servers share a database but not Python memory. What must replace this local-only guarantee?

See what changes

Use a shared transactional boundary, such as a unique claim constraint plus a durable idempotency record committed with the state change. Define retries after crashes and conflicts explicitly; a local lock cannot coordinate another process.

Connect

Design a parking lot: Move the allocation invariant into one protected boundary.

Design an expense ledger: Validation and atomic updates protect conservation too.