Test Case Design and Edge Case Analysis Questions
Systematically deriving the cases, inputs, and conditions most likely to expose defects. Covers formal test-design techniques (equivalence partitioning, boundary value analysis, decision tables, state transitions, and pairwise/combinatorial design) and writing clear, maintainable test cases with documented expected results. Also covers the edge-case mindset: boundary conditions, invalid and unexpected inputs, corner cases, and the attention to detail that anticipates failures when validating complex behavior.
For a bank ledger system that records transfers between accounts, propose a set of invariants suitable for property-based testing (for example: sum of all balances remains constant excluding external deposits/withdrawals). Describe how you would model transactions and sequences of operations, generate randomized operation sequences, detect invariant violations using Hypothesis or an equivalent tool, and shrink failing cases to minimal counterexamples for developers.
Sample Answer
Direct answer
For a bank ledger, the core property-based invariant is conservation: the sum of all account balances stays constant across any sequence of internal transfers (excluding genuine external deposits/withdrawals), and a secondary invariant is that no balance ever goes negative; the design work is modeling transactions as a sequence of RULES a state machine can apply, generating randomized sequences of those rules, checking both invariants after every step, and letting the property-testing tool shrink any violating sequence down to the shortest sequence of operations that still breaks the invariant.
Structured elaboration
This is the same conservation-invariant property whether it's framed as a bank ledger, a payment-reconciliation system (money in minus money out across a settlement window must equal the net balance change), or a debit/credit accounting ledger (every debit must be matched by an equal credit, so the sum across all entries nets to zero); all three are the identical mathematical invariant applied to a different vocabulary, which matters because it means the SAME property-based test design transfers directly between them.
Modeling with a stateful property-based testing tool (Hypothesis's RuleBasedStateMachine in Python, though the same shape exists in other ecosystems, e.g. QuickCheck-style state machines):
- State: a dict of account balances, initialized to known starting values, plus the known invariant total.
- Rules: a
transfer(src, dst, amount)rule that the state machine can call with randomly generatedsrc,dst, andamountvalues; a well-modeled rule also needs to correctly handle its OWN expected-failure path (an insufficient-funds transfer should raise and leave every balance untouched, which is itself part of the invariant, not a separate concern). - Invariants, checked after every applied rule:
sum(balances.values()) == expected_total(conservation) andall(b >= 0 for b in balances.values())(no negative balances). - Shrinking: when a generated sequence of transfers violates an invariant, the tool automatically searches for a shorter, simpler sequence that still violates it (fewer transfers, smaller amounts, fewer distinct accounts involved), which is the property-based testing payoff over a hand-written unit test: a random 40-operation sequence that fails shrinks down to the 2-3 operations that actually matter, which is what a developer needs to see to fix the bug.
Worked example (executed): a genuine conservation-invariant violation found and shrunk
Two ledger implementations were built and tested with a Hypothesis RuleBasedStateMachine against the conservation invariant above:
from hypothesis import settings
from hypothesis.stateful import RuleBasedStateMachine, rule, initialize, invariant
from hypothesis import strategies as st
ACCOUNTS = ["A", "B", "C"]
class IntLedgerMachine(RuleBasedStateMachine):
@initialize()
def setup(self):
self.balances = {"A": 1000_00, "B": 1000_00, "C": 1000_00}
self.total = sum(self.balances.values())
@rule(src=st.sampled_from(ACCOUNTS), dst=st.sampled_from(ACCOUNTS),
amount=st.integers(min_value=1, max_value=50000))
def transfer(self, src, dst, amount):
if src == dst or self.balances[src] < amount:
return
self.balances[src] -= amount
self.balances[dst] += amount
@invariant()
def conserved(self):
assert sum(self.balances.values()) == self.total
class FloatLedgerMachine(RuleBasedStateMachine):
@initialize()
def setup(self):
self.balances = {"A": 1000.0, "B": 1000.0, "C": 1000.0}
self.total = sum(self.balances.values())
@rule(src=st.sampled_from(ACCOUNTS), dst=st.sampled_from(ACCOUNTS),
amount=st.floats(min_value=0.01, max_value=500, allow_nan=False, allow_infinity=False))
def transfer(self, src, dst, amount):
if src == dst or self.balances[src] < amount:
return
self.balances[src] -= amount
self.balances[dst] += amount
@invariant()
def conserved(self):
assert sum(self.balances.values()) == self.total
IntLedgerMachine.TestCase.settings = settings(max_examples=300, stateful_step_count=50)
FloatLedgerMachine.TestCase.settings = settings(max_examples=300, stateful_step_count=50)
The integer-cents machine, run for 300 examples of up to 50 transfer steps each, PASSED with no invariant violation found.
The float-balance machine, run with the identical settings, FOUND a violation and shrunk it, quoted verbatim from the executed run:
state = FloatLedgerMachine()
state.setup()
state.transfer(amount=416.76907716096747, dst='A', src='B')
state.transfer(amount=258.0, dst='A', src='B')
state.transfer(amount=374.0, dst='A', src='C')
state.transfer(amount=0.3333333333333333, dst='A', src='B')
AssertionError: assert sum(self.balances.values()) == self.total
This is a genuine, measured floating-point conservation drift: each individual transfer is internally correct (debit exactly equals credit at the moment it's applied), but repeated binary floating-point addition and subtraction accumulates rounding error, so the SUM across all balances drifts away from the true conserved total of 3000.0 after four transfers. (The exact shrunk sequence and step count are not a fixed property of the bug: a different Hypothesis version, seed, or run will shrink to a different, similarly small, sequence of float transfers; re-running the code above is expected to reproduce the CLASS of failure, not necessarily this literal transcript.) This is exactly the class of bug property-based testing is well-suited to catch and a fixed set of hand-picked unit-test amounts would very likely miss, since it depends on the specific bit-level rounding behavior of the particular float values chosen, which random generation reliably stumbles into and shrinking reliably minimizes to a small, reproducible reporting case.
Trade-offs and pitfalls
The most common wrong turn is writing the conservation invariant as exact equality (==) when the underlying representation is floating-point, as demonstrated above; the fix is either representing money as integer minor units (cents) so exact-equality conservation genuinely holds (confirmed passing in the integer version above), or, if floating-point is unavoidable for some other reason, relaxing the invariant to an explicit, documented tolerance (abs(actual - expected) < epsilon) rather than silently accepting drift with no check at all. A second pitfall is modeling only the successful-transfer path and never generating transfers that SHOULD fail (insufficient funds, zero or negative amount, transferring to a nonexistent account); the invariant check after a REJECTED transfer (balances unchanged) is just as valuable a thing for the state machine to verify as the check after an accepted one, and skipping it misses bugs where a supposedly-rejected transfer partially mutates state before raising.
You're QA for a payment system that stores balances as 32-bit signed integers in cents. Describe test cases to detect integer overflow and underflow across deposits, withdrawals, transfers, currency conversions, batch jobs, and repeated operations. Explain how you would automate detection, decide acceptance criteria for safe behavior, and work with engineers to mitigate and monitor overflow risks.
Sample Answer
Direct answer
A 32-bit signed integer stores cents in the range −231 to 231−1, which is -21,474,836.48 dollars to 21,474,836.47 dollars. Any operation that can push a balance past either end (a large deposit, a batch of many small deposits, a currency conversion that multiplies by a rate) either silently wraps to a huge negative number (classic overflow) or throws, depending on the language and whether checked arithmetic is used; a payment system test plan has to enumerate every code path that can reach that boundary, not just the "add a huge number once" case.
Structured elaboration
INT32_MAX=231−1=2,147,483,647 cents=$21,474,836.47
INT32_MIN=−231=−2,147,483,648 cents=−$21,474,836.48
Overflow/underflow risk is different per operation type and needs its own test cases:
- Deposits: a single deposit that pushes a balance from just under
INT32_MAXto over it; also a deposit whose OWN value exceedsINT32_MAXbefore it even touches the existing balance. - Withdrawals: pushing a balance below
INT32_MINis the underflow analog; also verify a withdrawal larger than the current balance is rejected by business logic (insufficient funds) before it ever reaches the arithmetic, since a naive implementation might let the subtraction wrap instead of validating first. - Transfers: a debit-then-credit pair where the credit side overflows the recipient's balance even though the debit side is well within range; transfers also introduce a NEW risk class (partial application: debit succeeds, credit overflows and fails, leaving money in neither account) that pure deposit/withdrawal tests do not cover.
- Currency conversions: multiplying by a floating-point exchange rate before rounding back to integer cents can overflow at a much smaller starting balance than same-currency operations, and rounding direction (round half up vs. banker's rounding) needs its own dedicated cases independent of the overflow question.
- Batch jobs: a batch that touches many accounts is not just "the single-transaction case run N times"; test that ONE account hitting overflow mid-batch does not corrupt or silently skip the other accounts in the same run, and that the batch's failure mode (abort all, skip and log, or partial-commit) matches the documented contract.
- Repeated operations: many small deposits that each individually look safe but cumulatively cross the boundary; this is the case most likely to be missed because no single transaction looks dangerous in isolation, and it directly tests whether overflow protection is checked before or after each addition rather than only at input validation time.
Worked example
| Case | Starting balance (cents) | Operation | Expected behavior | Risk |
|---|---|---|---|---|
| Deposit at the boundary | 2,147,483,646 | Deposit 1 cent | Balance becomes exactly INT32_MAX (2,147,483,647); succeeds | Medium |
| Deposit past the boundary | 2,147,483,646 | Deposit 2 cents | REJECTED with an explicit overflow error, balance unchanged; never silently wraps to a large negative number | High |
| Withdrawal past the boundary | -2,147,483,647 | Withdraw 2 cents | REJECTED with an explicit underflow error | High |
| Transfer, recipient overflows | Sender: 100,000; Recipient: 2,147,483,600 | Transfer 100 cents | Entire transfer REJECTED atomically; sender's balance is untouched, not debited-then-stuck | High |
| Currency conversion rounding | 1,000,000 cents (source currency) | Convert at rate 1.0000001 | Result matches a pinned, independently-computed expected value to the cent, proving the rounding rule (not just "no overflow") | Medium |
| Cumulative small deposits | 2,147,483,600 | 100 separate 1-cent deposits in sequence | INT32_MAX - 2,147,483,600 = 47, so the first 47 deposits succeed, bringing the balance to exactly INT32_MAX (2,147,483,647); the 48th deposit would make it 2,147,483,648 and is REJECTED at that specific step, not before | High |
Automating detection, acceptance criteria, and mitigation: automate detection with property-based tests that generate random sequences of deposits/withdrawals/transfers and assert an invariant (sum of all account balances is conserved across transfers, no balance ever exceeds INT32_MAX or goes below INT32_MIN) rather than hand-writing every combination; run this alongside targeted boundary cases like the table above, since property tests are good at finding surprising sequences but boundary cases are more reliable for the exact edge itself. Acceptance criteria for "safe": every arithmetic operation on a balance is either using a wider integer type (64-bit) internally with a final range check, or uses checked/saturating arithmetic that raises rather than wraps; there is no code path where an overflow can occur silently. Mitigation with engineers: migrate balance storage to 64-bit integers (removing the realistic risk entirely, since 263−1 cents is far beyond any real account balance) or add explicit overflow-checked arithmetic at every mutation point if a schema migration isn't feasible short-term; monitor in production with an alert on any balance within, say, 1% of the 32-bit boundary, so a customer legitimately approaching the limit is caught before they hit it, not after.
Trade-offs and pitfalls
A common mistake is testing only the deposit case and assuming withdrawal/transfer/conversion "obviously" behave the same; they do not, because transfers add the partial-application risk and conversions add floating-point rounding on top of the integer-overflow risk. Another mistake is testing overflow only as a single huge value, which misses the cumulative small-deposits case entirely, and that case is disproportionately likely to occur in real production data (a busy account with thousands of small transactions) compared to one dramatic deposit. Finally, silently wrapping on overflow (the C-style undefined/wraparound behavior in some 32-bit contexts) is categorically worse than throwing: a wrapped balance can go negative or reset near zero, and if that value is trusted downstream (e.g. displayed to the customer or used in a subsequent calculation) it becomes a real financial-integrity incident, not just a test failure.
Explain integer overflow and underflow and how behavior differs between C/C++ (wrap or UB), Java (wrap/defined?), and Python (big ints). As an SRE, how would you test for overflow in ingestion pipelines, telemetry aggregation, and logs where numbers may exceed expected ranges?
Sample Answer
Direct answer
Integer overflow happens when an arithmetic result exceeds the maximum value a fixed-width integer type can hold; C/C++ signed-integer overflow is undefined behavior (the compiler is allowed to assume it never happens, which can produce surprising results beyond simple wraparound), Java integers wrap around silently and deterministically (Integer.MAX_VALUE + 1 reliably becomes Integer.MIN_VALUE), and Python integers have no fixed width at all, transparently growing to arbitrary precision, so overflow in the C/Java sense simply cannot occur for plain Python ints.
Structured elaboration: the three-language comparison
| Language | Behavior on overflow | Practical implication |
|---|---|---|
| C/C++ | Undefined behavior (signed); unsigned integers wrap by the standard | A compiler MAY assume overflow never happens and optimize based on that assumption, which can eliminate intended overflow checks entirely, not just produce a 'wrong number' |
| Java | Silent, defined wraparound (two's complement) | Integer.MAX_VALUE + 1 == Integer.MIN_VALUE deterministically; no exception is thrown by default, so bugs pass silently unless explicitly checked (Math.addExact throws) |
| Python | No fixed-width overflow for int | Arbitrary-precision integers grow as needed; the practical overflow risk moves to fixed-width contexts INSIDE Python, such as numpy's int32/int64 arrays, which DO wrap like C |
Worked example: testing overflow in ingestion pipelines, telemetry, and logs, as an SRE
- Ingestion pipelines: if raw event counts are accumulated into a 32-bit counter column (common in a database schema or a numpy-backed aggregation) rather than Python's native arbitrary-precision int, a test should push the accumulator past 2^31-1 (2,147,483,647) with a controlled batch of synthetic events and assert the stored value is correct, not silently wrapped negative. This is a genuine, previously-seen production bug class: a counter that looks fine at low volume and becomes visibly wrong (negative counts) only once traffic crosses the 32-bit boundary.
- Telemetry aggregation: a metric like total-bytes-transferred summed across a long time window in a language/runtime using 32-bit floats or ints for the accumulator can silently wrap or lose precision; the test strategy is to seed a synthetic high-volume period and assert the aggregate against an independently-computed 64-bit or arbitrary-precision reference sum (an oracle-by-differential-computation, not by trusting the pipeline's own math).
- Logs: numeric fields parsed FROM log lines (e.g. a request-duration-in-microseconds field written by a system that itself overflowed) need tests confirming the LOG PARSER doesn't compound the problem by re-parsing an already-wrapped negative number as if it were valid, and ideally surfaces an anomaly flag (a negative duration is never physically meaningful) rather than silently accepting it.
The security-hardening angle
In C/C++ specifically, an integer overflow that controls a buffer-size calculation (e.g. malloc(count * size) where count * size overflows to a small positive number) can lead to a buffer that is allocated far smaller than intended, and subsequent writes based on the ORIGINAL, un-overflowed count then write past the buffer's real boundary; this is a well-documented memory-safety vulnerability class, not just a numeric-correctness bug, which is why overflow-prone size/length calculations in C/C++ warrant deliberate hardening (checked-arithmetic helpers, or unsigned-overflow-safe idioms) beyond what a Java or Python equivalent would need.
Trade-offs & pitfalls
A common mistake is assuming Python's arbitrary-precision integers make an SRE's pipeline immune to overflow entirely; in practice most high-throughput pipelines use numpy, pandas, or a database column with a FIXED width for performance reasons, reintroducing exactly the overflow risk that plain Python ints don't have, so "we use Python" is not itself a mitigation without checking every numeric boundary the pipeline actually touches.
As Solutions Architect integrating a third-party payment gateway, enumerate and prioritize edge cases that may lead to duplicate charges, lost transactions, or inconsistent state (network retries, partial failures, idempotency key misuse, delayed callbacks). For the top five cases propose concrete tests and mitigation strategies (idempotency, reconciliation, webhooks verification).
Sample Answer
Direct answer
Third-party payment gateway integrations fail in ways that are financially expensive rather than merely broken: network retries, partial failures, idempotency-key misuse, and delayed callbacks can each independently cause a duplicate charge, a lost transaction, or a state where the merchant's system and the gateway's system disagree about whether a payment succeeded. The design principle that resolves all four is treating "did this payment happen" as a question that must be answerable idempotently and reconciled asynchronously, never assumed from a single synchronous response.
Structured elaboration: prioritized edge cases with tests and mitigations
- Client retries a request after a timeout, with no idempotency key (or the key is generated fresh per retry). This is the single highest-priority case because a naive retry-on-timeout is the most common cause of duplicate charges: the original request may have actually succeeded at the gateway even though the response never reached the client. Test: simulate a request that times out client-side after the gateway has already processed it, then retry, and assert exactly one charge exists. Mitigation: generate the idempotency key once per logical payment attempt (not per HTTP request) and pass the same key on every retry of that attempt; the gateway is contractually responsible for returning the original result for a repeated key rather than processing twice.
- Idempotency key reused across genuinely different payment attempts. The inverse failure: reusing a key for a legitimately new charge (e.g. a bug that derives the key from something that doesn't change, like a cart ID reused across two separate checkouts) causes the second, real charge to be silently dropped because the gateway treats it as a duplicate of the first. Test: assert that two distinct purchases, even from the same user in the same session, produce two distinct idempotency keys and two distinct successful charges. Mitigation: derive the key from something that is unique per attempt (a freshly generated attempt ID at the moment the user initiates payment), never from stable business data like a cart or order ID that could repeat.
- Partial failure: charge succeeds at the gateway, but the merchant's own database write fails before confirming. The gateway believes the payment succeeded; the merchant system has no record of it, so the order never fulfills and the customer is charged with nothing to show for it. Test: simulate the merchant-side write throwing after the gateway call returns success, then assert a reconciliation job detects the mismatch. Mitigation: never treat "gateway call returned success" as the final state; write a local pending record before calling the gateway, then reconcile against the gateway's own transaction log on a schedule, independent of whether the synchronous call path completed cleanly.
- Delayed or out-of-order webhook callbacks. A
payment_succeededwebhook can arrive after apayment_failedwebhook for the same transaction (network reordering), or arrive very late (minutes to hours) after an outage. Test: feed webhook events to the handler out of chronological order and assert the final recorded state matches the LATEST event by the gateway's own event timestamp, not by arrival order. Mitigation: make the webhook handler idempotent and order-aware (compare the incoming event's timestamp or sequence number against the currently stored state before applying it), and never assume webhook delivery order matches event occurrence order. - Webhook signature or replay attack surface. Not a race condition but still a duplicate-charge-adjacent risk: an attacker (or a misconfigured retry from the gateway itself) replaying a captured webhook payload could trigger the handler's success path twice. Test: replay an already-processed, validly-signed webhook and assert it is a no-op the second time. Mitigation: verify the webhook's cryptographic signature on every delivery AND make the handler idempotent on the gateway's own event ID, so a legitimate replay (which does happen, most gateways retry undelivered webhooks) is safely absorbed rather than double-applied.
Worked example
A checkout flow calls the gateway with idempotency key attempt-7f3a, the request times out client-side after 30 seconds with no response received, and the client automatically retries the same call with the same key attempt-7f3a. Trace: (a) if the original request never reached the gateway (network failure before processing), the retry is the gateway's first sight of attempt-7f3a and it processes normally, one charge; (b) if the original request DID reach the gateway and completed, but the response was lost on the way back, the retry arrives at the gateway with a key it has already seen, and a correctly implemented gateway returns the original transaction's result without charging again, still one charge; (c) only if the idempotency key were regenerated per retry (the bug in edge case 1 above) would this scenario produce two charges. The test that actually exercises the distinction between (a)/(b) and the buggy (c) is asserting on the CHARGE COUNT after a simulated timeout-then-retry, not merely asserting that the retry "succeeds," since a buggy implementation also reports success on both charges.
Trade-offs and pitfalls
The most common wrong turn is trusting the synchronous API (application programming interface) response as the sole source of truth and skipping reconciliation entirely, which works until the first partial failure or webhook delay, both of which are not rare in practice for any gateway operating at scale. A second pitfall is generating idempotency keys from data that seems unique but is not guaranteed to be (a timestamp with insufficient precision, a cart ID that survives across retries) rather than a purpose-generated attempt identifier. A third is building tests that only cover the fast-success path plus one hardcoded timeout, rather than the interaction between retries and idempotency keys, which is where the actual duplicate-charge risk concentrates; the individual failure modes are each easy to reason about, the risk lives specifically in how they interact.
A distributed job scheduler enqueues tasks with retries and exponential backoff. Enumerate edge cases that can cause duplicate or lost tasks (clock skew, process crash during ack, duplicate enqueue, queue reordering) and propose tests to detect each issue. Specify instrumentation or assertions you'd add to prove correctness under these scenarios.
Sample Answer
Direct answer
Duplicate and lost tasks come from two distinct failure classes: at-least-once delivery mechanics that create duplicates (crash-during-ack, retried enqueue, reordered redelivery), and any true gap in the enqueue/ack protocol that creates genuine loss. The single mechanism that neutralizes duplicates from any of the first three causes, without introducing new loss risk, is a durable dedup store keyed on a stable task identifier, checked independently of the scheduler's own retry logic.
Structured elaboration
| Edge case | How it causes duplicate/lost tasks | Test to detect it | Instrumentation/assertion |
|---|---|---|---|
| Clock skew | If redelivery timeout decisions use each worker's local clock rather than a shared or logical deadline, a worker running behind can appear to time out prematurely (spurious duplicate), and one running ahead can suppress a legitimate timeout for a worker that actually died (loss) | Inject a fixed clock offset (for example +/-30 seconds) on a worker in an integration test | Assert the scheduler's timeout decision is driven by its own monotonic clock/logical deadline, not the worker's reported time; redelivery count for a correctly-behaved skewed worker should stay at the expected baseline of 0 spurious redeliveries |
| Process crash during ack | The worker applies the task's effect, then crashes before the ack reaches the scheduler; the scheduler redelivers, and the effect runs twice unless the consumer is idempotent | Deterministically kill the worker process between "effect applied" and "ack sent," a fault-injection point in the worker code, then let redelivery proceed | Assert the downstream side effect (for example a ledger balance) matches exactly-once semantics after redelivery and reprocessing, not the raw at-least-once delivery count |
| Duplicate enqueue | The producer itself enqueues the same logical task twice, for example a retried enqueue-API call that actually succeeded the first time but the caller never saw the response | Fire the enqueue call twice with the same idempotency key/task_id from the producer side, simulating a producer retry | Assert queue depth after N duplicate-enqueue attempts of the same task_id equals 1, not N (enqueue-side dedup) |
| Queue reordering | Tasks are redelivered or reprocessed out of original order, common with partitioned/sharded queues; if downstream logic implicitly assumes order, reordering can cause a stale-looking duplicate effect or a lost update when a later task is processed ahead of one it depended on | Explicitly interleave delivery of a set of related task_ids in scrambled order in an integration test | Assert the consumer's correctness invariant holds regardless of delivery order, either because tasks are independent by design or because a version/sequence check inside the task rejects out-of-order application |
The idempotent dedup-store consumer pattern is the concrete fix that threads through the crash-during-ack, duplicate-enqueue, and queue-reordering rows uniformly: a durable dedup store keyed by task_id, checked and set atomically before applying any effect, turns "at-least-once delivery" into "at-most-once effect application" regardless of which of those three causes produced the redelivery. It does not, by itself, fix true task loss (a task that was never durably enqueued, or whose only record was dropped); that needs a separate durability guarantee such as a write-ahead log at enqueue time, ack-only-after-persist, which is a different problem from deduplication.
Worked example (executed)
class DedupStore:
def __init__(self):
self._seen = set()
def seen_before(self, task_id):
if task_id in self._seen:
return True
self._seen.add(task_id)
return False
class Ledger:
def __init__(self):
self.balance = 0
self.applied = []
def credit(self, task_id, amount):
self.balance += amount
self.applied.append(task_id)
def process_task(task_id, amount, dedup, ledger):
if dedup.seen_before(task_id):
return "skipped_duplicate"
ledger.credit(task_id, amount)
return "applied"
Three tests, all executed and PASSED:
test_duplicate_delivery_applies_once: task-1 delivered 3 times (simulating a redelivered task) returns["applied", "skipped_duplicate", "skipped_duplicate"]; final ledger balance is 500, not 1500.test_distinct_tasks_all_apply: 5 distinct task_ids each applied once; final balance is 500, confirming dedup does not suppress genuinely distinct tasks.test_reordered_delivery_still_dedups: task_ids delivered in scrambled order["task-b", "task-a", "task-b", "task-a", "task-c"](3 distinct ids, 2 duplicated); final balance is 30 (3 distinct tasks x 10), not 50 (5 deliveries x 10), confirming the dedup key is purely the task_id, not arrival position.
Trade-offs and pitfalls
- The dedup store's check-and-set must itself be atomic with respect to concurrent redelivery; the
DedupStoreabove is a single-threaded illustration and does not by itself prove safety when two redeliveries of the same task_id race each other at the check, that needs a real atomic compare-and-set (for example a database unique constraint or an atomic RedisSETNX), tested under actual concurrency. - Dedup key choice matters more than it looks: a task_id assigned by the producer survives duplicate enqueue; a task_id the scheduler assigns at dequeue time does not, since it looks "new" on every redelivery. Choosing the wrong owner for the identifier is a common design mistake.
- The dedup store's retention window is a real trade-off, not a solved problem: unbounded retention grows storage forever, but too short a retention lets a very-delayed redelivery slip past the check and duplicate again, the window has to be chosen against the maximum delivery delay the system must actually tolerate.
- Dedup alone cannot prove a task was never lost; that requires separately testing write-ahead durability at enqueue time. Conflating "duplicates do not double-apply" with "nothing was ever dropped" is the most common design mistake in this space.
Unlock Full Question Bank
Get access to all 47 Test Case Design and Edge Case Analysis interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.