Architecture Patterns
Microservices Architecture
Splitting one deployable into many trades one class of problem, deploy coupling, for another: network calls where function calls used to be, and who owns which table now that one database is several.
Architecture PatternsIntermediate6 min read
After this lesson, you should be able to:
- Explain what changes when a monolith splits into services: in-process calls become network calls, one database becomes several.
- Diagnose why a feature that used to be one database transaction can no longer be atomic once its data spans two services.
- Identify deploy coupling as a monolith's real failure mode versus operational overhead as microservices' real cost.
- Recognize that splitting trades one class of problem for another, it isn't a net reduction in risk on its own.
At a Glance
- A single codebase where every team's code ships in the same deploy means one team's broken build blocks every other team's release, whether or not their changes touched it.
- Splitting into services lets each team deploy independently, at the cost of turning what used to be a function call into a network call, with everything a network call can do that a function call can't: fail, time out, or arrive twice.
- The data question doesn't go away, it gets harder: one database with clear table ownership becomes several, and a question that used to be one JOIN now needs a network call, a duplicated copy, or an accepted window of disagreement.
01 The Problem
A retail company's backend is one deployable: orders, inventory, shipping, and pricing, four teams, one codebase, one build, one deploy pipeline. The orders team needs to ship a fix today for customers being double-charged. The pricing team pushed a change yesterday that broke a flaky integration test somewhere in the shipping module, a test nobody on the orders team wrote and nobody currently owns, and the whole pipeline is red. The orders team's fix, which touches none of pricing's or shipping's code, is stuck behind a failure that has nothing to do with it, because the deploy is one unit: green or it doesn't ship, for everyone, regardless of whose code broke it.
Splitting the monolith into services, one per team, each independently deployable, fixes exactly this: the orders team's fix ships the moment orders' own tests pass, unaffected by shipping's flaky test. But an order confirmation page that used to call a local inventory.checkAvailability() function now makes a network call to the inventory service instead, and that call can time out, fail, or arrive twice in a way a function call never could. And the orders table, which used to sit next to the inventory table in one database where a single JOIN answered "is this item in stock," now lives in two different services' two different databases, and the same question needs a network call, a duplicated copy of the data, or an acceptance that the two answers might briefly disagree.
Neither is free. This is the actual trade in microservices: one class of coupling for another.
02 What It Is
Microservices architecture splits a system into multiple independently deployable services, each owning its own codebase, its own deploy pipeline, and usually its own datastore, communicating over the network instead of in-process function calls. The opposite is a monolith: one codebase, one deploy, one shared process, where modules call each other directly as functions.
The actual decision isn't "monolith bad, microservices good." It's which coupling you'd rather have. A monolith couples every team's release to every other team's code being in a shippable state, in exchange for calls between modules being fast, reliable, in-process function calls that either work or the whole process crashes cleanly. Microservices decouple releases, at the cost of every one of those calls now crossing a network, with its own latency, its own failure modes, and its own version of "is the data on both sides actually in agreement right now."
In Plain Terms
A single household budget spreadsheet that every family member edits means one person's half-finished entry can lock the file for everyone else. Giving each person their own spreadsheet for their own expenses fixes that: nobody's blocked waiting on anyone else. It also means the monthly total, which used to be one column sum in one file, now means opening several spreadsheets and adding them up by hand, or building something that pulls the numbers together, and until that happens, nobody actually knows the real total on demand.
03 How It Works
Rendering diagram…
Rendering diagram…
04 Trade-offs
| Monolith | Microservices | |
|---|---|---|
| Deploy coupling | One team's broken build blocks every team's release | Each service deploys on its own schedule, independent of the others |
| Calls between modules | In-process function calls: fast, and either succeed or the whole process crashes | Network calls: can time out, fail partway, or arrive twice, and the caller has to handle all of it |
| Data consistency | One database; a single transaction can span everything | Data is split across services; a change spanning two services can't be one atomic transaction anymore |
| Operational overhead | One thing to deploy, monitor, and debug | N services, N deploy pipelines, and a request touching three services means three logs to correlate |
Watch out
It's tempting to reach for microservices because "that's how big companies do it," as if service count alone were the win. Splitting a monolith along the wrong lines, by technical layer instead of by team or by data ownership, buys every cost on this page, network calls, deploy overhead, data split across databases, without the actual benefit, independent releases. Two "independent" services that still have to deploy together every time are just a monolith with extra network hops.
05 Where You'll Actually See This
Neither case study is written as a multi-service system, each is a single app server talking to Postgres and Redis. Worth naming rather than glossing over: that's a deliberate simplification, and a real version of either would likely split into services once enough teams were touching it, exactly the trigger this article's Problem section describes.
Load balancers and API gateways both take "multiple services" as a given: a load balancer distributes traffic across instances of one service, a gateway routes to whichever different service a request belongs to. This article is the answer to the question both of those presuppose, why there's more than one service to route to or balance across in the first place, and what it actually costs to get there.
Once services exist, they have to coordinate, and there are two ways to do it. Direct calls, one service invoking another's API, wire every caller to every callee it depends on. Event-driven architecture is the other: a service announces what happened and stops caring who's listening.
Everything above is the "how": what splitting actually costs once you do it. Monolith vs microservices is the "when," whether the trade this article describes is even worth making for a given team, and it usually comes down to team size and Conway's Law more than anything technical.
Splitting also changes how you debug a slow request. A monolith's stack trace already shows which function was slow, there's nowhere else the time could have gone. The moment a request crosses several services, that stack trace stops existing, and metrics, logs & traces covers what replaces it. Distributed tracing is the concrete mechanism that makes that replacement actually work, a trace id propagated through every hop so a request's real path and timing survive the split, not just the code.
How to reason about it
When asked whether a system should be microservices, don't answer with a service count. Ask what's actually coupled today, teams blocked on each other's deploys, or calls that are already effectively network calls anyway, and split along that line, not along whatever technical layers happen to exist in the codebase. A service boundary that doesn't remove a real coupling is just a network call you didn't need.
06 Quick Check
A team splits their monolith into an orders service and an inventory service, each with its own database. A feature needs to atomically decrement inventory and create an order, guaranteed both happen or neither does. What changed about implementing this?