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:

Mobile appAPI gatewayLessonsown databaseProgressown databaseSubscriptionsown databaseSpeech scoringGPU machinesEvent streameventsMobile appAPI gatewayLessonsown databaseProgressown databaseSubscriptionsown databaseSpeech scoringGPU machinesEvent streamevents

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:

Mobile appRouting facadeby pathMonolitheverything not movedSpeech serviceextracted firsteverything else/speech/*Mobile appRouting facadeby pathMonolitheverything not movedSpeech serviceextracted firsteverything else/speech/*

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

1. Two services read and write the same database tables. What is the main problem?
2. A page makes 30 sequential calls to another service, each about 0.4 ms. What is the best fix?
3. Every feature requires changing and deploying three services together, in a fixed order. What is this?
4. A team of six is starting a new product with an uncertain domain. Which shape is usually the best start?

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.