Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Architecture Trade-offs

Architecture decisions shape the trajectory of a codebase for years. Unlike a library upgrade, an architectural choice is difficult to reverse. This section covers the most common and consequential trade-offs evaluated in system design interviews.

Monolith vs Microservices

When to Choose Monolith

  • Team size is small (under ~10 developers).
  • The domain is well-understood with clear boundaries (or not yet understood enough to define service boundaries).
  • Time-to-market is the priority over independent scalability.
  • You are building an MVP or early-stage product.

When to Choose Microservices

  • Teams are large (20+ developers) and independently organized (Conway’s Law).
  • Different services have vastly different scaling requirements.
  • Failure isolation is critical (one subsystem crashing must not bring down others).
  • Polyglot requirements exist (ML service in Python, API in Go, etc.).

Key Trade-offs

DimensionMonolithMicroservices
Development SpeedFast initially, slows as code growsSlower initially (infrastructure setup), faster per-service iteration
DeploymentAll-or-nothingIndependent deployments
ScalingVertical, or scale entire monolithPer-service horizontal scaling
Data ManagementSingle database, easy joinsPer-service databases, eventual consistency required
Operational ComplexityLowHigh (service discovery, circuit breakers, distributed tracing)
TestingSimple unit/integrationComplex contract testing, integration testing across services

Interview Tip

Mention the “monolith-first” pattern: start with a well-structured monolith, extract services when clear scaling or organizational boundaries emerge. Premature microservices create distributed monoliths—worst of both worlds.


Synchronous vs Asynchronous Communication

When to Choose Synchronous

  • The caller needs an immediate response to proceed (user-facing request).
  • The operation is fast and latency-sensitive.
  • Error handling requires immediate feedback.

When to Choose Asynchronous

  • The operation is long-running (video processing, report generation).
  • You want to decouple services for independent scaling.
  • You need to handle traffic spikes with a buffer (queue-based backpressure).
  • The caller does not need the result immediately.

Key Trade-offs

DimensionSynchronousAsynchronous
LatencyImmediate, blocks callerDecoupled, non-blocking
CouplingTight (temporal)Loose
Error HandlingDirect, immediateRequires dead-letter queues, retry logic
ComplexityLowerHigher (message ordering, idempotency, exactly-once)
ThroughputLimited by slowest dependencyBounded by queue capacity + consumer speed

REST vs GraphQL vs gRPC

When to Choose REST

  • Public APIs with broad client diversity (mobile, web, third-party).
  • Caching is critical (HTTP caching, CDN-friendly).
  • Simple CRUD operations.

When to Choose GraphQL

  • Clients need flexible, self-service queries (avoid over/under-fetching).
  • Aggregating data from multiple services (BFF pattern).
  • Rapidly evolving frontend requirements.

When to Choose gRPC

  • Internal service-to-service communication.
  • High performance is critical (binary protocol, HTTP/2 multiplexing).
  • Strongly-typed contracts with code generation (Protobuf).

Comparison Table

DimensionRESTGraphQLgRPC
ProtocolHTTP/1.1 (text)HTTP (text)HTTP/2 (binary)
ContractOpenAPI (optional)Schema (required)Protobuf (required)
CachingHTTP-nativeLimited (requires persisted queries)No built-in
StreamingServer-sent (limited)SubscriptionsBidirectional
OverheadHigher (JSON text)Higher (JSON text)Lower (binary)
Browser SupportNativeNativeRequires gRPC-Web proxy

Polling vs WebSockets vs Server-Sent Events (SSE)

When to Choose Polling

  • Simplicity is paramount.
  • Updates are infrequent (status checks every 30 seconds).
  • HTTP infrastructure constraints (proxies, firewalls block WebSocket upgrades).

When to Choose WebSockets

  • Bidirectional real-time communication (chat, gaming, collaborative editing).
  • High-frequency updates (sub-second latency).
  • Both client and server send messages frequently.

When to Choose SSE

  • Server-to-client streaming only (notifications, live feeds, stock prices).
  • You want HTTP-native simplicity (auto-reconnection, works through proxies). | Dimension | Polling | WebSockets | SSE | |———–|———|———–|—–| | Direction | Client → Server | Bidirectional | Server → Client | | Latency | High (interval-dependent) | Low | Low | | Overhead | High (repeated connections) | Low (persistent) | Low (persistent) | | Browser Support | Universal | Universal | Universal except IE | | Complexity | Lowest | Highest (connection management) | Medium | | Scalability | Easy (stateless) | Hard (stateful connections per client) | Medium |

Vertical vs Horizontal Scaling

When to Choose Vertical

  • Quick scaling for short-term needs.
  • Applications that are not designed for distribution (stateful in-memory data).
  • Budget allows for powerful machines (databases often scale vertically).

When to Choose Horizontal

  • Stateless services (web servers, API gateways).
  • Workloads that exceed single-machine capacity.
  • Cost-efficient scaling: many cheap machines vs. one expensive machine.
  • High availability requirements (eliminate single point of failure).

Key Trade-offs

DimensionVerticalHorizontal
CostExponential (high-end hardware)Linear (commodity hardware)
ComplexityLowHigh (load balancing, state management, consistency)
LimitHardware ceilingArchitectural ceiling (CAP theorem, coordination cost)
AvailabilitySingle point of failureFault-tolerant with redundancy

Containers vs VMs

When to Choose Containers

  • Rapid deployment and rollback.
  • Resource efficiency (shared OS kernel, lower overhead).
  • Microservices architecture (small, isolated workloads).
  • CI/CD pipelines requiring reproducible builds.

