Distributed Systems Testing
What breaks at the seams between services — contract testing, a microservices test strategy, asynchronous integrations and webhooks, eventual consistency, message-queue duplicates and ordering, resilience under a failing dependency, deployment verification, and real-time state sync.
10 questions
MiddleDesignCommonYou are the QA on a document-upload feature. The API returns 202 Accepted the instant a file arrives; a background pipeline then runs an asynchronous virus scan, and only once it passes does a separate service fire a webhook callback into your app and email the user a 'ready' notice. A cron job drives some steps and retries them on failure. Nothing is finished when the HTTP response returns. Design a test approach for this integration: how do you assert the eventual outcome without a fixed sleep, how do you cover both the infected-file branch and the step-fails-then-retries branch, and how do you keep the suite from passing only when the pipeline happens to run fast?
You are the QA on a document-upload feature. The API returns 202 Accepted the instant a file arrives; a background pipeline then runs an asynchronous virus scan, and only once it passes does a separate service fire a webhook callback into your app and email the user a 'ready' notice. A cron job drives some steps and retries them on failure. Nothing is finished when the HTTP response returns. Design a test approach for this integration: how do you assert the eventual outcome without a fixed sleep, how do you cover both the infected-file branch and the step-fails-then-retries branch, and how do you keep the suite from passing only when the pipeline happens to run fast?
Assert on the eventual state, not on timing: poll the callback endpoint or a status field with a bounded timeout and back-off instead of a fixed sleep. Drive each branch deliberately — a clean file, an infected file the scan must reject, and a forced step failure so the cron retry runs — and verify the webhook payload, the email, and idempotency when a retry re-delivers. Capture the callback with a stub receiver so the outcome is observable.
Common mistakes
- ✗Replacing polling with a fixed sleep sized to the current pipeline speed
- ✗Testing only the happy path, skipping the reject and retry branches
- ✗Ignoring idempotency when a retry re-delivers the same callback
Follow-up questions
- →How does bounded polling with back-off beat a fixed sleep here?
- →How do you force the cron step to fail so its retry path runs in a test?
MiddleDesignCommonA rollout review. Your team is about to ship a risky change and is weighing three options: a canary deploy that routes a small slice of live traffic to the new version, a blue-green switch that stands the new version up beside the old and flips all traffic at once, and gating the change behind a feature flag with an A-B experiment. You own verification. Design how QA verifies a deployment under each of these — not just the pre-release build: what signals you watch on the new version versus the old, how you define an automatic rollback trigger, and how you keep the A-B or canary comparison honest when the two cohorts are running different code.
A rollout review. Your team is about to ship a risky change and is weighing three options: a canary deploy that routes a small slice of live traffic to the new version, a blue-green switch that stands the new version up beside the old and flips all traffic at once, and gating the change behind a feature flag with an A-B experiment. You own verification. Design how QA verifies a deployment under each of these — not just the pre-release build: what signals you watch on the new version versus the old, how you define an automatic rollback trigger, and how you keep the A-B or canary comparison honest when the two cohorts are running different code.
Verification shifts from a one-time gate to a live comparison: watch the new version's error rate, latency, and key business metric against the old baseline serving real traffic, not a fixed threshold alone. Define the rollback trigger up front — a bounded regression on those signals auto-reverts or drains the canary. Keep the comparison honest by splitting cohorts randomly and comparing like-for-like traffic, so a metric shift reflects the code, not a skewed audience or time of day.
Common mistakes
- ✗Treating deployment verification as just the pre-release suite rerun
- ✗Defining no automatic rollback trigger on live signals
- ✗Comparing cohorts that saw skewed traffic and calling the shift the code
Follow-up questions
- →What makes a good automatic rollback trigger versus a noisy one?
- →Why is blue-green's instant flip riskier to verify than a canary slice?
MiddleDesignCommonA pre-release go/no-go for a real-time bidding platform. Bids flow through a message queue between the ingest service and the matching engine. Under at-least-once delivery the queue can re-deliver a message after a consumer crash, and with multiple partitions two bids can be processed out of the order they were placed. The product owner wants to ship, arguing 'the queue guarantees delivery'. You must decide what to verify before release. Design your test approach: how you would prove the matching engine tolerates duplicate delivery, what ordering guarantee actually holds and how you would test it, and what evidence you need before signing off on bidding correctness.
A pre-release go/no-go for a real-time bidding platform. Bids flow through a message queue between the ingest service and the matching engine. Under at-least-once delivery the queue can re-deliver a message after a consumer crash, and with multiple partitions two bids can be processed out of the order they were placed. The product owner wants to ship, arguing 'the queue guarantees delivery'. You must decide what to verify before release. Design your test approach: how you would prove the matching engine tolerates duplicate delivery, what ordering guarantee actually holds and how you would test it, and what evidence you need before signing off on bidding correctness.
Test that processing is idempotent: re-deliver the same bid and assert the outcome is unchanged — no double bid, no double charge. Pin down the real ordering guarantee: a queue orders only within a partition or key, not globally, so route bids for one item to one key and assert per-item order while accepting that unrelated bids interleave. Sign off only when duplicates are provably absorbed and the ordering the product needs matches what the partitioning enforces.
Common mistakes
- ✗Believing at-least-once means no duplicates ever reach the consumer
- ✗Expecting global ordering when a queue orders only within a partition
- ✗Assuming more partitions tighten ordering rather than loosen it
Follow-up questions
- →How do you route bids so per-item order is guaranteed under partitioning?
- →How do you force a re-delivery to prove the engine absorbs a duplicate?
MiddleDesignCommonA strategy argument on your team: one engineer insists the only trustworthy coverage for your checkout system — spanning a cart service, a pricing service, a payment service and an inventory service — is a large suite of full end-to-end tests driven through the UI. Another wants almost no e2e, with everything pushed down to unit and contract tests. You are asked to propose the test strategy for these microservices. Design your answer: how you would allocate coverage across the levels, what each level is responsible for, where the seams between services get verified, and how you keep the e2e tier small without leaving the risky cross-service paths untested.
A strategy argument on your team: one engineer insists the only trustworthy coverage for your checkout system — spanning a cart service, a pricing service, a payment service and an inventory service — is a large suite of full end-to-end tests driven through the UI. Another wants almost no e2e, with everything pushed down to unit and contract tests. You are asked to propose the test strategy for these microservices. Design your answer: how you would allocate coverage across the levels, what each level is responsible for, where the seams between services get verified, and how you keep the e2e tier small without leaving the risky cross-service paths untested.
Neither extreme. Push the bulk down: unit tests per service for logic, contract tests at every seam so each pair stays compatible without deploying everything, and a thin e2e tier over only the critical journeys — a real checkout, a declined payment, an out-of-stock item. Each level owns a distinct failure class and the pyramid keeps feedback fast. Reserve e2e for the few paths where a cross-service interaction, not one service's logic, is the actual risk.
Common mistakes
- ✗Covering everything through slow, flaky end-to-end tests
- ✗Deleting contract tests and assuming green units mean a correct system
- ✗Weighting every level equally instead of shaping a pyramid
Follow-up questions
- →Which checkout paths earn a scarce e2e slot, and why those?
- →How do contract tests cover the seams the thin e2e tier skips?
MiddleDesignCommonA resilience design review before launch. Your product page pulls a 'recommended for you' block from a downstream recommendation service and stock levels from an inventory service. Both are separate deployments that can be slow, return errors, or go down entirely; the inventory cluster is multi-node and a single node can fail mid-request. The team wants the page to stay useful when a dependency degrades, and to lose no committed data if one inventory node dies. You own the test plan. Design how you would verify graceful degradation: which failure modes you would inject, what the page must still do when the recommendation service is down versus merely slow, and how you would prove a single node failure loses no data.
A resilience design review before launch. Your product page pulls a 'recommended for you' block from a downstream recommendation service and stock levels from an inventory service. Both are separate deployments that can be slow, return errors, or go down entirely; the inventory cluster is multi-node and a single node can fail mid-request. The team wants the page to stay useful when a dependency degrades, and to lose no committed data if one inventory node dies. You own the test plan. Design how you would verify graceful degradation: which failure modes you would inject, what the page must still do when the recommendation service is down versus merely slow, and how you would prove a single node failure loses no data.
Inject the failure modes deliberately — latency, errors, and full outage on each dependency, plus killing one inventory node mid-request. Assert the page degrades rather than breaks: with recommendations down it drops that block and still renders the product; when the service is merely slow, a timeout and fallback keep the page inside its budget instead of hanging. For the node kill, assert a committed write survives and the request either retries or fails cleanly — no partial or lost data.
Common mistakes
- ✗Assuming a healthy happy path proves behaviour under failure
- ✗Blocking the whole page when one non-critical dependency is down
- ✗Not distinguishing a down dependency from a merely slow one
Follow-up questions
- →Why must a slow dependency be timed out rather than waited on?
- →How do you inject a mid-request node failure to check for lost writes?
MiddleTheoryOccasionalHow does a consumer-driven contract test with the tool Pact differ from an end-to-end test, and when do you reach for each?
How does a consumer-driven contract test with the tool Pact differ from an end-to-end test, and when do you reach for each?
A Pact contract test pins one consumer–producer pair: the consumer records the requests and responses it expects, the producer replays them, and each side is verified alone without both deployed. End-to-end runs the whole chain live — it catches wiring and data-flow bugs but is slow and flaky. Contract tests pinpoint which side broke a contract; e2e proves the assembled system works.
Common mistakes
- ✗Thinking a contract test deploys the whole system like e2e does
- ✗Believing a green contract makes end-to-end coverage redundant
- ✗Forgetting the producer must replay the consumer's expectations to verify
Follow-up questions
- →How does the Pact broker let the two sides verify without deploying together?
- →What class of bug does e2e still catch that contract testing misses?
MiddleDesignOccasionalTicket triage: a user changes their profile display name, sees the new name on the confirmation screen, then opens their public page and still sees the old name — but only sometimes, and it corrects itself within about a minute. The write goes to a primary database that replicates to read replicas, and a caching layer sits in front of the read path. Some engineers want to file it as a data-loss bug. You are triaging. Design how you would test whether this is a genuine defect or expected eventual consistency: what you would measure, how you would set an acceptable staleness bound, and what evidence would turn 'the cache is stale' into a real bug worth fixing.
Ticket triage: a user changes their profile display name, sees the new name on the confirmation screen, then opens their public page and still sees the old name — but only sometimes, and it corrects itself within about a minute. The write goes to a primary database that replicates to read replicas, and a caching layer sits in front of the read path. Some engineers want to file it as a data-loss bug. You are triaging. Design how you would test whether this is a genuine defect or expected eventual consistency: what you would measure, how you would set an acceptable staleness bound, and what evidence would turn 'the cache is stale' into a real bug worth fixing.
First confirm the write is durable, not lost: read from the primary to check it persisted, which rules out data loss. Then measure the convergence window — how long until every replica and the cache serve the new value — and compare it to an agreed staleness bound. It is expected eventual consistency if reads converge inside that bound; it is a real bug if the window exceeds the bound, a stale value outlives its cache TTL, or the value never converges.
Common mistakes
- ✗Calling a not-yet-visible write a data-loss bug without checking the primary
- ✗Assuming replicas and caches update synchronously with the primary write
- ✗Never agreeing a staleness bound, so 'stale' can never become a real defect
Follow-up questions
- →How do you set the staleness bound that separates expected from broken?
- →What evidence distinguishes replication lag from a stuck cache entry?
MiddleDesignOccasionalProduction incident: users of a real-time chat report that messages sometimes arrive out of order, a message occasionally appears twice, and two people editing the same thread see different message lists for a few seconds before they converge. The transport is a WebSocket layer with a polling fallback, and clients reconnect after brief network drops. You cannot reproduce any of it on your laptop with a single client open. Design how you would test real-time state sync to reproduce and pin these symptoms down: what conditions you would force, what you would assert about ordering and delivery, and how you would separate a genuine sync bug from expected eventual convergence.
Production incident: users of a real-time chat report that messages sometimes arrive out of order, a message occasionally appears twice, and two people editing the same thread see different message lists for a few seconds before they converge. The transport is a WebSocket layer with a polling fallback, and clients reconnect after brief network drops. You cannot reproduce any of it on your laptop with a single client open. Design how you would test real-time state sync to reproduce and pin these symptoms down: what conditions you would force, what you would assert about ordering and delivery, and how you would separate a genuine sync bug from expected eventual convergence.
Reproduce with several concurrent clients, not one: script two or more sessions, inject reconnects and network drops, and interleave sends so the races surface. Assert per-sender ordering, exactly-once display after a reconnect, and that every client converges to the same final list within a bounded window. Separate a real bug — permanent divergence, or messages lost or reordered after convergence — from expected transient lag while the state is still settling.
Common mistakes
- ✗Trying to reproduce a concurrency bug with a single client
- ✗Treating any transient cross-client difference as a defect
- ✗Assuming TCP order across one socket guarantees order across all clients
Follow-up questions
- →How do you tell permanent divergence from a normal settling window?
- →Why does forcing a reconnect mid-conversation expose duplicate delivery?
SeniorDesignOccasionalYou are the senior QA designing the test strategy for a checkout that integrates a third-party payment gateway you do not control. You cannot move real money, and the vendor bills per live transaction, so a suite that hits production is off the table. The gateway returns some results synchronously — auth accepted or declined — and confirms settlement asynchronously through a webhook that can arrive minutes later, be duplicated, or arrive out of order. You must cover the full matrix — successful payment, decline, timeout, partial failure, refund, and duplicate webhook — with no real charges. Design the strategy: how you exercise the gateway without live transactions, how you keep your test double faithful to the real contract over time, and how you verify the asynchronous settlement path.
You are the senior QA designing the test strategy for a checkout that integrates a third-party payment gateway you do not control. You cannot move real money, and the vendor bills per live transaction, so a suite that hits production is off the table. The gateway returns some results synchronously — auth accepted or declined — and confirms settlement asynchronously through a webhook that can arrive minutes later, be duplicated, or arrive out of order. You must cover the full matrix — successful payment, decline, timeout, partial failure, refund, and duplicate webhook — with no real charges. Design the strategy: how you exercise the gateway without live transactions, how you keep your test double faithful to the real contract over time, and how you verify the asynchronous settlement path.
Test against the vendor's sandbox and a contract-verified stub, not production: the sandbox runs the real protocol with test cards for each outcome, while the stub deterministically drives declines, timeouts, and duplicate or out-of-order webhooks. Keep the double faithful with a contract test against the vendor so it cannot silently drift. Verify settlement by asserting the eventual state your webhook handler reaches — idempotent and order-independent — never a fixed wait.
Common mistakes
- ✗Hitting the vendor's live API and relying on refunds to cancel the cost
- ✗Trusting a hand-written stub with no contract test to catch API drift
- ✗Verifying the async settlement with a fixed sleep instead of eventual state
Follow-up questions
- →How does a contract test against the vendor keep your stub from drifting?
- →How do you assert the refund and duplicate-webhook cases without real money?
JuniorTheoryRareWhat breaks only at the seam between two services, and what does an integration test catch that a unit test cannot?
What breaks only at the seam between two services, and what does an integration test catch that a unit test cannot?
The seam carries the contract — field names and types, serialisation, status codes, timeouts, auth, and version drift. A unit test stubs the other side, so it only ever confirms your own assumption; an integration test makes the real call and catches that assumption being wrong.
Common mistakes
- ✗Believing a mock proves the contract, when it only encodes your assumption about it
- ✗Treating every seam failure as infrastructure the operations team will notice
- ✗Thinking an integration test is a slow unit test with no unique failure class
Follow-up questions
- →Why can a fully green unit suite still ship a broken integration?
- →Which seam failures does an integration test still fail to catch?