Interview Handbook by Sayed Elmahdy العربية

Topic 32 · System design & career

System design

A framework for answers, scaling, caching, queues, multi-tenancy, worked examples.

15 questionsJunior · 1Mid · 6Senior · 810 quiz

Concepts

What you will learn: Sketch a service that meets a user need and handles failure and growth, explaining the cost of each component instead of adding boxes by habit. Order, payment, notification and upload examples show how requirements drive choices.

Before you start: Know the basics of , databases and . A functional requirement says what the system does; a quality attribute sets a level such as or availability. Latency is response time; is operations completed per unit of time. If these terms are new, start with the answer framework and estimates before the designs.

Select a term with a dotted underline for its meaning, then continue reading from the same place.

#A framework for system design answers

An open design question tests trade-off decisions. Establish scope before drawing. For a store, distinguish catalog from checkout, ask about regions, and clarify whether orders may remain pending while payment is confirmed.

  1. Define functional requirements, non-goals and users.
  2. Ask about peak load, data size, , availability, and .
  3. Estimate quantities and state assumptions.
  4. Define , the data model and .
  5. Draw the simplest working path, then explore the hardest issue: consistency, fan-out or security.
  6. Challenge the design with an outage and traffic spike, explaining measurement, scaling and cost.

RPO is acceptable data loss; RTO is the target recovery time. Both need business context. In a 45-minute interview, an illustrative is 5 minutes clarifying, 5 estimating, 10 sketching, 20 exploring details and 5 discussing follow-ups; adapt to the interviewer.

#Back-of-envelope estimates and latency budgets

A worked example
  1. Assume one million requests per day. Dividing by 86,400 seconds gives about 11.6 requests per second as a daily average, not peak load.
  2. An illustrative peak assumption of ten times average gives about 116 requests per second. Label that multiplier as an assumption requiring evidence, not a rule.
  3. At roughly 2 kilobytes per response, multiply by request rate to estimate response traffic, then account for requests, headers and background work as needed. Estimate scale before choosing unnecessary machinery.

Numbers keep infrastructure decisions grounded. State units and distinguish averages from peaks and requests from business operations. The following figures are exercise assumptions, not benchmarks or performance promises.

50,000 orders/day / 86,400 seconds/day ~= 0.58 orders/second average
Assume peak = 10 * average ~= 5.8 orders/second
At 8 API calls/order: ~= 46 order-related requests/second at peak
At 2 KB/order: ~= 100 MB/day raw, before lines, indexes, replicas and backups
1 million catalog reads/day ~= 11.6 reads/second average
At 200 requests/second and 0.15 seconds average response time:
Little's law: average in-flight requests ~= 200 * 0.15 = 30
Use a stable system boundary; queue wait belongs in response time.
OperationExercise assumptionImplication
Local memory lookupUnder 1 msNot every cache hit crosses a network
Same-region cache round trip1–5 msNetwork and still matter
Indexed database query5–30 msQuery shape and contention can change this
Cross-region request50–200 msDistance costs time before application work
External payment call200–1,000 msA dependency outside your control

Measure p50, and p99 in the actual deployment. End-to-end p99 is not obtained by simply adding every service's p99; identify sequential and parallel paths and measure the complete journey. Low estimated QPS does not prove capacity: verify with load tests and query inspection.

#Building blocks: each box has a cost

ComponentSolvesIntroduces
Load balancerRequest distribution and unhealthy-instance removal, timeouts and trusted proxy headers
Nearby delivery of cacheable contentInvalidation and private-data cache safety
CacheRepeated readsStaleness, stampedes and tenant key
QueueDecoupled arrival and processing ratesDuplicates, backlog, ordering and dead letters
Read replicaRead scaling lag and read-your-writes routing
Data and write distributionCross-shard joins, rebalancing and hot keys
Rate limiterTenant fairness and capacity protectionKey selection and counter consistency during failure

