Skip to content
Laviyx Learn

Try

System Design course menu

Monolith vs Microservices: When to Use Each

Compare monoliths, modular monoliths and microservices, how services communicate, API gateways, the database per service rule, sagas, and how to split a system.

Intermediate6 min readUpdated
On this page

Monolith#

A monolith is one application, deployed as one unit:

Text
[ Users | Orders | Payments | Search ]  --> One database

It is simple to build, test and deploy, and calls between parts are plain function calls. Problems arrive as it grows: slow builds, teams getting in each other's way, and one bug that can take everything down.

Microservices#

Microservices split a system into small services, each with its own job, deployed on its own:

Text
              +--> Users service    --> Users DB
Client -> API +--> Orders service   --> Orders DB
        gateway--> Payments service --> Payments DB
MonolithMicroservices
DeploymentAll togetherIndependently
ScalingEverything togetherEach part separately
Team independenceLowHigh
Calls between partsFunction callsNetwork calls
Data consistencyOne transactionHarder
Operating costLowerMuch higher
DebuggingEasierNeeds tracing

Which should I choose?#

Start with a modular monolith: one deployment, with clear boundaries between modules. Move parts out when there is a real reason, such as a team that needs to ship on its own, or a part with very different scaling needs.

Microservices solve problems of organisation and scale. For a small team, they mostly add work.

Network calls can fail#

Each call between services can fail or be slow, and the chances add up:

Python
success_per_call = 0.999
for calls in (1, 5, 10, 25):
    ok = success_per_call ** calls
    print(f"{calls:>2} calls in a request: {ok:.2%} of requests fully succeed")
Output
 1 calls in a request: 99.90% of requests fully succeed
 5 calls in a request: 99.50% of requests fully succeed
10 calls in a request: 99.00% of requests fully succeed
25 calls in a request: 97.53% of requests fully succeed

That is why timeouts, retries and circuit breakers matter so much here.

How services talk#

StyleExampleGood for
SynchronousREST, gRPCNeeding an answer now
AsynchronousQueues and eventsWork that can happen later

Too many chained synchronous calls make slow, fragile systems. Prefer events when the caller does not need the result.

Supporting parts#

  • API gateway: one entry point for clients, handling routing, authentication and rate limits.
  • Service discovery: finds where services run. Kubernetes Services do this.
  • Service mesh: handles retries, encryption and metrics between services, using tools such as Istio or Linkerd.
  • Observability: logs, metrics and traces are essential.

Database per service#

Each service owns its data, and others go through its API. Otherwise, a change to a shared table can break several teams at once. This means no joins across services, and no shared transactions.

Transactions across services#

Booking a trip may need a payment, a flight and a hotel. A saga is a chain of local steps, each with a compensating action that undoes it if a later step fails:

Python
def saga(steps):
    done = []
    for name, action, undo in steps:
        try:
            action()
            done.append((name, undo))
            print("OK:", name)
        except RuntimeError as err:
            print("Failed:", name, "-", err)
            for prev, compensate in reversed(done):
                compensate()
                print("Undo:", prev)
            return False
    return True


def fail():
    raise RuntimeError("no rooms left")


saga([
    ("charge card", lambda: None, lambda: None),
    ("book flight", lambda: None, lambda: None),
    ("book hotel", fail, lambda: None),
])
Output
OK: charge card
OK: book flight
Failed: book hotel - no rooms left
Undo: book flight
Undo: charge card

Between steps, the data is only eventually consistent, so screens need states such as "pending".

Splitting a monolith#

The strangler fig pattern moves one feature at a time. A proxy sends that feature's traffic to the new service, and the old code is removed once it is proven. Split along business areas, such as orders or payments, not along technical layers.

Common mistakes#

Practice#

Practice exercise

A request calls services in a chain. Each adds 20 ms on average. If 8 calls run one after another, and you then run 6 of them in parallel, taking as long as the slowest (say 30 ms), what is the new total?

Show solution
Python
one_by_one = 8 * 20
parallel = 2 * 20 + 30
print(one_by_one, "ms before")
print(parallel, "ms after")
Output
160 ms before
70 ms after

Key takeaway#

Next: Monitoring and Observability.

Finished this lesson?

Some links are affiliate links. If you buy through them, Laviyx Learn may earn a commission at no extra cost to you. As an Amazon Associate, we earn from qualifying purchases.