Beginner System design concept · How Systems Actually Work · 22 mins read
Application Servers & Services
See how backend services organize business logic and why statelessness makes scaling simpler.
Microservices
Learn when splitting an application into independently deployable services pays off, and when a well-structured monolith beats it.
Intuition
As a codebase and team grow, a monolith becomes a coordination bottleneck: every deploy ships the whole application, one team's change can break another's module, and scaling means replicating the entire process even when only the checkout path is hot. Teams start stepping on each other's releases, and deploy frequency drops from daily to weekly. Microservices trade local simplicity for independent deployability, isolated scaling, and team autonomy — at the cost of network calls, distributed data, and operational complexity. Understanding this tradeoff is one of the most common decision points in system design interviews and in real architecture reviews.
Mental Model
A microservices architecture decomposes an application into small services, each owning a single business capability (a 'bounded context'), its own database, and its own deployment pipeline. Services communicate over the network via synchronous APIs (REST/gRPC) or asynchronous messages. The key properties are independent deployability (ship service A without touching B), independent scalability (run 50 checkout pods and 3 search pods), and per-service data ownership (no shared database tables). The hidden cost is that every function call that used to be in-process is now a network hop that can fail, time out, or return stale data. Think of it like: A monolith is a single large restaurant where one kitchen makes everything — efficient to manage, but one sick chef shuts down the whole menu. Microservices are a food hall: each stall has its own kitchen, staff, and cash register. Stalls open and close independently and a busy stall can hire more cooks, but now you need a seating system, a way to route customers, and coordination when an order spans multiple stalls.
Building Blocks
- Bounded Context: The domain-driven design concept that defines where a service's responsibility begins and ends. 'Orders', 'Payments', and 'Inventory' are separate bounded contexts because they have different data, rules, and rates of change. Getting boundaries wrong — splitting by technical layer instead of business capability — produces 'distributed monoliths' where every change touches five services.
- Independent Deployability: The defining property: each service ships on its own pipeline without coordinating releases with other teams. This requires stable API contracts, backward-compatible changes, and contract testing. If two services must always deploy together, they are not really separate services.
- Per-Service Data Ownership: Each service exclusively owns its database; other services must go through its API. This prevents hidden coupling through shared tables and lets each service pick the right store (Postgres for orders, Redis for sessions, Elasticsearch for search). The cost: no joins across services and no ACID transactions spanning services.
- Inter-Service Communication: Synchronous calls (REST, gRPC) are simple but couple availability: if service B is down, service A's request fails. Asynchronous messaging (Kafka, SQS) decouples services in time and absorbs spikes, but introduces eventual consistency and harder debugging. Most real systems use both.
- Distributed Transactions & Sagas: Without a shared database, a multi-step operation (reserve inventory → charge card → create shipment) cannot be one ACID transaction. The saga pattern models it as a sequence of local transactions with compensating actions on failure (unreserve inventory if the charge fails). This replaces rollback semantics with business-level undo logic.
- Observability Stack: One user request may fan out across 10+ services, so a single log file is useless. You need centralized structured logs, metrics per service, and distributed tracing (OpenTelemetry, Jaeger) with correlation IDs propagated through every hop. Without tracing, a 2-second p99 latency becomes an un-debuggable mystery.
Definitions
- Monolith
-
An application deployed as a single unit, with all modules sharing one codebase, process, and usually one database.
- Not inherently bad: Shopify and GitHub run massive monoliths successfully.
- Strengths: simple deployment, in-process calls, ACID transactions, easy local debugging.
- Weaknesses emerge at team scale: merge conflicts, slow test suites, all-or-nothing deploys.
- Modular Monolith
-
A single deployable unit with strictly enforced internal module boundaries that mimic microservice boundaries.
- Modules communicate through defined interfaces, not shared tables.
- Gives much of the team-autonomy benefit without network or operational overhead.
- Shopify restructured its monolith this way after finding microservices too costly for many domains.
- Service Mesh
-
An infrastructure layer (e.g., Istio, Linkerd) that handles service-to-service concerns — mTLS, retries, load balancing, telemetry — via sidecar proxies.
- Moves cross-cutting networking logic out of application code.
- Adds resource overhead (a proxy per pod) and operational complexity.
- Worth it at dozens of services; overkill for five.
- Saga
-
A sequence of local transactions across services where each step has a compensating action to undo it if a later step fails.
- Choreographed sagas: services react to events (works for few participants).
- Orchestrated sagas: a central coordinator drives the steps (easier to reason about at scale).
- Requires idempotent steps and careful handling of partial failure.
- API Contract
-
The agreed interface between services: endpoints, request/response schemas, error codes, and versioning policy.
- Backward-compatible changes (adding optional fields) let services deploy independently.
- Breaking changes require versioned endpoints (v1/v2) or consumer-driven contract tests.
- Tools like OpenAPI, Protobuf, and Pact formalize and enforce contracts.
- Distributed Monolith
-
The worst of both worlds: services that must be deployed together and share databases, gaining all the overhead of distribution with none of the independence.
- Common symptom: a 'simple' change requires coordinated releases across teams.
- Usually caused by splitting along technical layers (a 'validation service') instead of business capabilities.
- The fix is often merging services back together and redrawing boundaries.
- Eventual Consistency
-
A guarantee that data replicated or updated asynchronously across services will converge to the same value, but not immediately.
- Inherent consequence of per-service databases and async messaging.
- UIs must handle states like 'payment pending' instead of assuming instant truth.
- Acceptable for most business flows; not acceptable for things like account balance debits without extra design.
Patterns
- Strangler Fig — When migrating an existing monolith to microservices incrementally without a rewrite.
- Database per Service — Whenever services must be independently deployable and their data models evolve at different rates.
- Saga with Compensation — For business transactions spanning multiple services that each own their data.
- Backend for Frontend (BFF) — When different clients (web, iOS, Android) need differently shaped API responses.
Strategies
- Split by Business Capability, Not by Layer When: When deciding where service boundaries go in a new design or a monolith decomposition. How: Map the domain into bounded contexts using domain events and team ownership. Each service should be ownable by one team end-to-end, from API to database. Avoid 'technical' services like a shared 'notification-util service' that everyone depends on. Example: Uber organized services around rider-facing capabilities (trip matching, pricing, payments) rather than horizontal layers, letting the pricing team change surge logic without touching trip matching.
- Start Monolith, Extract When Pain is Proven When: For new products where requirements and team structure are still fluid. How: Build a modular monolith with clean internal boundaries. Extract a module into a service only when there is a measurable reason: a hot path needing independent scaling, a team blocked by release coupling, or a component needing a different technology. Example: Many startups burn months on Kubernetes and 10 services for an app with 500 users; a single Rails or Django app with good module boundaries would have shipped faster and scaled fine until tens of thousands of users.
- Prefer Async Messaging for Cross-Service Workflows When: When a user action triggers downstream work that does not need to block the response. How: The synchronous path returns after persisting the core state and emitting an event. Downstream consumers (email, analytics, search indexing) process events at their own pace with retries and dead-letter queues. Example: Order placement commits the order and publishes 'OrderCreated' to Kafka; the receipt email, loyalty points, and warehouse notification are handled asynchronously, so a slow email provider never delays checkout.
- Design for Partial Failure Between Services When: Always — every network call between services can fail, hang, or be slow. How: Set aggressive timeouts on every call, retry idempotent requests with exponential backoff and jitter, wrap dependencies in circuit breakers, and define a fallback behavior (cached data, degraded response) for each dependency. Example: Netflix's recommendation page falls back to a cached 'popular titles' list if the personalized recommendation service times out, so one slow service never blanks the homepage.
- Version APIs Explicitly When: When multiple teams consume a service and cannot deploy in lockstep. How: Keep changes additive; for breaking changes, run v1 and v2 in parallel with a published deprecation window. Use contract tests in CI to catch accidental breakage before deploy. Example: A payments service adds '/v2/charges' with a new currency field format while keeping '/v1/charges' alive for six months, and tracks which consumers still call v1 via request logs.
The Real Cost Ledger of Microservices
Microservices convert code complexity into operational complexity. Each service needs its own CI/CD pipeline, alerting, dashboards, on-call rotation, and capacity plan. Latency adds up: an in-process call is nanoseconds, while a network call within a data center is roughly 0.5–1 ms — a request fanning out across 8 services serially adds ~5–10 ms before any computation, and tail latencies multiply (if each service has a 1% chance of a slow response, a request touching 10 services has ~10% chance of hitting one). Data consistency becomes a design problem: joins move from SQL into application code or denormalized read models. Finally, debugging requires distributed tracing, because no single log tells the story. None of this means microservices are wrong — it means the benefits (team autonomy, independent scaling, fault isolation) must be large enough to pay this tax. For most systems under ~20 engineers, they are not.
Tradeoffs
| Decision | Upside | Downside |
|---|---|---|
| Microservices vs Modular Monolith | Microservices give independent deployability, independent scaling, and clear team ownership; monoliths give simpler operations, fast in-process calls, and easy ACID transactions. | Microservices add network failure modes, distributed data problems, and heavy observability/platform investment; monoliths create deploy coupling and scaling inefficiency at large team sizes. |
| Synchronous (REST/gRPC) vs Asynchronous (Messaging) Communication | Sync is simple to reason about and gives immediate responses; async decouples services, absorbs traffic spikes, and survives downstream outages. | Sync couples availability — a chain of five 99.9% services yields ~99.5% end-to-end; async introduces eventual consistency, message duplication, and harder debugging. |
| Shared Database vs Database per Service | A shared database gives joins, ACID transactions, and one schema to manage; per-service databases give true independence and let each service pick the right storage technology. | A shared database makes services tightly coupled through the schema and blocks independent deployment; per-service databases eliminate cross-service joins and force sagas or denormalization for multi-entity operations. |
Real World
| System | How it's used |
|---|---|
| Netflix | Runs roughly a thousand microservices on AWS, each owned by a small team with its own deploy pipeline. Pairs the architecture with heavy resilience engineering: the Hystrix circuit-breaker library (now Resilience4j-era patterns), the Simian Army / Chaos Monkey for deliberately killing instances in production, and full distributed tracing to debug cross-service latency. |
| Amazon | Pioneered the organizational model behind microservices: 'two-pizza teams' that own a service end-to-end, communicating only through documented service interfaces (mandated by Jeff Bezos's famous early-2000s API mandate). Prime Video later published a well-known case where they moved one monitoring tool back to a monolith and cut infrastructure costs by ~90% — proof the tradeoff cuts both ways. |
| Uber | Grew from a monolith into thousands of microservices as the company expanded globally, hitting a point where a single request could traverse dozens of services. Uber later documented the pain of this sprawl and pushed toward 'domain-oriented microservice architecture' — grouping services into domains with gateways — to restore understandable boundaries. |
| Shopify | Deliberately stayed a modular monolith for core commerce. Shopify enforces strict module boundaries inside one Rails codebase, getting team autonomy without network overhead, and extracts a service only when there is a concrete reason (e.g., isolating a component with very different scaling characteristics). |
| Segment | Famously published a postmortem of moving back from microservices to a monolith for their data pipeline: the distributed version created a queue-per-destination operational nightmare where one slow destination backed up shared queues for everyone. The consolidation simplified operations dramatically and increased throughput. |
Interview
Questions interviewers ask
- When would you choose microservices over a monolith — and when would you not?
- How do you handle a transaction that spans multiple services?
- How do services find and talk to each other, and what happens when one is down?
- How would you migrate an existing monolith to microservices without downtime?
What a strong answer covers
Candidate should frame microservices as an organizational and operational tradeoff, not a default. They should name bounded contexts, database-per-service, sagas/compensation, sync vs async tradeoffs, circuit breakers and fallbacks, versioning, and the observability requirements. Bonus points for knowing when a modular monolith is the better answer and citing the strangler-fig migration approach.
Common traps
- Recommending microservices by default for a small team or early-stage product.
- Letting multiple services share one database, which quietly recreates monolith coupling.
- Ignoring distributed transaction semantics — pretending a multi-service flow is ACID.
- Forgetting that every inter-service call needs timeouts, retries, and a fallback.
Quiz
What is the strongest justification for splitting a module out of a monolith into its own service?
- The module's code has grown past 10,000 lines
- A team is blocked by release coupling or the module needs independent scaling
- Microservices always improve performance
- The module uses a different programming language style
Service extraction should be driven by proven pain: team autonomy blocked by coupled deploys, or a component with genuinely different scaling or technology needs. Code size alone is not a reason.
Why does 'database per service' matter in a microservices architecture?
- It makes SQL queries faster
- It removes the need for backups
- It prevents hidden coupling through shared tables and enables independent schema evolution
- It guarantees ACID transactions across services
Shared tables let teams bypass APIs and couple deployments through the schema. Owning data per service enforces boundaries — the tradeoff is losing cross-service joins and ACID transactions.
An order flow spans Inventory, Payments, and Shipping services. The payment fails after inventory was reserved. What pattern handles this?
- A global ACID transaction across all three databases
- A saga with a compensating action that releases the reserved inventory
- Retrying the payment forever until it succeeds
- Merging all three services into one
With per-service databases, you cannot roll back across services. A saga models the flow as local transactions plus compensating actions (e.g., unreserve inventory) when a later step fails.
Service A calls B, which calls C, which calls D, each with 99.9% availability. What is the approximate end-to-end availability of the synchronous chain?
- 99.9% — the weakest link sets the floor
- 99.99% — availability adds up
- About 99.7% — probabilities multiply (0.999^4)
- 100% if we add retries
Serial dependencies multiply: 0.999^4 ≈ 0.996. Long synchronous call chains degrade availability, which is why critical paths are kept short and non-critical work is moved to async messaging.
A team has 8 services, but every feature requires deploying 4 of them together in a specific order. What is this architecture called and what is the likely cause?
- A service mesh, caused by too many sidecars
- A distributed monolith, caused by boundaries drawn along technical layers instead of business capabilities
- Event-driven architecture, caused by too many Kafka topics
- A modular monolith, caused by strict module boundaries
Lockstep deployments mean the services are not actually independent. Splitting by technical concern (validation service, formatting service) rather than business capability creates distributed monoliths — all the costs of distribution with none of the autonomy.
Service Discovery
Learn how services find each other when instances scale up, scale down, and fail constantly — and why hardcoded addresses break immediately in production.
This section is part of the full PRISM roadmap, with worked examples, trade-off tables, interview questions and a quiz.
Unlock the full lessonApplication Layer Design
Learn how to structure the compute tier of a system — stateless services, separate web/app/worker tiers, session strategies, and safe deploys — so it scales horizontally and survives failure.
This section is part of the full PRISM roadmap, with worked examples, trade-off tables, interview questions and a quiz.
Unlock the full lessonPractice application servers & services in PRISM
Concepts stick when you watch them fail. Build an architecture that depends on application servers & services, push traffic through it in the PRISM simulator, and see the latency and error rates change as you adjust the design.