Blog

Distributed Monolith: Definition, Signs and How to Fix It

A distributed monolith is a system split into separately deployed services that are still so tightly coupled they have to be changed, released and run together. You pay the costs of a distributed system (network calls, partial failures, harder debugging) and get none of the independence microservices are supposed to give you. The quick test: if you cannot deploy one service on its own without breaking another, or one service being down takes the whole product down, you have one.

It is an anti-pattern, not a formal term with a single author, and people use it loosely. This post gives a working definition, the signs to look for, the usual causes, and fixes ordered from cheapest to most expensive.

Distributed monolith vs microservices vs monolith

Monolith Microservices Distributed monolith
Deployment One unit Each service on its own Many units, released together
Data One database, in-process access Each service owns its data Shared database or tables
A call fails Exception in the same process Handled by a fallback or retry Failure spreads to callers
Debugging One stack trace Needs tracing, but scoped to one service Needs tracing and you still touch many services per bug
Cost Low operational cost High, paid for independence High, with no independence

A monolith is not the problem here. A well-structured monolith is often the better choice, and the distributed monolith is the worst of the three.

7 signs your microservices are a distributed monolith

1. Releases are coordinated

If the release plan says “deploy A, then B, then C, in that order”, the services are not independently deployable. This is the clearest sign. A change to a response format or an event schema that forces every consumer to ship the same day is the usual cause.

2. Services share a database

When several services read and write the same tables, the schema is the real API. Renaming a column becomes a multi-team project, and nobody can say which service is allowed to change which row.

-- orders-service
SELECT * FROM users WHERE status = 'active';

-- billing-service (a different team, same table)
UPDATE users SET plan = 'pro' WHERE user_id = ?;

3. Long synchronous call chains

One user request fans out into calls from service to service, each waiting for the next. Latency adds up, and so does failure. If a request needs five services and each is available 99.9% of the time, the request succeeds only about 99.5% of the time. This calculation shows the effect:

for (const n of [1, 3, 5, 10]) {
  const a = Math.pow(0.999, n);
  console.log(n + " services at 99.9% each -> " + (a * 100).toFixed(2) + "% (" +
    ((1 - a) * 525600).toFixed(0) + " min down per year)");
}
1 services at 99.9% each -> 99.90% (526 min down per year)
3 services at 99.9% each -> 99.70% (1575 min down per year)
5 services at 99.9% each -> 99.50% (2623 min down per year)
10 services at 99.9% each -> 99.00% (5232 min down per year)

This assumes failures are independent, which is optimistic, so real numbers are usually worse. Each hard dependency you add lowers the availability of everything upstream of it.

4. Cascading failures

One slow or failing service takes others down with it, because callers have no timeout, no fallback and no limit on retries. A single dependency that is merely slow can exhaust the callers’ thread or connection pools and spread the outage.

5. A shared library that carries business logic

Shared code for logging or HTTP helpers is fine. A shared library holding domain models, validation rules or generated clients that every service must upgrade together is coupling by another route. If a version bump in that library forces ten services to redeploy, those ten are one deployable.

6. One feature touches many services

Look at your last ten features. If most of them needed changes in three or more services, the boundaries cut across how the business actually changes. A good boundary keeps most changes inside one service.

7. Transactions that span services

If one business operation must either fully succeed or fully roll back across several services, and you are tempted to use distributed transactions or two-phase commit, the data probably belongs together. Splitting it created a consistency problem you now have to solve.

Why teams end up here

  • Splitting too early. Service boundaries are hard to get right and expensive to move. Martin Fowler’s MonolithFirst note argues for starting with a monolith so you can learn where the boundaries are, and he calls his own advice tentative. Splitting before you know the domain is a common path to coupled services.
  • Splitting by technical layer. Services such as “database service”, “validation service” and “notification service” mean every feature touches all of them. Boundaries should follow business capabilities (orders, billing, catalog), not layers.
  • Migrating by copy. Cutting a monolith’s codebase into pieces while keeping the shared database and the call graph just moves the same coupling onto the network.
  • Team structure. Conway’s Law says a system’s design tends to mirror the communication structure of the organization that builds it. If one team owns many services or five teams share one, the services stay entangled.
  • Ignoring data. Teams decouple the APIs and leave the data coupled. Shared data is the harder coupling to remove.

How to fix a distributed monolith

Start with the cheapest steps. Not every system needs to become true microservices; merging services is a valid fix.

