Skip to main content

Distributed Monolith

Microservices independent in deployment but tightly coupled in design.

TL;DR

A distributed monolith is the worst of both worlds: microservices deployed independently, but tightly coupled in design. Change one service and you must update 5 others. All services deployed together despite being "independent." Shared databases create implicit dependencies. Circular service dependencies break the entire system. You lose every advantage of microservices (independent scaling, deployment, failure isolation) while gaining all the disadvantages (complexity, latency, debugging difficulty). Solution: design services with loose coupling through clear API contracts, independent data stores, and asynchronous communication.

Learning Objectives

You will be able to:

  • Identify distributed monolith characteristics in microservice architectures
  • Understand how tight coupling defeats microservice benefits
  • Design service boundaries using Domain-Driven Design
  • Create service contracts with versioning
  • Implement asynchronous communication patterns
  • Migrate from distributed monolith to loosely-coupled services
  • Evaluate when microservices are (not) the right choice

Motivating Scenario

Your company split a monolith into microservices three months ago. You now have:

  • User Service
  • Order Service
  • Inventory Service
  • Payment Service
  • Notification Service

You want to change the Order API response format slightly. But:

  • The User Service hardcodes expectations about Order responses
  • The Inventory Service directly queries the Order database
  • Payment Service and Order Service call each other synchronously
  • Notification Service must be updated to handle new order fields
  • You can't deploy Order Service without coordinating with 4 other teams

You end up deploying all services together anyway, defeating the whole point.

A network call fails between Order and Payment Service. The entire ordering system fails because there's no fallback. In a true microservice architecture, services would be resilient to each other's failures.

You realize: this isn't microservices, it's a distributed monolith.

Core Explanation

What Makes a Distributed Monolith

  1. Synchronous Dependencies: Service A makes a blocking call to Service B. If B is slow or down, A hangs. They're tightly coupled.

  2. Shared Database: Multiple services read/write the same database tables. Services are coupled through the database schema.

  3. Circular Dependencies: Service A calls Service B, Service B calls Service C, Service C calls Service A. You can't deploy or scale independently.

  4. Hard-Coded Contracts: Services assume specific response formats. Change the format, all callers break.

  5. Coordinated Deployments: You deploy services together because they're interdependent. You've lost deployment independence.

Why This Happens

  • Premature Microservices: Split a monolith without clear domain boundaries. Services emerge with fuzzy responsibilities.
  • Ease Over Architecture: Sharing a database is easier than building proper APIs. Synchronous calls are simpler than async messaging.
  • No Service Contract Discipline: No versioning, no compatibility layer, no API evolution strategy.
  • Fear of Complexity: "Async messaging is complex, we'll just call each other synchronously."

The Cost

