Definition
- Coordination without autonomy – the boundary adds network hops and operational cost without granting independent deployment.
- Signal 1 – same team owns both; nearly every feature touches both services.
- Signal 2 – lockstep deployment: never deployed one without the other.
- Signal 3 – synchronous chain: A calls B for every request; A is useless when B is down.
- Signal 4 – shared consistency: a simple operation requires a distributed transaction.
- Signal 5 – lockstep schema/API changes: changing one always forces a coordinated change in the other.
- Healthy architecture – merging and deleting services is a first-class operation, not a failure.
At National Bank of India, imagine two departments were split apart, but they share one manager, always submit budgets together, constantly phone each other to finish a single task, must balance the same ledger, and can never change a form without the other changing the same form on the same day. They are not really two departments; they are one department pretending to be two, with couriers running pointlessly between them. The healthy move is to merge them back.
Understanding the Concepts
When should you merge services back?
Merge when the boundary produces coordination without autonomy. The split is not buying independent deployment; it is just adding network hops and operational cost.
Five signals that two services should be merged:
1. SHARED TEAM + SHARED CHANGE CADENCE
Same team owns both. Every feature touches both.
The split does not buy independence.
2. LOCKSTEP DEPLOYMENT
Service A is never deployed without Service B.
Has A ever gone to production alone? If never -> merge.
3. SYNCHRONOUS REQUEST CHAIN
Every call to A immediately calls B.
A is useless when B is down.
Split did not improve resilience; it made it worse.
4. SHARED CONSISTENCY
A simple operation must update both atomically.
Forced into a Saga for what should be one local tx.
5. LOCKSTEP SCHEMA/API CHANGES
Changing a column in A always forces a change in B.
Models were never truly separated.
What does signal 1 look like in code?
When one team owns both services and every feature change touches both, the boundary is fake.
package com.bank.antipattern;
// WRONG: Two services owned by the same team.
// Every transfer feature changes BOTH services.
public class TransferService {
// Calls LimitCheckService for every transfer.
// Same team writes both.
// Same sprint, same PR, same deploy.
// This is one service pretending to be two.
}
// CORRECT: Merge limit checking into Transfer Service.
public class MergedTransferService {
private boolean checkLimit(String accountId, java.math.BigDecimal amount) {
// In-process method call. No network hop.
// One team, one service, one deploy.
return true;
}
}
What does signal 3 look like? (synchronous chain)
When A calls B synchronously for every request and A is useless when B is down, they share one availability fate.
package com.bank.antipattern;
import java.math.BigDecimal;
import java.util.logging.Logger;
// WRONG: Synchronous chain. Transfer cannot work without LimitCheck.
public class WrongTransferService {
private static final Logger LOG = Logger.getLogger(WrongTransferService.class.getName());
public void transfer(String fromId, String toId, BigDecimal amount) {
// Blocking call to LimitCheck Service
boolean allowed = limitCheckClient.check(fromId, amount); // if down, transfer fails
if (allowed) {
// proceed
}
// Two services, one availability fate.
// Split WORSENED resilience: two processes + network link can all fail.
}
}
// CORRECT: Merge into one service.
public class CorrectTransferService {
public void transfer(String fromId, String toId, BigDecimal amount) {
boolean allowed = checkLimit(fromId, amount); // in-process, no network
if (allowed) {
// proceed
}
}
private boolean checkLimit(String accountId, BigDecimal amount) {
return amount.compareTo(new BigDecimal("1000000")) <= 0;
}
}
Before merge: Transfer Service -> LimitCheck Service (network call)
Transfer down when LimitCheck down.
Two deploys, two pipelines, two failure points.
After merge: TransferService.checkLimit() (in-process call)
One deploy, one pipeline, one failure point.
Same business logic, fewer moving parts.
How do you merge services?
Combine the codebases, merge the databases, and replace network calls with in-process method calls.
Merge steps:
1. Combine codebases into one repository
2. Merge databases (if artificially split)
3. Replace REST/gRPC calls with in-process method calls
4. Remove serialization, network latency, partial-failure handling
5. Consolidate to one CI/CD pipeline
6. Delete the old service's infrastructure
Result:
- Fewer network hops
- No serialization overhead
- No partial-failure handling for the merged calls
- Simpler operational footprint
How is a distributed monolith different from real microservices?
| Aspect | Real Microservices | Distributed Monolith |
|---|---|---|
| Deploy independently | Yes | No (always together) |
| Separate teams | Yes | Same team |
| Separate databases | Yes | Often shared |
| Resilience | Partial failure isolated | One down = all down |
| Change scope | One service per feature | Multiple services per feature |
| Network cost | Justified by independence | Unjustified overhead |
Why should a healthy architecture merge services?
The correct number of services is not maximal. It is whatever maximizes autonomy for the current team structure and domain. Boundaries are hypotheses.
package com.bank.architecture;
/**
* Architecture decision record:
*
* Decision: Merge LimitCheck Service back into Transfer Service.
*
* Context: LimitCheck was split 6 months ago.
* Since then:
* - Same team owns both (Signal 1)
* - Always deployed together (Signal 2)
* - Transfer calls LimitCheck synchronously (Signal 3)
* - Every feature change touches both (Signal 1 + 5)
*
* Consequence: One fewer service, one fewer pipeline,
* one fewer network hop, simpler operations.
* Transfer now handles limits as an in-process method.
*
* The split was a reasonable hypothesis that turned out wrong.
* Merging back is the mature response.
*/
public class MergeLimitCheckADR {
// This is a documentation class, not runnable code.
}
What are common mistakes?
The most common mistake is refusing to merge because splitting felt like progress.
WRONG mindset: "We already split it. Merging feels like going backward."
CORRECT mindset: "Boundaries are hypotheses. This one failed. Merge and move on."
WRONG: Keep the split because of sunk cost.
CORRECT: Merge because the boundary creates cost without benefit.
What should a senior engineer know?
A mature team revises a wrong hypothesis by merging back without ego. The correct number of services is whatever maximizes autonomy. A boundary that was right last year can be wrong this year as the domain evolves.
Visual Overview
Five Signals to Merge:
┌──────────────────────────────────────────┐
│ Signal 1: Same team, shared changes │
│ Signal 2: Always deployed together │
│ Signal 3: Synchronous chain (A->B) │
│ Signal 4: Shared consistency needed │
│ Signal 5: Lockstep schema/API changes │
└──────────────────┬───────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ Before merge: │
│ Transfer Svc ──REST──> LimitCheck Svc │
│ 2 repos, 2 pipelines, 2 DBs, 2 deploys │
└──────────────────┬───────────────────────┘
│ merge
▼
┌──────────────────────────────────────────┐
│ After merge: │
│ TransferService.checkLimit() (in-process)│
│ 1 repo, 1 pipeline, 1 DB, 1 deploy │
│ Fewer hops, simpler, more resilient │
└──────────────────────────────────────────┘
Key Takeaway
Knowing when to merge services back is just as important as knowing when to split them, and most engineers only learn the splitting half. Two microservices should be merged when the boundary produces coordination without autonomy: same team with shared changes, lockstep deploys, synchronous chains where one cannot work without the other, shared consistency forcing distributed transactions, and lockstep schema changes.
At National Bank of India, Pankaj once split a limit-check service from the transfer service. Every single transfer made a blocking call to it, so when limit-check was down no transfer could proceed. The two services shared one availability fate. Merging turned a fragile distributed check into one resilient local method call with identical business logic.
The deeper principle is that a healthy architecture must be able to delete and merge services, not only create them. The correct number of services is not the maximum; it is whatever maximizes autonomy. Boundaries are hypotheses, and a mature team revises a wrong hypothesis by merging back without ego.
Remember one sentence: merge two services back when the boundary creates coordination without autonomy, because the right number of services is whatever maximizes team independence, not the maximum possible.
---