Skip to content

Interview Prep

System Design Interview Questions

How to reason about scale, trade-offs and failure — the open-ended questions senior interviews lean on.

10 of 10 System Design questions shown. Answers are hidden by default — try each one before revealing it.

  1. 1.How would you design this system? Walk through the data model, the short-code scheme, and how you'd handle the read/write imbalance.

    Medium

    Design a URL shortener that takes a long URL and returns a short one which redirects to it. Creation happens steadily, but redirect traffic on existing short links is far heavier than creation traffic.

    Show model answer

    The core data model is small: a record per short code holding the original long URL, a created_at timestamp, and optionally an owner and an expiry. The access pattern is a single-key lookup — given a short code, fetch the long URL — so this doesn't need a relational schema with joins; a key-value store or a simple indexed table both work fine, with the short code as the primary lookup key either way. For generating the short code, one straightforward approach is to base62-encode an auto-incrementing id (digits plus upper and lower case letters gives a large space in very few characters), which guarantees uniqueness for free since it's derived from a unique id. The trade-off is that a single shared counter becomes a contention point at high write volume, which can be addressed by handing out blocks of ids to each application server to encode from, rather than round-tripping to a central counter on every request. An alternative is generating a short random code directly and checking for a collision before committing it, retrying on the rare collision — simpler to reason about, at the cost of an extra existence check per creation. The read/write imbalance is the more interesting design point. Since redirects vastly outnumber creations, and a relatively small set of links tend to account for a disproportionate share of redirect traffic, I'd put a cache in front of the datastore keyed by short code, so hot links are served without touching the database at all. I'd also think carefully about the HTTP redirect type: a 301 (permanent) redirect lets browsers and intermediate caches remember it and stop asking my service on subsequent visits, which reduces load the most but means I lose visibility into every future click and can't easily redirect an existing code elsewhere later; a 302 (temporary) redirect keeps every visit hitting my service, which costs more load but keeps click analytics and the ability to change the destination. I'd lean toward 302 unless click tracking genuinely isn't needed. As the system grows, application servers scale horizontally behind a load balancer since they're stateless, and the datastore itself can be sharded by short code once a single instance's capacity is the limiting factor. Expiry and custom aliases are natural extensions to the same data model, not architectural changes.

    A strong answer touches

    • distinguishes the low-volume creation path from the high-volume redirect path and designs for the read-heavy skew
    • proposes a concrete short-code scheme (base62 of an id, or random code with collision retry) and explains how collisions or contention are handled
    • puts a cache in front of the datastore for popular short codes
    • discusses the 301 vs 302 redirect trade-off and its effect on load versus analytics
    • explains how the datastore scales as volume grows (indexing, sharding by short code)
    • keeps extra features like expiry or custom aliases as extensions of the core model rather than complicating it

    This is one genuine answer, not the only correct one — compare it with your own reasoning rather than matching it word for word.

    url-shortenercachingscalability

  2. 2.What algorithm would you use, where would the limiter live, and how would you make it correct across multiple API servers?

    Medium

    Design a rate limiter for an API that caps each client at a fixed number of requests per time window — for example, 100 requests per minute — and rejects requests over that limit. The API runs on multiple server instances behind a load balancer.

    Show model answer

    I'd start with the algorithm, since it drives everything else. A naive fixed-window counter (reset the count to zero every minute) is simple but lets a client burst up to double the limit right across a window boundary — nearly the full quota at the end of one minute, and again right at the start of the next. A sliding window log (store every request timestamp and count how many fall in the trailing window) is precise but expensive to store per client at any real volume. A practical middle ground is a sliding window counter, which weights the previous and current fixed windows to approximate a true sliding window without storing individual timestamps, or a token bucket, where a client has a bucket that refills at a steady rate and each request consumes a token — this has the nice property of allowing brief legitimate bursts up to the bucket size while still enforcing a steady average rate. For a general API, I'd default to token bucket or a sliding window counter over a naive fixed window. Placement matters for effectiveness: ideally the limiter sits at the edge, in an API gateway or reverse proxy in front of the application servers, so a client that's over its limit gets rejected before it consumes any backend capacity at all, rather than after the request has already reached application code. The part that's easy to get wrong is state. Because requests from the same client can land on any of several API server instances behind a load balancer, an in-memory counter local to one instance is useless — a client could get 100 requests through each instance. The counter has to live in a fast shared store all instances can reach, typically an in-memory key-value store, keyed by client identifier (API key, account id, or IP depending on what's being protected against). The check-and-increment has to be atomic — using the store's native atomic increment command, or a small server-side script that reads and updates in one step — otherwise two servers checking the count at the same instant can both see 'under limit' and both let a request through past the cap. On rejection, I'd return HTTP 429 with a header telling the client when it can retry. I'd also consider layering limits — per-IP to blunt anonymous abuse, and per-account for authenticated abuse — since a single key doesn't protect against both.

    A strong answer touches

    • names and compares at least two rate-limiting algorithms (fixed window, sliding window, token bucket) and their burst/accuracy trade-offs
    • explains why per-instance in-memory counters don't work behind a load balancer, and requires shared state across instances
    • proposes a specific shared store (a fast key-value store) with an atomic check-and-increment to avoid race conditions
    • places the limiter at the edge/gateway so rejected requests don't consume backend capacity
    • specifies the client-facing response (429 status, retry information) on rejection
    • considers limiting by more than one key (IP and account) to cover different abuse patterns

    This is one genuine answer, not the only correct one — compare it with your own reasoning rather than matching it word for word.

    rate-limitingapiscalability

  3. 3.How would you design the pipeline from event to delivered notification, and how would you handle a channel that's slow or unavailable?

    Medium

    Design a notification system that delivers a message to a user through multiple channels — push notification, email, SMS — when an event happens elsewhere in the product, such as someone commenting on their post.

    Show model answer

    The first decision is decoupling: the service that detects the event (a new comment being saved) should not call notification delivery logic directly and wait on it. Instead it publishes an event onto a queue and moves on, so a slow or struggling notification pipeline can never slow down or fail the core feature that triggered it. A notification service consumes those events. For each one, it looks up the target user's preferences — which channels they've enabled, any quiet hours, whether they've opted out of this notification type — and their contact details, decides which channel or channels apply, and renders the message using a template appropriate to each channel (a push payload looks nothing like an email body). Actual delivery then goes through a separate worker queue per channel, each talking to its own provider (a push service, an email provider, an SMS gateway). Splitting by channel this way means each one can be scaled, rate-limited, and retried independently — a struggling SMS provider doesn't back up push notifications. For a channel that's slow or down, I'd apply retry with exponential backoff up to a bounded number of attempts, and move anything that keeps failing into a dead-letter queue for inspection rather than retrying forever and blocking the messages queued behind it. If a channel is failing at a high rate, a circuit breaker that stops sending to it for a short period avoids piling up doomed attempts and gives the provider room to recover. Because retries mean a delivery attempt can happen more than once, each attempt needs a stable idempotency key (event id plus user id plus channel) so a provider retry never results in the user receiving the same notification twice. On the data side, I'd track a notification record with a status (queued, sent, delivered or failed, and read once the user has seen it), and I'd consider batching or digesting lower-priority notifications — collapsing five likes on a post into one summary push rather than five separate ones — so a burst of activity doesn't spam the user, independent of how reliable any single channel is. Scaling-wise, partitioning the event queue by user id keeps per-user ordering without needing a single global order across all users.

    A strong answer touches

    • decouples the triggering event from delivery via a queue so a slow notification pipeline never blocks the core feature
    • models per-user, per-channel preferences and uses them to decide which channels apply
    • gives each channel its own worker/queue so one slow provider doesn't stall the others
    • covers retry with backoff and a dead-letter path for messages that keep failing
    • requires idempotency keys so retried deliveries don't reach the user twice
    • considers batching/digesting notifications to avoid overwhelming the user during a burst of activity

    This is one genuine answer, not the only correct one — compare it with your own reasoning rather than matching it word for word.

    notificationsqueuesmessaging

  4. 4.How would you design the queue and worker pool, and how would you handle a job that fails partway through?

    Medium

    Design a background job queue that lets application code enqueue tasks — such as resizing an uploaded image or generating a report — to be processed asynchronously by a pool of workers.

    Show model answer

    The basic shape is producers enqueueing jobs (a type, a payload, and metadata like priority and attempt count) onto a durable queue, and a pool of worker processes pulling from it and executing them. Durability matters because a job that's been accepted should survive a crash of the queue service itself, not just live in memory. The trickier part is making sure multiple workers pulling from the same queue don't double-process a job, while also making sure a job isn't lost if the worker handling it crashes mid-task. A common mechanism is: a worker pulls a job and it becomes invisible to other workers for a visibility timeout, rather than being deleted immediately; only once the worker finishes and explicitly acknowledges the job is it removed for good. If the worker crashes or hangs, the visibility timeout expires and the job becomes available again for another worker to pick up. The consequence of this is that delivery is at-least-once, not exactly-once — a job can be picked up more than once under failure conditions — so job handlers need to be written to be idempotent. Resizing an image is naturally idempotent (running it twice produces the same output); sending a generated report is not, unless the handler checks a dedupe key (the job id) before sending. For jobs that fail outright rather than timing out, I'd retry with exponential backoff up to a capped number of attempts, and after that move the job to a dead-letter queue rather than retrying indefinitely, so a systematically broken job doesn't loop forever and a human can inspect what went wrong. I'd also separate jobs by type or priority into distinct queues — a flood of low-priority thumbnail jobs shouldn't be able to starve an urgent job just because they were enqueued first — and let some workers specialize in specific queues if certain job types need different resources (for example, image processing being CPU-bound versus report generation being I/O-bound). The worker pool scales horizontally and independently of the producers, and I'd size it dynamically off queue depth rather than a fixed count, so a backlog triggers more workers rather than growing indefinitely. Each job record tracks its status and attempt count so failures are visible rather than silent.

    A strong answer touches

    • explains how the queue avoids losing a job if a worker crashes mid-processing (visibility timeout and requeue)
    • identifies that this implies at-least-once delivery and requires idempotent job handlers, with a concrete example
    • designs retry with backoff and a dead-letter queue for jobs that keep failing
    • separates jobs by type or priority so low-priority work can't starve urgent jobs
    • scales the worker pool independently of producers, ideally driven by queue depth
    • tracks job status and attempt count so failures are observable

    This is one genuine answer, not the only correct one — compare it with your own reasoning rather than matching it word for word.

    job-queueasyncreliability

  5. 5.How would you model conversations and messages, and how would a new message reach an online recipient quickly?

    Hard

    Design the data model and high-level architecture for a simple one-to-one and small-group chat feature, where users see message history and new messages arrive in near real time.

    Show model answer

    I'd model three things: a conversation (representing either a one-to-one or a group thread), a conversation_members table linking user ids to conversation ids (this is what makes group chat not a special case — a one-to-one conversation is just one with exactly two members), and a messages table with conversation_id, sender_id, content, and an ordering value. For ordering, I'd avoid relying purely on a wall-clock timestamp, since two messages can arrive within the same millisecond and timestamps aren't guaranteed strictly increasing across servers; an incrementing sequence number (globally or per-conversation) gives reliable ordering and makes cursor-based pagination straightforward — fetch messages after sequence N, rather than paginating by offset, which gets expensive and can skip or repeat rows as new messages arrive. For read status, I'd avoid a row per message per member, which grows without bound; instead each member of a conversation tracks a single last_read_message_id, which is enough to compute unread counts and render read markers cheaply. For real-time delivery, the client holds a persistent connection to the server — a WebSocket is the standard choice — so the server can push a new message the moment it arrives instead of the client polling for it. The complication is that with many server instances behind a load balancer, a given user's live connection could be on any one of them, so when a message needs to be delivered, the server handling the send doesn't inherently know which instance holds the recipient's connection. I'd solve this with a connection registry — a shared, fast key-value store mapping user id to the server instance (and connection identifier) currently holding their live connection — so the sending server can look up where to route the push, or publish it on a channel that the right instance is subscribed to. Critically, I'd write the message to durable storage first, before attempting real-time delivery, so the message is never lost even if the push fails or the recipient is offline. If the recipient isn't connected, delivery falls back to a push notification, and the client fetches anything it missed by sequence number on reconnect rather than assuming the push channel is a reliable transport on its own.

    A strong answer touches

    • separates conversations and membership from messages so group chat isn't a special case bolted onto one-to-one chat
    • picks an ordering scheme (sequence number, not raw timestamp) that supports reliable pagination
    • keeps read status cheap with a last-read pointer per member instead of a row per message per member
    • explains persistent connections (e.g. WebSocket) for push delivery instead of polling
    • explains how a connection registry routes a message to whichever server instance holds the recipient's live connection
    • writes the message durably before attempting delivery, and handles the offline case with a fallback rather than assuming delivery always succeeds

    This is one genuine answer, not the only correct one — compare it with your own reasoning rather than matching it word for word.

    chatdata-modelingreal-time

  6. 6.How would you introduce caching to relieve the database, and how would you keep the cache from serving stale or wrong data?

    Medium

    An API serves product detail pages. Reads outnumber writes by a wide margin, and a relatively small set of popular products account for most of the traffic, but the underlying database is starting to struggle under read load.

    Show model answer

    I'd add a cache-aside (lazy-loading) layer in front of the database, typically a fast in-memory key-value store, keyed by product id. On a request, the application checks the cache first; on a hit, it returns the cached value without touching the database at all; on a miss, it reads from the database, writes the result into the cache with a time-to-live, and returns it. This is a good fit for the described access pattern specifically because traffic is skewed toward a small set of popular products — a cache sized for that hot set absorbs the bulk of read load even though it holds only a fraction of the full catalog. The harder part is correctness. On a write to a product, I'd delete the corresponding cache key rather than try to update it in place: deletion is simpler to reason about and can't leave the cache holding a subtly wrong value if the update logic misses an edge case, at the cost of one cache miss (and a database read) the next time that product is requested. I'd keep the TTL as a safety net regardless, so even if an invalidation is ever missed due to a bug, staleness is bounded to at most one TTL window rather than indefinite. One failure mode worth designing around explicitly is a cache stampede: if a very popular key expires and a burst of concurrent requests all miss at once, they can all hit the database simultaneously, defeating the point of the cache right when it matters most. I'd mitigate this with a single-flight pattern — only the first request that misses actually queries the database and repopulates the key, while concurrent requests for the same key wait on that result instead of each querying independently — or by staggering TTLs slightly so hot keys don't all expire in the same instant. For the product pages themselves, since they're largely identical for all users, I'd also consider a CDN or edge cache in front of the application entirely, which offloads load from the application servers as well as the database. Throughout, I'd treat the cache purely as an optimization: the database remains the source of truth, and the system has to behave correctly even if the cache were flushed entirely.

    A strong answer touches

    • names a specific caching pattern (cache-aside/lazy loading) rather than just saying to add a cache
    • ties the approach to the described access pattern (a small hot set driving most traffic)
    • addresses invalidation on writes (delete vs. update) and picks a default with reasoning
    • uses a TTL as a bound on staleness even if an invalidation is missed
    • identifies cache stampede risk on a hot key's expiry and names a mitigation
    • treats the cache as a disposable optimization with the database remaining the source of truth

    This is one genuine answer, not the only correct one — compare it with your own reasoning rather than matching it word for word.

    cachingscalabilityread-heavy

  7. 7.How would you design the upload path and where you'd store the files, and how would you make large or flaky uploads reliable?

    Medium

    Design a service that lets users upload files — such as documents or images up to a few hundred megabytes — reliably, including over unreliable mobile connections.

    Show model answer

    I'd split the file bytes from the file's metadata. The bytes go into object/blob storage built for large binary data, while a metadata record — file id, owner, original filename, size, content type, storage location, and an upload status — lives in the regular application database. Putting large binaries directly into a relational database is a poor fit; it bloats backups and row storage for data the database isn't optimized to serve. Reliability over a flaky connection is really about avoiding one giant all-or-nothing request. I'd use chunked or multipart upload: the client splits the file into fixed-size parts and uploads them individually. If the connection drops mid-upload, only the in-flight chunk needs to be retried, not the whole file — for a few-hundred-megabyte file on mobile, that difference is the difference between uploads completing at all. To support resuming after a disconnect entirely (not just retrying one chunk), the server (or the object storage's own multipart-upload tracking) keeps a record of which parts have already been received, so the client can query that on reconnect and continue from where it left off instead of restarting. Where possible, I'd have the client upload chunks directly to object storage using a short-lived signed URL issued by the application server, rather than proxying every byte of the file through the application tier. This keeps the application servers doing light work — authorizing the upload and tracking metadata — so their capacity doesn't scale with upload volume or file size, only with request count. Once all parts are received, the server finalizes the upload: verifying a checksum against what the client reports to catch corruption, confirming the multipart upload is complete on the storage side, and only then marking the metadata record's status as complete so other parts of the system can treat the file as available. I'd treat the finalize step as idempotent, since a retried finalize call shouldn't create a duplicate record. Validation like malware scanning I'd run asynchronously after the upload completes rather than blocking the user on it, updating the status again once the scan finishes. I'd also periodically clean up metadata for uploads that were started but never finalized.

    A strong answer touches

    • separates binary storage (object storage) from metadata storage (the application database)
    • proposes chunked/multipart upload so a dropped connection costs a retry of one chunk, not the whole file
    • explains how an interrupted upload resumes by tracking which parts already succeeded
    • has the client upload directly to object storage (e.g. via a signed URL) instead of proxying bytes through the application server
    • verifies integrity (checksum) and finalizes only once all parts are confirmed, with an idempotent finalize step
    • treats malware scanning and other validation as asynchronous post-processing rather than blocking the upload

    This is one genuine answer, not the only correct one — compare it with your own reasoning rather than matching it word for word.

    file-uploadstoragereliability

  8. 8.How would you serve suggestions fast enough to feel instant, and how would you keep the suggestion data useful over time?

    Hard

    Design an autocomplete feature for a search box: as a user types, the system suggests up to five likely completions, and suggestions need to appear with no perceptible lag.

    Show model answer

    The binding constraint here is latency, not the completeness of a full-text search, so I would not query the primary transactional database or run text search on every keystroke — both are far too slow for a feature that has to feel instant. Instead, I'd precompute a structure built specifically for prefix lookups: either a trie, or more practically at scale, a flat key-value index in a fast in-memory store where the key is a prefix string and the value is the pre-ranked top few completions for that prefix. Either way, serving a suggestion becomes a direct lookup by the exact prefix the user has typed so far, not a scan or a search — that's what keeps it fast regardless of how much total query data exists behind it. On the client side, I'd debounce slightly so a request isn't fired on every single keystroke when the user is typing quickly, and call a lightweight endpoint that does nothing but that prefix lookup. Building and maintaining the ranked data is a separate concern from serving it. I'd aggregate real search query logs in a periodic batch job — hourly or daily, depending on how fresh suggestions need to be — that counts how often each query occurs, derives the top completions for every prefix up to some length, and republishes the precomputed index. This deliberately decouples 'what's fast to serve' from 'how the ranking gets computed,' so the serving path stays simple even as the ranking logic evolves. I'd avoid trying to make popularity fully real-time: unlike a trending-topics feed, autocomplete doesn't need to reflect the last few minutes of activity, and a periodic refresh capturing genuine popularity shifts is enough; if surfacing a fast-emerging spike genuinely matters for the product, that's a targeted addition (a lightweight recency boost) rather than a reason to rebuild the whole pipeline in real time. I'd also treat personalization — blending in a user's own recent searches — as an optional layer on top of the base global suggestions, not a requirement of the core design. And since network round-trip time tends to dominate at this feature's timescale, serving the suggestion data from a location close to the user (a regional cache or CDN edge for the static portion of it) often matters as much as the lookup structure itself.

    A strong answer touches

    • identifies latency as the binding constraint and avoids querying the primary database per keystroke
    • proposes a concrete prefix-lookup structure (trie or precomputed prefix-to-top-k index)
    • separates the fast read path from how the ranked data is built (a periodic batch job over search logs)
    • explains what ranking signal drives suggestions (query frequency, optional recency weighting)
    • explains why near-real-time popularity updates are usually unnecessary here, unlike a trending feed
    • considers reducing network latency (debouncing, serving data close to the user) since round-trip time dominates

    This is one genuine answer, not the only correct one — compare it with your own reasoning rather than matching it word for word.

    searchautocompleteperformance

  9. 9.How would you store and update scores so both the top-100 view and an individual player's rank stay fast, even as scores change constantly?

    Hard

    Design a leaderboard for a game that shows the top 100 players by score globally, and also lets any individual player look up their own current rank among potentially millions of players, with scores updating frequently.

    Show model answer

    These are two different access patterns that both need to survive frequent writes: 'top 100' is a small, heavily-read slice of globally ordered data, while 'my rank' means computing where one particular player falls among potentially millions of others, on demand, for any player who asks. A plain relational table falls short for the second pattern — finding a player's rank naively means counting how many players have a higher score, which means touching a large portion of the table on every single rank request, and that only gets worse as the player base grows. A better fit is a ranked data structure held in a fast in-memory store — the common building block is a sorted-set-style structure keyed by player id with score as the ordering value. This kind of structure is built to support exactly the operations this feature needs efficiently: inserting or updating a player's score, reading a contiguous top-N slice directly since the data is already ordered, and looking up an individual member's rank without scanning the full set, typically in logarithmic time. Every score update — after each game, say — writes straight into this structure, so both the top-100 view and any player's rank reflect the update immediately, without a separate recomputation pass. Because a structure like this is often memory-resident, I wouldn't treat it as the only copy of the data. I'd keep raw game results in the primary database as the source of truth, and treat the ranking structure as derived — rebuildable by replaying results if it's ever lost — rather than something whose loss would mean losing the leaderboard permanently. Periodic snapshots reduce how much replay would be needed. As the player base grows large enough that a single instance of this structure becomes a bottleneck, I'd partition the leaderboard along a boundary that's meaningful to players anyway — regional leaderboards, or weekly or seasonal resets — rather than trying to keep one unbounded global structure forever; this tends to serve the product better too, since a single all-time global rank matters less to most players than how they're doing this week or in their region. Finally, since the top-100 view is read constantly relative to how often it actually changes, I'd cache that specific response for a short duration rather than recomputing it from the ranked structure on every request.

    A strong answer touches

    • identifies that 'top N' and 'individual rank' are different access patterns that both need to stay fast under frequent writes
    • rejects a naive 'count players with a higher score' approach as too expensive at scale
    • proposes a concrete ranked structure (a sorted-set-style structure in a fast in-memory store) supporting insert, top-N read, and rank lookup efficiently
    • addresses durability given the structure is likely in-memory (source-of-truth data plus rebuild or snapshot)
    • discusses partitioning the leaderboard (region, time window) as the player base grows
    • considers caching the frequently-read top-100 view separately from individual score updates

    This is one genuine answer, not the only correct one — compare it with your own reasoning rather than matching it word for word.

    leaderboarddata-structuresscalability

  10. 10.How do you decide between vertical and horizontal scaling, and what has to be true about a system for horizontal scaling to actually work?

    Medium

    A service is starting to struggle under load. The option exists to scale up (move to a more powerful machine) or scale out (add more machines behind a load balancer).

    Show model answer

    Vertical scaling — a bigger CPU, more memory, faster disk on the existing machine — is usually the right first move when there's headroom available, because it requires no architectural change at all; the application doesn't know or care that it's running on a larger machine. Its limits are real, though: there's a hard ceiling on how large a single machine can get, the cost of the largest instances tends to grow faster than the capacity they add near the top of that range, and it does nothing for availability — one machine, however powerful, is still a single point of failure. Horizontal scaling — adding more machines and spreading load across them with a load balancer — raises the practical ceiling much further and improves availability, since one instance failing no longer takes the whole service down. But it isn't a drop-in replacement for vertical scaling; it requires the system to be built in a way that supports it. The key precondition is that application servers need to be stateless, or at least not depend on state that only exists on one specific instance — a user's session stored in that instance's memory, or a file written to that instance's local disk, breaks the moment a later request from the same user lands on a different instance. That state has to move somewhere shared: a shared cache or store for sessions, or a self-contained signed token the client holds instead. In most systems, the harder part of horizontal scaling isn't the application tier — stateless application servers are relatively easy to add more of — it's the datastore they all talk to, which can become the new bottleneck once the application tier is no longer the limiting factor. Scaling that out means read replicas for read-heavy load, or partitioning and sharding for write-heavy load, both of which bring their own trade-offs, like replication lag or more complex cross-partition queries. As a practical rule: scale vertically first while it's cheap and simple, and move to horizontal scaling once you're approaching a single machine's ceiling, need availability a single machine can't provide, or the cost curve of staying vertical stops making sense — and do the statelessness work before it's forced on you under load, since retrofitting it during an incident is far harder than designing for it early. The two aren't mutually exclusive either; a horizontally scaled fleet is still usually built from reasonably capable machines, not the smallest available.

    A strong answer touches

    • explains vertical scaling's appeal (no architecture change) and its real limits (hardware ceiling, cost curve, still a single point of failure)
    • explains what horizontal scaling buys (higher ceiling, better availability) and that it requires the system to be designed for it
    • names statelessness, or externalizing state like sessions, as the key precondition for horizontal scaling to work
    • identifies the datastore, not the application tier, as usually the harder part to scale horizontally, naming at least one technique (read replicas, sharding)
    • gives a practical decision rule for when to move from vertical to horizontal rather than treating one as always better
    • notes the two approaches are complementary rather than mutually exclusive

    This is one genuine answer, not the only correct one — compare it with your own reasoning rather than matching it word for word.

    scalingarchitecturefundamentals

Put this into practice

Connect the idea to a worked lesson, a practical task or your interview preparation.

← All interview prep categories