Monoliths vs Microservices: The Boring Truth
The False Dichotomy
The internet will tell you that monoliths are legacy and microservices are the future. Reality is messier. After building both at scale, here's what actually matters.
When Monoliths Win
Start with a monolith. Always. Here's why:
1. Faster Development
You don't need service discovery, API gateways, or distributed tracing just to ship a feature. Everything is in one codebase:
// Monolith: Change two files, one commit
class UserService {
async createUser(data) {
const user = await db.users.create(data);
await this.emailService.sendWelcome(user); // Direct call
return user;
}
}
// Microservices: Change two repos, coordinate deployment
// user-service/createUser.ts
const user = await db.users.create(data);
await messageQueue.publish('user.created', user);
// email-service/handlers/userCreated.ts
await emailClient.send(welcomeEmail);
2. Atomic Transactions
Database transactions just work. No distributed sagas, no eventual consistency headaches:
// Monolith: Simple & reliable
await db.transaction(async (trx) => {
await trx.orders.create(order);
await trx.inventory.decrement(productId);
await trx.payments.charge(userId, amount);
});
// Microservices: Complex orchestration
try {
const order = await orderService.create(orderData);
try {
await inventoryService.reserve(productId);
try {
await paymentService.charge(userId, amount);
} catch (e) {
await inventoryService.release(productId); // Rollback
throw e;
}
} catch (e) {
await orderService.cancel(order.id); // Rollback
throw e;
}
} catch (e) {
// Handle failure
}
3. Easier Debugging
Stack traces that actually make sense. One log file to check. No network issues to debug.
When to Split
Microservices solve organizational problems, not technical ones. Consider splitting when:
1. Team Boundaries Are Clear
If you have separate teams for payments, inventory, and shipping, microservices let them deploy independently. But if everyone touches everything, you've just made coordination harder.
2. Scale Requirements Differ
Your search service needs 50 instances during the day but your background job processor needs 5. With a monolith, you scale everything together.
// Different scaling needs
# Search: CPU intensive, bursty
searchService:
replicas: 50
resources:
cpu: "2"
memory: "4Gi"
autoscaling: true
# Jobs: I/O intensive, steady
jobProcessor:
replicas: 5
resources:
cpu: "0.5"
memory: "2Gi"
autoscaling: false
3. Technology Needs Differ
Your ML model runs on Python with GPUs. Your API is Node.js. Your data pipeline is Go. Microservices let you use the right tool for each job.
The Middle Ground: Modular Monolith
You can have both. Organize code into modules with clear boundaries, but deploy as one unit:
// Clear module boundaries
src/
modules/
users/
service.ts
repository.ts
types.ts
orders/
service.ts
repository.ts
types.ts
payments/
service.ts
repository.ts
types.ts
// Enforce boundaries with linting
// orders can import users, but not payments
"@nx/enforce-module-boundaries": [
"error",
{
"allow": [],
"depConstraints": [
{
"sourceTag": "scope:orders",
"onlyDependOnLibsWithTags": ["scope:users"]
}
]
}
]
Real Migration Story
At TrekStone, we started with a monolith. At 50K users, we extracted the payment service because:
- PCI compliance required isolation
- It had different uptime requirements (99.99% vs 99.9%)
- The team was separate and deployed on different cycles
Everything else stayed in the monolith. Result: 80% faster feature development than if we'd gone full microservices.
The Hidden Costs of Microservices
Infrastructure
- Service mesh (Istio, Linkerd)
- API gateway
- Message queue
- Distributed tracing (Jaeger, Zipkin)
- Service discovery
- Centralized logging
Development Overhead
- Running 10 services locally
- Coordinating deployments
- Versioning APIs
- Testing across services
- Debugging network issues
Decision Framework
// Monolith when:
- Team < 10 engineers
- < 100K active users
- All features are tightly coupled
- Speed of development matters most
// Microservices when:
- Team > 50 engineers
- Clear service boundaries
- Different scaling/availability needs
- Polyglot requirements
- Team autonomy matters
Practical Advice
- Start monolith - You can always split later
- Keep modules clean - Make future extraction easy
- Extract strategically - Only split when you feel real pain
- Don't split just to split - Each service has a cost
- Measure before splitting - Have data, not assumptions
Conclusion
Architecture is about trade-offs, not best practices. Monoliths are simpler and faster for most teams. Microservices solve specific scaling and organizational problems.
Choose based on your constraints, not what's trendy. A well-built modular monolith beats a poorly architected microservices system every time.