What is it? #
Microservices split a system into separately deployable services, each owning its data and communicating over the network.
The benefit is independence. Teams deploy on their own schedule, choose their own runtime, and scale their service to its own needs.
The cost is that you have converted function calls into network calls. Every call can now be slow, fail, or partially succeed, and you no longer have transactions across services.
The honest guideline: microservices solve organisational problems more than technical ones. If one team can deploy the whole system comfortably, you are paying the cost without receiving the benefit.
Think of it like this #
Splitting one restaurant into several specialist kitchens in different buildings, each with its own supplies and staff.
Each kitchen can be upgraded without closing the others. But an order spanning three kitchens now needs couriers, tracking and a plan for what happens when one of them is closed.
Simple example #
An e-commerce platform with separate services for catalogue, orders, payments and notifications. Placing an order spans three of them, which means the transaction from the monolith lesson no longer exists.
Code #
┌─────────────┐
clients ─────────▶│ API gateway │
└──────┬──────┘
┌──────────────────┼──────────────────┐
▼ ▼ ▼
┌───────────┐ ┌──────────┐ ┌────────────┐
│ catalogue │ │ orders │ │ payments │
│ own DB │ │ own DB │ │ own DB │
└───────────┘ └────┬─────┘ └────────────┘
│ events
┌─────▼──────────┐
│ notifications │
└────────────────┘
# The transaction is gone. A saga coordinates with compensating actions.
def place_order(cart, user):
order = orders_db.create(user_id=user.id, lines=cart.lines, status="pending")
try:
reservation = inventory_client.reserve(cart.lines, timeout=2)
except Exception:
orders_db.update(order.id, status="failed")
raise OrderFailed("could not reserve stock")
try:
payment = payment_client.charge(user.id, order.total,
idempotency_key=f"order-{order.id}",
timeout=5)
except Exception:
inventory_client.release(reservation.id) # compensating action
orders_db.update(order.id, status="failed")
raise OrderFailed("payment failed")
orders_db.update(order.id, status="paid", payment_id=payment.id)
events.publish("order.placed", {"order_id": order.id, "email": user.email})
return order
# Every step needs a timeout, an idempotency key, and an undo path.
# The monolith version of this was one transaction block.
What you gain What you pay
independent deployment network latency and partial failure
independent scaling no cross-service transactions
technology choice per service distributed debugging and tracing
smaller codebases per team data duplication and eventual consistency
fault isolation, if done well many pipelines, environments and dashboards
clear team ownership versioning between service interfaces
How it works #
Each service owns its database. That is the defining rule: if two services share a database, they are one service with extra network hops, because neither can change its schema independently.
The saga replaces the transaction. Each step is durable and each has a compensating action to undo it. Reserving stock is undone by releasing it; a charge is undone by a refund.
Notice how much machinery each call needs: a timeout, an idempotency key so retries are safe, and an explicit failure path. In the monolith this was one transaction block, and the database guaranteed correctness.
Consistency becomes eventual. Between reserving stock and confirming payment, the system is in an intermediate state that must be represented and recoverable — which is why the order has a pending status at all.
Events handle the non-essential follow-ups, which keeps the synchronous path short. Notifications does not need to be available for an order to succeed.
Debugging requires distributed tracing, because one user action now produces work in several services with separate logs. A correlation ID propagated in headers is the minimum.
Real-world use #
Microservices work well in large organisations where independent deployment is the binding constraint. When fifty engineers share one pipeline, the coordination cost is real and services relieve it.
They work poorly when adopted for perceived modernity at small scale. A five-person team running twelve services spends its time on infrastructure rather than product.
Boundaries should follow business capabilities and team ownership. Services split by technical layer — a "database service", a "validation service" — produce chatty systems where every request crosses several hops.
The infrastructure requirement is substantial: service discovery, centralised logging, distributed tracing, per-service pipelines, contract testing and a deployment platform. All of it must exist before the first service is useful.
A widely repeated lesson from teams who have done it: start with a well-structured monolith, extract services when a concrete problem demands it, and expect the first extraction to teach you where the boundaries really are.
Common mistakes #
- Adopting microservices for a team small enough to deploy a monolith comfortably.
- Sharing one database between services, which removes the independence entirely.
- Splitting by technical layer instead of business capability.
- No distributed tracing, making cross-service debugging nearly impossible.
- Assuming a network call behaves like a function call — no timeouts, no retries, no compensation.
Practice #
Take the order flow from the monolith lesson and rewrite it as a saga across three services. For each step, specify the timeout, the idempotency key and the compensating action. Then write down what the user sees if the payment service is unreachable.