Lesson 55 of 129
Microservices Trade-offs and the Distributed Monolith
Splitting an application into services converts in-process calls into remote calls and one database into several. Both changes add costs that a monolith does not have. The costs fall into four groups: network, consistency, operations, and testing. The worst outcome is a distributed monolith, which has all these costs and none of the benefits of independence.
Costs
Network
| Property | In-process call | Remote call |
|---|---|---|
| Latency | Nanoseconds | Around a millisecond or more within a data center; higher across regions; variable |
| Failure | Call returns or the process dies | Timeout, connection reset, partial failure, unknown outcome |
| Arguments | Shared memory | Serialized and versioned |
| Ordering | Deterministic | Messages may be reordered, duplicated, or lost |
Latency accumulates along call chains. A request that traverses five services in sequence pays five network hops, and its total latency is the sum of the per-service latencies. Tail latency compounds too: if each service exceeds its own 99th-percentile latency on 1% of calls, the probability that at least one of five sequential calls is slow is 1 - 0.99^5, about 4.9%, so roughly one request in twenty experiences a slow call somewhere.
Availability also multiplies: if a request needs five services in sequence and each is 99.9% available, the combined availability is about 0.999^5, roughly 99.5%. That is about 44 hours of unavailability per year instead of about 8.8 hours for one service. This assumes independent failures and no fallbacks.
Consistency
Each service owns its data, so a business operation spanning services cannot use one ACID transaction. Options are sagas with compensating actions, or accepting eventual consistency between services. Consequences:
- Intermediate states are visible to other requests.
- Every step must be retryable and idempotent because messages can be delivered more than once (see delivery semantics and idempotency).
- Queries that used to be SQL joins become API compositions or replicated read models (see eventual consistency and cross-service queries).
Operations
- Each service needs a build pipeline, deployment, configuration, secrets, dashboards, alerts, and on-call ownership.
- Debugging requires correlating logs and traces across services.
- Infrastructure needs service discovery, routing, and certificate management.
- Versioning: producers and consumers of an API are released at different times, so contracts must stay compatible.
Testing
- Unit tests remain local, but integration tests need other services, which are slow, flaky, or unavailable.
- End-to-end tests across many services are slow and hard to diagnose.
- Contract tests and test doubles replace some integration tests (see API versioning and contract testing).
- Failure behavior (timeouts, partial outages) must be tested deliberately (see chaos engineering).
Fallacies of distributed computing
The fallacies of distributed computing are assumptions that engineers new to distributed systems tend to make. The first seven were listed by L. Peter Deutsch at Sun Microsystems, and James Gosling added the eighth (the network is homogeneous). Each is false, and each shapes microservice design:
| Fallacy | Consequence for design |
|---|---|
| The network is reliable | Timeouts, retries, idempotent operations |
| Latency is zero | Limit call chain depth, batch calls, cache |
| Bandwidth is infinite | Avoid large payloads, paginate, compress |
| The network is secure | Encrypt and authenticate service-to-service traffic |
| Topology does not change | Service discovery instead of fixed addresses |
| There is one administrator | Contracts and versioning between teams |
| Transport cost is zero | Serialization and infrastructure cost money and CPU |
| The network is homogeneous | Interoperable protocols and formats |
The distributed monolith
A distributed monolith is a system split into separately deployed services that still cannot be changed or released independently. It keeps the coupling of a monolith and adds the network's costs.
Symptoms
- A feature change requires coordinated changes and releases in several services.
- Services must be deployed in a specific order.
- Many services share one database or the same tables.
- A failure in one service makes many others fail, because callers depend on synchronous responses without fallbacks.
- Services share a library containing domain models, so a change to that library forces every service to update (see shared libraries).
- Teams need cross-team meetings to ship routine changes.
- A single request fans out through long synchronous call chains.
graph LR
A["Order service"] -->|"sync"| B["Customer service"]
B -->|"sync"| C["Pricing service"]
C -->|"sync"| D["Inventory service"]
D -->|"sync"| A
A --> DB[("Shared database")]
B --> DB
C --> DB
D --> DBIn this diagram, a cycle of synchronous calls means no service works when any other is down, and the shared database means schema changes affect all of them.
Causes
| Cause | Mechanism |
|---|---|
| Boundaries drawn by technical layer | A feature crosses UI, logic, and data services, so each change touches all |
| Boundaries drawn before the domain is understood | Services turn out to change together |
| Shared database | Schema is a shared contract; services cannot evolve independently |
| Synchronous chains | Runtime coupling: availability depends on every service in the chain |
| Chatty interfaces | Fine-grained calls imply that the data or behavior belongs in one service (see service granularity) |
| Shared domain libraries | Compile-time coupling |
| Team structure not aligned to services | Ownership is unclear, so changes need cross-team coordination (see Conway's law) |
Remedies
- Align boundaries to business capabilities and bounded contexts (see domain-driven design and bounded contexts).
- Give each service its own datastore (database per service).
- Replace synchronous chains with asynchronous events where the caller does not need an immediate answer (see synchronous vs asynchronous communication).
- Merge services that always change together. Merging is a valid correction.
- Version APIs and keep changes backward compatible so releases do not need coordination.
Interview questions
- How does a distributed monolith differ from a monolith? Both have components that must change and release together. The distributed monolith adds network latency, partial failure, and operational overhead, without the local transactions and simple deployment of a monolith.
- How does chaining synchronous services affect availability? The availability of the chain is roughly the product of the availabilities of its members, so it is lower than any single one. Latency also adds up along the chain.
- What is a sign that two services should be merged? They always change together, need coordinated releases, or call each other with many fine-grained requests.
- Which fallacy of distributed computing motivates retries and timeouts? "The network is reliable." Requests and responses can be lost or delayed, so callers must bound waiting and handle repeat delivery.