A stateless does not depend on one instance's memory for session correctness. The system still has state in databases, queues and object storage, all requiring recovery plans.

#Design 1: online-store order processing

Assume one region, modest order volume, no overselling and no duplicate charges. Start with a modular monolith and a relational database. Catalog reads can tolerate staleness, but reserving the final item requires an atomic decision.

Client -> LB -> Stateless .NET API -> DB: orders, reservations, idempotency, outbox
                       |                                      |
                       v                                      v
                Payment provider                   Outbox relay -> broker
                       |                                      |
             Verified webhook -> API                  Notifications / shipping
                       ^
              Reconciliation worker

POST /orders                  Idempotency-Key scoped to customer and operation
GET  /orders/{id}              Ownership check
POST /payments/webhooks        Verify signature, amount, currency and event ID
  1. In one , reserve stock and persist the pending order, key, payload hash and . A unique constraint prevents concurrent reuse; reject the same key with a different payload.
  2. After commit, call the payment provider outside the database transaction using a supported idempotency key. A timeout is an unknown outcome, not a decline.
  3. A verified webhook performs a conditional state transition and records its event ID; duplicate callbacks must not repeat effects.
  4. Reconciliation resolves pending attempts. Coordinate reservation expiry with late payment: renew stock or refund according to explicit policy.
  5. An outbox relay can publish twice after a crash between publishing and marking sent. Consumers need idempotency; ordering, where required, is per order rather than global.
-- (1) Inside the transaction that also creates the order and reservation.
UPDATE Stock
SET Available = Available - @quantity
WHERE TenantId = @tenant AND Sku = @sku
  AND Available >= @quantity;
-- (2) Require exactly one affected row; otherwise roll back and report unavailable.
🧾 Code walkthrough
  1. (1) Validate positive quantity. The database atomically checks and updates stock; a C# lock does not coordinate different instances.
  2. (2) Multi-product orders roll back every reservation if one fails. Access rows in a consistent order to reduce and bound safe transaction retries.

External payment is outside the database's boundary. Splitting ownership into services requires a state machine, compensation and monitoring, beyond moving methods behind .

#Design 2: URL shortener

Support link creation, fast redirection, expiry and abuse takedown, assuming a read-heavy workload. Use Links(Code PK, Destination, OwnerId, ExpiresAt, Disabled). Validate / schemes, length and mutation ownership.

  1. POST /links generates a sufficiently random Base62 code and inserts under a unique constraint, retrying collisions. A Base62 sequence is simpler but enumerable.
  2. GET /{code} checks a hot-mapping cache, then the database, including expiry and disabled status. Cache must not outlive expiry.
  3. Use 302 for changeable destinations with deliberate cache policy; 301 is permanent and can persist in clients. Set Cache-Control explicitly rather than relying on status alone.
  4. Bounded asynchronous analytics must not prevent redirection when analytics fails.
7 Base62 characters: 62^7 = 3,521,614,606,208 possible codes
Large address space does not eliminate random collisions.
Redirect: client -> edge/cache -> API -> Links DB
                                      -> async click event

A short code is not an access ; private files need separate authorization. If a preview fetches the destination server-side, protect against and redirects into internal networks. Immediate takedown needs explicit edge and cache invalidation.

#Design 3: notification delivery

The same event can arrive twice, and a provider may accept a message before timing out. Consume an event, apply user preferences and consent, choose template and language, and persist the notification with channel tasks before delivery.

OrderShipped -> preferences + templates -> delivery records
                                             |
                       Email queue / SMS queue / Push queue
                                             |
                    Rate-limited workers -> provider -> status callbacks
Unique logical delivery: (EventId, RecipientId, Channel)
States: Pending -> Sending -> Accepted -> Delivered / Failed / Unknown

Distinguish accepted from delivered. Use bounded backoff and jitter for transient failures, and dead-letter handling with a controlled replay process. Use provider when available; local deduplication cannot guarantee exactly-once after a crash. Failing over while the outcome is unknown may duplicate delivery, so define an appropriate policy. Respect unsubscribe and quiet hours, and use expiring links for sensitive attachments instead of logging their contents.

