important senior · part of Skills & Topics · Senior SWE Roadmap · related: API Design · Message Queues & Event-Driven Architecture · Observability · Load Balancing
Monolith architecture
A single deployable unit: one codebase, one build artifact, one process type (usually run as N identical replicas behind a load balancer for scale), typically one shared database. Internal structure can still be modular — separate packages/namespaces for orders, inventory, payments — but they’re linked together and deployed atomically as one thing. This “clean internal boundaries, single deploy unit” style is often called a modular monolith, and it matters later: it’s the version of a monolith that’s cheap to split apart when the time comes.
flowchart TB subgraph Mono["Monolith"] direction TB MLB[Load Balancer] --> M1[App Instance] MLB --> M2[App Instance] M1 --> MDB[(Single Shared Database)] M2 --> MDB M1 -.contains.-> MOrd[Orders module] M1 -.contains.-> MInv[Inventory module] M1 -.contains.-> MPay[Payments module] end
- Pros: simple mental model — one codebase, one build, one deploy; end-to-end testing is real function calls, not network mocks; cross-module “transactions” are free (real ACID, since it’s all one DB); low operational overhead — one thing to deploy, monitor, and scale as a unit; fast iteration early on, no cross-service contract negotiation.
- Cons: scaling is coarse — a CPU-heavy module (say, recommendations) forces you to replicate the entire app to add capacity for just that part; a crash in one module can take down the whole process; deploys are all-or-nothing, so a bad change to any part risks every feature at once; and as headcount grows, many teams committing to one codebase, one release train, and one deploy window creates real coordination friction (Conway’s Law working against you).
Microservices architecture
The system decomposed into multiple independently deployable services, each typically owning its own datastore, talking to each other over the network (REST/gRPC calls, or asynchronous events).
flowchart TB subgraph Micro["Microservices"] direction TB GW[API Gateway] --> SOrd[Orders Service] GW --> SInv[Inventory Service] GW --> SPay[Payments Service] SOrd --> DOrd[(Orders DB)] SInv --> DInv[(Inventory DB)] SPay --> DPay[(Payments DB)] SOrd <-.->|API / events| SInv SOrd <-.->|API / events| SPay end
- Pros: independent deployability — a team ships its service without a company-wide release; independent, targeted scaling — only scale the service that’s actually hot; fault isolation, if calls are made resiliently (timeouts, retries, circuit breakers — see Load Balancing and Rate Limiting for the supporting pieces); technology heterogeneity — each service can pick the best-fit stack/datastore for its own workload; ownership boundaries that map cleanly to team boundaries.
- Cons: real operational complexity — N services means N deploy pipelines, N sets of dashboards/alerts/on-call runbooks (see Observability); network calls replace function calls, so latency and partial failure become first-class concerns; no more free cross-module ACID transactions — multi-service consistency needs an explicit pattern (Sagas, below); API contracts between services need versioning discipline so a producer’s change doesn’t silently break a consumer (see API Design); and integration testing gets genuinely harder once it requires standing up multiple services or heavy mocking.
The actual tradeoff: team autonomy vs operational complexity
The common wrong answer to “why microservices?” is “they scale better.” That’s true in a narrow sense — you can scale one hot service instead of the whole app — but it’s not the primary reason organizations adopt microservices, and leading with it is a tell that someone hasn’t actually made this tradeoff at an org that lived with the consequences. A monolith scales horizontally too: run N copies behind a load balancer, and that covers most real workloads, since most monoliths aren’t uniformly CPU/memory-bound across every feature at once.
The axis that actually drives the decision is organizational: team autonomy and deployment independence. A microservice lets one team own something end-to-end — its code, its data, its deploy cadence, its on-call — without needing sign-off from, or coordination with, every other team touching the same codebase. This starts to matter once an engineering org passes a certain size (rule of thumb: dozens to hundreds of engineers across many independent teams), because the bottleneck stops being hardware and becomes how fast can N teams ship without stepping on each other’s deploys. This is Conway’s Law in practice: system architecture tends to mirror communication structure, so if the org is made of independent teams, the codebase either mirrors that or fights it forever.
The cost is real, not a rounding error: distributed-systems failure modes (partial failures, network partitions, retries-gone-wrong), the infra maturity required to run it well (service discovery, centralized logging/tracing, per-service CI/CD, container orchestration), and genuinely harder reasoning about consistency. A five-person startup adopting microservices on day one is paying that operational tax with none of the organizational payoff — there’s no second independent team that needs deployment isolation from the first, so all that’s been bought is distributed-systems debugging with none of the benefit.
| Axis | Monolith | Microservices |
|---|---|---|
| Deployment | One unit, one release train | Independent per service |
| Scaling | Whole app scales together | Per-service, targeted |
| Team ownership | Shared codebase, coordination overhead grows with headcount | Clear per-service ownership |
| Transactions | Real ACID across modules (free) | Distributed — needs Sagas or similar |
| Testing | End-to-end tests are direct calls | Needs multi-service integration setup or mocks |
| Operational surface | One deploy pipeline, one set of dashboards | N pipelines, N dashboards, N on-call surfaces |
| Failure blast radius | A crash can take down the whole process | Isolated per service, if calls are resilient |
| Right-sized for | Small/medium teams, early-stage products | Multiple independent teams, mature infra |
Interview framing: answer “would you use microservices here?” by asking about team count and deployment cadence needs first, not by asserting a scalability claim. Scalability is a secondary benefit that’s often achievable other ways; team autonomy is the primary reason mature orgs actually pay this cost.
Service boundary design: DDD bounded contexts
Domain-Driven Design’s bounded context: a boundary within which a particular domain model and its terminology are internally consistent and unambiguous. Good service boundaries align with bounded contexts — coherent business capabilities — not arbitrary technical layers. Splitting by layer (an “API service,” a “business logic service,” a “data-access service”) produces a distributed monolith: all the network/coordination overhead of microservices, none of the independent-deployability benefit, since a single business change still has to touch every layer-service in lockstep, and they can’t really deploy independently of each other.
flowchart LR subgraph Monolith["E-commerce Monolith (before)"] direction TB Mod1[Order logic] Mod2[Stock logic] Mod3[Charging logic] SharedDB[(One database, all tables)] Mod1 --> SharedDB Mod2 --> SharedDB Mod3 --> SharedDB end subgraph Split["Bounded contexts (after)"] direction TB BC1["Orders service<br/>owns: order lifecycle, line items"] BC2["Inventory service<br/>owns: stock levels, reservations,<br/>warehouse location"] BC3["Payments service<br/>owns: charges, refunds,<br/>processor integration (PCI scope)"] end
Worked example — why Orders / Inventory / Payments, and not some other split:
- Orders owns the concept of “an order”: its lifecycle (created → confirmed → shipped → cancelled), its line items, and orchestrating the checkout flow. “Order” is a coherent, self-contained concept here.
- Inventory owns stock levels, reservations, and warehouse/location data. Notice that “item” means something different inside Inventory (a SKU with a stock count and a shelf location) than it does inside Orders (a line item referencing a product, quantity, and price at the time of purchase). That’s expected under DDD, not a bug to reconcile — the same English word can mean different things in different bounded contexts, and each service’s model should stay internally consistent rather than forcing one global “item” schema everyone shares.
- Payments owns payment methods, charge/refund records, and processor integration (Stripe, etc.). It also has a very different compliance surface (PCI-DSS) than the other two — a strong reason to isolate it on its own, independent of domain-modeling purity: you want to minimize the blast radius and audit scope of anything that touches card data.
Why not split by technical layer, or too finely (a service per table)? Neither corresponds to an independent axis of change or ownership. Layer-splitting means one business change touches every layer-service at once — a distributed monolith. Over-fine splitting means a single order touches a dozen tiny services, and every request becomes a fan-out of network calls for what should have been one local operation, with no team actually owning a full concept end to end.
Rule of thumb: a good boundary is one a single team can own end to end, where most changes to that business capability stay inside the boundary. If you keep finding yourself needing a coordinated change across two “different” services for one feature, that’s the DDD signal the boundary is drawn in the wrong place.
Data ownership rules
Each service owns its datastore exclusively. No other service reads — and especially, no other service writes — another service’s tables directly, even though nothing at the network layer stops you from pointing two services’ connection strings at the same database. Any need for another service’s data goes through that service’s API (synchronous — “give me current state”) or an event it publishes (asynchronous — “notify me on change,” often used to maintain a local, read-optimized copy inside the consuming service).
flowchart TB subgraph Anti["Anti-pattern: shared database"] direction TB AOrd[Orders Service] --> ADB[(Shared DB)] AInv[Inventory Service] --> ADB APay[Payments Service] --> ADB end subgraph Correct["Correct: owned databases"] direction TB COrd[Orders Service] --> CODB[(Orders DB)] CInv[Inventory Service] --> CIDB[(Inventory DB)] CPay[Payments Service] --> CPDB[(Payments DB)] COrd -->|API call or event| CInv COrd -->|API call or event| CPay end
Why the shared-DB pattern is a real anti-pattern rather than just a style preference: the DB schema becomes a de facto shared API. Any service can silently come to depend on another service’s internal table structure, and now nobody can change their own schema without a cross-team coordination effort — which quietly destroys the entire “independent deployability” benefit microservices exist to provide. It also creates a noisy-neighbor risk: a slow query or lock contention from one service’s traffic can starve connections or rows a completely unrelated service needs. Net effect: you’ve built a distributed system with all the coupling risk of a shared DB and none of a monolith’s transactional simplicity — worst of both worlds.
Distributed transactions and the Saga pattern
Once data is split across services and databases, there’s no BEGIN; ...; COMMIT; that spans them. Two-phase commit (2PC) offers a theoretical answer but is rarely used at this scale in practice: it’s a blocking protocol with a coordinator SPOF, it holds locks across every participant for the full transaction duration (killing availability under load), and it doesn’t play well with heterogeneous datastores.
Saga pattern: model the multi-step business operation as a sequence of local transactions, one per service. Each step either triggers the next step, or — on failure — triggers compensating transactions that walk backward through the already-completed steps, semantically undoing their business effect. This is not a technical rollback (you can’t roll back a transaction that already committed on another service’s database); it’s a new, explicit action with the opposite business meaning — “refund the charge,” not “undo the charge.”
Worked example — Place Order saga (order → payment → inventory), including the failure/compensation path:
sequenceDiagram participant O as Order Service participant P as Payment Service participant I as Inventory Service Note over O,I: Happy path O->>O: create order (status = PENDING) O->>P: charge payment P-->>O: payment succeeded O->>I: reserve inventory I-->>O: reservation failed (out of stock) Note over O,I: Failure — compensate in reverse order O->>P: refund payment (compensating transaction) P-->>O: refund confirmed O->>O: mark order CANCELLED
Inventory failing after payment already succeeded is the interesting case: the saga can’t just “stop,” because a customer would be charged for an order that never shipped. It has to actively walk backward — refund the payment, then mark the order cancelled — to leave the system in a consistent end state. This is the core mental model to have ready: every forward step in a saga needs a corresponding compensating step defined up front, before the saga is considered done.
Orchestration vs choreography
Two implementation styles for coordinating a saga’s steps.
flowchart TB subgraph Orchestration["Orchestration"] direction TB Orch[Saga Orchestrator] Orch -->|1. charge| OP[Payment Service] OP -.->|result| Orch Orch -->|2. reserve| OI[Inventory Service] OI -.->|result| Orch Orch -->|3. confirm| OO[Order Service] end subgraph Choreography["Choreography"] direction TB CO[Order Service] -->|OrderCreated event| CP[Payment Service] CP -->|PaymentCompleted event| CI[Inventory Service] CI -->|InventoryReserved event| CO end
- Orchestration: a dedicated coordinator (a purpose-built saga service, or a workflow engine like Temporal, Camunda, or AWS Step Functions) explicitly calls each participant in order and tells it what to do next, including invoking compensating actions on failure. The whole flow lives in one place — easy to read, easy to add a step to, easy to build monitoring for “where is saga #42 stuck right now.” Cost: the orchestrator becomes a component every step depends on (needs its own HA story) and has to know every participant’s API.
- Choreography: no central coordinator. Each service publishes an event when it finishes its local step; interested services subscribe and react by doing their own step and publishing their own completion (or failure) event, via a broker (see Message Queues & Event-Driven Architecture). Fully decoupled — services only know about events, not each other, and there’s no new central component to build or scale. Cost: the overall business flow isn’t written down anywhere as a single artifact — it’s implicitly defined by “whichever services happen to subscribe to which events,” which gets genuinely hard to trace as the step count grows, and adding a new step means touching multiple services’ subscriptions instead of one orchestrator definition.
Default interview answer: for anything beyond ~3–4 steps, or with nontrivial compensation logic, prefer orchestration — explicit beats implicit for anything that has to be debugged at 3am. Choreography earns its keep for simpler, genuinely decoupled, fire-and-forget flows where participants don’t need to know about the overall business process.
Strangler fig pattern
Named for the vine that grows around a host tree and gradually replaces it without the tree ever coming down all at once — the practical, incremental pattern for migrating an existing monolith to microservices in production, instead of a big-bang rewrite (a well-documented way to lose a year and ship nothing).
flowchart TD subgraph Before["Stage 1: before"] direction LR R1[Router] -->|100% of traffic| Mono1[Monolith] end subgraph During["Stage 2: mid-migration"] direction LR R2[Router] -->|/orders/*| Svc2[Orders Service] R2 -->|everything else| Mono2[Monolith - shrinking] end subgraph After["Stage 3: fully extracted"] direction LR R3[Router] --> Svc3a[Orders Service] R3 --> Svc3b[Inventory Service] R3 --> Svc3c[Payments Service] end
Mechanism: put a routing layer (reverse proxy / API gateway) in front of the monolith; initially all traffic still goes to it. Pick one bounded context to extract first — ideally something with a clean boundary and lower blast radius, not the riskiest, highest-traffic core. Build the new service, backed by its own database (typically with a dual-write or backfill period during the transition). Repoint the routing layer’s rules for that capability’s endpoints at the new service instead of the monolith. Repeat, capability by capability, until the monolith either disappears or shrinks down to whatever core genuinely doesn’t need splitting.
The system stays fully in production and functional at every point in this process — there’s never a multi-month cutover with everything at risk simultaneously, and any single extraction can be paused or reversed if it’s not working out. This matters for the interview because “design microservices from scratch” is a rare real job; “we have a monolith and need to peel services off it safely” is what most senior engineers actually do, and being able to talk through the incremental path — not just the end-state diagram — is what separates a senior answer from a textbook one.
When to definitely not use microservices
- Small team. If there’s no second independent team that needs deployment isolation from the first, microservices solve an organizational problem you don’t have yet.
- Early-stage / pre-product-market-fit. The domain model is still changing fast; service boundaries drawn before the domain is well understood tend to be the wrong boundaries — and a wrong boundary is far more expensive to fix across a network/process line (data migration, API versioning, coordinated multi-service deploys) than to refactor as a package boundary inside one codebase.
- No dedicated platform/infra investment. Microservices need CI/CD maturity, service discovery, centralized logging and tracing, and container orchestration to run well. Without that investment, every engineer pays an operations tax on every feature, for no organizational benefit.
- Interview-ready framing: “Start with a monolith — ideally a modular one, with clean internal boundaries, so a later extraction is a refactor rather than a redesign — and split out services only with concrete evidence of the specific pain microservices solve: a team blocked on another team’s release train, or one component with scaling needs wildly different from the rest of the system.” This is not just theory — Shopify and GitHub both run large, deliberately-monolithic cores at scale, and Segment’s public postmortem of migrating to microservices and then partly back is a well-known real-world data point worth naming if it comes up.
This kind of order → payment → inventory saga shows up as a sub-component of most transactional HLD prompts — Design a Payments System, Design a Ticket Booking System (Ticketmaster-like), Design a Hotel Reservation System — worth pointing at directly if the interview goes there.
Interview angles
- “Would you use microservices for X?” — lead with team size and deployment-independence need, not a scalability claim; explicitly name and correct the “microservices are just more scalable” framing if the interviewer leads with it.
- Be ready to sketch a Saga for a concrete example (order/payment/inventory) including the compensating-action path, and name orchestration vs choreography as the two implementation styles with a real tradeoff for each, not just definitions.
- “How would you decide service boundaries?” — DDD bounded contexts, the “changes cluster inside boundaries, not across them” test, and a worked example — not “one service per table” or “one service per verb.”
- “You’ve got a monolith in production — how do you migrate it safely?” — strangler fig, capability by capability, behind a routing layer, never a big-bang rewrite.
- “What’s the biggest risk you’d want to guard against in a microservices design?” — the distributed-monolith anti-pattern (services split along the wrong axis, still forced to deploy together) and the shared-DB anti-pattern (killing independent deployability even though services are nominally separate processes).
- Good adjacent pages if the conversation goes deeper: Message Queues & Event-Driven Architecture for the pub/sub mechanics under choreography, API Design for the contracts between services, Observability for why N services means N times the monitoring surface, Load Balancing for the routing layer that makes strangler fig possible.