Back to Microservices Fundamentals
Q1Question 1 of 12

What is a "distributed monolith," and how do you avoid it?

Key Concepts
  • A distributed monolith has the complexity of distribution with none of the independence of microservices.
  • Symptom 1: shared database -- schema changes force coordinated simultaneous redeploys.
  • Symptom 2: shared libraries with business logic -- changing the library forces rebuilding every consumer.
  • Symptom 3: long synchronous call chains -- latency is the sum and availability is the product of all hops.
  • Fix: database-per-service, share contracts not code, prefer async events, add resilience to sync calls.
  • If you cannot cleanly separate data and deployment, the services were probably split along wrong boundaries.
  • Starting as a modular monolith and extracting only when needed is the safer path.

Definition

A distributed monolith is microservices that still deploy, fail, and scale together through shared databases, shared code, and synchronous call chains, giving you all the complexity of distribution with none of the independence.
  • Shared database -- several services read/write the same tables, so a schema change forces coordinated simultaneous redeploys.
  • Shared business-logic library -- changing the library forces rebuilding and redeploying every consumer, destroying independent deployment.
  • Synchronous call chains -- A calls B calls C calls D; latency is the sum, availability is the product, and one slow service cascades failure backward.
  • Temporal coupling -- services must deploy in a specific order or all at once; they are not independently deployable.
  • Fix -- database-per-service, share contracts not code, prefer async events, add resilience to remaining sync calls, evolve APIs backward-compatibly.

National Bank of India split one giant department into several to be modern, but chained them so tightly that nothing improved. They share one filing cabinet (shared DB), all use the same rulebook binder (shared library), and to approve one loan the loan clerk must phone the account clerk who must phone the fraud clerk (sync chain). If any clerk is on break the whole approval freezes. The bank now has many departments with all the coordination headaches and still fails as one.

Understanding the Concepts

What is a distributed monolith?

A system split into multiple services across the network but that cannot deploy, fail, or scale independently. It has the operational complexity of microservices with none of their benefits. You pay the tax of network calls, service discovery, and distributed tracing but get none of the independence.

JAVA
package com.bank.antipattern;

public class DistributedMonolithSymptoms {
    public static void main(String[] args) {
        System.out.println("Symptom 1: shared database");
        System.out.println("Symptom 2: shared business-logic library");
        System.out.println("Symptom 3: synchronous call chains");
        System.out.println("Test: can each service deploy, fail, and scale independently?");
        System.out.println("If NO --> distributed monolith");
    }
}
TEXT
Expected output:
Symptom 1: shared database
Symptom 2: shared business-logic library
Symptom 3: synchronous call chains
Test: can each service deploy, fail, and scale independently?
If NO --> distributed monolith

How does a shared database create coupling?

When multiple services read and write the same tables, a schema change in one service's context breaks all other services that touch those tables. Every column rename, type change, or constraint addition requires coordinated simultaneous redeploys.

JAVA
package com.bank.antipattern;

import org.springframework.jdbc.core.JdbcTemplate;

public class SharedDatabaseProblem {
    // WRONG: both AccountService and LoanService touch the same accounts table
    public void accountServiceQuery(JdbcTemplate jdbc, String id) {
        jdbc.queryForObject(
            "SELECT balance FROM accounts WHERE id = ?", String.class, id);
    }

    public void loanServiceQuery(JdbcTemplate jdbc, String id) {
        jdbc.queryForObject(
            "SELECT balance FROM accounts WHERE id = ?", String.class, id);
    }
    // If AccountService renames 'balance' to 'available_balance',
    // LoanService breaks --> forced coordinated deploy
}

How does a shared library create coupling?

A shared library with business logic (domain models, validation rules) forces every consumer to rebuild and redeploy when the library changes. Independent deployment is destroyed.

XML
<!-- WRONG: shared domain model in a common library --syn-kw">>
syn-kw"><dependencysyn-kw">>
    syn-kw"><groupIdsyn-kw">>com.banksyn-kw"></groupIdsyn-kw">>
    syn-kw"><artifactIdsyn-kw">>common-domainsyn-kw"></artifactIdsyn-kw">>
    syn-kw"><versionsyn-kw">>2.3.0syn-kw"></versionsyn-kw">>
syn-kw"></dependencysyn-kw">>
<!-- Changing Account.java in common-domain forces ALL 5 consumers
     to rebuild and redeploy. This is lockstep, not independent. --syn-kw">>
JAVA
package com.bank.antipattern;

public class SharedLibraryProblem {
    public static void main(String[] args) {
        // WRONG: sharing domain code
        System.out.println("WRONG: com.bank.common.Account used by 5 services");
        System.out.println("Change Account.java --> rebuild + redeploy all 5");

        // CORRECT: share only contracts (OpenAPI spec, Avro schema)
        System.out.println("CORRECT: each service has its OWN Account class");
        System.out.println("Share API schema, not code");
    }
}
TEXT
Expected output:
WRONG: com.bank.common.Account used by 5 services
Change Account.java --> rebuild + redeploy all 5
CORRECT: each service has its OWN Account class
Share API schema, not code

How do synchronous call chains create cascading failures?

When the gateway calls Transfer which calls Account which calls Fraud which calls Credit, latency is the sum of all hops and availability is the product. One slow service blocks threads and cascades failure backward.

JAVA
package com.bank.antipattern;

public class SyncChainProblem {
    public static void main(String[] args) {
        double[] availability = {0.99, 0.99, 0.99, 0.99};
        double combined = 1.0;
        for (double a : availability) combined *= a;
        System.out.printf("Individual availability: 99%% each%n");
        System.out.printf("Chain availability: %.2f%% (4 hops)%n", combined * 100);
        System.out.println("4% failure rate from the chain alone!");
    }
}
TEXT
Expected output:
Individual availability: 99% each
Chain availability: 96.06% (4 hops)
4% failure rate from the chain alone!

