Cloud Architecture Design Principles and Trade-offs Questions
The cross-pillar reasoning skill for architecting cloud systems: weighing reliability, scalability, security, performance, and cost against each other to justify ONE architectural choice over another under real constraints (budget, team size, timeline, existing systems). Covers well-architected-style design reviews, resilience and failure-mode reasoning (blast radius, graceful degradation, idempotency), consistency-versus-availability trade-offs (CAP/PACELC), and scenario-based decisions such as choosing a managed versus self-hosted component or an architectural style (monolithic, microservices, or serverless) for one system. Provider-agnostic: no specific cloud vendor's service catalog. This topic is the JUSTIFICATION layer, not a subsystem deep dive: a full design of observability, disaster recovery, identity and access management, networking, caching, or Kubernetes orchestration belongs to that subsystem's own topic. Comparing compute abstractions (VM versus container versus serverless versus GPU/TPU) belongs to compute options and trade-offs. Choosing an architectural style is covered here, but the internal implementation patterns of that style (service mesh, sagas, two-phase commit, event sourcing) belong to microservices architecture and service design. Multi-year roadmaps, vendor evaluation, and governance belong to infrastructure strategy and technology selection. Spanning multiple cloud providers or bridging on-premises and cloud belongs to multi-cloud and hybrid cloud architecture. The IaaS/PaaS/SaaS delivery-model taxonomy belongs to cloud service and deployment models. Region-crossing replication and failover design belongs to multi-region and geo-distributed systems.
You're designing the compute architecture for a medium-sized, customer-facing web application that needs to ship features frequently and handle unpredictable traffic growth. Explain the cloud-native design principles you would apply, and for each one, give a concrete example of how it shapes a compute architecture decision for this application.
Sample Answer
For a customer-facing app that must ship features often and absorb traffic growth it cannot forecast, the right compute architecture rests on six cloud-native design principles: statelessness, loose coupling, design-for-failure, elastic horizontal scaling, immutable infrastructure with automated delivery, and decoupling deploy from release. These are not abstract hygiene. Each one determines a specific, concrete decision about how the compute fleet is built, scaled, and rolled forward, and skipping any one of them reintroduces exactly the bottleneck the others exist to remove.
The six principles and the compute decision each one forces
1. Statelessness: nothing that matters lives on one instance
Application and session state is kept outside the instance, in a shared store, so any instance can serve any request. Compute decision: run the app tier as identical, interchangeable instances behind a load balancer (a component that spreads incoming requests across many backend instances), with session data in a shared cache such as Redis rather than in instance memory. That is what lets an autoscaler add or remove instances in the middle of a spike without dropping a single user's session, because "add capacity" no longer means "provision a machine that already knows about this specific user."
2. Loose coupling: decouple whatever scales on a different curve
Components talk through well-defined boundaries such as queues or APIs, not shared memory or a shared deploy unit. Compute decision: separate background and batch work (sending emails, processing images, running analytics) from the request-serving web tier, connected by a message queue. A spike in checkout traffic then never forces the unrelated email workload to scale, and each piece can be released and rolled back on its own schedule, which is the compute-side precondition for shipping features frequently at all.
3. Design for failure: assume an instance dies mid-request
Build in automated checks that an instance is still responding correctly, and let an orchestrator replace unhealthy instances without paging a human. Compute decision: configure automated health checks so a struggling instance is pulled out of rotation and replaced automatically, and make request handling idempotent (safe to retry without a side effect happening twice) so a mid-request instance replacement cannot double-charge a customer or duplicate an order.
4. Elastic, horizontal scaling driven by a leading signal
Capacity tracks demand automatically, and it grows by adding more identical instances (horizontal) rather than making one instance bigger (vertical), since vertical scaling has a hard ceiling and usually needs a restart. Compute decision: pick a compute platform whose autoscaler reacts to a leading indicator, such as request rate or queue depth, instead of a lagging one like a five-minute average of CPU load. By the time a lagging average notices an unpredictable spike, users have already timed out.
5. Immutable infrastructure and automated delivery
Every release is a new, versioned artifact (a machine image or container image) deployed fresh, never a running server edited in place. Compute decision: build one artifact per commit through a continuous integration and continuous delivery (CI/CD) pipeline, and roll it out through automated blue-green or canary gates (running the new version alongside the old and shifting traffic to it gradually) instead of logging into servers to patch them. Shipping ten times a day this way does not accumulate configuration drift nobody can reproduce, and a bad release is a one-command rollback rather than an incident.
6. Decouple deploy from release
Getting new code running in production and exposing it to users are two separate events. Compute decision: ship code behind feature flags (a runtime switch that turns a code path on or off without a new deploy), so the whole compute fleet runs one build while only a chosen slice of traffic sees the new behavior. That lets the team deploy continuously and release on a separate, business-driven schedule, which is what "ship features frequently" actually requires without tying every release to fleet-wide risk.
Worked example: absorbing an unplanned spike
Assume, purely for illustration, that each stateless web instance is sized to handle about 50 requests per second before p99 (99th-percentile) latency degrades. On a normal day the fleet runs 5 instances, covering 250 requests per second. The product gets an unplanned surge of attention and traffic jumps to 5,000 requests per second within a few minutes: 5,000 / 50 = 100 instances are needed, so the autoscaler has to add 95 instances (100 minus the 5 already running) on top of the existing fleet.
This only works cleanly because principles 1 and 4 act together. The 95 new instances are stateless, so the load balancer can route any request to any of them the moment they pass their health check, and the autoscaler is watching request rate rather than a slow-moving CPU average, so it starts adding capacity within the first tens of seconds of the spike instead of after a multi-minute averaging window. Loose coupling (principle 2) means the checkout-page spike does not also force the email-sending workers to scale by 20x, since they are a separate pool sized against their own queue depth. Because the fleet is built from immutable images (principle 5), the 95 new instances are bit-for-bit identical to the 5 already running: no first-boot configuration step to fail under load, no drift between old and new capacity.
Trade-offs and pitfalls
- Statelessness carries a migration cost: an app built around sticky sessions or local file storage has to be re-architected to push that state out, and the shared session store becomes a new dependency that must itself be made highly available, or it becomes the new single point of failure.
- Elastic scaling that only scales up quietly inflates the bill. The same automation needs a scale-in policy, and scale-in has its own failure mode (terminating an instance mid-request), which is exactly why design-for-failure and statelessness have to already be in place before elasticity is safe to add.
- Feature flags accumulate. A flag meant to decouple deploy from release for a few weeks is easy to leave in the codebase for two years, and an app with hundreds of stale flags is harder to reason about than the deploy risk the flag was solving.
- The common wrong turn on this question is treating "cloud-native" as a synonym for one specific compute product, serverless functions being the usual guess. These six principles hold whether the fleet is virtual machines, containers, or managed functions. What matters is that whichever compute abstraction gets chosen is stateless, loosely coupled, self-healing, horizontally elastic, and deployed immutably, not which vendor implements it.
For a global e-commerce platform, choose appropriate data stores for these components: (a) transactional orders, (b) product catalog, (c) user sessions, and (d) product images. For each choice, justify your pick based on consistency needs, query patterns, expected scale, latency, and cost.
Sample Answer
Direct answer
Each of these four components has a distinct consistency, query, scale, latency, and cost profile, so a single "one database for everything" choice is wrong for at least two of them: transactional orders need strong consistency and transactional guarantees, the product catalog needs to be read-heavy and flexible for search and browse, user sessions need to be fast and disposable, and product images need to be stored as large blobs, not database rows.
Structured elaboration
| Component | Recommended store type | Why |
|---|---|---|
| (a) Transactional orders | A relational database with strong (ACID: atomicity, consistency, isolation, durability) transactional guarantees | Orders involve money and inventory decrement together; a lost or double-applied order write is a real business incident, so the strong consistency and multi-row transaction support of a relational engine is worth its lower write-throughput ceiling |
| (b) Product catalog | A document or search-optimized store, a document database, or a dedicated search index alongside a simpler backing store | Catalog reads vastly outnumber writes, query patterns are flexible, filter by category, attribute, free text, and schema varies by product type; eventual consistency, a new product taking a few seconds to become searchable, is a non-issue |
| (c) User sessions | An in-memory key-value store with a time-to-live (TTL) | Sessions are read and written on nearly every request, so raw speed matters most, and they are inherently disposable, a lost session just forces a re-login, which makes an in-memory store's weaker durability guarantee an acceptable trade for its latency |
| (d) Product images | Object storage, referenced by a URL or key from the catalog record | Images are large, immutable-once-uploaded blobs; storing them as database rows wastes an expensive, latency-optimized engine on cheap, bulk-optimized data, and object storage pairs naturally with a content delivery network (CDN) for fast delivery |
Justification detail per component
- Orders: consistency need is high, a double-applied order or a lost inventory decrement is a real financial and operational problem; query pattern is transactional, read-then-write within one logical operation; scale is moderate relative to catalog reads; latency tolerance is moderate, users expect an order confirmation in seconds, not milliseconds; and cost is acceptable to spend on the pricier, higher-guarantee engine precisely because order volume is the smallest of the four, so the more expensive per-operation cost is applied to the lowest-volume, highest-stakes workload.
- Catalog: consistency need is low, eventual consistency on a new listing appearing in search is invisible to users; query pattern is read-heavy and highly variable, filters, free text, sorting; scale is the largest of the four in read volume; latency needs to be low for a good browsing experience; and cost per read on a search-optimized store is low, which matters most here because this is the highest-volume read path of the four, running it through a pricier transactional engine would be the expensive mistake.
- Sessions: consistency need is minimal, losing a session is an inconvenience, not a correctness bug; query pattern is a simple key lookup; scale is very high in request volume but small in data size per session; latency needs to be the lowest of the four; and cost per gigabyte for in-memory storage is higher than disk-based storage, but session data is tiny per user and time-to-live-bounded, so total cost stays small despite the pricier storage tier.
- Images: consistency need is essentially none, an image is written once and read many times; query pattern is a simple key or URL fetch; scale is large in total bytes but simple in access pattern; latency is best served by caching close to the user; and cost per gigabyte for object storage is the cheapest of the four storage tiers, which matters because images are by far the largest total byte volume of the four components.
Worked example
A customer places an order for a product. The order write, component a, goes through a transaction that decrements inventory and creates the order record atomically, using the relational store's transactional guarantee to ensure both happen together or not at all. The product page they ordered from was served from the catalog store, component b, which returned in a few milliseconds from a search-optimized index without touching the transactional database at all, keeping catalog browsing traffic, the highest-volume read path, off the system protecting transactional correctness. Their session, component c, was checked on every single page request via a sub-millisecond in-memory lookup, cheap enough to do on every request without adding meaningful latency. The product images on that page, component d, were served from object storage through a content delivery network edge cache, never touching any database, and a regional content-delivery outage would degrade image loading without touching order processing, catalog search, or session handling at all, demonstrating the isolation benefit of separating these four concerns into four purpose-fit stores.
Trade-offs and pitfalls
- Putting everything in one relational database, a common early-stage shortcut, works fine at small scale but couples catalog read load and session read and write load to the same engine that needs to protect transactional order correctness; a catalog traffic spike from a viral product can then degrade order processing, the worst possible failure to have coupled together.
- Putting session data in a database "for durability" trades away the latency benefit sessions actually need, for a durability guarantee sessions do not actually require, a common overcorrection once teams learn to distrust in-memory stores.
- Storing images as database blobs instead of in object storage bloats the database's storage and backup size and slows every backup and restore operation, for data that gets no benefit from living in a transactional engine.
Explain eventual consistency, read-your-writes consistency, monotonic reads, and causal consistency. For each, give a concrete requirement where it would be the right guarantee to offer, and describe how you would support it in a cloud service through your choice of caching and replication strategy.
Sample Answer
Direct answer
These four guarantees are points on a spectrum of how fresh and ordered the data a reader sees has to be. Eventual consistency only promises convergence once writes stop; read-your-writes guarantees a client sees its own writes; monotonic reads guarantees a client's view never goes backward in time; causal consistency guarantees that if one write causally depends on another (it read it, or the same actor made both), everyone sees them in that order, though unrelated concurrent writes can appear in any order. Pick the weakest guarantee that still satisfies the real requirement, because every guarantee above eventual consistency costs latency, availability, or both.
Structured elaboration
The four guarantees
| Guarantee | What it promises | What it does not promise |
|---|---|---|
| Eventual consistency | If writes stop, all replicas converge to the same value eventually | No bound on staleness while writes continue, no ordering guarantee between reads |
| Read-your-writes | A client always sees writes it made itself, on its next read | Nothing about seeing other clients' writes promptly |
| Monotonic reads | A client's successive reads never go backward, never see an older value after a newer one | Nothing about seeing the very latest write quickly |
| Causal consistency | Writes that are causally related (B read A, or the same client wrote both) are seen by everyone in that order | Concurrent, unrelated writes can be seen in different orders by different clients |
Where each is right, and how to implement it
- Eventual consistency fits a "like count" or view counter on a social post: nobody notices if it is off by a few seconds. Implement with asynchronous replication and a cache with a short time-to-live (TTL), reads hitting the nearest replica or cache with no coordination.
- Read-your-writes fits a shopping cart: a customer adds an item and must see it on the very next page load, but does not need to see someone else's cart changes instantly. Implement by routing a client's reads to the same replica or region that handled their last write (sticky routing, or a read-after-write token the client passes back), or by writing through a cache synchronously for that client's session.
- Monotonic reads fits an account balance shown across several pages in one session: a user should never see a balance regress from $100 to $80 back to $100 as they navigate, even before any new transaction happens, because replica lag could otherwise expose an older value after a newer one. Implement with a session-bound read version (a logical timestamp or vector clock, a small per-write counter set that lets a node detect which operations causally precede which), routing reads only to a replica that has caught up to at least that version.
- Causal consistency fits a comment thread or a bank transfer's downstream notifications: if a bank debits account A and credits account B, and a fraud check reads B's new balance to decide whether to hold funds, that check must see the credit; two unrelated customers' unrelated transfers can be seen in any order relative to each other. Implement by propagating a causal token (a vector clock or dependency list) with each write and having replicas withhold delivery of a write until its dependencies have already been applied locally.
Applied to a machine learning feature store and model metadata
A feature store serving a model at inference time typically only needs eventual consistency for feature values themselves: a slightly stale feature rarely changes a prediction meaningfully, and the alternative, blocking inference on cross-region replication, would blow the latency budget. Model metadata, such as which model version is currently active or what its rollback target is, needs a stronger guarantee than the features: a rollback decision made by one operator must be immediately visible to every serving node, which is read-your-writes at minimum and often causal consistency if the rollback event has dependencies, such as rolling back the model and the feature schema together. This is a case where two data classes inside the same system legitimately sit at different points on the spectrum.
Worked example
Account A has $500. A transfer service debits A by $200 and credits B by $200. If a support dashboard reads B's balance right after the transfer and the read lands on a stale replica, it might show B's pre-credit balance: an eventual-consistency gap that would be embarrassing but not incorrect, because the transfer log stays the source of truth. If instead a downstream automated fraud check reads B's balance to decide whether $200 looks anomalous for that account, and it must evaluate the post-credit state to be correct, causal consistency is required: the fraud check's read is causally dependent on the credit write, so the system must guarantee it sees that write, by routing the check to a replica the write has already reached, verified via a causal token, or by having the check read from the same node that processed the write.
Trade-offs and pitfalls
- Reaching for the strongest guarantee "to be safe" is the most common mistake: causal or read-your-writes consistency usually requires sticky routing or coordination that adds latency and reduces how freely the system can load-balance or fail over.
- Monotonic reads and read-your-writes are easy to conflate: read-your-writes is about your own writes, monotonic reads is about time never going backward regardless of who wrote it. A system can offer one without the other.
- A subtle pitfall is implementing causal consistency with a coarse dependency tracker, such as "same user ID" as a proxy for causality: it over-serializes unrelated writes from the same user and under-serializes genuinely causal writes across users, like the transfer-then-fraud-check example above, so dependency tracking has to be based on actual read-then-write chains, not identity.
A payments service must keep operating correctly through a partial regional network partition, and it must never double-charge a customer or lose a transaction. Design the resilience patterns you would put across the request path (client retries, the write path, and cross-service coordination) to guarantee correctness under partition and retries, and explain how you would audit for and reconcile any anomalies afterward.
Sample Answer
Direct answer
Preventing a double-charge or lost transaction under a partial regional network partition comes down to three things working together: the client must retry safely, an idempotency key attached to every payment attempt, the write path must make the idempotency key the actual source of truth, reject or return the original result for a duplicate key, never process it twice, and cross-service coordination must use a pattern, a saga with compensating actions, or a durable outbox, that survives a partition without leaving money in a half-applied state. On top of that, an independent reconciliation process has to audit for anomalies afterward, because no design is provably perfect against every partition timing.
Structured elaboration
Client side: idempotency keys
Every payment request carries a client-generated idempotency key, typically a version-4 universally unique identifier, generated once per user action, not once per network attempt. If the client's request times out and it retries, the retry carries the same key. The client never generates a new key for a retry of the same logical action; that single discipline is what makes retries safe instead of dangerous.
Write path: the idempotency key as source of truth
The payment service persists the key and its result as an atomic part of the same transaction that processes the payment, not as a separate step that could itself fail out of sync. On receiving a request, it first checks whether this key has been seen before. If yes, and the prior attempt completed, it returns the original result, not a new charge. If the prior attempt is still in progress, it returns a "still processing, retry shortly" response rather than starting a second concurrent attempt. If the key is new, it processes it and records the result atomically with the charge itself. This makes "processed twice" structurally impossible for any request carrying the same key, regardless of how many times the client retries.
Cross-service coordination under partition
A payment usually spans at least two operations, debit the payer, credit the payee, maybe notify a ledger service, which cannot be a single database transaction across services. Use a saga: each step is a local transaction, and each step has a compensating action defined up front, if the credit fails after the debit succeeds, run an automatic compensating credit back to the payer. Pair this with a durable outbox pattern: the debit step and the "please credit the payee" event are written atomically in the same local transaction, so a partition or crash between "debit succeeded" and "credit event published" cannot happen, either both happened or neither did, and a background process reliably delivers the outbox event once connectivity is restored, retrying safely because the credit step is also idempotent by its own key.
Handling the partition specifically
If the network partition isolates the credit step's target service, the debit's local transaction already committed and the outbox event is durably stored; it simply cannot be delivered yet. This is safe: money is not lost, the debit is real and recorded, and no double-charge occurs, since the outbox event, when eventually delivered, carries the same idempotency key, so even if it is redelivered multiple times after the partition heals, the credit step processes it exactly once. The customer sees "payment processing" rather than a false success or a false failure, both worse than an honest "still working on it."
Auditing and reconciliation after the fact
Run a periodic reconciliation job that compares the payment ledger against actual money-movement records, bank or card-network settlement data, and flags any mismatch: a debit with no matching credit past a time threshold, a credit with no matching debit, or two records sharing what should have been a unique idempotency key. Every flagged anomaly should be investigated and, once understood, either resolved automatically, replaying a stuck outbox event, or escalated to a human for manual correction, with the resolution logged for audit purposes. This step exists precisely because "we designed it to be correct" is not the same claim as "we verified it stayed correct," and partitions are exactly the condition most likely to expose a subtle bug in the design.
sequenceDiagram
participant C as Client
participant P as Payment service
participant O as Outbox / event log
participant L as Ledger service (payee credit)
C->>P: Charge request (idempotency key K)
P->>P: Check key K, not seen before
P->>P: Debit payer and write outbox event (K) atomically
P-->>C: "Processing" (or success once confirmed)
Note over P,L: Network partition isolates L
O--xL: Credit event (K) cannot be delivered yet
Note over O,L: Partition heals
O->>L: Redeliver credit event (K)
L->>L: Check key K, apply credit exactly once
L-->>O: Acknowledge
C->>P: Client retries with same key K (thinks it failed)
P->>P: Key K already recorded, return original result
P-->>C: Original result, no second debit
Worked example
A customer's payment times out on their phone during a regional partition and they tap "pay" again. The second tap generates a request with the same idempotency key, because the client library correctly treats it as a retry of the same action, not a new one. The payment service sees the key already recorded from the first attempt, which had already debited the payer and written the outbox event, and returns that original result instead of debiting again. Meanwhile the outbox event to credit the payee, blocked by the partition, is redelivered once connectivity returns and applied exactly once because the ledger service checks the same key. The customer is charged once, the payee is credited once, and the reconciliation job later confirms the debit and credit match, closing the loop with evidence rather than assumption.
Trade-offs and pitfalls
- The single most common bug in real systems is a client that generates a new idempotency key on every retry, say from an auto-generated request ID, instead of reusing the key for the same logical action. That silently defeats the entire idempotency mechanism and reintroduces the double-charge risk it was built to prevent.
- Storing the idempotency-key check in a different transaction than the charge itself, a common shortcut, reintroduces a race: two near-simultaneous requests with the same key can both pass the "not seen before" check before either has recorded the key, causing a double charge. The check and the charge must be atomic together.
- Skipping the reconciliation step because "the design is provably correct" is a mistake; partitions have edge-case timings, a crash at the exact moment between two writes, a bug in the compensating-action logic, that a design review will not catch but a reconciliation audit will.
Explain the difference between vertical scaling (scale up) and horizontal scaling (scale out) for compute resources in the cloud. Describe typical use cases for each, how each affects availability and fault domains, practical limits you might hit (instance-size ceilings, licensing), and how the choice changes application architecture and cost over time. Give one concrete scenario where horizontal scaling is clearly the better choice.
Sample Answer
Direct answer
Vertical scaling (scale up) gives one instance more compute, memory, or disk so it can handle more load; horizontal scaling (scale out) adds more instances behind a load balancer so load is spread across them. Vertical scaling is simpler because the application does not change, but it tops out at the largest machine size available and keeps you on a single point of failure. Horizontal scaling has no hard ceiling and improves fault tolerance, but only works if the application can run as multiple independent copies, which usually means redesigning around externalized state.
Structured elaboration
What changes physically
- Vertical scaling: resize the same instance to a bigger size in the same compute family (more vCPUs, more RAM, faster local disk). The instance identity, IP address, and local disk state stay the same.
- Horizontal scaling: run N copies of the same instance type behind a load balancer or a pool that a queue or scheduler dispatches work to. Every request or job could land on a different copy.
Typical use cases
| Dimension | Vertical scaling fits | Horizontal scaling fits |
|---|---|---|
| Workload shape | Single-writer relational database, an in-memory cache pinned to one node, legacy software with tight internal coupling | Stateless web/API tier, background job workers, anything read-heavy that can be replicated |
| Traffic pattern | Fairly flat, predictable load | Bursty, spiky, or fast-growing load |
| Licensing | Software licensed per-socket or per-core-package (fewer, bigger cores can be cheaper) | Software licensed per-instance or open-source with no per-core cost |
| Team/architecture maturity | Early-stage system where redesigning for statelessness is not worth it yet | System already stateless, or one being deliberately redesigned to be |
Availability and fault domains
A vertically scaled instance is one fault domain: if it fails, restarts, or needs a resize (which usually requires a stop/start cycle), 100 percent of that component's capacity disappears during the event. There is no partial degradation, only up or down.
A horizontally scaled fleet spreads instances across multiple fault domains, typically multiple availability zones (an availability zone, or AZ, is an isolated data center location within a region). Losing one instance or one AZ only removes a fraction of total capacity. This is also what makes rolling deployments possible without downtime: instances are taken out of rotation a few at a time, upgraded, and put back, while the rest keep serving traffic. A single vertically scaled instance has no "rest of the fleet" to fall back on during its own upgrade window.
Practical limits you hit
- Instance-size ceiling: every compute family has a largest available size. Once demand exceeds what the biggest single instance in that family can deliver, no amount of budget lets you scale vertically further. This is a hard ceiling, not a soft one.
- Licensing ceilings: some enterprise software is licensed per-socket or per-core-package. Scaling vertically onto fewer, more powerful sockets can be materially cheaper under that licensing model than scaling horizontally onto many small instances, which is a genuine constraint on how freely horizontal scaling can be chosen for a licensed component, not just a cost preference.
- Non-linear cost at the top of a family: the largest instances in a family are usually priced at a premium beyond a straight-line multiple of the smaller sizes, because you are paying for a scarcer physical configuration.
- Diminishing software returns: legacy or single-threaded-bottlenecked software does not automatically use extra cores. Doubling vCPUs does not double throughput if the application funnels work through one lock, one connection pool, or one event loop.
- Horizontal's own limits: adding instances does not help if the bottleneck is not compute (for example a single-writer database or a downstream rate limit). Coordination overhead (service discovery, health checking, cross-instance chatter) grows with fleet size and eventually eats into the throughput gained by adding the next node.
How scaling triggers work in practice
Horizontal scaling is usually automated by an autoscaling policy watching a trigger metric: CPU or memory utilization, queue depth (for worker pools), or request latency. Reactive (dynamic) scaling responds after the metric crosses a threshold, which means there is a lag equal to however long a new instance takes to boot and become healthy, so capacity always arrives a little late. Scheduled scaling pre-provisions ahead of a known traffic pattern (a marketing send, a fixed daily peak) and avoids that lag for predictable events, at the cost of needing someone to know the schedule in advance. Production systems on bursty traffic typically run both: scheduled scaling for known peaks, reactive scaling as a safety net for the unknown ones.
How the choice changes architecture and cost over time
Choosing vertical scaling defers architecture decisions: you can keep a monolith, keep session state in local memory, keep a single database writer, and the code does not need to change to grow. But the cost curve is a step function driven by instance size, and because there is no elastic "scale down," you typically pay for a size large enough to survive your peak, all day, every day.
Choosing horizontal scaling forces upfront investment: sessions must be externalized (a shared cache or a signed token instead of in-memory state), operations must be made idempotent or retry-safe (because a retry may hit a different instance), and health checking plus load balancing become part of the architecture. The payoff is that cost becomes elastic: the fleet scales down during quiet hours and up during peaks, converting a chunk of fixed peak-capacity spend into pay-for-what-you-use spend, at the price of a small fixed overhead (load balancer, extra monitoring, redundant capacity for failover).
Worked example
A checkout API normally serves 2,000 requests/second and comfortably runs on 4 instances (500 req/s each). A 2-hour flash sale drives traffic to 40,000 requests/second, 20x normal.
Vertically, compute families commonly top out around 8 to 16 times the vCPU/memory of a small instance in the family. A ceiling of 10x on a per-instance basis would still cap a single vertically scaled box at roughly 5,000 req/s, an order of magnitude short of the 40,000 req/s target. There is no bigger box to buy: the requirement is structurally impossible to meet with one instance, regardless of budget.
Horizontally, the same fleet scales from 4 instances to 80 instances (80 x 500 = 40,000 req/s) for the 2-hour window, then back down. Total compute for that day is roughly 4 instances x 22 hours + 80 instances x 2 hours = 88 + 160 = 248 instance-hours, versus 80 instances x 24 hours = 1,920 instance-hours if peak capacity had to be provisioned statically all day. This is the concrete scenario where horizontal scaling is clearly the better, and in this case the only structurally viable, choice: a traffic multiple that exceeds any single instance's ceiling, held for a bounded window, with an elastic fleet that can shrink back afterward.
Trade-offs and pitfalls
- Vertical scaling looks simpler but hides a real risk: it keeps a single point of failure and a hard ceiling behind a "just resize it" story that stops working exactly when you need it most, an unexpected spike.
- Horizontal scaling's autoscaling has a reaction lag: if new instances take a couple of minutes to become healthy, a very sudden spike still causes a period of degraded service before capacity catches up. Scheduled scaling for known events covers this gap; reactive scaling alone does not.
- Per-core or per-socket software licensing can flip the cost comparison: for that specific component, fewer, bigger cores (vertical) can be cheaper than many small cores (horizontal) even though horizontal wins on availability. This is a real reason to keep a licensed component vertical while everything around it scales horizontally.
- A common mistake is scaling out a component whose real bottleneck is not compute (a single database writer, a downstream API's rate limit). Adding instances in front of that bottleneck adds cost and coordination overhead without adding real throughput.
Unlock Full Question Bank
Get access to all 22 Cloud Architecture Design Principles and Trade-offs interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.