Engineering Atlas › System Design Fundamentals

Splitting a system into microservices

11 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

How it works

Boundaries and data ownership

Lewis and Fowler's 2014 article, which named the style, lists organisation around business capabilities and decentralised data management among its nine common traits (martinfowler.com). Each service owns a cohesive capability such as Catalog, Pricing, Orders, or Payments, and it is the only writer to its database. Other services read that data through the owner's API or through events it publishes. If two services share a table, they are coupled at the schema level and cannot deploy independently, which defeats the point. A useful test: can the owning team change its schema on a Tuesday afternoon without telling anyone? If not, the boundary is leaking.

Communication

Services talk synchronously (HTTP/JSON or gRPC) when the caller needs an answer now, and asynchronously (a log or queue such as Kafka or SQS) when it only needs to announce a fact. Synchronous chains multiply latency and failure probability. If a request fans through five services that are each 99.9% available, the chain is at best about 99.5% available, and its p99 latency is driven by the slowest hop. Asynchronous events decouple availability but add eventual consistency and harder debugging.

Consistency across services

There is no cross-service ACID transaction. Multi-step business operations use a saga: a sequence of local transactions, each with a compensating action if a later step fails. To publish an event reliably alongside a database write, use the transactional outbox: write the event to an outbox table in the same local transaction, then relay it to the broker. The relay can crash after publishing but before marking the row sent, so it may publish twice, and consumers must be idempotent (microservices.io).

The platform you now need

Independent deploys require per-service CI/CD pipelines, service discovery, centralised logs, distributed tracing with propagated trace IDs, consistent timeouts and retries, and versioned API contracts. Teams usually build or buy this as an internal platform. Without it, every team reinvents it badly. Fowler's short list of what must exist before the first split is rapid provisioning, basic monitoring and rapid application deployment (Microservice Prerequisites).

Boundaries also follow the org chart whether you plan it or not. Conway's law (1968) says a system's structure mirrors the communication structure of the teams that build it (Fowler on Conway's Law), so a service owned by two teams drifts toward two services or a coordination bottleneck. Microsoft's guidance puts it as each service implementing one business capability inside a bounded context, with its own data store (Azure Architecture Center).

Down the stack

Layer 1The contract

A call to another service promises much less than a function call. It may succeed, fail, or time out with the outcome unknown, so the callee may or may not have done the work. Retries give at-least-once execution unless the callee deduplicates. There is no atomicity across services: a local commit in Orders says nothing about Payments. Latency has a floor of one network round trip plus serialisation and a long tail driven by queueing, garbage collection, and retransmits.

Layer 2The mechanism

Most internal calls are gRPC over HTTP/2 or JSON over HTTP/1.1. A gRPC call is a HEADERS frame with :path /orders.v1.Orders/Get and a grpc-timeout header carrying the caller's remaining deadline, then DATA frames holding length-prefixed protobuf messages (1 byte compressed flag, 4 bytes big-endian length), and finally trailers with grpc-status, which is sent even on success (gRPC over HTTP/2). Deadline propagation means each hop subtracts elapsed time and passes the rest on. Discovery in Kubernetes maps a Service name to an EndpointSlice of ready pod IPs; kube-proxy programs iptables or nftables rules that DNAT the virtual ClusterIP to one of them. The IPVS mode that many older guides recommend is deprecated since Kubernetes 1.35 and due for removal in 1.43; nftables is the recommended replacement and is expected to become the default (Kubernetes docs). The transactional outbox is just an outbox table written in the same database transaction, read by a relay or by change data capture.

Layer 3The machine

An in-process call costs nanoseconds. A same-zone network call costs roughly 100 to 500 microseconds round trip, and a cross-zone call 0.5 to 2 ms, plus cloud data-transfer charges per GB in each direction. Each hop serialises and parses the payload (a few microseconds per KB for protobuf, several times more for JSON), makes sendmsg and epoll_wait syscalls, and crosses the kernel TCP stack twice. New connections pay a TCP handshake, a TLS handshake, and slow start from an initial window of 10 segments (about 14 KB), which is why clients keep pooled, long-lived HTTP/2 connections. One lost packet stalls every multiplexed stream on that TCP connection until retransmission, a source of p99 spikes.

Why it's built this way

  1. Teams need to ship independently, so code is split into separately deployed processes.
  2. Separate processes cannot share memory or a transaction, so each service owns its data and communicates by messages.
  3. Messages cross a network that is a thousand times slower than memory and fails partially, so APIs are coarse-grained and every call needs a deadline.
  4. Partial failure leaves outcomes unknown, so operations must be idempotent and multi-step work uses sagas.
  5. Connection setup is expensive, so services reuse multiplexed connections, accepting head-of-line effects in exchange.

Diagram

flowchart LR
    C[Client] --> GW["API gateway"]
    GW --> O["Orders service"]
    GW --> CAT["Catalog service"]
    O --> ODB[("Orders DB")]
    CAT --> CDB[("Catalog DB")]
    O -->|"sync: reserve"| INV["Inventory service"]
    INV --> IDB[("Inventory DB")]
    O -->|"OrderPlaced event"| BUS["Event log"]
    BUS --> PAY["Payments service"]
    BUS --> NOTIF["Notifications service"]
    PAY --> PDB[("Payments DB")]

Worked example

A retailer runs a monolith with 60 engineers. Releases happen weekly, take a 3-hour freeze, and roughly one in eight is rolled back because an unrelated change broke checkout.

They extract Catalog first: it is read-heavy (8,000 QPS reads, 20 writes per second), has a clear owner, and few inbound dependencies. The new service gets its own Postgres plus a Redis cache and deploys 15 times a week. The monolith calls it with a 150 ms timeout and falls back to a stale cached copy.

Next is Orders. Checkout now spans Orders, Inventory, and Payments. A synchronous version would chain three calls with p99s of 40, 60, and 250 ms, about 350 ms worst case before retries. They instead reserve inventory synchronously (the user must know stock exists) and run payment capture asynchronously from an OrderPlaced event, with a compensating ReleaseInventory step if capture fails.

Cost check: 12 services, each with 3 replicas for availability, plus tracing, a broker cluster, and a platform team of 4. Infrastructure spend rises about 35%. The win is deploy frequency rising from weekly to dozens per day and change failure rate dropping, because each blast radius is smaller.

In the wild

Trade-offs

OptionStrengthsWeaknessesChoose when
MonolithOne deploy, ACID transactions, easy debuggingTeam contention, all-or-nothing scaling and releasesSmall team, unclear domain, early product
Modular monolithEnforced module boundaries, still one deployShared runtime and release trainMedium team that wants boundaries before distribution
MicroservicesIndependent deploys and scaling, fault isolationNetwork failures, eventual consistency, platform costMany teams, stable domain boundaries, mature ops

Pitfalls in production

Leadership lens

Check yourself

  1. Why does a shared database between two services undermine independent deployment?

    Show answer

    Any schema change must be coordinated with every service that reads or writes those tables, so deploys become coupled even though the code is separate.

  2. Five synchronous hops each have 99.9% availability. What is the best-case availability of the chain, and what can you do about it?

    Show answer

    About 99.5%, because failures compound multiplicatively. Cut hops off the critical path with async events, caching, or fallbacks.

  3. Why use a saga instead of a distributed transaction?

    Show answer

    Two-phase commit across services couples their availability and holds locks across the network. Sagas use local transactions plus compensations, trading atomicity for availability.

Sources and further study

Primary

Systems docs

Books

Other curricula

Watch

Free resources

Open in the interactive app: practice, quiz, mock interview →