How do you fix each symptom?

| Symptom | Fix |
|---|---|
| Shared database | Database-per-service; access only through APIs or events |
| Shared business library | Share contracts (API schemas, event schemas), not code |
| Synchronous chains | Prefer async events; add timeouts, circuit breakers, bulkheads to remaining sync calls |
| Temporal coupling | Backward-compatible API evolution; never break a contract |

JAVA
package com.bank.service;

import org.springframework.kafka.annotation.KafkaListener;
import java.math.BigDecimal;

public class FixedArchitecture {

    // CORRECT: database-per-service
    // LoanService does NOT touch AccountService's tables.
    // Instead it calls the API:
    public BigDecimal getBalanceViaApi(String accountId) {
        // return accountClient.getBalance(accountId);
        return new BigDecimal("150000.00"); // stub
    }

    // Or reacts to events:
    @KafkaListener(topics = "account.balance-changed")
    public void onBalanceChanged(String event) {
        System.out.println("Received balance-changed event: " + event);
        // Update local read-only view
    }
}

When should you merge services back?

If you cannot cleanly separate data and deployment, the services were split along the wrong boundaries. The right fix may be to merge them back into a modular monolith rather than fighting the coupling with more infrastructure.

JAVA
package com.bank.architecture;

public class MergeDecision {
    public static void main(String[] args) {
        boolean canSeparateData = false;
        boolean canDeployIndependently = false;

        if (!canSeparateData || !canDeployIndependently) {
            System.out.println("Cannot separate data or deploy independently.");
            System.out.println("MERGE back into a modular monolith.");
            System.out.println("Wrong boundaries were chosen.");
        }
    }
}
TEXT
Expected output:
Cannot separate data or deploy independently.
MERGE back into a modular monolith.
Wrong boundaries were chosen.

What are common mistakes?

The distributed monolith is usually the result of splitting prematurely before boundaries were understood.

JAVA
package com.bank.antipattern;

public class CommonMistakes {
    public static void main(String[] args) {
        // WRONG: splitting for "microservices" without understanding boundaries
        System.out.println("WRONG: 5 services, 1 shared DB, 1 shared library");
        System.out.println("Every release requires coordinating 5 teams");

        // CORRECT: start as modular monolith, extract when proven
        System.out.println("CORRECT: modular monolith with enforced boundaries");
        System.out.println("Extract only when independent need is proven");
    }
}
TEXT
Expected output:
WRONG: 5 services, 1 shared DB, 1 shared library
Every release requires coordinating 5 teams
CORRECT: modular monolith with enforced boundaries
Extract only when independent need is proven

What should a senior engineer know?

  • The distributed monolith is the most common failure mode of a microservices migration.
  • Start as a modular monolith and extract only when a real need arises.
  • If every release requires coordinating multiple services, you have a distributed monolith.
  • The mark of seniority is recognizing when to merge back, not just when to split.
  • The litmus test: can this service deploy, fail, and scale independently? If not, it is not a microservice.
JAVA
package com.bank.architecture;

public class IndependenceTest {
    public static void main(String[] args) {
        boolean canDeployAlone = false;
        boolean canFailAlone   = false;
        boolean canScaleAlone  = false;

        if (!canDeployAlone || !canFailAlone || !canScaleAlone) {
            System.out.println("FAIL: service is not independently deployable/failable/scalable");
            System.out.println("This is a distributed monolith, not a microservice.");
        }
    }
}
TEXT
Expected output:
FAIL: service is not independently deployable/failable/scalable
This is a distributed monolith, not a microservice.

Visual Overview

DIAGRAM
  Distributed Monolith (the problem)
  ┌──────────┐  ┌──────────┐  ┌──────────┐
  │ Service A│  │ Service B│  │ Service C│
  └────┬─────┘  └────┬─────┘  └────┬─────┘
       │             │             │
       └──────┬──────┘──────┬──────┘
              ▼             ▼
        ┌──────────┐  ┌────────────┐
        │Shared DB │  │Shared Lib  │
        └──────────┘  └────────────┘
  Schema change breaks all three
  Library change forces all to rebuild

  Fixed Architecture
  ┌──────────┐  ┌──────────┐  ┌──────────┐
  │ Service A│  │ Service B│  │ Service C│
  │ own DB-A │  │ own DB-B │  │ own DB-C │
  └────┬─────┘  └────┬─────┘  └────┬─────┘
       │    events    │    API     │
       └──────────────┘────────────┘
  Each deploys independently
  Schema changes are local

Key Takeaway

A distributed monolith has been split across the network but still cannot deploy, fail, or scale independently, giving you all the operational complexity of microservices with none of their benefits. The symptoms are a shared database where schema changes force coordinated redeploys, shared libraries with business logic that force lockstep rebuilds, and long synchronous call chains where one slow service cascades failure backward.

The fixes are concrete: database-per-service so each service owns its data exclusively, share contracts (API and event schemas) not code, prefer async events over sync chains, and add resilience patterns to any remaining sync calls. Evolve APIs backward-compatibly so services can deploy in any order.

At National Bank of India, the distributed monolith showed up clearly: four services shared one database, and loan approval was a sync chain through account, fraud, and credit. Pankaj fixed it by giving each service its own database, moving fraud and account checks onto Kafka, and wrapping the remaining credit check in a circuit breaker. If you genuinely cannot separate the data, the services were split along the wrong boundaries and the right move is to merge them back.

Remember one sentence: a distributed monolith is microservices that deploy, fail, and scale together through shared databases, shared code, and sync chains, and you fix it by isolating each service's data, sharing contracts not code, and decoupling with async events.

---

Share
Use ← → arrow keys to navigate