When to Choose VMs

  • Strong isolation requirements (multi-tenant, untrusted code).
  • Running heterogeneous OS types (Windows + Linux).
  • Legacy applications not designed for containers.
  • Full OS control needed (custom kernels, kernel modules).

Key Trade-offs

DimensionContainersVMs
IsolationProcess-level (shared kernel)Hardware-level (separate kernel)
Boot TimeSecondsMinutes
Resource OverheadLow (~MB per container)High (~GB per VM)
DensityHigh (many per host)Low (few per host)
SecurityLower (kernel attack surface shared)Higher (hardware isolation)

Kubernetes vs Serverless

When to Choose Kubernetes

  • Long-running, predictable workloads.
  • Complex orchestration needs (stateful sets, daemon sets, custom operators).
  • You need fine-grained control over networking, storage, and scheduling.
  • Hybrid/multi-cloud requirements.

When to Choose Serverless

  • Event-driven, sporadic workloads (API endpoints triggered by requests).
  • Rapid prototyping with minimal ops overhead.
  • Cost optimization for low-traffic or bursty workloads (pay-per-invocation).
  • Small team without dedicated platform engineering.

Key Trade-offs

DimensionKubernetesServerless
ControlFull (configurable everything)Minimal (managed runtime)
CostFixed (cluster nodes always running)Variable (pay per use)
Cold StartN/A (always warm)Yes (up to seconds)
Ops BurdenHighNear-zero
Vendor Lock-inLow (portable)High (Lambda, Cloud Functions, etc.)

Threads vs Processes vs Coroutines

When to Choose Threads

  • CPU-bound parallelism on multi-core systems.
  • Shared-memory data structures.
  • Languages with native thread support (Java, C++, Go goroutines are threads).

When to Choose Processes

  • Maximum isolation (crash in one process does not affect others).
  • Languages without thread safety or with a GIL (Python multiprocessing).
  • Sandboxing untrusted code.

When to Choose Coroutines

  • I/O-bound workloads with high concurrency (thousands of concurrent connections).
  • You need cooperative multitasking with low memory overhead.
  • Languages with async/await support (Python asyncio, JavaScript, Rust async).

Key Trade-offs

DimensionThreadsProcessesCoroutines
Memory Overhead~1-2 MB per thread~10s MB per process~KB per coroutine
ConcurrencyOS-managed, preemptiveOS-managed, preemptiveUser-managed, cooperative
CommunicationShared memory (needs synchronization)IPC (pipes, sockets, shared memory)Through event loop (channels)
ParallelismTrue (multi-core)True (multi-core)Single-threaded (unless combined with threads)
ComplexityHigh (race conditions, deadlocks)Medium (IPC complexity)Low (sequential reasoning)

Queues vs Streams

When to Choose Queues (Message Queues)

  • Discrete work items (job processing, email sending, image resizing).
  • At-least-once delivery semantics are sufficient.
  • Consumers compete for messages (competing consumer pattern).
  • Throughput with backpressure (buffer bursts).

When to Choose Streams (Event Streams)

  • Continuous data flows (user activity logs, sensor data, financial transactions).
  • Replayability matters (consume from arbitrary offsets). | Dimension | Queues | Streams | |———–|––––|———| | Delivery Model | Point-to-point | Publish-subscribe | | Retention | Until consumed | Configurable time-based | | Ordering | Per-queue or per-partition | Per-partition guaranteed | | Replay | Generally no | Yes (offset-based) | | Processing | Once per consumer group | Multiple consumer groups independently | | Examples | RabbitMQ, SQS, Celery | Kafka, Kinesis, Pulsar |

Shared Database vs Database per Service

When to Choose Shared Database

  • Microservices are early-stage and boundaries are unclear.
  • Cross-service reporting and analytics are primary needs.
  • You need ACID transactions across service domains.

When to Choose Database per Service

  • Services have distinct data models and access patterns. | Dimension | Shared Database | Database per Service | |———–|––––––––|———————| | Coupling | Schema coupling (changes affect all services) | Loose coupling | | Transactions | ACID across services | Requires Sagas or 2PC | | Scalability | Shared bottleneck | Independent scaling | | Data Ownership | Ambiguous | Clear ownership per service | | Migration Risk | High (coordinated migrations) | Low (independent migrations) |

Interview Questions

  1. “When should a startup choose microservices over a monolith?” Answer: Almost never at inception. Choose microservices when team size, scaling requirements, or deployment independence demands it. Premature adoption creates operational overhead that diverts from product development.

  2. “Design a real-time chat system. Which communication protocol do you use?” Answer: WebSockets for the core messaging (bidirectional, low latency), SSE for presence/typing indicators (server push only), and REST for profile/management APIs (CRUD with caching).

  3. “Compare Kubernetes and serverless for a new API gateway.” Answer: If traffic is predictable and you need consistent latency, Kubernetes. If traffic is bursty, the API is simple, and you want minimal ops, serverless. In practice, many systems use both—Kubernetes for core services, serverless for event handlers.

  4. “Why would you use coroutines instead of threads for a web crawler?” Answer: A crawler makes thousands of concurrent HTTP requests, most of which are I/O-bound (waiting for network responses). Coroutines provide massive concurrency with minimal memory overhead (KB vs MB per thread) and avoid synchronization complexity.

  5. “When does a shared database become a problem in microservices?” Answer: When schema changes in one service require coordinated deployments with others, when one service’s heavy queries impact another’s latency, or when you cannot independently scale data storage per service.