#Design 4: rate limiter

Clarify whether the goal is limiting concurrent work or requests over time. Fixed windows are simple but allow boundary bursts. Sliding windows smooth boundaries; buckets allow bounded bursts with sustained refill. A concurrency limiter caps in-flight work rather than requests per minute.

An in-memory limiter applies separately to each instance. Use a gateway or atomic distributed state for a global quota. Partition by tenant, user and endpoint as required; -only keys penalize users behind shared .

Token bucket state per key: tokens, lastRefillTime
Atomically:
  tokens = min(capacity, tokens + elapsed * refillRate)
  if tokens >= requestCost: subtract cost and allow
  otherwise: reject with 429 and a meaningful Retry-After when available
Never split read/refill/decrement into independent distributed commands.

Choose fail-open versus fail-closed if the counter store fails: a catalog may use a local fallback, while expensive endpoints need stricter protection. Limit key and expiry to prevent memory abuse. Prefer a short bounded queue or rejection over unbounded waiting.

Rate limiting design

#Design 5: file upload service

Buffering a large file in memory wastes bandwidth and . Separate the control plane that authenticates and authorizes uploads from the data plane that transfers bytes to object storage.

  1. POST /uploads checks quota and ownership, creates a random object key and pending record, and returns a short-lived signed scoped to a specific operation.
  2. The client uploads directly, using multipart or resumable transfer for large files. Never grant permanent storage credentials.
  3. A callback or POST /uploads/{id}/complete triggers server-side verification of object existence, size, checksum and actual type; the client claim is insufficient.
  4. Keep content quarantined until malware and policy checks finish. Transition idempotently to ready or rejected.
  5. Downloads need authorization and expiring access. Periodic cleanup removes incomplete uploads and orphaned objects.
Client -> API: authorize, reserve quota, issue upload permission
Client -> private object storage: upload bytes
Storage event -> queue -> scanner -> metadata DB: Ready / Rejected
Client -> API: authorize download -> expiring object URL

The database and object store do not share one . Use a state machine and reconciliation, binding approval to the exact scanned object version so content cannot be replaced afterward. Verify which restrictions the provider's signed URL can enforce, and validate the remainder server-side.

Restricted direct-storage access

#Multi-tenancy and isolation choices

ModelBenefitCost
Shared database with TenantIdLower cost and centralized operationsLeak prevention, noisy neighbors and difficult per-tenant restore
per tenantLogical separation of some objectsMany migrations while server resources remain shared
Database per tenantIndependent restore, movement and data Provisioning, migrations, connections and higher cost
HybridShared small tenants and dedicated large tenantsRouting, movement and two operational models

A tenant is not a user; one organization may contain users with different permissions. Resolve tenants from trusted identity and scope indexes, unique constraints, caches, jobs and rate limits appropriately. filters help, while can add database defense; review raw , administrative paths and pooled connection state.

Separate databases on one server do not provide complete compute isolation. Clarify whether isolation concerns data, resources, region or encryption keys.

Multitenant architecture guidance

#Scaling a .NET API and preserving reliability

  1. Find the bottleneck with and load tests: , database, dependency or thread pool. Define an first.
  2. Fix queries, indexes, pagination and async . Watch connection pools and downstream capacity.
  3. Cache reads that tolerate staleness and use a for files; test cache outages and stampedes.
  4. Run stateless instances behind a load balancer, externalizing required session state. Test readiness and graceful shutdown.
  5. Queue deferrable work with bounded workers; return 202 with a status resource where the contract allows.
  6. Add read replicas with a lag policy, and shard only when measured needs justify complexity.

Redundancy requires separate failure domains and tested failover. Backups need restore drills. Graceful degradation may disable recommendations while preserving checkout; reduces admission when consumers fall behind. Monitor queue age as well as length, and use deadlines, retry budgets and circuit breakers for dependencies.