You get:

  • Complexity of distributed systems (latency, failures, debugging)
  • Tight coupling of a monolith (can't change independently)
  • Deployment coordination nightmare
  • Testing complexity without the benefits of independence

You lose:

  • Independent scaling
  • Independent deployment
  • Failure isolation (one service failure cascades)
  • Technology diversity

Pattern Visualization

Distributed Monolith vs. True Microservices

Code Examples

order_service.py
import requests

class OrderService:
def __init__(self):
self.user_service_url = "http://user-service:8001"
self.inventory_service_url = "http://inventory-service:8002"
self.payment_service_url = "http://payment-service:8003"
self.db = SharedDatabase()

def create_order(self, user_id, items):
"""Create an order - tightly coupled to other services"""
# Get user (synchronous call - blocks if user service is slow)
user_resp = requests.get(
f"{self.user_service_url}/users/{user_id}",
timeout=5
)
if user_resp.status_code != 200:
raise Exception("User service unavailable")
user = user_resp.json()

# Check inventory (synchronous call - blocks)
for item in items:
inv_resp = requests.get(
f"{self.inventory_service_url}/check/{item['id']}",
timeout=5
)
if not inv_resp.json()['available']:
raise Exception("Item not in stock")

# Create order record in shared database
order = self.db.execute(
"INSERT INTO orders (user_id, items, status) VALUES (?, ?, ?)",
(user_id, items, 'pending')
)

# Process payment (synchronous call - blocks, tightly coupled)
try:
payment_resp = requests.post(
f"{self.payment_service_url}/charge",
json={'user_id': user_id, 'amount': self._calculate_total(items)},
timeout=5
)
if payment_resp.status_code != 200:
# Payment failed but order already created
# Now in inconsistent state
self.db.execute("UPDATE orders SET status = ? WHERE id = ?", ('failed', order['id']))
raise Exception("Payment failed")
except requests.Timeout:
# Payment service is slow
# Order is stuck in pending state forever
raise Exception("Payment service timeout")

# Update order status
self.db.execute("UPDATE orders SET status = ? WHERE id = ?", ('confirmed', order['id']))
return order

def get_order(self, order_id):
# Directly query shared database
return self.db.execute("SELECT * FROM orders WHERE id = ?", (order_id,))

# Problems:
# 1. If user service is down, orders can't be created
# 2. If inventory service is slow, entire order creation is slow
# 3. If payment service times out, order is in inconsistent state
# 4. Shared database means Inventory and Payment services can directly query orders
# 5. Can't deploy Order Service without coordinating with User, Inventory, Payment
# 6. Can't scale Order Service independently (scales all to avoid DB bottleneck)

Patterns and Pitfalls

How Distributed Monoliths Form

1. Premature Microservices Splitting a monolith without understanding domain boundaries. Services end up with blurry responsibilities.

2. Synchronous by Default Taking the easier path: direct HTTP calls instead of async messaging.

3. Shared Database "For Efficiency" Thinking a shared database is more efficient. It's simpler initially but creates coupling.

4. No API Contract Discipline Services assume specific response formats. No versioning, no compatibility layers.

When This Happens / How to Detect

Red Flags:

  1. Changing one service requires updating 5+ others
  2. Services deployed together despite being "microservices"
  3. Shared database used by multiple services
  4. Circular service dependencies
  5. Service A can't function without Service B
  6. Chain of synchronous HTTP calls
  7. Tests require mocking entire service network
  8. Network failures cascade across all services

How to Fix / Refactor

Step 1: Identify Service Boundaries (Domain-Driven Design)

Define bounded contexts:

  • User Service owns user data
  • Order Service owns order data
  • Inventory Service owns stock

Step 2: Introduce API Contracts

Services communicate through versioned APIs:

GET /api/v1/orders/123
{
"id": "123",
"status": "confirmed",
"items": [...]
}

Step 3: Separate Data Stores

Each service gets its own database. Data sync through events, not direct queries.

Step 4: Implement Event-Driven Communication

Replace synchronous calls with events:

OrderCreated → Payment Service listens → Charges customer → PaymentProcessed event

Step 5: Gradual Migration

Don't rewrite everything. Migrate one service at a time. Create an adapter layer for legacy synchronous dependencies while building the new event system.

Operational Considerations

When NOT to Use Microservices

Distributed monoliths exist because someone split systems that shouldn't have been split. If services can't operate independently, use a monolith.

Monitoring & Tracing

With async communication, tracking request flows becomes harder. Invest in distributed tracing (Jaeger, Zipkin) to understand system behavior.

Design Review Checklist

  • Can each service be deployed independently?
  • Do services have separate databases (not shared)?
  • Are there circular dependencies between services?
  • Are synchronous calls kept to a minimum?
  • Do API contracts have versioning?
  • Is async communication used for non-critical paths?
  • Can a service fail without cascading to others?
  • Is there an event bus or message queue for communication?
  • Are service boundaries based on domain concerns?
  • Can each service be scaled independently?
  • Are database schemas private to each service?

Showcase

Signals of Distributed Monolith

  • All services deployed together
  • Services share a database
  • Chain of synchronous HTTP calls
  • Circular dependencies between services
  • Change in one service requires updates in 5+
  • Network failure cascades to entire system
  • Services deployable independently
  • Each service has its own database
  • Async communication via events
  • No circular dependencies
  • Services loosely coupled
  • Failure in one service isolated

Self-Check

  1. Can you deploy one service without deploying others? If no, it's a distributed monolith.

  2. Do multiple services share the same database? If yes, you're missing service boundaries.

  3. How many synchronous service-to-service calls for one user action? If > 2, likely too tightly coupled.

Next Steps

  • Map: Draw service dependencies - identify cycles
  • Identify: Which services share databases
  • Segregate: Create separate databases for each service
  • Event-ify: Replace sync calls with events
  • Test: Verify independent deployability

One Takeaway

info

Microservices without loose coupling are worse than monoliths. If services must be deployed together, deploy them as a monolith. True microservices are loosely coupled, independently deployable, and independently scalable.

References

  1. Microservice Architecture Patterns ↗️
  2. Martin Fowler - Microservices ↗️
  3. Domain-Driven Design ↗️
  4. Event-Driven Architecture with RabbitMQ ↗️