1. Add timeouts, fallbacks and limits first

This does not remove coupling, but it stops one failure from spreading. A non-critical dependency (recommendations on a product page) should never block the page. This runs as shown:

function withTimeout(promise, ms) {
  return Promise.race([
    promise,
    new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), ms)),
  ]);
}
const slowRecs = () => new Promise(r => setTimeout(() => r(["a", "b"]), 500));

async function productPage() {
  const product = { id: 7, name: "Keyboard" };
  let recs = [];
  try { recs = await withTimeout(slowRecs(), 100); } catch (e) { recs = []; }
  return { product, recs };
}
productPage().then(r => console.log(JSON.stringify(r)));
{"product":{"id":7,"name":"Keyboard"},"recs":[]}

The page renders without recommendations instead of failing. Circuit breakers extend the same idea: after repeated failures, stop calling the broken service for a while.

2. Make the contracts safe to change

Version your APIs and events, add fields instead of renaming them, and let consumers ignore fields they do not know. Consumer-driven contract tests let a provider check it has not broken anyone before it ships. This is what restores independent deploys: a provider can release at any time because the change is backward compatible.

3. Give each service its own data

Pick one owner per table. Other services get the data through the owner’s API or through events, and keep their own read copy if they need fast access. Do this one table at a time. Expect to give up joins across services and to accept that some data will be briefly out of date.

4. Replace chains of calls with events where the caller does not need an answer now

Placing an order does not need to wait for shipping to be scheduled. The order service saves the order and publishes an order.placed event; shipping and email react on their own time. Keep synchronous calls for things the user is waiting on, such as checking a card. Events bring their own costs: duplicate delivery, ordering and harder end-to-end reasoning, so consumers must be idempotent.

5. Redraw the boundaries or merge

If two services always change together, share data and cannot be released separately, they are one service. Merging them is often the honest fix. Domain-driven design’s bounded contexts are a useful way to find boundaries: ask which parts of the business use different vocabulary and change for different reasons.

6. Migrate gradually

The strangler fig approach routes one capability at a time to a new service and retires the old code behind it, rather than a rewrite. Each step should leave the system working.

7. Invest in tracing

Pass a trace or correlation ID through every call and log it. When a request crosses five services, you need to follow it with one ID. OpenTelemetry is the common standard for this.

A worked example

Take a shop split into User, Product, Order and Payment services that share one database. Checkout calls Product for stock, Payment for the charge, and User for the address, all synchronously. Changing the product table needs a joint release, and a slow Payment service makes the whole checkout hang.

  1. Add timeouts and a clear error to the Payment call, so a slow provider fails fast with a message instead of hanging.
  2. Give Product its own tables. Order stops querying them directly and uses Product’s API, with a cached copy of price and name taken at order time.
  3. Have Order publish order.placed; fulfillment and email consume it.
  4. If User and Order turn out to change together every time, merge them.

After this, Product can change its schema without telling anyone, and a failure in recommendations or email no longer blocks checkout.

When a monolith is the better answer

Microservices pay off when several teams need to release on their own schedule, or when parts of the system need very different scaling. A small team with one product and a domain still taking shape usually does better with a modular monolith: one deployable with strict module boundaries and one database schema per module. You can extract a service later along a line that is already clean. Microservices also carry an operational cost (deployment pipelines, monitoring, service discovery) that Fowler calls the microservice premium.

Distributed monoliths in system design interviews

Interviewers often ask whether you would split a system into services, and a good answer names the trade-off. Say what each service owns, how data is kept consistent, what happens when a dependency is down, and why you would not start with a dozen services. Mentioning shared databases and long synchronous chains as the failure modes to avoid shows you understand the cost of distribution. For more on structuring such answers, see trade-off analysis in system design interviews and key questions to ask during a system design interview.

Checklist

  • Can each service deploy alone, on any day?
  • Does each table have exactly one owning service?
  • Does a failing dependency degrade a feature instead of the whole request?
  • Do most features change one service?
  • Can you follow one request across services with a single ID?

If you answer no to most of these, the next step is not more services. It is fewer dependencies between the ones you have.

A lesson in AlgoCademy: the step to write, a picture of what it does, the line written in the editor, and the check: Step 2 passed

Before you go

Start thinking like a software engineer.

Learn to code and solve interview problems, one small, checked step at a time.

Start learning free

No account needed. You write real code in your browser.