Scaling instances can exhaust database connections. A queue absorbs temporary bursts; it cannot fix permanently excessive arrival rates.

Interview questions

Check your reasoning

Why does this choice fit? When would it fail? What changes under higher load or a dependency failure?

32.1How do you begin a system design interview?Mid
Model answer

I begin by clarifying the user journeys and explicit non-goals, then ask about peak traffic, data volume, , availability and consistency requirements. I state assumptions when numbers are missing and estimate the scale before choosing components. Next I define the main , data model and , draw a simple baseline and explore the hardest part. I finish by testing failure scenarios and explaining how measurements would justify scaling or changing the design.

32.2How do you estimate QPS and concurrent requests?Mid
Model answer

I divide daily operations by 86,400 for an average rate, then apply an explicit peak assumption and account for multiple requests per business operation. I estimate storage separately, including indexes, retention, replicas and backups. For a stable boundary, Little's law gives average in-flight work as multiplied by average response time, including queue wait. These are sizing hypotheses rather than capacity guarantees, so I validate them with workload-shaped load tests and tail- measurements.

32.3What is the difference between vertical and horizontal scaling?Junior
Model answer

I use vertical scaling to add , memory or capacity to one machine. It is operationally simple but has a ceiling and may require downtime. Horizontal scaling adds instances, normally behind a load balancer, and works best when request correctness does not depend on one instance's memory. It improves capacity and redundancy, but introduces coordination and shared-state concerns. More instances do not automatically increase database or downstream capacity, so I scale the measured bottleneck.

32.4How would you prevent overselling across several API instances?Senior
Model answer

I enforce the stock in the database, for example with an atomic update that decrements availability only when sufficient quantity remains. The order and reservation are written in the same , and zero affected rows means the reservation failed. For multiple items I roll back the complete reservation if any item fails. Expiry and release must also be and coordinated with payment state. An application lock cannot protect stock shared by several processes.

32.5What happens if the payment provider times out?Senior
Model answer

I treat the outcome as unknown because the provider may have charged successfully before the response was lost. I persist the attempt and use the provider's mechanism for safe retries, then resolve the outcome through verified webhooks or reconciliation. The order remains in an explicit intermediate state. I also define what happens if stock reservation expires before payment confirmation, such as re-reservation or refund. A database rollback cannot undo a charge in an external system.

32.6Why does an outbox still require idempotent consumers?Senior
Model answer

I use an to persist the business change and publication intent in one database . The relay can still publish successfully and crash before recording that success, so it will publish again after recovery. Consumers therefore need stable message identities and an effect, often with a deduplication record committed alongside the local state change. External providers need their own idempotency or reconciliation. I describe the delivery assumptions explicitly rather than claiming end-to-end exactly-once processing.

32.7How would you generate and serve short URLs?Mid
Model answer

I store a code-to-destination mapping with ownership, expiry and disabled status. Codes can come from a Base62 sequence or sufficiently random generation with a unique database constraint and collision retries. I cache hot mappings and keep analytics off the redirect path. Redirect and cache policy depend on whether destinations can change. I also consider abuse takedown and enumeration; a short code is not authorization, and any server-side preview fetch needs protection.

32.8Can a notification system promise exactly-once delivery?Senior
Model answer

I would not promise universal exactly-once delivery across arbitrary providers. A worker can lose the response after a provider accepts a message, making a retry ambiguous. I deduplicate logical delivery records and use provider where supported, but still model unknown outcomes and reconcile delivery callbacks. Failover can also duplicate a message if the original provider accepted it. I agree on acceptable duplicate risk and channel-specific policy, distinguishing provider acceptance from confirmed delivery.

32.9How would you build a global rate limiter?Mid
Model answer

I first define the partition key, sustained rate, allowed burst and whether the quota must be global. A bucket is useful when bounded bursts are acceptable. For several instances, I place enforcement at a gateway or use shared state with atomic refill-and-consume operations; independent local counters multiply the effective limit. I bound key and waiting, return useful rejection information, and define fallback behavior if the shared counter fails according to endpoint risk.

