System designCore10 min
Monolithic vs microservices
A monolith ships the whole application as one unit; microservices split it into many small services that deploy on their own and own their own data. Neither is more modern or more correct. Microservices buy team independence and separate scaling, and pay for them with network calls, scattered data and a lot more to run.
1 · The shapes
The difference is what deploys together and who owns the data
A language-learning app has lessons, user progress and streaks, subscriptions and payments, and a speech-scoring feature that grades pronunciation. Three ways to build it:
Shape | Deploys as | Data | Parts talk by |
|---|---|---|---|
Monolith | One application | One database, shared by all code | Function calls |
Modular monolith | One application, with enforced module boundaries inside | One database, each module owning its own tables | Function calls through each module's public interface |
Microservices | Many services, each deployed on its own | Each service owns its data; no one else touches it | Network calls and messages |
A microservice is a small service built around one business capability, running in its own process, deployable without redeploying anything else, and the only owner of its data. "Small" is about ownership more than lines of code: one team can build, run and change it alone. The app as microservices looks like this:
Each service has its own deploy pipeline and its own storage. The gateway gives the app one front door (Reverse proxies & API gateways); services that need to react to each other listen to events instead of reading each other's tables.
The modular monolith in the middle row is easy to overlook and often the right answer. It gets clear boundaries and separate ownership of code and tables, while keeping one deploy, one process and real transactions.
2 · The gains
Microservices buy independence: of teams, of deploys and of scaling
The app's company grows from 8 engineers to 120. In the monolith, every release bundles the work of a dozen teams; a bug in streaks blocks a payment fix; the speech team waits for the weekly release train. That is the pressure microservices relieve:
Gain | What it means here | The condition attached |
|---|---|---|
Independent deploys | Payments ships a fix at 10:00 without waiting for anyone | Only if the API stays compatible for its callers |
Team ownership | Each team owns a service end to end, code to on-call | Only if boundaries match how the teams are split |
Independent scaling | Speech scoring runs on 40 GPU machines; lessons on 6 small ones | Only if the heavy part is really separate |
Fault isolation | A crash in speech scoring doesn't take down lessons | Only with timeouts, breakers and fallbacks between them |
Different technology | Speech scoring in Python with ML libraries, the rest in Go | Each extra stack is one more thing to hire for and patch |
The second row is Conway's law: a system's structure ends up copying the communication structure of the organization that builds it. Microservices use it on purpose, cutting the system along the lines the teams are already cut along. Without that match, a team must coordinate with three others for every feature, and the independence never arrives.
Note the last column. Every gain is conditional. A crash isolates nothing if the caller waits forever on it (The circuit breaker pattern, Bulkheads), and independent deploys are a fiction if two services must always be released together.
3 · The costs
Every function call that becomes a network call gets slower and can fail
In the monolith, the lesson page asks the progress module for the user's score on each of its 30 exercises: 30 function calls, each taking nanoseconds. Split progress into a service, and each call becomes a network round trip of about 0.1 to 0.5 ms inside a data center (Latency), plus encoding and decoding the data. Thirty in a row is 3 to 15 ms, and each one can now time out, fail or arrive at a service mid-deploy.
In a monolith | Across services |
|---|---|
A function call: nanoseconds, never lost | A network call: a fraction of a millisecond at best, sometimes lost |
One stack trace shows the whole request | Distributed tracing is needed to follow a request (Observability: metrics, logs & traces) |
A rename is one refactor | Changing an API means versions, compatibility and coordinated rollouts |
One deploy pipeline, one thing to monitor | Dozens of pipelines, dashboards, alerts and on-call rotations |
A database transaction covers everything | No transaction spans services (next section) |
The fixes for chattiness are design, not tuning: coarser APIs ("scores for these 30 exercises" in one call), keeping data that is always used together in one service, and asynchronous messages where the caller doesn't need the answer right away. Calls chained in a row also multiply failure: each service in a request's path adds its own chance of being down (Availability & reliability).
The rest of the cost is the platform you now need before the first service is useful: automated builds and deploys per service, service discovery (Service discovery & registration), an API gateway, central logs, metrics and traces, and timeouts, retries and breakers on every call. For a small team, that platform can be most of the work.
4 · The data
Each service owns its data, so joins and transactions become messages
If the subscriptions service and the lessons service both read and write the same users table, neither can change its schema without breaking the other, and they are one system in disguise. So each service keeps its data private and others reach it only through its API or its events.
That rule changes three everyday things:
- No cross-service joins. "Lessons this user can open" needs subscription data. Either lessons asks subscriptions at request time, or it keeps its own copy of each user's plan, updated from events.
- No cross-service transactions. Upgrading to premium charges a card in subscriptions and unlocks lessons in another service; no single commit covers both. Multi-step changes become sagas with compensations (Distributed transactions: 2PC & sagas).
- Copies lag. A copy updated from events is briefly out of date, so the design must say what users see meanwhile.
The upgrade commits in subscriptions; lessons learns about it a moment later. For that moment, the app must not show a locked lesson to someone who just paid, so it trusts the upgrade response on screen until the copy catches up.
Copying data is normal here, not a smell: a service that keeps a local copy of what it needs keeps working when the owner is slow or down. The rules are to have one owner per piece of data, publish changes as events (Event-driven architecture), and never write to someone else's copy.
5 · Boundaries
Wrong boundaries give you a distributed monolith
Where to cut matters more than how many pieces there are. Good boundaries follow business capabilities, the things the business does and talks about: lessons, progress, subscriptions. Each is a bounded context, an area with its own model and its own vocabulary. Bad boundaries follow technical layers, such as a "database service" or a "validation service", which every feature has to touch.
Cut along the wrong lines and you get the worst of both shapes, a distributed monolith: services that are separate on paper but can't change or deploy without each other. The symptoms are easy to spot:
Symptom | What it means |
|---|---|
Services must be deployed together, in order | They are one release unit split across a network |
Most features change three services at once | The boundaries don't match how the product changes |
Services read each other's tables | There is still one schema with many owners |
Long chains of calls for one user request | Logic that belongs together was split apart |
A shared library everyone must upgrade at once | Coupling moved from the network into the build |
The fix is usually to merge services back or redraw the line, which is much harder across services than inside one codebase. Moving a function between modules is a refactor; moving it between services is a migration with data, APIs and two deploys. That cost is the strongest argument for learning the boundaries inside a modular monolith first.
6 · In practice
Start with a modular monolith and extract services when a reason appears
Good reasons to pull a module out into a service are concrete:
- Team contention: many teams blocked on one release train.
- Different resources: speech scoring needs GPUs; nothing else does.
- Different risk: payments needs stricter access, audits and slower, safer releases.
- Different pace: one part changes daily, the rest monthly.
"It's the modern way" and "to scale" without a measured bottleneck are not reasons.
Extraction is done gradually with the strangler fig pattern, named after a vine that sprouts on a host tree, grows around it down to the ground, and can end up standing in its place. A routing layer in front sends each path either to the old monolith or to the new service, and paths move over one at a time:
The app never notices the migration. Each path can move back if the new service misbehaves, and the monolith shrinks until it can be retired or kept as a smaller core.
Granularity can be wrong in the other direction too. Prime Video once built a stream-quality monitoring tool from many small serverless functions, passing video frames between steps through object storage. It hit a scaling limit at about 5% of the expected load. Rebuilt as a single process passing frames in memory, it scaled further and cost over 90% less. The lesson is not "monoliths win"; it is that each split must pay for its network hop.
Check yourself
4 questions
Takeaways
Remember this
- A monolith deploys as one unit with one database; microservices deploy independently and each owns its data.
- Microservices buy independent deploys, team ownership and separate scaling, each only under conditions such as stable APIs and good boundaries.
- Every split turns function calls into network calls that are slower and can fail, and turns joins and transactions into APIs, events and sagas.
- Cut along business capabilities; services that must change or deploy together are a distributed monolith.
- Start with a modular monolith and extract services gradually, with a strangler fig, when a concrete reason appears.