open atlas
↑ Back to track
Architecture Patterns ARCH · 10 · 03

The Distributed Monolith

Services that deploy in lockstep, call each other synchronously across N hops, and share a database are a distributed monolith: every operational cost of distribution with none of the autonomy. The dual-write problem and temporal coupling are its defining symptoms.

ARCH Senior ◷ 22 min
Level
FoundationsJuniorMiddleSenior

Eighteen months after the B2B billing platform split into “microservices,” the on-call rotation was miserable. A single order-placement request now touched five services in sequence: ApiGateway called OrderService, which called InventoryService, which called BillingService, which called NotificationService. Each hop added 20–40ms of latency and a new failure mode. If NotificationService was slow, order placement timed out. The team could not deploy BillingService without coordinating with OrderService because the two services expected the same version of the Order schema — which lived in a shared database both services wrote to. Every deploy required a Slack thread to confirm ordering. A post-mortem revealed the root cause: they had split the monolith by technical layer (API, logic, data as separate services), not by business capability. They had five deployable units, five on-call pages, five sets of deployment pipelines, and five times the network surface area — with zero independent deployability. What they built was a distributed monolith.

What makes a distributed monolith

A distributed monolith is a set of services that appear independent but are tightly coupled at runtime and at deploy time. It combines the worst properties of both architectures:

  • From the monolith: a change to one service requires changes and coordinated deploys across many services; the codebase is hard to understand as a whole because the coupling is implicit (network calls and shared schema, not visible imports).
  • From distributed systems: network latency on every inter-service call, partial failure modes that are harder to debug than process-level exceptions, and the operational overhead of multiple deployable units.

The diagnostic question is not “how many services do we have?” — it is “can each service be deployed and failed independently?” If the answer is no, you have a distributed monolith regardless of how many services the architecture diagram shows.

Three structural symptoms define the pattern:

1. Lockstep deployment. Services must be deployed together, or in a specific sequence, because they share a data schema or API contract that is not versioned. A change to the orders table requires coordinating OrderService, BillingService, and InventoryService in the same deploy window.

2. Chatty synchronous call chains. A single client request fans out to N synchronous hops through services that cannot respond until downstream services respond. Each hop adds latency and failure probability. Under load, a slow service at the end of the chain produces timeouts that cascade upstream.

3. Shared database. Multiple services read and write the same tables via a shared ORM or shared connection pool. No service exclusively owns its data; schema changes affect every service that queries the changed table.

The dual-write problem

The shared database produces a structural defect that surfaces as data inconsistency: the dual-write problem.

Consider BillingService creating a charge and updating an order status in sequence:

1. INSERT INTO charges (order_id, amount) VALUES (123, 450.00)
2. UPDATE orders SET status = 'charged' WHERE id = 123

Both statements succeed locally. But orders is also written by OrderService. If OrderService concurrently writes status = 'cancelled' after step 1 but before step 2, the order ends up in the charged state even though it was cancelled. BillingService and OrderService each wrote their local view of the truth to the same table, with no owner enforcing consistency.

The dual-write problem cannot be fully solved by transactions when the writes span services: a transaction that wraps both services’ writes is a distributed transaction — which reintroduces 2PC and its failure modes (unit 09). The correct fix is to give each service exclusive ownership of its tables, eliminating the possibility of concurrent writes from different services to the same record.

Why this works

The dual-write problem is not a bug in the application code — it is a structural consequence of shared data ownership. No amount of careful sequencing eliminates it when two services write to the same table, because the sequencing guarantee requires a distributed lock, which is 2PC in disguise. The only structural fix is exclusive data ownership per service.

Quiz

The billing platform's BillingService and OrderService both write to the shared `orders` table. A developer proposes fixing the dual-write inconsistency by always writing in a fixed order: OrderService writes first, then BillingService reads OrderService's write before making its own write. What is the structural flaw in this approach?

Temporal coupling: the hidden coupling of synchronous chains

Temporal coupling is a form of runtime coupling that occurs when one service can only complete its work if another service is available at the same moment. Every synchronous service call creates temporal coupling.

In the billing platform’s five-service chain, OrderService is temporally coupled to InventoryService, BillingService, and NotificationService. If any one of them is unavailable when an order is placed — even briefly, during a deploy, or during a GC pause — the entire order-placement operation fails. The failure of a notification service (low business priority) causes order placement (high business priority) to fail. This is the inverted risk profile of temporal coupling: the most critical business operation fails when the least critical service is unavailable.

