Scalability Patterns and Techniques Questions
Scaling a system to handle growth in traffic and data: horizontal versus vertical scaling, statelessness, sharding and partitioning strategies, read replicas, and connection pooling. Covers capacity estimation, identifying bottlenecks, and the tradeoffs each scaling axis introduces. The general toolkit for taking a design from thousands to millions of users.
Explain backpressure in a distributed system and why it matters for reliability. What mechanisms would you use to implement it between services, such as request quotas, flow control in a messaging system, or reactive streams, and how do they prevent cascading failure?
Sample Answer
Direct answer
Backpressure is a flow-control mechanism where an overloaded consumer signals upstream producers to slow down, so the system stays within its real processing capacity instead of silently queuing work until it runs out of memory or falls over. It matters for reliability because, without it, a slow or overwhelmed component doesn't fail cleanly; it builds an unbounded backlog that eventually causes a resource exhaustion failure, which can then cascade into components that depend on it.
Structured elaboration
The core loop: a consumer reports its available capacity (a credit, a token, an explicit "slow down" signal) back to whatever is sending it work, and the producer honors that signal by slowing down, buffering locally, or rejecting new work rather than forcing it through.
Backpressure versus rate limiting. These are often confused but apply at different points in the request path:
| Backpressure | Rate limiting | |
|---|---|---|
| Who it protects | The receiving component itself, based on its own real-time capacity | The service as a whole, from any single client consuming more than its fair share |
| Where it applies | Internally, between cooperating components (a queue and its consumer, two microservices) | At the edge or ingress, against external or untrusted clients |
| Signal basis | Actual, live capacity (queue depth, in-flight work) | A pre-set policy (N requests per minute), regardless of current internal load |
They compose well together: rate limiting caps what's allowed in at the edge; backpressure handles what happens internally once accepted work outpaces a specific consumer's real capacity.
Concrete mechanisms:
- Reactive Streams (an interface pattern used by libraries like Project Reactor and RxJava): the consumer explicitly calls
request(n)to say how many items it can accept next, rather than the producer pushing an unbounded stream. - TCP windowing (transport layer): a TCP receiver advertises a receive window, the amount of unacknowledged data it's willing to hold, and a sender must stop once that window fills; this is backpressure operating below the application entirely, and it's why a slow reader can stall a writer even with no application-level queue involved.
- gRPC / HTTP/2 flow control: stream-level flow-control windows, multiplexed over one TCP connection, mean a client cannot outrun what the server has said it can currently accept; this is a separate, application-layer analog of the same TCP-level idea, not the same mechanism.
- Kafka consumer-side flow control: a consumer can
pause()/resume()specific partitions, andmax.poll.recordsbounds how many records a single poll returns, both of which let a consumer throttle its own intake rate. - RabbitMQ consumer prefetch: limiting unacknowledged messages delivered to a consumer at once prevents one slow consumer from being handed more work than it can hold.
- Bounded queues with a blocking or rejecting producer: the simplest application-layer mechanism; the queue has a fixed capacity, and a full queue either blocks the producer (creating backpressure) or rejects new work outright (a flow-control protocol decision that trades data loss for keeping the consumer alive).
Worked example
Consider a Go-style worker pool: a hot request-handling path writes work items into a fixed-size in-memory channel that a pool of workers reads from. Under normal load, the channel rarely fills and writes return immediately. Under a sustained spike, the channel fills up. At that point, without an explicit flow-control decision, the sending goroutine (the request handler) blocks on the write, which means the original HTTP request handling itself now blocks, which then makes that server's own request queue back up, which then causes its own health checks or upstream timeouts to start failing, all originating from one bounded channel filling up. This is backpressure working exactly as designed, propagating a real capacity signal backward, but only if the layer receiving that backpressure (the request handler here) has a bounded, timeout-aware way to react to it; without a timeout on the channel write, "backpressure" quietly becomes "the request handler hangs," which is an availability failure with a different shape than an unbounded queue overflowing. This is the same low-level pattern as TCP windowing above, a full buffer stalling a writer, just one layer higher in the stack.
Trade-offs & pitfalls
- Backpressure without a bounded, timeout-aware reaction just moves the failure upstream rather than preventing it, as the worked example shows; the mechanism only helps if every layer that receives a "slow down" signal has a defined, bounded way to act on it.
- A queue that's too generously sized delays the pain instead of preventing it, and the eventual failure (running out of memory) is harder to diagnose than an early, deliberate rejection would have been.
- Backpressure does not by itself prevent data loss, only unbounded resource growth; if a producer's reaction to a "slow down" signal is to drop the work rather than retry or buffer it durably, backpressure alone is not enough, and a durable retry or dead-letter path is a separate design decision.
- This is a distinct concept from cascading-failure prevention mechanisms like circuit breakers, which stop sending traffic to an unhealthy dependency rather than throttling to match a healthy one's real-time capacity; the two are complementary but answer different questions.
At scale, a centralized lock can become a bottleneck for high-throughput, write-heavy workloads. What architectural alternatives would you reach for instead? Discuss optimistic concurrency, partitioned ownership, sequence-based approaches, and application-level conflict resolution, and explain how each preserves correctness without a central lock.
Sample Answer
Direct answer
A centralized lock caps throughput at whatever one coordinator can serialize, so the fix is to stop needing a single point of serialization: use optimistic concurrency where conflicts are rare, partition ownership so most operations never contend at all, replace "acquire before act" with a monotonically increasing sequence that orders operations after the fact, or push conflict resolution into the application where domain knowledge can merge concurrent updates without blocking either writer.
Structured elaboration
Optimistic concurrency control. Instead of acquiring a lock before reading and writing, read a version (a row version number or timestamp), do the work locally, then write conditionally: "update only if the version is still what I read." If another writer got there first, the write is rejected and the operation retries. This preserves correctness (exactly one of the racing writers wins, the loser sees a definitive failure rather than silent overwrite) without ever blocking a reader or holding a lock across the work. It is a good fit when conflicts are the exception, not the rule; under high contention the retry rate itself becomes the bottleneck.
Partitioned ownership. Instead of one lock guarding the whole resource, split the resource (by key range, by tenant, by shard) so each partition has its own independent point of coordination, or no coordination need at all if each partition is owned by exactly one writer. Correctness comes from the partitioning key guaranteeing that concurrent operations on different partitions never touch the same data; cross-partition operations are the residual hard case and usually need one of the other three approaches or a higher-level transaction pattern.
Sequence-based approaches. Instead of gating access before the fact, assign every operation a monotonically increasing sequence number (from a counter, a log offset, or a timestamp with a tie-breaker) and let operations apply in sequence order, detecting and resolving out-of-order arrivals after the fact rather than blocking to prevent them. This is the pattern behind append-only logs and event sourcing: writers never wait for each other, and correctness comes from every reader processing the sequence in the same order.
Application-level conflict resolution. When two writers legitimately modify the same data concurrently and neither should simply lose, encode a merge rule in the application: last-write-wins with a well-defined tiebreak, a domain-specific merge (like summing two concurrent counter increments instead of picking one), or a conflict-free replicated data type whose merge function is mathematically guaranteed to converge regardless of arrival order. Correctness here means "any order of application produces the same final result," which is a stronger and harder-to-verify property than the other three approaches, but it is the only one of the four that never rejects or retries a write.
Worked example
Assume, as a planning input illustrating the shape of the trade-off rather than a measured fact: a single mutex-protected counter, serialized through one lock, sustains roughly 500 operations/sec before lock contention and context-switch overhead dominate. Partitioning it into 16 independent shard counters, each capable of the same per-shard throughput since they no longer share a lock, gives:
16×500=8,000 operations/sec aggregateThe cost of that 16x gain is that no single read sees an exact real-time global total; a caller who needs the true total sums all 16 shards, which is eventually consistent with in-flight writes rather than instantaneously exact. That is the general shape of the trade-off across all four techniques: throughput is bought by relaxing some property (blocking-free correctness for retries, or a single serialization point for a scan-and-sum read) that the centralized lock previously gave you for free.
Trade-offs & pitfalls
Optimistic concurrency degrades under high contention (retries compound instead of resolving), so it is the wrong default for a hot key with many concurrent writers, exactly the case a centralized lock was protecting. Partitioning shifts, rather than removes, the hard problem to any operation that spans partitions. Sequence-based approaches move complexity from "prevent the race" to "make every consumer correctly handle re-ordering and idempotent replay," which is a real engineering cost, not a free lunch. Application-level merge functions are the hardest to get right and the easiest to get subtly wrong (a merge rule that is not truly associative and commutative will produce different results depending on arrival order, defeating the whole point); they deserve the most scrutiny and the smallest blast radius when first deployed. The senior-level judgment call is matching contention pattern to technique, not defaulting to one everywhere.
For a read-heavy product catalog service, weigh the trade-offs between replicating a full cache to every region versus partitioning (sharding) cache entries by product or region. Consider read latency, cache-miss patterns, memory and network cost, consistency, and rebalancing complexity, then recommend an approach for a global retailer that sees traffic bursts from multiple regions.
Sample Answer
Direct answer
For a global retailer with bursty, multi-region traffic, neither pure full replication nor pure partitioning wins outright: full replication gives the best latency and simplest rebalancing but pays for it in memory and cross-region sync cost, while partitioning is cheaper but concentrates risk into hotspots when demand shifts. The right default is a hybrid: keep a small, region-local cache of the hottest slice of the catalog fully replicated in every region for latency, and back it with a sharded cache for the long tail, promoting items into the local cache when a region's traffic to them justifies it.
Structured elaboration
| Dimension | Full replication (every region holds the whole cache) | Partitioned (sharded by product or region) |
|---|---|---|
| Read latency | Best: any product is a local hit | Good only when the request lands on a local shard; a remote shard adds a cross-region hop |
| Cache-miss pattern | Only on first global write or expiry; predictable | Lower miss rate per shard for that shard's hot items, but a burst on one product can overload the single shard that owns it |
| Memory & network cost | High: full catalog held N times, one per region, plus cross-region invalidation traffic | Lower: no duplication of the catalog, and update broadcasts are smaller |
| Consistency | Async replication is simplest and typical; synchronous replication for strong consistency adds real latency | Simpler for the shard that owns a given item, since there's one writer path, but reads from other regions still need a remote call or a replication mechanism |
| Rebalancing complexity | Low: adding a region just means standing up another full copy | Higher: partition migrations and consistent-hashing-style reassignment are needed; hotspots require live re-sharding or targeted replication |
Why a global retailer with bursty traffic needs the hybrid, not either extreme
Bursty, multi-region traffic on a retail catalog is rarely uniform: a small set of products (a flash sale, a viral item) drive a disproportionate share of reads at any given time, and which products are hot can shift quickly. Pure partitioning puts that risk on a single shard, since consistent-hashing-style assignment (products and cache shards are placed as points on a circular hash space, so only nearby points move when shards are added; the mechanics of the hash ring itself are covered in more depth under load balancing's consistent-hashing pattern, and what matters here is the caching consequence) doesn't know a key is about to become hot until it already is. Pure full replication avoids that risk entirely but pays a flat memory and cross-region sync tax for the entire long tail of the catalog, most of which is rarely read in any given region.
The hybrid keeps region-local, fully replicated caches sized to each region's actual working set (the products that region's users actually read), backed by a sharded cache holding the full catalog. A traffic-based promotion rule (an item crossing a per-region hit-rate threshold gets pushed into that region's local cache) handles the shifting-hotspot case without requiring the whole catalog to be replicated everywhere.
Worked example
Assume, as a planning input rather than a measured fact, a product catalog sized at 50 GB, served across 6 regions.
Full replication cost:
50GB×6regions=300GB total cache memory
Partitioned cost (no duplication, split evenly across 6 shards, plus a replication factor of 2 within each shard for availability rather than for cross-region latency):
6shards50GB≈8.3GB per shard,50GB×2=100GB total with the availability replica
The partitioned approach uses roughly a third of the memory of full replication (100 GB versus 300 GB) at this illustrative catalog size. The hybrid sits between the two: if each region's working set is, say, 10% of the catalog (5 GB), replicating just that slice to all 6 regions costs:
5GB×6=30GB
on top of the 100 GB sharded backing store, for roughly 130 GB total, a fraction of full replication's 300 GB while still giving most reads (the ones hitting each region's working set) a local hit.
Trade-offs & pitfalls
- The hybrid's promotion rule needs a threshold and a demotion path; without demotion, the region-local cache grows unbounded as items get promoted but never removed, eventually approaching full replication's cost anyway.
- Cross-region invalidation is still required for the sharded backing store even in the hybrid; underestimating that traffic (versioned, pub/sub-style invalidation messages rather than synchronous broadcasts) is a common way the "cheaper" option ends up not being cheaper.
- A single globally hot product (a flash sale item) can still overload the shard that owns it even with promotion in place, if promotion reacts slower than the traffic spike; this is the scenario that specifically motivates proactive cache warming ahead of known events rather than purely reactive promotion.
- A content delivery network (CDN, a network of edge servers that cache content close to users) is a natural complement for static product assets (images, descriptions) but doesn't solve the dynamic pricing/inventory caching problem this comparison is about; don't conflate the two layers.
- Getting the region-local cache's time-to-live (TTL, how long a cached value is considered valid before refresh) too long trades staleness (wrong price or stock shown) for the latency win; too short and the hybrid starts behaving like the sharded-only design under load.
A product has 200,000 monthly active users growing 10% month over month. Show how you'd project traffic over the next 12 months and calculate when capacity, measured in RPS, will double. How would a seasonal spike (say, +50% in November) and a marketing campaign that doubles traffic for two weeks change your planning?
Sample Answer
Direct answer
Project monthly active users (MAU) forward with compound growth, MAU(t)=MAU0×(1+r)t, convert that into a traffic curve, and solve for when it crosses 2x today's traffic by taking a logarithm. For 200,000 MAU growing 10% month over month, capacity roughly doubles in about 7.3 months, so plan the next major capacity jump around month 7-8, not month 12. Seasonality and marketing campaigns then layer multiplicatively on top of that sustained-growth curve as separate, temporary spikes, not as part of the underlying trend line.
Projection
MAU(t)=200,000×(1.10)tAfter 12 months:
MAU(12)=200,000×(1.10)12≈627,686When does capacity double?
Solve (1.10)t=2 for t:
t=ln1.10ln2≈7.27 monthsSo baseline traffic, and the required requests-per-second (RPS) capacity that scales with it, doubles in roughly 7.3 months, meaning capacity planning should target month 7 or 8 (rounding up for safety) for the next major scale-up, not month 12 as a straight annual-planning cycle might assume.
Seasonality and campaign effects, layered on top
Both effects are multiplicative spikes on top of the baseline curve above, not adjustments to the growth rate itself:
- Seasonal spike (November, +50%): if November falls at month k in the projection, expected peak MAU (and peak RPS) for that month is MAU(k)×1.5.
- Marketing campaign (2x traffic for 2 weeks): averaged over a 4-week month, this is (2 weeks×2×+2 weeks×1×)/4 weeks=1.5× for that month's average, though the campaign's actual peak within those 2 weeks is the full 2x, which matters more for capacity than the monthly average does.
- If both land in the same month: the combined peak multiplier is 1.5×1.5=2.25× baseline for that month, not simply additive.
A different worked comparator: layering a seasonal spike on a sustained-growth target
A related planning problem shows up for a data-ingestion pipeline sized against a 10x sustained-growth target (a longer planning horizon than the 7-month doubling above): solving (1.10)t=10 gives t=ln(10)/ln(1.10)≈24.2 months, roughly two years to reach 10x baseline at the same 10% monthly growth rate. Layering a 2x seasonal spike on top of that 10x sustained target means the pipeline's peak provisioning needs to cover 10××2×=20× baseline, not just the 10x sustained figure, since the seasonal spike and the multi-year growth trend both have to be true at the same time during that one peak month. This is a materially different exercise from the 2-week campaign case above: a 20x peak on a data pipeline is usually validated with a dedicated load test that replays a realistic ingestion pattern at that multiple, rather than trusted from the arithmetic alone, since ingestion systems often have hard limits (storage throughput, downstream processing capacity) that do not scale as smoothly as a stateless request-handling tier.
Operational planning recommendations
- Use autoscaling with fast ramp policies so capacity can respond within the campaign's 2-week window, not just the multi-month growth trend, since a policy tuned only for gradual growth will react too slowly to a 2x spike that starts and ends within days.
- Load test at the predicted peak multiplier (2.25x for a combined seasonal-plus-campaign month, or 20x for the layered sustained-growth-plus-seasonal case) plus an additional safety margin, rather than testing only at the smoothed monthly-average multiplier.
- Use caching and content delivery network (CDN) offload to reduce the RPS that actually reaches origin infrastructure during a spike, which lowers how much raw compute capacity has to scale to cover the same user-facing multiplier.
- Track real-time metrics against the projected curve, so a campaign or season that outperforms (or underperforms) the assumed multiplier is caught early rather than discovered at the point of user-facing degradation.
Trade-offs and pitfalls
The most common mistake in this kind of projection is treating a marketing campaign's monthly average multiplier (1.5x) as the number to provision for, when the campaign's actual burst behavior within those 2 weeks, and especially within the peak hours of the peak days, is what capacity needs to survive; averaging over a full month smooths away exactly the spike that matters. A second pitfall is planning capacity purely off the compound-growth trend line and treating seasonality as an afterthought, when in a case like the layered 10x-plus-2x data-ingestion example, the seasonal multiplier applied to an already-large sustained-growth baseline can dwarf the trend-line number the team spent the most time modeling.
A user request traverses six microservices. How would you measure and attribute its P95/P99 tail latency, and what would you do to reduce it? Cover your instrumentation and sampling/tracing strategy, how you'd detect a spike, and mitigation techniques such as hedged requests, request prioritization, resource partitioning, and admission control.
Sample Answer
Direct answer
Measuring tail latency across six hops means separating two questions: which hop is actually responsible for a given slow request (attribution), and is the tail getting worse over time (detection). Attribution needs per-hop distributed tracing with sampling that preserves slow traces even when it drops fast ones; detection needs P95/P99 (95th- and 99th-percentile latency, the response times only the slowest 5% and 1% of requests exceed) tracked as their own alertable series, since a stable median (this is the common trap: P99 spikes while the median looks completely healthy) hides exactly this class of problem. Once a hop is identified, the fix is rarely "make everything faster" but a targeted mitigation such as hedged requests, request prioritization, resource partitioning, or admission control aimed at that specific hop, each of which trades some cost or complexity for the latency it buys back.
Measurement and attribution
- Instrumentation: every one of the six services emits a span per request with start/end timestamps, propagated trace context, and enough metadata (host, downstream call outcome, queue wait time) to distinguish "this hop was slow" from "this hop was waiting on the next one."
- Sampling strategy: pure random sampling at low rates (say 1%) will almost never happen to capture a P99 request, since by definition only 1% of requests qualify and the sample and the tail rarely overlap. Use tail-preserving (tail-based) sampling: buffer a trace briefly and only decide to keep it once you know whether any span exceeded a latency threshold, so slow traces are captured close to 100% of the time while typical traces are still sampled cheaply.
- Attribution: once a slow trace is captured, break its total duration into a waterfall of per-hop contributions to see which hop consumed the largest share.
- Spike detection: alert on the rate of change of P95/P99 against a rolling baseline (for example, a sustained jump relative to the trailing window), not on a single fixed threshold, since normal traffic variation would otherwise cause constant false alarms.
Worked example: attributing a P99 spike across six hops
As an illustrative example, not measured data, suppose a captured slow trace shows an 800 ms end-to-end duration split across the six hops as follows:
| Hop | Contribution to trace duration |
|---|---|
| A (edge/gateway) | 50 ms |
| B (auth/lookup service) | 300 ms |
| C (business logic) | 100 ms |
| D (data-access service) | 150 ms |
| E (enrichment service) | 100 ms |
| F (response assembly) | 100 ms |
| Total | 50+300+100+150+100+100 = 800 ms |
Hop B accounts for 800300=0.375=37.5% of the total, the single largest share, so it is the first place to investigate and the first place a mitigation should target, rather than spreading effort evenly across all six services.
Mitigation techniques, their overhead, and their risk
| Technique | What it does | Operational overhead | Risk |
|---|---|---|---|
| Hedged requests (replica hedging) | Send a second request to a different replica after a short delay if the first hasn't responded; take whichever finishes first and cancel the other | Requires idempotent operations and extra downstream capacity headroom to absorb the duplicate load | Can amplify load during a genuine overload, since a slow dependency triggers hedges everywhere at once; needs a cap on hedge rate or it makes the underlying problem worse |
| Request prioritization (priority queues) | Classify requests as interactive versus batch and schedule interactive traffic ahead of batch at every hop | Every hop in the path must honor the same priority scheme consistently, adding coordination and scheduling complexity | Low-priority traffic can starve entirely if there's no guaranteed minimum share for it |
| Resource partitioning (resource isolation) | Dedicate CPU/memory pools to latency-sensitive services so a noisy batch workload can't steal their resources | Deliberately reduces overall utilization efficiency in exchange for isolation, and adds infrastructure to manage separately | If the partitions are too small or the isolation boundary is drawn at the wrong level, the workloads you meant to separate can still interfere |
| CPU pinning | Bind a hot service's threads to specific cores to reduce cross-core cache misses and scheduler-induced jitter | Removes the scheduler's flexibility to pack other work onto those cores, reducing overall efficiency | Pinning to cores that still share a memory controller or cache with a noisy neighbor gives no benefit while still costing the flexibility; needs revisiting if hardware topology changes |
| GC tuning (garbage-collection tuning) | Reduce allocation rate and favor a pause-time-oriented garbage collector so tail latency isn't dominated by stop-the-world pauses | Requires runtime-specific expertise and ongoing revalidation as code and allocation patterns evolve | Trading pause time for throughput is a real trade, not a free win; a poorly chosen configuration can make both worse |
| Avoiding blocking I/O | Perform network and disk calls asynchronously so a thread isn't held idle waiting on a slow dependency | Async code is harder to write, test, and debug: error propagation and cancellation get more complex | Can hide backpressure (a signal that would otherwise tell the caller to slow down because you can't keep up) if not paired with bounded queues, since a service can accept far more concurrent work than it can actually finish in time |
| Admission control | Reject or shed excess load at the edge before it enters the six-hop path | Needs per-tenant or per-class quotas and clear client-facing signaling (retry-after style responses) | Overly aggressive shedding converts a latency problem into an availability problem for legitimate traffic |
Trade-offs and pitfalls
- Chasing every hop at once instead of attributing first. Without the waterfall breakdown, teams tend to optimize the hop that's easiest to touch rather than the one actually driving the P99.
- Sampling uniformly at a low rate and concluding tail latency "looks fine" because slow traces were simply never captured. The sampling strategy has to be tail-aware, not just cheap.
- Applying hedging without a cap. It is the mitigation most likely to backfire under genuine overload, since it adds load exactly when the system can least afford it.
- Treating any single mitigation here as free. Every row in the table above buys latency at the cost of either infrastructure efficiency, code complexity, or operational risk; picking one should follow from what the attribution step actually showed, not from familiarity with the technique.
Unlock Full Question Bank
Get access to all Scalability Patterns and Techniques interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.