Distributed Monolith
What it is
A distributed monolith is a system that has been decomposed into multiple independently deployed services on the surface, but which retains the tight coupling and coordination requirements of a traditional monolith underneath. The services share databases, communicate synchronously across the call graph, require coordinated deployments, and cannot evolve or scale independently. You get the operational complexity of a distributed system — network failures, latency, distributed tracing requirements — without any of the independence benefits that motivated the migration in the first place.
Sam Newman, author of Building Microservices, coined this term to describe one of the most common failure modes in microservices adoptions. Organizations decompose their monolith along technical lines (one service for "users", one for "orders") rather than along domain boundaries, and then wire them back together through shared databases, shared libraries containing domain logic, and rigid synchronous call chains that must all succeed for a business operation to complete.
The result is a system that is harder to understand than the original monolith (because the call graph is now distributed and non-obvious), harder to deploy (because services must be deployed in a specific order or simultaneously), and harder to debug (because a single failed business transaction spans multiple logs, multiple services, and multiple networks). It is, as many practitioners put it, the worst of both worlds.
Why it emerges
Distributed monoliths most commonly arise when an organization decides to adopt microservices as a technical exercise rather than as an outcome of genuine domain analysis. Teams decompose by technical layer ("the auth service", "the data access service", "the notification service") rather than by business capability, which means every business operation still requires coordination across multiple services. The seams cut across the wrong boundaries, leaving the real coupling intact but now expressed as network calls rather than in-process method calls.
Database sharing is the most insidious enabler. When multiple services read and write the same tables, they are not truly independent. A schema migration in one service's "domain" breaks another service without warning. Transactional consistency requires cross-service coordination. Teams resort to shared ORM models or shared schema libraries to avoid duplication, which creates compile-time coupling between service codebases. Any of these patterns signals that the service boundary was drawn incorrectly.
Time pressure also plays a significant role. When teams are under pressure to ship, they take shortcuts: a quick synchronous HTTP call instead of asynchronous messaging, a join across a shared table instead of event-driven data replication, a shared library with business logic instead of duplicating a small model. Each shortcut makes sense in isolation but collectively they recreate the monolith's coupling graph in a distributed context, where those couplings are far more expensive to maintain and much harder to remove later.
How it manifests
- Services are deployed together in a fixed order or require simultaneous releases to avoid breaking changes.
- Multiple services read from or write to the same database tables or schemas.
- A single user-facing request requires synchronous calls to five or more downstream services before it can return a response.
- Shared libraries containing business logic (not just utilities) are imported by multiple services, creating build-time coupling.
- A failure in one service causes cascading failures across a wide swath of unrelated services because the synchronous call graph has no isolation.
- Teams cannot independently release their service without checking with other teams to avoid compatibility issues.
How to recognize it early
- Deployment choreography: If deploying a new version of Service A requires deploying Service B and Service C at the same time, you have a distributed monolith.
- Cross-service database queries: If a service needs to join against another service's tables — even via a view or a stored procedure — the services share a database and are not truly decoupled.
- Thick shared libraries: A shared library that contains domain objects, business rules, or persistence logic is a monolith compiled into a package. Version disagreements will surface as deployment conflicts.
- No independent scalability: If every service must scale in lockstep because they all depend on a single shared resource (a database, a message broker, a service without its own cache), they are not independently scalable.
Typical manifestation
┌──────────────────────────────────────────────────────────┐
│ API Gateway / Load Balancer │
└────────────────────────┬─────────────────────────────────┘
│
┌────────────────┼────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Order │ │ Payment │ │ Inventory │
│ Service │ │ Service │ │ Service │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ sync call │ sync call │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ User │ │ Fraud │ │ Notification│
│ Service │ │ Service │ │ Service │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
└────────────────┴────────────────┘
│
▼
┌────────────────────────┐
│ │
│ SHARED DATABASE │ ◄── Single schema
│ │ All services
│ orders | payments | │ read/write
│ users | inventory| │ the same tables
│ fraud | audit | │
└────────────────────────┘
Shared Deployment Pipeline:
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
│Build │──►│Test │──►│Stage │──►│Coord │──►│Deploy│
│All │ │All │ │All │ │Release │All │
└──────┘ └──────┘ └──────┘ └──────┘ └──────┘
▲
Must align all
teams before release
Consequences and remediation
Consequences
- Maximum operational overhead: You pay the full cost of distributed systems observability, deployment, and failure isolation while getting none of the independence benefits.
- Compounded fragility: Synchronous call chains multiply the blast radius of any single service failure. If each service has 99.9% uptime, a chain of ten synchronous calls has at most 99.0% composite uptime.
- Development velocity collapses: Teams cannot ship independently. Every release requires coordination, which creates a synchronization bottleneck that slows all teams to the pace of the slowest.
- Debugging complexity: Tracing a single failed request through five services across five separate log streams is exponentially harder than reading a single stack trace in a monolith.
Remediation strategies
- Database per service: The most important step. Each service must own its data. Migrate to event-driven data sharing using CDC (Change Data Capture) or domain events to replicate necessary data across service boundaries.
- Introduce async messaging: Replace synchronous cross-service calls with asynchronous events wherever possible. Use a message broker (Kafka, RabbitMQ) for operations that don't require an immediate response.
- Re-draw service boundaries: Revisit the decomposition using Domain-Driven Design. Services should align to bounded contexts, not technical layers. If a business capability requires data from multiple services, the boundaries are wrong.
- Strangle with a façade: If the system is too tangled to decompose incrementally, introduce a façade (strangler fig) over the monolith and gradually move functionality behind new, correctly bounded services.
Detailed analysis
The fundamental problem with a distributed monolith is that the coupling that existed in the monolith — which was at least cheap to traverse (in-process function calls) — has been replaced by coupling expressed as network calls, which are orders of magnitude more expensive and inherently unreliable. The database is the most common coupling point. When OrderService and InventoryService both write to the same database, any schema change must be coordinated. If you add a column to the orders table for a new feature in OrderService, you must ensure that InventoryService's queries are not broken. You have recreated the monolith's deployment dependency but now it must be coordinated across teams rather than within a single codebase.
Synchronous call chains are the second major coupling point. When a user places an order, the request might fan out: OrderService calls InventoryService (to check stock), PaymentService (to process payment), FraudService (to assess risk), and NotificationService (to send confirmation) — all synchronously, all before returning a response to the user. This means the latency of placing an order is the sum of all these network calls, and the availability is the product of all service availabilities. More critically, if FraudService is slow during a traffic spike, it causes OrderService to accumulate threads waiting for responses, which causes OrderService to run out of thread pool capacity, which causes it to stop responding to the API Gateway, which causes the entire ordering system to go down. This is the cascade failure pattern that circuit breakers were invented to address — but circuit breakers are a palliative, not a cure. The cure is async messaging.
The path out of a distributed monolith requires treating the decomposition as a domain problem rather than a technical one. Start by mapping your domain using Event Storming or similar DDD techniques. Identify the bounded contexts — the natural groupings of domain concepts that have minimal cross-boundary coupling. Then draw service boundaries that align with those contexts. Accept that some data duplication is necessary and correct: OrderService may store a denormalized copy of customer name and shipping address because it needs that data for order fulfillment, even though UserService "owns" that data. The duplication is intentional. It enables independence. Events flowing from UserService to OrderService keep the copy reasonably fresh without creating a hard dependency.
Escalation patterns
- Schema coupling spiral: As services accumulate shared database dependencies, schema changes become increasingly risky. Teams stop migrating schemas forward because every change risks breaking other services, leading to a schema that cannot evolve and accumulates technical debt faster than the application code does.
- Shared library versioning hell: The shared domain library grows as each team adds its model requirements. Eventually it contains models for every domain in the system. Teams pin to specific versions to avoid surprises, creating a fragmented ecosystem where some services are three major versions behind and cannot be safely updated.
- Deployment window compression: As the coordination cost of releases grows, teams consolidate into larger, rarer releases to reduce coordination overhead. This concentrates risk, increases the blast radius of each deployment, and causes teams to batch unrelated changes together — exactly the opposite of continuous delivery.
- Observability bankruptcy: Without proper distributed tracing from the start, debugging production issues becomes so painful that teams stop investigating root causes and instead apply workarounds and timeouts, masking problems until they become critical failures.
Diagnostic checklist
- Can each service be deployed to production independently without coordinating with other teams?
- Does each service have its own dedicated database schema that no other service reads from directly?
- Can a single service fail completely without causing other unrelated services to become unavailable?
- Do services communicate primarily through asynchronous events rather than synchronous request/response calls for non-query operations?
- Are shared libraries between services limited to purely technical utilities (logging, HTTP clients) rather than containing business domain logic?
- Can a single team release a new feature that is entirely within their service boundary without requiring changes in other teams' codebases?
Real-world examples
- E-commerce platform migration: A retailer migrated their monolith to microservices by extracting services around database tables. OrderService, InventoryService, and ShippingService all shared the same MySQL database and called each other synchronously. Response times doubled and deployment coordination made weekly releases impossible, reverting to monthly deployments — worse than before the migration.
- Financial services SOA: An investment bank's service-oriented architecture consisted of dozens of services all routing through a centralized ESB that performed business logic transformations. The ESB became a god service that could not be updated without testing every downstream consumer, requiring weeks of regression testing before any change.
- Healthcare platform decomposition: A healthcare SaaS company split their application into services by team boundary (team A owns patients, team B owns appointments) but kept a shared Postgres database. A migration to add multi-tenancy to the patients table broke appointment queries that assumed single-tenant semantics, bringing down the entire scheduling system during a critical period.
Related anti-patterns
- Big Ball of Mud – the often-preceding condition before a failed microservices migration
- God Object / God Service – frequently found within distributed monoliths as the "hub" service
- Microservices Architecture – the intended goal; understand it to avoid the anti-pattern