32.10How do you design secure large-file uploads?Senior
Model answer

I keep authorization and metadata in the while transferring bytes directly to private object storage with short-lived, narrowly scoped permissions. Large uploads can be multipart or resumable. Completion triggers server-side checks of the exact object, size, type and checksum, followed by malware scanning before availability. I bind approval to the scanned version, authorize downloads separately and clean up abandoned objects. Since storage and metadata do not share a , I use state transitions and reconciliation.

32.11How do you choose a multi-tenancy model?Senior
Model answer

I compare requirements, tenant count, workload variation, compliance needs and operational cost. A shared database is economical but needs reliable tenant filtering, scoped constraints and controls against noisy neighbors. Separate databases simplify some restore and movement scenarios at the cost of provisioning and migration management. A hybrid model can dedicate large tenants. I also scope caches, jobs and rate limits, and distinguish data isolation from compute isolation because separate databases can still share a server.

32.12How do you handle read-your-writes with replicas?Mid
Model answer

I assume replicas may lag and decide which reads can tolerate that. Immediately after an order update, I can route the relevant confirmation read to the primary, while catalog browsing uses replicas. More advanced systems can use session routing or a watermark when supported. I monitor lag and define fallback behavior. A fixed sleep is not a correctness guarantee because lag varies, and routing every read to the primary would remove much of the scaling benefit.

32.13How would you respond to a sudden tenfold traffic spike?Senior
Model answer

I protect core capacity first with admission control and explicit degradation of nonessential work. Caches and a absorb repeat reads, while bounded queues buffer deferrable work and worker concurrency protects dependencies. Stateless can scale out, but I watch database connections and downstream limits so scaling does not amplify the bottleneck. I monitor queue age and tail , and reject excess work when budgets are exhausted. Known peaks need capacity testing and preparation rather than reliance on autoscaling alone.

32.14When would you shard the database?Senior
Model answer

I shard when measured storage or write constraints justify the operational cost after simpler options have been considered. I choose a key that aligns with access patterns while avoiding hot partitions, then plan routing, rebalancing, backup and tenant movement. Cross-shard joins and become harder, so I identify which remain local and how other operations coordinate. by tenant is useful when most access is tenant-local, but a very large tenant can still become a hotspot.

32.15What are backpressure and graceful degradation?Mid
Model answer

I use to make producers slow down or reject work when consumers cannot keep up. Bounded queues, concurrency limits and deadlines prevent an overload from turning into unbounded memory use and stale requests. Graceful degradation preserves essential behavior by reducing optional work, such as disabling recommendations while checkout remains available. I define these modes explicitly and monitor their impact. A growing queue without admission control only postpones failure, and a fallback must not silently violate payment or security .

Test yourself

Quiz answers and completed-topic marks are saved in this browser; they do not sync across devices.

Score: 0 / 0

What should happen before drawing architecture boxes?

Requirements justify the components.

50,000 orders per day is approximately how many per second on average?

50,000 divided by 86,400 is about 0.58; peak load is separate.

At 200 requests/second and 150 ms average response time, average in-flight work is about...

Little's law gives 200 × 0.15 = 30 for a stable boundary.

What protects the last stock item across API processes?

Instances need one shared atomic decision.

A charge request timed out. Which state is safest until reconciliation?

The provider may have accepted the charge before the response was lost.

Why can an outbox relay publish twice?

Consumers need even with an .

Which counter design supports a strict global token bucket?

Separate operations allow concurrent requests to exceed the quota.

When should an uploaded object become downloadable?

Keep content quarantined until trusted checks complete.

What is a key risk of read replicas?

Sensitive reads following writes may need the primary.

A queue grows indefinitely because arrival rate exceeds processing rate. What is needed?

A queue absorbs bursts, not unlimited sustained overload.