Prompt and When This Question Applies
A checkout API synchronously calls a third-party tax service. The dependency normally has an 80 ms p50 and a 180 ms p99, but it occasionally has connection failures, slow responses, HTTP 429s, 503s, and cases where the request may have reached the server but the response was lost. The checkout API has an 800 ms end-to-end target. The service call chain may be five layers deep, and several SDKs may already retry by default.
Design the calling policy. Explain how to divide the deadline into per-attempt timeouts, which failures are retryable, how to set the maximum attempts, exponential backoff, and jitter, how a circuit breaker opens and recovers, and how isolation, degradation, metrics, and fault injection prove that a local failure will not become a cascading failure.
The latency and depth are interview assumptions. The core skill is failure semantics, resource protection, and recovery control for synchronous service-to-service calls, so the category is backend. The existing idempotent-order question focuses on the database contract for one write operation. This question uses idempotency only to decide whether retrying after a timeout is safe; its center is the time and load budget across the call chain.
What the Interviewer Is Evaluating
The first signal is whether the candidate creates a time ledger. An 800 ms end-to-end deadline does not mean an 800 ms dependency timeout. Local validation, queuing, serialization, and response work all need budget. Each attempt must also account for connection, TLS, and response phases, and another retry should start only if enough time remains.
The second signal is retrying by failure semantics. A failure before connection establishment, a temporary 503, or a 429 with retry guidance may be retryable. Bad parameters, authentication, and authorization do not heal with time. When a write times out after being sent, the outcome may be “committed but response unknown.” A stable idempotency key, status lookup, or reconciliation process is required before retrying is safe.
The third signal is retry amplification. If every service in a five-deep chain makes up to three attempts, the bottom dependency can receive 3^5 = 243 calls. Retries should live at one layer that understands the business deadline, with limits on attempts, total elapsed time, and a token-based retry budget.
Finally, the interviewer wants a recovery loop. A timeout bounds one wait, a retry handles a brief failure, a circuit breaker rejects calls during a sustained failure, and a bulkhead limits concurrency and queue occupancy. A strong answer also bounds half-open probes, refuses dishonest fallbacks, and observes logical calls separately from physical attempts.
Questions to Clarify Before Answering
- Is the dependency call a read or a write with side effects? Reads are usually repeatable. A timed-out write may have an unknown outcome and needs an idempotency key, status lookup, or reconciliation.
- Is 800 ms a hard deadline or an observational target? Here it is the point after which the caller no longer waits. Cancellation should propagate downstream so work is not completed for an absent caller.
- Which errors does the dependency declare retryable? Classification must follow the protocol and vendor contract, especially
Retry-Afteron 429, 503, connection failures, and business errors. “Anything other than 200” is not a policy. - Do the SDK, proxy, or service mesh already retry? Inventory every layer's defaults and attempt limits. One visible application retry can otherwise compound into a retry storm.
- What are the dependency's latency distribution and capacity? Derive timeouts from percentiles, the tolerated false-timeout rate, and network allowance, then validate new connections, cross-region traffic, and peak load.
- Which degradation is acceptable? If tax calculation has compliance requirements, a fabricated “successful” default tax is unsafe. Explicit failure, manual review, or deferred checkout must follow a business contract.
- What is the circuit breaker's isolation boundary? Use an independent vendor, region, endpoint, or operation. One failed shard should not disconnect healthy resources.
30-Second Answer Framework
“I would split the 800 ms end-to-end deadline into a time budget and propagate an absolute deadline downstream. Every call has connection and request timeouts, and another attempt starts only when it fits the remaining budget. I classify failures by recoverability and whether side effects are known: honor server guidance for 429, retry only brief connection failures and selected 5xx responses, and require idempotency or status reconciliation when a write outcome is unknown. Retries live at one layer, use at most two attempts with capped exponential backoff and jitter, and consume a token budget that limits extra traffic during failures. Sustained failures open a circuit and fail fast; after cooldown, only a few half-open probes pass. A separate concurrency limit and bounded queue protect local resources. Fault injection then verifies the deadline, attempt count, duplicate effects, and recovery path.”
Step-by-Step Deep Dive
Start by turning time into an explicit budget. In this exercise, reserve 120 ms of the 800 ms for local work before the dependency call and 100 ms for processing the result. That leaves a 580 ms dependency budget. One testable starting configuration is a first attempt capped at 220 ms, a randomized backoff in [0, 80) ms, and a second attempt capped at 220 ms. The worst case consumes 520 ms and leaves 60 ms in the dependency budget. These are not universal constants; production values must be calibrated from downstream percentiles, tolerated false timeouts, network delay, and load tests.
Propagate an absolute deadline or a monotonically decreasing remaining budget through the call chain. Before retrying, recompute remaining. If it is smaller than “next attempt cap + minimum completion time,” return instead of starting a call that cannot finish. Define connection and request timeout semantics precisely, including whether DNS, TLS, and connection-pool wait are covered. A new instance can warm connections before accepting traffic so handshake time is not mistaken for a slow dependency. Propagate cancellation, while still assuming it can arrive after the remote side has committed work.
Next, build a failure-semantics matrix:
| Result | Retry? | Preconditions and action |
|---|---|---|
| Failure before connection is established | Yes | Enough budget remains; use backoff and jitter |
| HTTP 429 | Conditional | Honor Retry-After; the wait and next attempt must fit the deadline |
| HTTP 503 or selected 5xx | Conditional | The vendor declares it transient and retry budget remains |
| HTTP 400, 401, or 403 | Usually no | Fix parameters, credentials, or permissions; waiting will not repair the request |
| Timeout after a write was sent | Never blindly | Reuse the idempotency key or query and reconcile operation status |
| Caller cancellation or expired deadline | No | Stop adding work and return an explicit failure |
Three gates control every retry: the error is retryable, enough time remains, and the retry budget has a token. Use capped exponential backoff and randomized jitter to decorrelate clients that fail together. Prefer usable server-directed retry timing when it exists. The maximum-attempt count includes the initial call; this design starts with two total attempts. Read SDK wording carefully because maxAttempts = 2 and “two retries” may mean two total calls and three total calls, respectively.
Retry at one layer that best understands the business deadline. If all five layers make three attempts, the theoretical bottom-layer load grows by 243 times. Track downstream_attempts / logical_requests as the retry amplification factor. It should be near 1 in healthy conditions and explicitly capped during faults. A token bucket or equivalent retry quota drains as failures rise, then pauses retries or permits them at a low rate, so clients stop adding load when the dependency is most vulnerable.
Wrap the specific dependency operation in a circuit breaker. The closed state allows calls and measures attributable failures and slow calls over a sliding window with a minimum sample size. Crossing the threshold opens the circuit. The open state does not call the dependency and immediately returns a recognizable failure or a business-approved degradation. After cooldown, the half-open state permits only a small number of concurrent probes. Sufficient probe success closes the circuit; a critical failure reopens it. Thresholds come from traffic and recovery characteristics. “20 calls and 50% failures” is an example, not a universal constant.
The circuit breaker has costs. Modal state makes testing and recovery more complex, a long cooldown delays recovery, and a coarse boundary can block healthy shards. Record every state transition, cap half-open probe concurrency, and scope the breaker to a real failure domain. For brief, low-risk faults, strict timeouts, single-layer retries, and a retry quota may already be sufficient. Do not add a breaker just to name another pattern.
Local resources still need isolation. Give the tax dependency its own concurrency limit, connection pool, and bounded queue so slow calls cannot consume every checkout thread or connection. Queued calls also spend deadline budget; discard work that cannot finish when it leaves the queue. A fallback must be truthful and explainable. “Tax is temporarily unavailable” or a manual workflow can be valid; treating an unknown tax as zero and declaring checkout successful is not.
Test invariants under failure. Inject connection refusal, a 250 ms slow response, 429s with different Retry-After values, 503, a non-retryable 400, and “write committed, response lost.” Assert that one logical request makes at most two downstream attempts, total latency does not exceed 800 ms, permanent errors are not retried, and unknown writes are not duplicated. Then sustain failures until the circuit opens. Verify fast rejection, bounded half-open probes, successful closure after recovery, and an unsaturated bulkhead.
Production metrics must separate calls from attempts: end-to-end success and p95/p99, per-attempt results and latency, connection- versus request-phase timeouts, retry amplification, retry recovery rate and added latency, retry-budget balance, breaker state and rejection count, half-open probe results, pool concurrency and queue depth, plus idempotency conflicts and reconciliation outcomes. Final success alone can hide a system buying availability with three times the downstream traffic.
High-Quality Sample Answer
“First I would confirm that this is a synchronous tax lookup and that the caller stops waiting after 800 ms. If I reserve 120 ms for work before the call and 100 ms for response handling, the dependency gets 580 ms. My starting policy uses two total attempts: each can take at most 220 ms, with [0, 80) ms of jittered backoff between them. Before each attempt, I check the propagated remaining deadline, so a long queue does not mechanically trigger a second call.
Failures need classification. A failure before connection establishment and a vendor-defined transient 503 can be retried within budget. A 429 follows Retry-After, but if the wait would exceed the deadline, I fail immediately. I do not retry 400, 401, or 403. If the operation writes state, a timeout after sending means unknown outcome. I must reuse an idempotency key or query and reconcile status rather than issuing a fresh blind request.
I inventory retries in the SDK, gateway, and service mesh, then put the retry at one layer that can see the business deadline and enforce a token retry budget. Three attempts at each of five layers can amplify the bottom call by 243 times, so I monitor physical attempts divided by logical requests.
For sustained failure, I scope a circuit breaker by tax vendor and operation. Closed measures failure rate with a minimum sample, open fails fast, and half-open permits only a few probes before recovery. The dependency also gets a separate concurrency pool and bounded queue. Degradation returns only a business-approved, explicit state; it never fabricates zero tax.
Finally, I inject failures to verify the two-attempt maximum, the 800 ms boundary, no retries of permanent errors, no duplicate writes, and circuit recovery through bounded probes. In production I watch logical success, attempt results, retry amplification, budget balance, breaker state, and bulkhead saturation together.”
Common Mistakes
- Set the dependency timeout to the full 800 ms → No time remains for local completion, and the dependency keeps resources after the caller gives up → Subtract local and network budgets from the deadline and propagate the remainder.
- Retry every unsuccessful response → Parameter and permission errors do not self-heal, so retries only add load and latency → Classify by protocol, error code, and side-effect outcome.
- Enable three attempts at every layer → Five layers can amplify one call into 243 bottom-layer attempts → Retry at one suitable layer and measure amplification.
- Send a timed-out write again with a new ID → The first attempt may have committed, causing duplicate charges or resources → Reuse an idempotency key or query and reconcile status.
- Use exponential backoff without jitter or caps → Clients can still retry in synchronized waves → Cap attempts and elapsed time, and randomize the delay.
- Treat a circuit breaker as a timeout replacement → Calls already in flight still occupy threads, connections, and queues → Keep per-attempt timeouts and isolate concurrency.
- Restore all traffic as soon as half-open begins → A newly recovering dependency is overwhelmed again → Allow bounded probes and require explicit recovery success.
- Use one global breaker for every vendor and endpoint → One local fault blocks healthy resources → Scope breaker state to independent failure domains.
- Always return success from a fallback → Unknown data is presented as correct and breaks business semantics → Use only approved degradation with explicit limitations.
- Observe only final success rate → A retry storm can temporarily hide downstream deterioration → Observe logical calls, physical attempts, added latency, and saturation.
Follow-Up Questions and Responses
Follow-up 1: What multiple of downstream p99 should the timeout be?
There is no fixed multiple. Choose an acceptable false-timeout rate, start from the matching latency percentile, and add allowance for network delay, cross-region calls, connection establishment, and small shifts. The result must still fit the upstream deadline. When p99 is close to p50, a small latency shift can cause many timeouts, so additional padding may be appropriate. Verify whether DNS, TLS, and connection-pool wait are included in the timer.
Follow-up 2: Are both 429 and 503 retryable?
Only conditionally. Prefer Retry-After for 429, but the wait and another attempt must fit the remaining budget. Otherwise, return a recognizable failure. Retry 503 only when the vendor contract marks it transient. Both consume retry budget; neither should turn an overloaded server's signal into more immediate traffic.
Follow-up 3: Why can a high retry recovery rate still indicate an unhealthy system?
Final success may be purchased with extra load and latency. If 100 logical calls generate 180 downstream attempts, amplification is 1.8. When the dependency is near capacity, the extra 80% can delay recovery. Evaluate recovery rate together with amplification, attempt latency, budget exhaustion, and downstream saturation.
Follow-up 4: Do you still need rate limiting or bulkheads when the circuit is open?
Yes. The breaker blocks new calls to one dependency, but local requests can already be in flight or queued, and other dependencies can still consume resources. Concurrency limits, separate pools, and bounded queues own local capacity; entry rate limiting controls new load. Half-open probes also need a small independent allowance rather than unlimited shared capacity.
Follow-up 5: Is it a problem if each instance maintains its own breaker state?
States can diverge, but strongly shared state adds another synchronous dependency and more latency. A common design gives every instance the same configuration and local state; at high traffic, each sees enough samples and the failure signal spreads naturally. Low-volume instances or globally coordinated failure domains may justify a service mesh or centralized layer. In either case, state the scope, sample size, and maximum extra traffic during failure.
Follow-up 6: When should this become an asynchronous queue?
If the HTTP response does not require the tax result, or recovery can take far longer than the user's deadline, asynchronous processing is a better fit. Persist a task, return queryable status, and let a consumer use its own retry and dead-letter policy. A queue does not remove idempotency, expiry, or degradation concerns, but it moves long recovery out of synchronous connections and the 800 ms budget.
Follow-up 7: How would you roll this out without a configuration incident?
Start by recording the timeout, retry, and circuit decisions that would have been made, without adding retries or rejecting traffic. Confirm that SDKs do not hide extra attempts. Then canary by vendor or a few instances, cap the global retry budget, and keep a fast disable switch. Watch attempt volume, end-to-end tail latency, open duration, half-open failures, and dependency capacity before expanding.