Definition
- 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.
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");
}
}
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.
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.
<!-- 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">>
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");
}
}
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.
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!");
}
}
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 |
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.
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.");
}
}
}
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.
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");
}
}
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.
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.");
}
}
}
Expected output:
FAIL: service is not independently deployable/failable/scalable
This is a distributed monolith, not a microservice.
Visual Overview
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.
---