🕵️ The Scene of the Crime
Let me paint you a picture. You’re in a meeting room. Someone’s drawn boxes on a whiteboard. Lots of boxes. Each box has a name like UserService or PaymentService or my personal favorite, HelperService (we’ll come back to that one).
“We’re going microservices!” someone announces triumphantly, as if they’ve just discovered fire.
Six months later, you can’t change a user’s email address without deploying 14 services in the correct order, and your “quick fix” requires coordinating with 5 teams across 3 time zones.
Congratulations. You’ve built a distributed monolith — all the disadvantages of a monolith (tight coupling, coordinated releases, cascading failures) plus exciting new problems like network latency, distributed transact…
🕵️ The Scene of the Crime
Let me paint you a picture. You’re in a meeting room. Someone’s drawn boxes on a whiteboard. Lots of boxes. Each box has a name like UserService or PaymentService or my personal favorite, HelperService (we’ll come back to that one).
“We’re going microservices!” someone announces triumphantly, as if they’ve just discovered fire.
Six months later, you can’t change a user’s email address without deploying 14 services in the correct order, and your “quick fix” requires coordinating with 5 teams across 3 time zones.
Congratulations. You’ve built a distributed monolith — all the disadvantages of a monolith (tight coupling, coordinated releases, cascading failures) plus exciting new problems like network latency, distributed transactions, and debugging across 47 log aggregators.
⚔️ The Seven Deadly Sins of Fake Microservices
1. 🫣 The Shared Database of Shame
┌─────────────┐ ┌─────────────┐
│ UserService │ │ OrderService│
└──────┬──────┘ └──────┬──────┘
│ │
└────────┬───────────┘
│
┌──────▼──────┐
│ users_db │
│ │
│ (everyone │
│ writes to │
│ everything)│
└─────────────┘
Your services all talk to the same database. Directly. With joins. Across each other’s tables.
“But it’s faster!” you cry. Sure, it’s fast. A motorcycle without brakes is also fast. Right up until the moment it isn’t. You don’t have microservices. You have stored procedures with REST endpoints and a Kubernetes bill.
2. 🔗 The Synchronous Coupling Chain Reaction
// UserService needs to create a user
async function createUser(userData) {
const user = await db.users.create(userData);
// Better tell everyone!
await fetch('http://email-service/welcome', { userId: user.id });
await fetch('http://analytics-service/track', { userId: user.id });
await fetch('http://notification-service/notify', { userId: user.id });
await fetch('http://recommendation-service/initialize', { userId: user.id });
await fetch('http://gamification-service/create-profile', { userId: user.id });
return user;
}
If the gamification service is down, nobody can sign up. Because apparently, earning badges is more important than authentication.
This is the distributed equivalent of import * in Python. Technically it works. Morally, it’s indefensible.
3. ☠️ The “Microservice” That Calls All Its Friends
You know the one. It’s called something innocuous like UserService but it’s actually the Death Star of your architecture.
UserService calls:
├── AuthService
├── ProfileService (which calls UserService for validation... wait)
├── PermissionService
├── TeamService
├── OrganizationService
├── BillingService
├── AuditService
└── That-Random-Service-Nobody-Remembers-Creating
Total network hops for GET /users/me: 23
Timeout probability: Yes
This isn’t a microservice. This is a monolith that got drunk and fell into your service mesh.
4. 🧨 The Deployment Order Script
#!/bin/bash
# deploy.sh - Deploy order is CRITICAL. DO NOT CHANGE.
# Last modified: 47 times this month
kubectl apply -f config-service.yaml # Must be first!
sleep 30 # Give it time to wake up
kubectl apply -f user-service.yaml
sleep 20
kubectl apply -f auth-service.yaml
sleep 15
kubectl apply -f profile-service.yaml # MUST deploy before notification-service
sleep 25
kubectl apply -f notification-service.yaml
# ... 23 more services
# Total deployment time: 45 minutes
# Rollback strategy: Prayer
If your microservices have a deployment order, they’re not micro, they’re not services, and they’re definitely not independent. You’ve invented a distributed monolith with extra steps and worse performance.
5. 📦 The Shared Library That Contains Everything
// @company/shared-business-logic v47.3.2
// Size: 143 MB
// Contents: Everything
import {
validateUser,
calculateTax,
formatAddress,
checkPermissions,
processPayment,
sendEmail,
// ... 847 more exports
} from '@company/shared-business-logic';
Every service depends on this library. Every change to this library requires redeploying every service. You know what else works like that? A monolith. Except a monolith doesn’t make you wait for Docker builds.
6. 🚪 The “API Gateway” That’s Actually Business Logic
// api-gateway/routes/users.js
router.post('/users', async (req, res) => {
// Validate the user (that's fine...)
const errors = validateUserInput(req.body);
if (errors.length) return res.status(400).json(errors);
// Calculate their tier (wait...)
const tier = calculateUserTier(req.body);
// Check if email is allowed (hold on...)
if (isDisposableEmail(req.body.email)) {
return res.status(400).json({ error: 'Email not allowed' });
}
// Apply business rules (what are we doing?)
if (tier === 'premium' && !req.body.creditCard) {
return res.status(400).json({ error: 'Premium requires payment' });
}
// Finally, call the "microservice"
await fetch('http://user-service/users', { method: 'POST', body: req.body });
});
Your API Gateway contains 4,000 lines of business logic. At this point, just admit it’s your monolith and stop pretending.
7. 🕺 The Coordinated Release Dance
| Day | Description |
|---|---|
| Monday | “We’re deploying the new user creation flow” |
| Tuesday | Deploys UserServiceV2 |
| Wednesday | ProfileService breaks (unexpected payload format) |
| Thursday | Emergency rollback of UserService |
| Friday | Deploy ProfileServiceV2 (compatible with UserServiceV2) |
| Monday | Try UserServiceV2 again |
| Tuesday | NotificationService breaks (wasn’t in the meeting) |
| Wednesday | “Let’s schedule a deployment window” |
| Thursday | 2 AM deployment, all hands on deck |
| Friday | Everything works but nobody knows why |
If you need a project manager to coordinate your microservice deployments, they’re not microservices. They’re a distributed monolith with a Gantt chart.
So....
🧭 So where did it all go wrong? Let’s rewind the postmortem.
Let’s be honest about the journey:
| Month | Description |
|---|---|
| Month 0 | “Our monolith is too big! Let’s split it into microservices!” |
| Month 1 | Split the monolith along... organizational lines? Sure, why not. |
| Month 2 | Services need to talk to each other. Add synchronous HTTP calls everywhere. |
| Month 3 | Things are slow. Add caching. Now you have two problems. |
| Month 4 | Data is inconsistent. Add a message bus. Now you have three problems. |
| Month 5 | Services depend on each other. Add a service mesh. Now you have four problems. |
| Month 6 | Can’t deploy independently. Add orchestration. Now you have five problems. |
| Month 12 | Realize you’ve built a distributed monolith but with worse latency and a much higher AWS bill. |
❤️ The Shared Database: A Love Story
The shared database is the smoking gun of distributed monoliths. Let me show you how it happens:
| Week | Description |
|---|---|
| Week 1 | UserService owns the users table. |
| Week 2 | OrderService needs user data... let’s just read from the users table directly. It’s faster! |
| Week 3 | EmailService needs to know when users change their email... let’s just query the users table! |
| Week 4 | AnalyticsService needs user demographics... just join on users! |
| Week 12 | Nobody can change the users table schema because 14 services will break. |
You’ve created coupling through the database instead of through the code. This is like saying you quit smoking but started vaping.
Technically different, practically the same.
🧩 Signs You Might Have a Distributed Monolith
Take this quick quiz:
- Can you deploy one service without coordinating with other teams?
- If Service A is down, does Service B still mostly work?
- Can you change a database schema without updating multiple services?
- Do your services communicate through events rather than synchronous calls?
- Can a new developer understand one service without understanding all services?
If you answered “no” to most of these, congratulations! You have a distributed monolith.
Bonus points if:
- Your “deployment runbook” is 47 pages long
- You have a wiki page titled “Service Dependency Map” that nobody updates
- Your error messages include the phrase “service chain timeout”
- You’ve heard someone say “we need to deploy these 8 services together”
💸 The Real Cost
Let’s talk about what your distributed monolith actually costs: Cognitive Load: Developers need to understand 20 services to change one line of code.
Debugging: That bug? It’s in one of 15 services. Good luck finding which one. Hope you like grep-ing through JSON logs.
Performance: Your monolith did this in 50ms. Your “microservices” do it in 2,000ms across 12 network calls.
Infrastructure: You’re paying for 20 separate deployments, service meshes, load balancers, and that engineer who does nothing but maintain Kubernetes configs.
Developer Happiness: There’s a reason your senior devs keep mumbling about “the old days” when things just worked.
🛠️ How to Fix It (Or Not)
Here’s the hard truth: Sometimes you shouldn’t fix it. Sometimes the answer is to go back to the monolith.
gasp
I said it. The monolith wasn’t actually your problem. Your problem was that you had a badly structured monolith, and the solution wasn’t to distribute it, it was to structure it better.
But if you’re committed to the microservices path, here’s how to do it right:
🧠 Actually Decouple Things
Bad: Services call each other synchronously in a chain.
UserService → EmailService → TemplateService → AuditService
Good: Services publish events and don’t care who’s listening.
UserService → Event Bus ← [EmailService, AuditService, AnalyticsService]
🗄️ Own Your Data
Each service gets its own database. Yes, even if it means data duplication. Yes, even if it feels wrong.
If OrderService needs user data, it either:
- Calls UserService’s API (and caches aggressively)
- Subscribes to UserUpdated events and stores what it needs locally
- Accepts userId as a foreign key and doesn’t need the data at all
🧍♂️ Make Services Actually Independent
A good microservice should be able to run in isolation. Not perfectly, but well enough that the rest of the system doesn’t collapse.
// Good: Graceful degradation
async function getUserRecommendations(userId) {
try {
return await recommendationService.get(userId);
} catch (error) {
logger.warn('Recommendation service down, using fallback');
return getPopularItems(); // Still works, just not personalized
}
}
🧭 Question Every Service Boundary
“Should this be a separate service?” is the wrong question. The right question is: “Does this have a reason to be deployed independently?”
Your “EmailService” doesn’t need to be separate just because it sends emails. It needs to be separate if:
- It might be scaled independently (you send way more emails than you do other things)
- It might be deployed independently (email template changes don’t require redeploying everything)
- It has genuinely different operational characteristics (it’s more fault-tolerant because emails can be queued)
Otherwise, it’s a module. And modules are great! They’re just... in the same deployment.
💬 The Uncomfortable Truth
Here’s what nobody wants to admit: For most applications, a well-structured monolith is better than badly designed microservices.
Microservices solve organizational problems (letting multiple teams work independently) and scale problems (scaling different parts of your system differently).
If you don’t have those problems, you don’t need microservices.
You know what you probably do need?
- Better code organization
- Clearer module boundaries
- More comprehensive testing
- Better deployment practices
And guess what? You can have all of that in a monolith. Without the network calls.
Without the distributed transactions. Without the 3 AM debugging sessions trying to figure out why the payment service can’t reach the user service through the API gateway after the load balancer update.
🧘 In Conclusion
If you can’t deploy your services independently, they’re not microservices.
- If they share a database, they’re not microservices.
- If they need to be deployed in a specific order, they’re not microservices.
- If one being down means the others can’t function, they’re not microservices.
You have a distributed monolith. And that’s okay. Admitting it is the first step.
The second step? Either fix it properly or go back to the monolith. Your future self will thank you. So will your AWS bill.
Next time: “Your Event-Driven Architecture Is Just a Distributed State Machine (And It’s Stuck)”