Temporal coupling also makes independent deployment harder. If OrderService must call InventoryService synchronously on every request, they cannot be tested independently. The behavior of OrderService under InventoryService failures must be tested with a live InventoryService or with carefully crafted test doubles. In practice, teams skip this testing because it is hard, producing the distributed monolith’s characteristic fragility.

Quiz

The billing platform's on-call team observes that NotificationService failures (email/SMS sending) cause order-placement failures. The root cause is the synchronous call chain. An engineer proposes: 'We should increase NotificationService's SLA to match OrderService's SLA — if notifications are more reliable, the chain will be more reliable.' What is the structural problem with this approach?

Diagnosing a distributed monolith

The billing platform’s architecture review checklist for the distributed monolith:

Change one service, must change three. If adding a field to the Order domain object requires code changes in OrderService, BillingService, and InventoryService — even when only one of them introduces the field — the services are structurally coupled. A true service boundary means the field change is internal to one service’s codebase.

A single request fans out to N sync hops. If the system’s sequence diagram for a single user action shows a waterfall of synchronous calls through four services, each of which must complete before the previous can respond, the services are an implementation detail of one workflow, not autonomous capabilities.

Deploys require a coordination thread. If deploying BillingService requires a Slack thread to confirm with the OrderService team that the shared schema migration is compatible, the deployment coupling is explicit. Autonomous services can deploy any time without inter-team coordination.

No service can fail in isolation. If bringing InventoryService down for maintenance causes order placement to fail, order placement is not isolated from inventory. Autonomous services degrade gracefully when their dependencies fail — they do not propagate failure upstream.

Quiz

The billing platform's architecture team audits their five-service system. They find: (1) Every order-placement request makes four synchronous calls in sequence. (2) All five services share a single PostgreSQL instance with tables owned by no one in particular. (3) BillingService deploys to production three times a week; OrderService deploys once a month. (4) When BillingService is deployed, OrderService continues to function correctly. An engineer argues: 'Finding (3) and (4) prove our services are independent — different deploy cadences and BillingService deploys don't break OrderService.' Is this argument sound?

How to fix a distributed monolith

The distributed monolith is fixed by the same approach that prevents it: decompose by business capability along bounded-context seams (lesson 02), enforce data ownership, and replace chatty synchronous chains with asynchronous event-driven communication where the business allows eventual consistency (unit 09).

The incremental fix:

  1. Identify the chattiest synchronous chain. The call chain that fans out the furthest is the most urgent temporal coupling to break.
  2. Determine which calls are blocking vs. which are notifications. Sending a confirmation notification does not need to block the order transaction. Reserving inventory does — the business requires knowing if stock is available before committing the order.
  3. Replace notifications with events. OrderService publishes an OrderPlaced event; NotificationService subscribes and sends the email asynchronously. The order-placement transaction completes without waiting for notifications.
  4. Address data ownership. Identify which tables each service actually owns. Move exclusive write access to the owning service. Replace all other access with API calls to the owning service.
  5. Version APIs, not schemas. When two services must share a concept (the order status), they share it through an API with a versioned contract — not through a shared table that both write to.
Recall before you leave
  1. 01
    What are the three structural symptoms that define a distributed monolith?
  2. 02
    What is the dual-write problem and why can it not be fixed by a transaction wrapping both services' writes?
  3. 03
    What is temporal coupling and how does it differ from deployment coupling?
Recap

The billing platform’s five-service system was a distributed monolith in every meaningful sense: a change to the order domain required changes in three services, order placement blocked on notification sending, and a shared database made every schema change a coordinated event. They had five deployment pipelines and five on-call rotations, but zero independent deployability.

The distributed monolith emerges when teams split by technical layer rather than business capability — producing fragments of a workflow rather than autonomous capabilities — or when they route API traffic to new services without transferring data ownership. The network boundary is cosmetic if the data coupling and temporal coupling remain.

The diagnostic is simple: can each service be deployed and failed independently? If deploying billing requires a coordination thread with the ordering team, the answer is no. If a notification service outage brings down order placement, the answer is no. The number of services on the architecture diagram is irrelevant.

The fix is the same prescription as prevention: decompose by bounded-context seam, give each service exclusive data ownership, and replace synchronous chains with asynchronous events wherever the business can accept eventual consistency. The hard-won lesson is that distribution is not the goal — autonomous deployability is.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 4 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

Apply this

Put this lesson to work on a real build.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.