Monolith vs Distributed Systems: Architecture Choices That Define Your System
One of the most consequential architecture decisions is whether to build a monolithic application or a distributed system. This choice affects development velocity, operational complexity, scalability, team structure, and how you handle failures. Get it wrong and you either struggle to scale or drown in unnecessary complexity.
This guide covers both architectures in depth, explains when to use each, details migration strategies, and examines the eight fallacies of distributed computing that trip up even experienced engineers.
What Is a Monolith?
A monolithic application is a single, unified codebase deployed as one unit. All components ā user interface logic, business logic, data access ā run in the same process and share the same memory space. A single deployment artifact (a JAR file, a Docker image, a binary) contains everything.
Monolithic Application Structure:
my-ecommerce-app/
āāā src/
ā āāā controllers/
ā ā āāā UserController.java
ā ā āāā ProductController.java
ā ā āāā OrderController.java
ā ā āāā PaymentController.java
ā āāā services/
ā ā āāā UserService.java
ā ā āāā ProductService.java
ā ā āāā OrderService.java
ā ā āāā PaymentService.java
ā āāā models/
ā ā āāā User.java
ā ā āāā Product.java
ā ā āāā Order.java
ā āāā database/
ā āāā DatabaseConnection.java (single shared database)
āāā pom.xml
āāā Dockerfile (one image, one deployment)
Advantages of Monoliths
- Simple development: One codebase, one IDE project, one build system. New developers get productive fast.
- Simple deployment: Deploy one artifact. No service discovery, no API versioning between services.
- Simple debugging: One process means stack traces are complete, breakpoints work across modules, and logs are in one place.
- No network overhead: Function calls within the same process are nanoseconds, not milliseconds. No serialization/deserialization.
- ACID transactions: A single database makes transactions straightforward. No distributed transactions needed.
- Efficient at small scale: Less infrastructure, fewer moving parts, lower operational cost.
Disadvantages of Monoliths
- Scaling constraints: Must scale the entire application even if only one module needs more resources.
- Deployment risk: A small change requires redeploying everything. A bug in one module can bring down the whole system.
- Technology lock-in: One language, one framework, one database for the entire application.
- Team coupling: Large teams working on the same codebase face merge conflicts, coordination overhead, and slow build times.
- Growing complexity: As the codebase grows, module boundaries blur, dependencies become tangled, and changes have unexpected side effects.
What Is a Distributed System?
A distributed system is a collection of independent services that communicate over a network. Each service owns its own data, runs in its own process, and can be deployed independently. Microservices are the most common form of distributed architecture.
Distributed System (Microservices):
user-service/ ā Owns user data, auth, profiles
āāā user-db (PostgreSQL)
product-service/ ā Owns product catalog, search
āāā product-db (Elasticsearch)
order-service/ ā Owns orders, cart, checkout
āāā order-db (DynamoDB)
payment-service/ ā Owns payments, refunds
āāā payment-db (PostgreSQL)
notification-service/ ā Owns email, SMS, push notifications
āāā message-queue (SQS)
Each service:
- Has its own database (no shared state)
- Communicates via HTTP/gRPC or message queues
- Can be deployed independently
- Can be written in different languages
Advantages of Distributed Systems
- Independent scaling: Scale only the services that need it. If search is heavy, scale the product service only.
- Independent deployment: Deploy one service without touching others. Faster releases, less risk.
- Technology flexibility: Use the best language/database for each service. Python for ML, Go for high-performance APIs, PostgreSQL for transactions, Elasticsearch for search.
- Team autonomy: Each team owns their service end-to-end. No coordination needed for deployment.
- Fault isolation: A crash in one service does not bring down others (if properly designed).
Disadvantages of Distributed Systems
- Network complexity: Every inter-service call can fail, be slow, or return unexpected results.
- Distributed transactions: ACID transactions across services are extremely difficult. You need sagas or eventual consistency.
- Operational overhead: More services means more to deploy, monitor, debug, and maintain.
- Data consistency: Each service owns its data, so consistency across services requires careful design.
- Debugging difficulty: A request may traverse 10 services. Tracing bugs requires distributed tracing (Jaeger, Zipkin).
Comparison Table
| Aspect | Monolith | Distributed (Microservices) |
|---|---|---|
| Development Speed (early) | Fast ā everything in one place | Slower ā setup overhead, contracts |
| Development Speed (late) | Slower ā tangled dependencies | Faster ā independent teams |
| Deployment | One artifact, all-or-nothing | Independent per service |
| Scaling | Scale entire app | Scale individual services |
| Data Consistency | ACID (easy) | Eventual (complex) |
| Debugging | Single process, simple | Distributed tracing needed |
| Team Size | Small teams (1-20 engineers) | Large organizations (50+ engineers) |
| Infrastructure Cost | Low | Higher (more services, more infra) |
The Eight Fallacies of Distributed Computing
In 1994, Peter Deutsch (with additions from James Gosling) identified eight false assumptions that developers new to distributed systems commonly make. Every one of these has caused production outages at scale.
1. The network is reliable
Networks fail. Packets get dropped, connections time out, DNS resolution fails. Always design for network unreliability ā use retries with exponential backoff, circuit breakers, and timeouts on every network call.
2. Latency is zero
Every network call has latency. A function call within a monolith takes nanoseconds. The same call across a network takes milliseconds ā a million times slower. Distributed designs must minimize network round trips through batching, caching, and colocating related services.
3. Bandwidth is infinite
Network bandwidth is limited and shared. Sending 10MB payloads between services might work in development but causes problems at scale. Compress data, use efficient serialization (Protocol Buffers, MessagePack), and send only what is needed.
4. The network is secure
Every network hop is an attack surface. Inter-service communication must be encrypted (mTLS), authenticated, and authorized. Zero-trust networking assumes no network is safe, even internal ones.
5. Topology doesn't change
Services move between hosts, IPs change, new instances spin up and down. Never hardcode addresses. Use service discovery (Consul, Kubernetes DNS) and load balancers to abstract away the physical topology.
6. There is one administrator
In distributed systems, different teams own different services, different databases have different admins, and cloud providers manage their own infrastructure. Coordination is essential but distributed. Runbooks, shared documentation, and clear ownership boundaries are critical.
7. Transport cost is zero
Serializing data, establishing connections, and moving bytes across the network all have CPU, memory, and bandwidth costs. These costs are invisible in a monolith but significant in a system making millions of inter-service calls per second.
8. The network is homogeneous
Real networks mix different hardware, protocols, and configurations. Your service might communicate over HTTP within a data center, gRPC across regions, and WebSocket to mobile clients. Design for protocol diversity.
Communication Patterns in Distributed Systems
Synchronous Communication
# Synchronous: REST/HTTP
import requests
def create_order(user_id, product_id, quantity):
# Call user service to validate user
user = requests.get(f"http://user-service/users/{user_id}").json()
# Call product service to check inventory
product = requests.get(f"http://product-service/products/{product_id}").json()
if product["stock"] < quantity:
raise InsufficientStockError()
# Call payment service to charge
payment = requests.post("http://payment-service/charge", json={
"user_id": user_id,
"amount": product["price"] * quantity
}).json()
# Create order in our database
order = db.create_order(user_id, product_id, quantity, payment["id"])
return order
# Problem: If any service is slow or down, the entire operation fails.
# The chain is only as strong as its weakest link.
Asynchronous Communication
# Asynchronous: Event-driven with message queues
def create_order(user_id, product_id, quantity):
# Create order in pending state
order = db.create_order(user_id, product_id, quantity, status="PENDING")
# Publish event ā other services react asynchronously
message_queue.publish("order.created", {
"order_id": order.id,
"user_id": user_id,
"product_id": product_id,
"quantity": quantity
})
return order # Return immediately
# Payment service subscribes to "order.created"
# ā Processes payment
# ā Publishes "payment.completed" or "payment.failed"
# Order service subscribes to "payment.completed"
# ā Updates order status to "CONFIRMED"
# Notification service subscribes to "order.confirmed"
# ā Sends confirmation email
# Benefit: Services are decoupled. If payment service is slow,
# the order is still created. Processing happens asynchronously.
Migration Strategies: Monolith to Microservices
Strangler Fig Pattern
Named after the strangler fig tree that grows around a host tree and eventually replaces it. You gradually extract functionality from the monolith into services, routing new traffic to the new service while the monolith handles the rest.
Strangler Fig Migration:
Phase 1: Monolith handles everything
[Client] ā [Monolith: Users + Products + Orders + Payments]
Phase 2: Extract one service, route through proxy
[Client] ā [API Gateway]
āā [User Service] (new)
āā [Monolith: Products + Orders + Payments]
Phase 3: Extract more services
[Client] ā [API Gateway]
āā [User Service]
āā [Product Service] (new)
āā [Monolith: Orders + Payments]
Phase 4: Monolith is fully replaced
[Client] ā [API Gateway]
āā [User Service]
āā [Product Service]
āā [Order Service]
āā [Payment Service]
Branch by Abstraction
Create an abstraction layer within the monolith that can route calls to either the old implementation or the new service. Toggle between them with feature flags. This allows gradual, safe migration with easy rollback.
Database Decomposition
The hardest part of migration is splitting the shared database. Steps: (1) Identify data ownership per service, (2) Create new databases for extracted services, (3) Sync data between old and new databases during migration, (4) Switch reads/writes to new databases, (5) Remove old tables from the monolith database.
When to Use Each Architecture
Choose Monolith When:
ā Small team (1-10 engineers)
ā New product / MVP / uncertain requirements
ā Simple domain model
ā Tight deadline (ship fast)
ā Limited DevOps expertise
ā Strong consistency requirements (ACID transactions)
Choose Distributed / Microservices When:
ā Large organization (50+ engineers)
ā Well-understood domain with clear boundaries
ā Need independent scaling of different components
ā Multiple teams need to deploy independently
ā Different components need different tech stacks
ā High availability requires fault isolation
Start with a monolith. Evolve to microservices when the pain of
the monolith exceeds the pain of distribution. This is the path
most successful companies followed: Amazon, Netflix, and Uber
all started as monoliths.
For related concepts, explore Scalability: Horizontal vs Vertical, Fault Tolerance, CAP Theorem, and Trade-offs in System Design.
Frequently Asked Questions
Is a monolith always bad?
Absolutely not. A well-structured monolith is often the best choice for small to medium teams. Shopify, Stack Overflow, and Basecamp are billion-dollar businesses running on monolithic architectures. The key is good modular design within the monolith ā clear boundaries between modules even if they share a process and database.
What is the biggest mistake teams make when moving to microservices?
Moving too early and too aggressively. A common anti-pattern is "distributed monolith" ā microservices that are tightly coupled and must be deployed together. This gives you the worst of both worlds: the complexity of distribution with none of the benefits of independence. Extract services only when you have clear boundaries and a genuine need.
How do you handle transactions across microservices?
Use the Saga pattern. Instead of a distributed ACID transaction, you chain local transactions with compensating actions. If step 3 of a 5-step process fails, you execute compensating transactions for steps 1 and 2 to undo their effects. This trades strong consistency for availability and resilience, accepting eventual consistency.
How small should a microservice be?
The name "micro" is misleading. A service should be small enough for one team to own (typically 5-8 engineers) but large enough to be meaningful. Amazon's "two-pizza team" rule is a good guide. If a service is so small that it cannot justify its operational overhead, it is too small. If it is so large that one team cannot understand it, it is too large.
What infrastructure do I need for microservices?
At minimum: container orchestration (Kubernetes), service discovery, an API gateway, centralized logging, distributed tracing, and CI/CD pipelines per service. This is significant overhead compared to a monolith. If your organization cannot support this infrastructure, you are not ready for microservices. Many teams use managed platforms (AWS ECS/EKS, Google Cloud Run) to reduce this burden.