What Is a Distributed Monolith? The Complete Tutorial
A beginner‑to‑production walkthrough of the most common — and most painful — mistake in modern software architecture: splitting an application into services without actually decoupling anything. Built from the ground up with diagrams, code, real‑world stories, and interview‑ready explanations.
Introduction & History
A distributed monolith is a system that has been physically split into many independent‑looking services, but is still logically wired together so tightly that it behaves like one giant, solid block.
Picture a toy castle built out of LEGO blocks. Every block is locked to the block next to it, so moving a single wall means lifting ten walls at once. Now imagine someone tells you, “Don’t worry, I split your castle into 10 separate small castles.” But when you look closely, all ten castles are still glued together with invisible strings. You can paint them different colours, put them in different rooms, and give them different names — but if you touch one, the others still shake.
That “glued together but pretending to be separate” castle is exactly what engineers call a distributed monolith. It is one of the most talked‑about problems in modern software architecture, and almost every large engineering team has built one by accident at least once.
What does the term mean, word by word?
Breaking the term into two words makes the whole idea click into place.
Monolith comes from the Greek words meaning “single stone” (monos = one, lithos = stone). Ancient monuments like Stonehenge were carved from single, giant, solid stone blocks. In software, a monolith is an application built as one solid unit — one codebase, one build, one deployment package, running as one process.
Distributed means “spread out across many places.” Think of a food‑delivery chain with 50 branches in 50 cities, rather than one giant restaurant in one building. Each branch is a separate physical location doing its own cooking.
So a distributed monolith is a contradiction that somehow exists in real life: a system that has been physically split into many independent‑looking services (microservices, on the surface), but whose services are so tightly wired to each other that they behave like one giant, solid block. You inherit every downside of a distributed system — network calls, more servers to manage, more places for things to break — without earning any of the upside a distributed system is supposed to give you (services that can change, deploy, and fail independently).
A short history: how did we get here?
To understand why distributed monoliths exist, it helps to trace the way software architecture has evolved.
| Era | Approach | What it looked like |
|---|---|---|
| 1990s — early 2000s | Single Monolith | One big application (a WAR or EXE file) containing the website, the business logic, and the database access, all in one process, deployed as one unit. |
| Mid 2000s | Service‑Oriented Architecture (SOA) | Big companies tried splitting large monoliths into “services” connected through an Enterprise Service Bus (ESB). The ESB itself often became a new, complicated monolith. |
| 2011 — 2014 | Microservices (popularised by Netflix and Amazon) | Applications split into small, independently deployable services, each owning its own data and talking over lightweight APIs (REST or messaging). |
| 2015 — present | The distributed monolith recognised | Teams rushed to copy Netflix and Amazon by physically splitting code into services, but kept the tight coupling of a monolith. Engineers started naming and warning about this failure pattern. |
The term “distributed monolith” spread widely around 2015–2017, when companies that adopted microservices too quickly — without changing team structure or coupling habits — discovered they now had more operational pain (deployments, network calls, cross‑service debugging) with none of the promised independence. Architects such as Martin Fowler and Sam Newman (author of Building Microservices) wrote extensively about the anti‑pattern as a warning to the industry.
Imagine a school project where five students are told to write five separate chapters of a report. That sounds like proper division of labour. But if every chapter starts with “As mentioned in Chapter 2…” and depends on exact wording from another chapter, nobody can submit their chapter alone. Change a sentence, and four other chapters break. The report looks like five separate files, but it behaves like one giant essay that must always be edited together. That is a distributed‑monolith report.
Why should you care?
The distributed monolith is one of the most commonly discussed failure patterns in system‑design interviews, real production incidents, and architecture reviews. Understanding it deeply will help you:
- Recognise the warning signs early in your own projects.
- Answer system‑design interview questions with real depth instead of buzzwords.
- Avoid multi‑year, multi‑million‑dollar re‑architecture projects caused by this mistake.
- Make better decisions about when to split a monolith into services — and when not to.
The Problem & Motivation
Distributed monoliths are what happens when a team performs the physical split into services but forgets the logical one — the services still call each other constantly, share the same database, and must be deployed together for anything to work.
Why teams started splitting monoliths in the first place
Before diagnosing the distributed monolith, it helps to remember why anyone wanted to break monoliths apart at all. A traditional monolith is a single application containing everything — user‑interface logic, business rules, database access — compiled and deployed as one unit.
A monolith is like one giant kitchen where 20 chefs cook every dish on the menu, using the same pots, the same stove, and the same shelf of ingredients. If one chef spills sauce on the shared stove, everyone’s cooking is affected. If the menu changes, the whole kitchen needs retraining, even if only one dish changed.
As companies grew, monoliths caused three big pains:
1. Slow deployments
Every small change required rebuilding and redeploying the entire application. A one‑line bug fix in the payments module meant redeploying search, profile, and checkout too.
2. Team collisions
Hundreds of engineers pushing code into one shared codebase caused constant merge conflicts and “who broke the build?” arguments.
3. Scaling waste
If only the video‑streaming part of Netflix needed more servers, the entire monolith still had to be scaled — wasting money on parts that didn’t need capacity.
The proposed fix was microservices: break the one big application into many small, independent services. Each service should own its own code, its own database, and its own deployment pipeline. In theory, teams could then deploy independently, scale independently, and even use different programming languages for different services.
Where it goes wrong: the birth of the distributed monolith
Here is the core problem. Many teams performed only the physical split — moving code into separate repositories, separate servers, and separate deployment pipelines — but they did not perform the logical split. The services still called each other constantly, shared the same database tables, and had to be deployed together in a specific order for anything to work.
Splitting code into multiple services does not automatically give you the benefits of microservices. If Service A cannot function without Service B being deployed at the exact same version, at the exact same time, you have not built microservices — you have built a distributed monolith. You simply moved the “single stone” across a network, which made it slower and more fragile.
The motivation to study this deeply
This topic deserves careful attention because it is invisible until it hurts. A team can build a distributed monolith for two years, feel proud of having “20 microservices,” and only discover the truth during a painful incident: one Friday‑night deployment of Service C breaks Services A, B, D, and E, because they were never really independent. Understanding the anatomy of the problem lets you spot it in a code review, in an architecture diagram, or in an interview — long before it becomes a 3 a.m. outage.
A well‑known real‑world pattern: several large retail and travel companies migrated from monoliths to “microservices” around 2016–2018 by splitting existing code by department (search team, cart team, payment team). They kept a single shared relational database because splitting the database felt too risky. Years later, engineering leadership discovered that a single schema migration on that shared database required coordinating deployments across 30+ teams — the exact same coordination cost the monolith always had, now with added network latency and debugging complexity. This is one of the most commonly cited real patterns behind the term “distributed monolith.”
Core Concepts
A short, precise vocabulary you will meet again in interviews, in code reviews, and in production war stories about what went wrong.
3.1 Coupling
What it is: how much one piece of code depends on the internal details of another piece of code. High coupling means changing one thing forces you to change another; low coupling means pieces can change independently.
Why it exists as a concept: engineers needed one word to measure “how tangled” a system is, so they can predict how expensive future changes will be.
Where it is used: every area of software design, from a single function calling another to services calling each other across a network.
Two dancers holding hands tightly (high coupling) must move together, step by step. Two dancers dancing solo nearby (low coupling) can each change their own moves without tripping the other.
If your Order Service directly reads a private table owned by the Inventory Service, that is high coupling. When Inventory renames a column, Order breaks — even though they are “separate services.”
3.2 Cohesion
What it is: how well the responsibilities inside one module or service belong together. High cohesion means a service does one clear job well.
Why it exists: a good service boundary groups things that change together for the same business reason, and separates things that change for different reasons.
A toolbox drawer with only screwdrivers has high cohesion. A drawer with screwdrivers, kitchen spoons, and a phone charger has low cohesion — things that do not belong together are mixed in one place.
A Billing Service that handles invoices, tax calculation, and payment receipts has high cohesion (all billing‑related). If it also handles “sending marketing emails,” that is low cohesion — a different concern has been mixed in.
3.3 Service Boundary
What it is: the line that decides what belongs inside a service versus outside it — the “walls” of a service.
Why it exists: without clear boundaries, teams do not know where to put new code, which leads to overlap and duplicated logic.
A country’s border decides which laws apply where. Cross a badly drawn border and it becomes unclear which rules apply — same story with badly drawn service boundaries.
3.4 Synchronous vs. Asynchronous Communication
What it is: synchronous communication means Service A calls Service B and waits for the answer before continuing (like a phone call). Asynchronous communication means Service A sends a message and continues its own work without waiting (like sending a text message).
Why it exists: different tasks need different guarantees. Some need an immediate answer (checking if a credit card is valid); others can happen eventually (sending a receipt email).
A phone call (synchronous) needs both people available at the same time. A letter in the mailbox (asynchronous) can be read whenever the receiver is ready.
If Order Service makes a synchronous REST call to Payment Service and waits, then when Payment is slow or down, Order also becomes slow or stuck. This tight, waiting relationship is a major cause of distributed monoliths.
3.5 Shared Database Anti‑pattern
What it is: two or more “independent” services read and write to the same database tables.
Why it’s a problem: it secretly re‑creates a single point of coupling. Even if the application code is split, the data layer is not, so schema changes still require coordinating across every team.
Five separate families living in five separate houses, but all five families share one single refrigerator in a random field. Even though they live apart, they must all agree before rearranging the fridge shelves.
3.6 Deployment Coupling
What it is: Service A and Service B must be deployed together, or in a specific order, for the system to work.
Why it matters: true microservices should be deployable in any order, at any time, independently. Writing a runbook like “deploy Service B first, wait 5 minutes, then deploy Service A” is a hallmark of the distributed monolith.
3.7 Contract (API Contract)
What it is: the agreed shape of a request and response between two services — a promise: “if you send me this shape of data, I will return you this shape of data.”
Why it exists: contracts let two teams work independently as long as both honour the same agreed shape, similar to plugging any lamp into a standard electrical socket.
A standard electrical socket is a contract. As long as your plug matches the socket shape, you do not need to know how the electricity is generated behind the wall.
3.8 Backward Compatibility
What it is: a new version of a service that still works correctly with older clients or older versions of other services that have not upgraded yet.
Why it exists: in a real distributed system, you cannot upgrade every service at the exact same second. Backward compatibility lets you upgrade one service at a time, safely.
3.9 CAP Theorem (brief)
What it is: a rule stating that a distributed data system can fully guarantee only two of three properties at once: Consistency (everyone sees the same data), Availability (every request gets a response), and Partition Tolerance (the system keeps working when network connections break).
Why it matters here: distributed monoliths often ignore CAP trade‑offs because they assume the network between services is always fast and always available — an assumption that fails in real production networks.
Architecture & Components
The moving parts that make up a system drifting into a distributed monolith — compared, piece by piece, to a healthy microservices architecture.
4.1 The typical (broken) architecture
4.2 Component breakdown
| Component | Role | Problem in a distributed monolith |
|---|---|---|
| API Gateway | Single entry point that routes client requests to the right service | Often fine on its own, but becomes a bottleneck if it also contains business logic that should live inside services |
| Individual Services | Should own one business capability end‑to‑end | Split by technical layer (“validation service,” “formatting service”) instead of by business capability, forcing every request through many hops |
| Shared Database | Central place data is stored | Becomes the biggest hidden coupling point — one schema change can break five “independent” teams |
| Synchronous Call Chains | Services calling each other to fetch data or trigger actions | Turns a distributed system into one giant, blocking call stack spread over a network — slower and more fragile than an in‑process function call |
| Shared Libraries | Common code reused across services | If a shared library contains business logic (not just utilities), every service must upgrade together, recreating the “single deployable unit” problem |
| Deployment Pipeline | Automates building and releasing each service | If pipelines require deploying services in a strict order, that is deployment coupling — a core symptom of the distributed monolith |
4.3 The healthy version, for comparison
Imagine four friends organising a birthday party. In the broken version, the cake person cannot buy the cake until the balloon person confirms balloons are bought, who cannot confirm until the invitation person sends invitations — a strict chain. In the healthy version, all three friends just post updates in a shared group chat (“Cake bought!”, “Balloons bought!”) whenever they finish, and nobody blocks anybody else.
Internal Working
Zoom into exactly how a single request behaves inside a distributed monolith, step by step, and compare it with a properly decoupled system.
5.1 Step by step: a single “Place Order” request inside a distributed monolith
1. Client sends “Place Order” to the API Gateway.
Nothing unusual so far — every architecture starts here.
2. Gateway forwards it to the Order Service.
Also normal — the gateway routes based on URL or client.
3. Order Service makes a synchronous HTTP call to Inventory Service to check stock, and waits.
First blocking hop.
4. Inventory Service, before answering, makes its own synchronous call to Warehouse Service to double‑check physical stock, and waits.
Second blocking hop, nested inside the first.
5. Once Warehouse replies, Inventory replies to Order.
The chain unwinds one level.
6. Order Service now calls Payment Service synchronously, and waits.
Third blocking hop.
7. Payment Service calls a Ledger Service to record the transaction, and waits.
Fourth blocking hop.
8. Only after all of this succeeds does Order Service write to the shared database and reply to the client.
The client has been waiting the entire time.
Notice the pattern: one single request now depends on the health, speed, and availability of six components in a straight chain. If Warehouse takes four extra seconds to respond, or briefly goes down, the entire “Place Order” flow slows down or fails — even though the client only cares about Order and Payment.
5.2 The same flow, done in a healthy, decoupled way
1. Client sends “Place Order.”
Same trigger as before.
2. Order Service checks a locally cached or eventually‑consistent view of stock, records the order as “pending,” and immediately replies “Order received” to the client.
No blocking call to another service is needed for the common case.
3. Order Service publishes an
OrderCreatedevent to a message broker.The event is durable — if anyone downstream is momentarily unavailable, it will still be delivered.
4. Inventory, Payment, and Shipping Services each independently listen for the event and process it at their own pace.
They no longer form a chain — they work in parallel.
5. If Payment fails, Payment publishes a
PaymentFailedevent; Order Service reacts by marking the order as failed and notifying the customer — without ever having been “stuck waiting.”Failure is a first‑class message, not a blocked call.
5.3 Java example: tight coupling vs. loose coupling
Below is a small, realistic Java example showing the difference. First, the tightly coupled (distributed‑monolith) style, where Order Service directly and synchronously calls other services and cannot proceed without them:
// Blocking calls: if any dependency is slow or down, the client waits.
public class OrderService {
private final InventoryClient inventoryClient; // HTTP client to Inventory
private final PaymentClient paymentClient; // HTTP client to Payment
public OrderService(InventoryClient inventoryClient, PaymentClient paymentClient) {
this.inventoryClient = inventoryClient;
this.paymentClient = paymentClient;
}
public OrderResult placeOrder(OrderRequest request) {
// Step 1: blocking call - if Inventory Service is down, this throws.
boolean inStock = inventoryClient.checkStock(request.getItemId());
if (!inStock) {
throw new OutOfStockException("Item not available");
}
// Step 2: blocking call - if Payment Service is slow, this is slow too.
PaymentResult payment = paymentClient.charge(
request.getCardToken(), request.getAmount());
if (!payment.isSuccess()) {
throw new PaymentFailedException("Payment declined");
}
// The client is left waiting for BOTH network calls to finish.
return new OrderResult(request.getOrderId(), "CONFIRMED");
}
}
Now the loosely coupled version using an event‑driven approach:
// Publish an event and return immediately. Downstream services react on their own.
public class OrderService {
private final EventPublisher eventPublisher; // publishes to a broker (e.g. Kafka)
private final OrderRepository orderRepository; // Order Service's OWN database
public OrderService(EventPublisher eventPublisher, OrderRepository orderRepository) {
this.eventPublisher = eventPublisher;
this.orderRepository = orderRepository;
}
public OrderResult placeOrder(OrderRequest request) {
// Save order locally as PENDING - no network call to other services required.
Order order = new Order(request.getOrderId(), request.getItemId(), "PENDING");
orderRepository.save(order);
// Publish an event; Inventory and Payment services react independently.
eventPublisher.publish(new OrderCreatedEvent(order.getId(), request));
// Respond immediately - the client does not wait for Inventory or Payment.
return new OrderResult(order.getId(), "PENDING");
}
// Later, this method reacts asynchronously when Payment confirms success.
public void onPaymentSucceeded(PaymentSucceededEvent event) {
Order order = orderRepository.findById(event.getOrderId());
order.markConfirmed();
orderRepository.save(order);
}
}
In the first version, placeOrder() cannot finish until two other services reply — this is the internal mechanism of a distributed monolith. In the second version, placeOrder() finishes instantly by saving locally and firing an event; other services catch up whenever they can, and failures are handled through separate events rather than exceptions bubbling up the entire chain.
Data Flow & Lifecycle
In a distributed monolith, data often has no single owner — and that is exactly why a schema change from one team routinely takes another team’s unrelated service down.
6.1 The lifecycle of a request in a distributed monolith
Trace the complete lifecycle, including all the places where things can go wrong, using our order‑placing example.
1. Request arrives.
Client sends an HTTP request to the API Gateway.
2. Routing.
Gateway decides which internal service handles it.
3. First hop.
Order Service starts processing and immediately needs data it does not own (stock level), so it must call out.
4. Network serialisation.
The request object must be converted to JSON, sent over the network, received, and deserialised by Inventory — a step that does not exist in a true monolith (an in‑process function call is instant, no serialisation needed).
5. Nested calls.
Inventory repeats the same serialisation dance with Warehouse.
6. Latency accumulation.
Every hop adds network latency (often 10–100 milliseconds each), so a request that would take 5 ms inside a true monolith might now take 300–800 ms.
7. Failure propagation.
If any hop times out, the failure must be caught and translated back up the chain, often losing context about the real root cause.
8. Data write.
The final result is written to the shared database, guarded by shared table locks that other services are also competing for.
9. Response.
The result travels back up through every hop to the client.
6.2 Data ownership lifecycle problem
In a distributed monolith, data often has no single owner. Multiple services read and write the same tables, so nobody can safely say “I know exactly how this data changes.” That creates a specific lifecycle problem:
1. Team A adds a new column to the shared
orderstable.Feels harmless, is deployed on a Tuesday morning.
2. Team B’s Reporting Service silently breaks because it selects
*.The extra column doesn’t match its data model.
3. On‑call engineer is paged at night.
Errors are climbing on a dashboard that has nothing to do with Team A’s change.
4. Root cause takes hours to find.
Two unrelated teams have to be pulled in to diagnose one change.
6.3 Correct data lifecycle: database‑per‑service
In a properly decoupled system, each service is the single owner of its own data. If another service needs that data, it must ask through a well‑defined API or subscribe to events — never read the database directly.
| Stage | Distributed Monolith | Proper Microservice |
|---|---|---|
Who can write to the orders table | Order Service, Reporting Service, Support Tool — all directly | Only Order Service |
| How other services read order data | Direct SQL query on the shared table | Call Order Service’s API, or consume its published events |
| Schema change process | Must notify and coordinate every team using the table (slow, risky) | Only Order Service team needs to agree; the contract (API shape) stays stable for others |
Think of your medical records. In a good system, only the hospital that created your test result can edit it — anyone else (your other doctors, insurance company) can only view a copy or request specific facts through an approved process. If any clinic could directly edit anyone’s raw medical file, the data would quickly become unreliable and dangerous. The same logic applies to shared databases in software.
Advantages, Disadvantages & Trade‑offs
A distributed monolith is not “evil” on purpose — it usually forms as a step along the way while a team is learning. Here’s what people think they’re gaining, versus what they actually lose.
7.1 Perceived advantages (why teams think they’re doing it right)
- Separate repositories per team — teams feel ownership over “their” codebase, at least visually.
- Separate deployment pipelines exist — there is at least a CI/CD pipeline per service, which is a step towards automation.
- Technically possible to scale services separately — even if it is rarely done safely because of tight coupling.
- Feels modern — matches the industry trend of moving away from “legacy monoliths.”
7.2 Real disadvantages
- All the complexity of distributed systems — network failures, serialisation, retries, timeouts — with none of the independence benefits.
- Slower than a true monolith — every network hop adds latency an in‑process function call never had.
- Harder to debug — a single user request now spans multiple services, multiple log files, and multiple servers, making root‑cause analysis much harder (this is exactly why distributed tracing exists — covered later).
- Coordinated deployments — teams still release multiple services together, defeating the entire purpose of “independent deployability.”
- Cascading failures — one slow or crashed service freezes a whole chain of dependent services (explained in Section 9).
- Higher infrastructure cost — more servers, more network calls, more monitoring tools, more operational overhead, for the same (or worse) functionality.
- False sense of security — teams believe they have “microservices” and stop worrying about coupling, when in fact the coupling is just as bad as before, only hidden behind network boundaries.
7.3 Trade‑off table: monolith vs. distributed monolith vs. true microservices
| Property | True Monolith | Distributed Monolith | True Microservices |
|---|---|---|---|
| Deployment independence | None (expected) | None (unexpected — the trap) | Full |
| Network calls per request | Zero (in‑process) | Many (blocking) | Few, mostly async |
| Debugging difficulty | Low | Very high | Moderate (with good tracing) |
| Operational cost | Low | High | Moderate to high, justified by scale gains |
| Team autonomy | Low | Low (hidden behind fake separation) | High |
| Failure isolation | None | None (cascades through the chain) | Strong (bulkheads, circuit breakers) |
| Best suited for | Small teams, small products, early startups | Nobody — this is the pattern to avoid | Large teams, large systems, independent scaling needs |
Here is a fact that surprises many engineers: a well‑built true monolith is often better than a poorly‑built distributed monolith. If you cannot achieve real service independence, staying as a single, well‑organised monolith — sometimes called a “modular monolith” — is usually safer, cheaper, and faster than a fake microservices split.
Performance & Scalability
Splitting services does not automatically make anything faster — in a distributed monolith, it usually makes things slower and stops adding capacity where you need it.
8.1 Why performance suffers
Every network call has overhead that does not exist in a normal function call. Compare, in rough numbers, to build intuition:
| Operation | Approximate time |
|---|---|
| In‑process function call (inside one monolith) | Nanoseconds to microseconds |
| Call to another service on the same data‑centre network | ~1–10 milliseconds |
| Call to another service across regions / data centres | ~50–150+ milliseconds |
| Database query with lock contention on a shared table | Highly variable — can spike to seconds under load |
In our earlier “Place Order” example, if Order → Inventory → Warehouse → Payment → Ledger each take even just 10 ms of pure network overhead (ignoring the actual work), that alone adds 40–50 ms of “wasted” time a true monolith would never have paid.
8.2 Scalability illusions
Teams often assume, “because it’s split into services, I can scale each service independently.” This is only true if the services are actually decoupled. In a distributed monolith, if Payment is the bottleneck but Order cannot proceed without Payment’s synchronous reply, then scaling Order alone does nothing — the chain is only as fast as its slowest link.
Imagine a single‑lane bridge. Adding more cars (scaling up Order Service) does not help if every car still has to cross one single‑lane bridge (the blocking call to Payment Service) one at a time.
8.3 How to actually gain scalability
- Use asynchronous messaging so slow services do not block fast ones.
- Cache read‑heavy data locally inside the service that needs it, instead of calling another service on every request.
- Apply the Bulkhead pattern (Section 15) to isolate failures so one slow dependency cannot consume all your server’s threads or connections.
- Design for eventual consistency where the business allows it (e.g., “Order confirmed” now, “Payment settled” a few seconds later) instead of forcing everything to be perfectly synchronous.
8.4 Load balancing role
A load balancer distributes incoming requests across multiple copies (instances) of the same service, so no single server becomes overwhelmed.
A load balancer is like a supermarket manager directing customers to the shortest checkout line instead of letting everyone pile into one register.
In a distributed monolith, load balancing individual services provides limited benefit, because the entire request chain is still gated by the slowest, most tightly‑coupled dependency. Load balancing works best when services are truly independent, so scaling one link in the chain actually speeds up the whole system.
High Availability & Reliability
A cascading failure is one small failure in one service spreading and taking down other, otherwise‑healthy services — exactly the kind of outage a distributed monolith is prone to.
9.1 Cascading failures explained
What it is: a cascading failure happens when one small failure in one service spreads and causes failures in other, otherwise‑healthy services.
Why it happens in distributed monoliths: because services block and wait on each other synchronously, a slow or crashed service can make its callers pile up waiting requests, eventually running out of memory, threads, or connections themselves — causing them to fail too, even though their own code was perfectly fine.
Imagine one broken traffic light at a busy intersection. Cars start backing up at that intersection, then the backup spreads to the next intersection, then the next, until the whole city’s traffic is jammed — even though only one traffic light was actually broken.
9.2 Patterns that prevent cascading failures
Circuit Breaker Pattern
What it is: a circuit breaker watches calls to a dependency. If failures cross a threshold, it “opens” and stops sending new requests to that dependency for a while — failing fast instead of waiting and piling up.
Just as an electrical circuit breaker in your house trips and cuts power when it senses too much current, a software circuit breaker “trips” and stops calling a failing service, protecting the rest of the system.
public class CircuitBreaker {
private int failureCount = 0;
private final int failureThreshold = 5;
private boolean open = false;
private long openedAt;
private final long resetTimeoutMillis = 30_000; // 30 seconds
public boolean allowRequest() {
if (open) {
// After some time, allow a single "trial" request through.
if (System.currentTimeMillis() - openedAt > resetTimeoutMillis) {
open = false;
failureCount = 0;
return true; // trial request
}
return false; // circuit still open, fail fast
}
return true;
}
public void recordFailure() {
failureCount++;
if (failureCount >= failureThreshold) {
open = true;
openedAt = System.currentTimeMillis();
}
}
public void recordSuccess() {
failureCount = 0;
open = false;
}
}
Bulkhead Pattern
What it is: the bulkhead pattern isolates resources (like thread pools or connection pools) per dependency, so a problem in one dependency cannot consume the resources needed to talk to a different, healthy dependency.
Ships are built with separate watertight compartments (bulkheads). If one compartment floods, the water is contained there, and the rest of the ship stays afloat.
Retry with Exponential Backoff
What it is: instead of retrying a failed call immediately (which can worsen an overloaded service), the caller waits progressively longer between retries (e.g., 1 s, 2 s, 4 s, 8 s).
Timeouts
What it is: a maximum time a service will wait for a reply before giving up. Without timeouts, a hanging dependency can freeze the caller forever.
9.3 Reliability in true microservices vs. distributed monoliths
| Failure‑handling tool | Distributed Monolith (usually missing) | Healthy Microservices (present) |
|---|---|---|
| Circuit breakers | Rarely implemented — direct calls assumed to “just work” | Standard practice on every external/service call |
| Timeouts | Often missing or set too high | Carefully tuned per dependency |
| Async messaging for non‑critical steps | Rare — synchronous calls used everywhere | Used wherever immediate consistency isn’t required |
| Graceful degradation | Usually absent — one failure means total failure | Common — e.g., show cached recommendations if the recommendation service is down |
Security
When a monolith becomes distributed, requests that used to be safe in‑memory function calls now travel across a network — new endpoints, new credentials, new attack surface.
10.1 New security surface area
When a monolith becomes distributed, requests that used to be safe in‑memory function calls now travel across a network — sometimes across the public internet, or between data centres. This introduces new risks:
- More network endpoints to secure — every service‑to‑service call is a potential entry point for attackers if not authenticated.
- Credential sprawl — many services now need their own credentials (API keys, database passwords), multiplying the ways secrets can leak.
- Broader attack surface for shared databases — if five services all connect directly to one database, a leaked credential from the weakest service can expose everyone’s data.
10.2 Core security concepts explained
Service‑to‑Service Authentication (mTLS)
What it is: mutual TLS means both sides of a connection (not just the client, but also the server) prove their identity using certificates before any data is exchanged.
Normal TLS (like HTTPS on websites) is like you checking a shop’s ID before buying — you know it’s really “Amazon.com.” Mutual TLS is like the shop also checking your ID before letting you in — both sides verify each other.
Zero‑Trust Networking
What it is: an approach where no service automatically trusts another service just because it is “inside the network.” Every request must prove its identity and permissions, every time.
Why it matters for distributed monoliths: in tightly coupled systems, teams often assume “it’s internal, so it’s safe,” skipping authentication between services entirely — dangerous, because if an attacker breaches any one service, they can freely call every other service.
Principle of Least Privilege for Shared Databases
What it is: each service should have only the exact database permissions it needs (e.g., read‑only on certain tables), not full access to everything.
In many distributed monoliths, every service connects to the shared database using the same powerful “admin” user, because it is simpler to set up. That means a bug or breach in the least important service (say, a small reporting tool) can lead to full read/write access over the entire company’s data.
10.3 Practical security checklist
- Authenticate every service‑to‑service call (mTLS or signed tokens like JWT).
- Give each service its own, narrowly‑scoped database credentials.
- Use an API Gateway to centralise authentication for external traffic, but do not let it become a single point of trust for internal traffic too.
- Rotate secrets and credentials regularly, and store them in a secrets manager, not in code or config files.
- Log and monitor cross‑service calls for unusual patterns (a service suddenly calling another it never called before could signal a breach).
Monitoring, Logging & Metrics
In a true monolith, if something breaks, you look in one log file. In a distributed system, a single user request may touch 5, 10, or 20 services — without good tooling, root cause becomes a needle in a beach of sand.
11.1 Why observability becomes urgent
In a true monolith, if something breaks, you look in one log file. In a distributed system — monolith or not — a single user request may touch 5, 10, or 20 services. Without proper tooling, finding “what actually broke” becomes like finding one specific grain of sand on a beach.
11.2 The three pillars of observability
Logging
What it is: recorded text messages describing what happened inside a service (e.g., “Order 12345 created,” “Payment failed: card declined”).
Why it’s harder in distributed systems: logs are scattered across many different servers and services. A centralised logging system (the ELK stack — Elasticsearch, Logstash, Kibana — or cloud‑native tools like AWS CloudWatch Logs) is needed to search across all services at once.
Metrics
What it is: numeric measurements over time — like request count, error rate, average response time, CPU usage.
Metrics are like a car’s dashboard: speed, fuel level, engine temperature — quick numeric signals that tell you the health of the system at a glance, without reading every detail.
Common tools: Prometheus (collects and stores metrics), Grafana (visualises them in dashboards).
Distributed Tracing
What it is: a technique that tags a single request with a unique “trace ID” as it travels through every service, so the entire journey — every hop, every delay — can be seen in one connected view.
Imagine putting a GPS tracker on one specific package as it moves through five different delivery trucks and warehouses. Instead of asking each warehouse separately “did you see this package?”, you see its entire journey on one map.
Common tools: Jaeger, Zipkin, and the OpenTelemetry standard (an open, vendor‑neutral way to collect traces, metrics, and logs).
11.3 Java example: adding a trace ID to logs
import org.slf4j.MDC;
import java.util.UUID;
public class TraceIdFilter {
public void handleIncomingRequest(HttpRequestContext ctx) {
String traceId = ctx.getHeader("X-Trace-Id");
if (traceId == null) {
traceId = UUID.randomUUID().toString();
}
// MDC (Mapped Diagnostic Context) attaches traceId to every log line
// automatically for this request, across the whole call.
MDC.put("traceId", traceId);
// Pass the same traceId header when calling downstream services,
// so all services can be joined together in tracing tools.
ctx.addOutgoingHeader("X-Trace-Id", traceId);
}
}
11.4 Alerting best practices
- Alert on symptoms the customer feels (error rate, latency) — not just internal server metrics that may not matter to users.
- Set up Service Level Objectives (SLOs) — a target like “99.9% of requests succeed within 300 ms” — and alert when at risk of missing them.
- Avoid alert fatigue: too many noisy alerts train engineers to ignore all of them, including the important ones.
Deployment & Cloud
If your deployment runbook says “deploy Payment first, wait for it to be healthy, then deploy Order, then restart Inventory” — you are looking directly at a distributed monolith. True microservices should be deployable in any order, any time, without coordination.
12.1 Deployment coupling: the biggest tell‑tale sign
If your deployment process requires a runbook such as “deploy Payment Service v2.3 first, wait for it to be healthy, then deploy Order Service v4.1, then restart the Inventory Service,” you are looking directly at a distributed monolith. True microservices should be deployable in any order, at any time, without coordination.
12.2 Containers and orchestration
What a container is: a lightweight, portable package that bundles application code together with everything it needs to run (libraries, settings), so it behaves the same on any machine.
A shipping container can carry any type of cargo and be loaded onto any ship, train, or truck, because its shape is standardised. Software containers (like Docker containers) work the same way — standardised packaging that runs consistently anywhere.
What container orchestration is: a system (like Kubernetes) that automatically starts, stops, restarts, and scales many containers across many machines, based on rules you define.
12.3 Deployment strategies that reduce risk
| Strategy | What it does | Why it helps |
|---|---|---|
| Blue‑Green Deployment | Runs two identical environments; traffic switches instantly from the old (“blue”) to the new (“green”) version | Instant rollback if something goes wrong — just switch traffic back |
| Canary Release | Sends a small percentage of traffic (e.g., 5%) to the new version first, then gradually increases it | Limits the blast radius if the new version has a bug |
| Feature Flags | New code is deployed but hidden behind a toggle that can be turned on/off without a new deployment | Decouples “deploying code” from “releasing a feature,” reducing the need for coordinated deployments |
Netflix pioneered heavy use of feature flags and canary releases specifically to allow hundreds of independent deployments per day without requiring cross‑team coordination — a direct, deliberate defence against distributed‑monolith‑style deployment coupling.
12.4 Cloud‑native considerations
- Auto‑scaling: cloud platforms (AWS, Azure, Google Cloud) can automatically add or remove server instances based on load — but this only helps if the service being scaled is not blocked waiting on a tightly‑coupled dependency.
- Managed message queues: services like Amazon SQS/SNS, Google Pub/Sub, or Apache Kafka (self‑hosted or managed) make it easier to adopt asynchronous communication, directly helping break the “distributed monolith” chain of blocking calls.
- Service mesh: tools like Istio or Linkerd handle service‑to‑service traffic (retries, timeouts, mTLS, load balancing) outside of application code, which can help apply resilience patterns consistently, even in a system that is still partially coupled.
Databases, Caching & Load Balancing
The single biggest lever for undoing a distributed monolith is giving each service its own database. Everything else — caching, load balancing, event flows — becomes easier once data ownership is clear.
13.1 Database‑per‑service pattern (the fix for shared databases)
What it is: each service gets its own database (or at minimum, its own schema or tables that no other service is allowed to touch directly).
Why it exists: it removes the hidden coupling point a shared database creates, letting each team change their own data model freely.
13.2 Handling data that used to be joined in one shared database
A common worry: “But I used to do a simple SQL JOIN between Orders and Customers — how do I do that now that they’re in separate databases?” Two common solutions:
- API Composition: call both services’ APIs and combine the results in application code (simple, but adds latency).
- Materialised View / Read Model: the Order Service subscribes to
CustomerUpdatedevents from the Customer Service and keeps a small local copy of just the customer fields it needs (like name and email), so it never needs to call out at all for reads.
13.3 Caching
What it is: storing a copy of frequently‑needed data somewhere fast to access (like memory), so you do not need to repeatedly fetch it from a slower source (like a database or another service).
Keeping a small jar of frequently used spices on your kitchen counter (cache) instead of walking to the store (database or another service) every time you need salt.
Common caching tools: Redis, Memcached.
Why it matters here: caching data that rarely changes (like product categories) locally inside a service can remove the need for many of the blocking, synchronous calls that create distributed‑monolith chains.
Caching introduces the risk of stale data — the cached copy may be slightly out of date. This is usually an acceptable trade‑off (called eventual consistency) for most business needs, but not for things like real‑time bank balances, which usually need stronger consistency guarantees.
13.4 Load balancing recap in this context
Load balancers distribute traffic across multiple instances of the same service (explained with an analogy in Section 8). In the context of fixing a distributed monolith, load balancers work best paired with: (1) each service owning its own database, so scaling does not create write conflicts, and (2) asynchronous communication, so scaling one service does not require also instantly scaling everything downstream of it.
APIs & Microservices
Good APIs are stable, coarse‑grained, and versioned. Bad APIs are chatty, brittle, and change constantly — the connective tissue that quietly turns services into a distributed monolith.
14.1 What makes an API “microservice‑friendly”
A good API contract between services should be:
- Stable: doesn’t change shape often; when it must change, it does so in a backward‑compatible way (adding new optional fields instead of removing old ones).
- Coarse‑grained: represents a meaningful business action (“place an order”), not a tiny technical detail (“update row 5 of table X”). Fine‑grained “chatty” APIs cause many small back‑and‑forth calls, which increases coupling and latency.
- Versioned: old clients using an older API version should keep working while new clients use the newer version.
14.2 REST vs. gRPC vs. Messaging — where each fits
| Style | What it is | Best for |
|---|---|---|
| REST (HTTP + JSON) | Simple, widely understood, human‑readable request/response style over HTTP | External APIs, and internal calls where an immediate answer is required |
| gRPC | A fast, binary, strongly‑typed protocol for service‑to‑service calls, often used inside a company’s internal network | High‑performance internal calls between services needing low latency |
| Messaging / Events (Kafka, RabbitMQ, SQS) | Asynchronous, “fire it and move on” communication through a message broker | Anything that does not need an immediate answer — the main fix for distributed‑monolith chains |
14.3 The “chatty API” anti‑pattern
What it is: completing one business action requires many small, sequential API calls between two services (e.g., “get customer,” then “get customer address,” then “get customer preferences” — three separate round trips instead of one).
Instead of calling Customer Service three separate times for name, address, and preferences, a well‑designed API would expose one endpoint like GET /customers/{id}/profile returning all three pieces of information in a single response, cutting three network round‑trips down to one.
14.4 API Gateway’s role in a healthy microservices setup
What it is: a single entry point that all external client requests pass through, which then routes each request to the correct internal service.
A hotel receptionist is like an API Gateway — guests (clients) do not need to know which department (service) handles housekeeping vs. room service; they just talk to the receptionist, who routes their request to the right place.
An API Gateway is healthy when it only handles cross‑cutting technical concerns (authentication, rate limiting, routing) — it becomes part of the “distributed monolith” problem if business logic starts leaking into it, because then it becomes yet another tightly‑coupled dependency every request must pass through.
Design Patterns & Anti‑patterns
Every anti‑pattern that turns services into a distributed monolith has a matching design pattern that undoes it — Domain‑Driven Design, Saga, Strangler Fig, and the Anti‑Corruption Layer are the essentials.
15.1 Anti‑patterns that create distributed monoliths
Anti‑pattern: Splitting by technical layer, not business capability
Creating a “Validation Service,” a “Formatting Service,” and a “Database Access Service” as separate deployables means nearly every single business action needs to call all three in sequence. Instead, split by business capability: “Order Service,” “Payment Service,” “Shipping Service” — each handling its own validation, formatting, and data access internally.
Anti‑pattern: Shared mutable library for business logic
If five services all import the same shared library containing core business rules, and that library changes, all five services must be recompiled and redeployed together — recreating the “single deployable unit” problem the split was supposed to remove.
Anti‑pattern: Synchronous call chains for non‑critical steps
Making the customer wait for an email confirmation service to respond before confirming their order is unnecessary — sending the email can safely happen asynchronously, after the order is already confirmed.
Anti‑pattern: The “Distributed Monolith Database”
As discussed extensively in Sections 6 and 13 — multiple services directly reading and writing one shared database.
15.2 Design patterns that fix these anti‑patterns
Domain‑Driven Design (DDD) — Bounded Contexts
What it is: a method for figuring out where one service’s responsibility ends and another begins, by grouping related business concepts (a “bounded context”) together.
The word “Order” might mean something different to the Shipping team (a physical package to send) than to the Finance team (a financial transaction to record). DDD encourages each team to have their own clear, bounded definition of “Order” rather than forcing one shared definition on everyone — this naturally leads to good service boundaries.
Saga Pattern
What it is: a way to manage a business transaction that spans multiple services, without requiring one giant locked database transaction. Each step publishes an event; if a later step fails, earlier steps run “compensating actions” to undo their work.
If Order Service reserves stock, then Payment Service fails to charge the card, the Saga triggers a compensating action: Inventory Service releases the reserved stock back. No single database transaction ever spanned both services — each did its own local transaction, coordinated through events.
Strangler Fig Pattern
What it is: a gradual migration strategy where new functionality is built as separate services around the edges of an old monolith, slowly “strangling” it, until the old monolith can be fully retired — instead of a risky, all‑at‑once rewrite.
Named after the strangler‑fig plant, which grows around a host tree, slowly replacing it, until eventually the old tree can be removed and the fig stands on its own.
Anti‑Corruption Layer
What it is: a thin translation layer placed between a new service and an old, messy monolith (or another poorly‑designed service), so the new service’s clean design does not get “corrupted” by old assumptions.
15.3 Quick‑reference: anti‑pattern to pattern mapping
| Anti‑pattern (causes distributed monolith) | Fixing pattern |
|---|---|
| Shared database across services | Database‑per‑service + API Composition or Materialised Views |
| Synchronous chains for non‑critical work | Event‑driven architecture / Messaging |
| Cross‑service locked transactions | Saga pattern with compensating actions |
| Poorly defined service boundaries | Domain‑Driven Design / Bounded Contexts |
| Risky all‑at‑once rewrites | Strangler Fig pattern |
| No failure isolation | Circuit Breaker + Bulkhead patterns |
Best Practices & Common Mistakes
Eight habits that keep a microservices system from silently becoming a distributed monolith — and five mistakes that guarantee it will.
Best Practices
- Design service boundaries around business capabilities, not technical layers — use Domain‑Driven Design to find natural seams.
- Give every service its own database, even if it feels like duplicating some data — duplication is often cheaper than coupling.
- Prefer asynchronous communication for anything that does not need an instant answer.
- Define stable, versioned API contracts, and evolve them in backward‑compatible ways.
- Add circuit breakers, timeouts, and retries to every service‑to‑service call, from day one — don’t wait for the first outage.
- Invest in distributed tracing and centralised logging early — feels optional at two services, essential at ten.
- Automate independent deployment pipelines, and actively test that services can be deployed in any order.
- Align team structure with service boundaries (related to Conway’s Law — organisations design systems that mirror their own communication structure; tangled teams produce tangled services).
Common Mistakes
- Splitting the codebase before understanding the business domain — because of pressure to “modernise” quickly; wrong boundaries drawn, constant cross‑service calls follow.
- Keeping one shared database “to save time” — splitting databases feels risky and slow upfront; hidden coupling then costs far more time later, during every schema change.
- Skipping resilience patterns (circuit breakers, timeouts) with “we’ll add it later” — the first real outage cascades across the whole system before anyone can react.
- No distributed tracing — seen as “extra tooling” not worth early investment; incidents then take hours instead of minutes to diagnose.
- Ignoring team structure — reorganising teams feels harder than reorganising code; even well‑designed service boundaries erode over time if team boundaries do not match.
Before splitting a monolith into services, ask: “Can Team A deploy their service on a random Tuesday afternoon, completely independently, without messaging Team B first?” If the honest answer is no, you likely have — or are about to build — a distributed monolith.
Real‑World & Industry Examples
Companies that avoided the distributed monolith — and companies that publicly discussed discovering they had built one, then how they climbed out.
Circuit breakers & async by default
Netflix is widely cited as one of the pioneers of true microservices done well. It invested heavily in tools like Hystrix (an early, famous circuit‑breaker library, now succeeded by newer resilience libraries like Resilience4j), Eureka (service discovery), and a strong culture of asynchronous, event‑driven design — specifically to avoid the cascading‑failure trap common in distributed monoliths.
API‑only mandate
Amazon’s move to a service‑oriented architecture in the early 2000s — famously enforced by an internal mandate that all teams must expose their functionality only through well‑defined APIs, with no direct database access between teams — is often cited as an early, deliberate defence against the shared‑database anti‑pattern that causes distributed monoliths.
Fan‑out and tracing
As Uber scaled from a few services to thousands, it publicly discussed challenges with excessive service‑to‑service calls per request (“fan‑out”) that mirrored distributed‑monolith symptoms, leading it to invest heavily in internal tools for tracing, dependency visualisation, and encouraging teams to reduce unnecessary synchronous coupling between services.
Post‑mortems & conference talks
Many companies that migrated from monoliths to “microservices” between 2015 and 2020, without redesigning their data‑ownership model, publicly discussed (in engineering blogs and conference talks) discovering they had built distributed monoliths — new operational complexity without gaining true team or deployment independence. The pattern was significant enough to prompt widespread industry discussion, including talks and writing by well‑known architects such as Martin Fowler and Sam Newman.
Think about a school with one giant timetable shared by every class (the monolith). Now imagine the school hires five new class monitors and says, “Congratulations, you’re now independent classes” — but keeps the exact same one shared timetable that only the principal can edit, and every class still must start and end at the same bell. Nothing really changed except job titles; the classes are still one single, tightly‑scheduled unit. Real independence would mean each class gets to set its own schedule as long as it meets shared, minimal requirements (like “must finish by 3 p.m.”) — that’s the difference between a distributed monolith and true microservices.
Interview Questions
Ten questions across beginner, intermediate, and advanced difficulty — the kind of distributed‑monolith questions that come up in nearly every senior system‑design interview.
What is a distributed monolith? Beginner
A system that is physically split into multiple deployable services but is still logically coupled like one giant application — giving you the complexity of distributed systems without the independence benefits of microservices.
Give three warning signs that you are looking at one. Beginner
Deploying services in a specific order is required, a schema change from one team routinely breaks another team’s service, and one slow service cascades into several “independent” services failing at the same time.
Why is a shared database the worst offender? Intermediate
Because it silently recreates the tightest form of coupling — the data layer — even after the code layer has been split. Every schema change ripples across every team using the tables, and every service’s bugs and breaches touch everyone else’s data.
How is a cascading failure different from a normal failure? Intermediate
A normal failure is one service being unhealthy; a cascading failure is healthy services also failing because they were synchronously waiting on the unhealthy one, and eventually ran out of threads, connections, or memory themselves.
What is a circuit breaker and where would you place it? Intermediate
A circuit breaker watches calls to a dependency and “opens” when failures cross a threshold, failing fast rather than piling up. You place one on every outbound call from a service to another service or external system.
How does the Saga pattern replace a cross‑service database transaction? Intermediate
Each service performs its own local transaction and publishes an event when done. If a later step fails, a compensating action for each previously completed step is executed in reverse order. No cross‑service lock is ever held.
How would you migrate a distributed monolith without a big‑bang rewrite? Advanced
Use the Strangler Fig pattern: build new capabilities as decoupled services around the edges, gradually replace shared‑database access with API composition or event streams, and add resilience patterns (circuit breakers, timeouts) as you go — retiring the old code path once traffic has fully shifted.
Where does the CAP theorem intersect with distributed monoliths? Advanced
Distributed monoliths implicitly assume the network is always fast and available — skipping the P in CAP. When a partition happens, they typically stop being available and become inconsistent at the same time, because there is no explicit decision about which property to preserve.
Is Conway’s Law relevant here? Advanced
Very much so. Conway’s Law states that a system’s architecture mirrors the communication structure of the organisation that builds it. If teams are tangled or share a database team, the services those teams build will end up tangled and share a database — a distributed monolith emerges naturally.
When would you argue against splitting a monolith at all? Intermediate
For small teams and small products, a well‑organised modular monolith is often the safer, cheaper, faster starting point. Splitting into microservices should be driven by a real, proven need (independent scaling, independent team ownership), not by industry trends alone.
FAQ, Summary & Key Takeaways
The questions people ask most about distributed monoliths, and the ideas worth carrying with you long after you close this tab.
Frequently Asked Questions
Is a distributed monolith always a bad thing?
It is rarely a deliberate goal, and almost always an unintentional stage a system passes through. It becomes a real problem when a team believes they have true microservices (and stops watching for coupling), while actually suffering all the operational costs of a distributed system.
How do I know if my system is a distributed monolith?
Ask: Do services have to be deployed in a specific order? Does one team’s schema change routinely break another team’s service? Does one slow or failing service cause several other “independent” services to fail too? If yes to any of these, you likely have a distributed monolith.
Is it better to just stay a monolith?
For small teams and small products, yes — often a well‑organised “modular monolith” (one deployable unit, but with clean internal boundaries between modules) is the safest starting point. Splitting into microservices should be driven by a real, proven need (independent scaling, independent team ownership) rather than following a trend.
Can I fix a distributed monolith without a full rewrite?
Usually yes. Common recovery steps include: gradually splitting the shared database (often the hardest and most important step), introducing asynchronous messaging for non‑critical flows, adding resilience patterns (circuit breakers, timeouts), and redrawing service boundaries using Domain‑Driven Design — done incrementally using patterns like Strangler Fig, rather than a risky big‑bang rewrite.
What’s the single biggest cause of distributed monoliths?
Based on widespread industry experience: sharing one database across multiple “independent” services is the most common root cause, followed closely by overusing synchronous, blocking calls between services for things that do not actually need an immediate answer.
Key Takeaways
- A distributed monolith is a system that is physically split into multiple deployable services, but is still logically coupled like one giant application — you get distributed‑system complexity without microservices’ independence benefits.
- The most common causes are: a shared database, synchronous blocking calls between services, shared business‑logic libraries, and services split by technical layer instead of by business capability.
- Warning signs include: needing to deploy multiple services in a specific order, one service’s failure cascading into others, and schema changes routinely breaking unrelated teams’ services.
- Fixes include: Database‑per‑service, event‑driven / asynchronous communication, the Saga pattern for cross‑service transactions, Circuit Breaker and Bulkhead patterns for resilience, and Domain‑Driven Design for drawing correct service boundaries.
- Observability (centralised logging, metrics, and distributed tracing) is not optional in a distributed system — without it, debugging becomes extremely painful.
- A well‑designed, single monolith is often better than a poorly split, tangled set of “microservices” — independence must be earned through good design, not assumed just because code lives in separate repositories.
At its heart, the distributed‑monolith story is not about a specific technology — it is about the difference between splitting code and splitting responsibility. Move code across repositories without moving ownership, and the coupling simply hides behind a network cable. Move ownership carefully — of data, of deployment, of failure — and each service can genuinely grow, fail, and recover on its own. The systems that stay calm under real‑world stress are almost always the ones whose engineers took that second step seriously, and refused to be fooled by architecture diagrams that looked microservice‑shaped but behaved like one giant, brittle stone.