Microservices: When to Use Them and When to Avoid
I've seen startups adopt microservices way too early and struggle for months with infrastructure complexity. I've also seen mature companies stuck with monoliths that took hours to deploy. Here's a realistic guide.
The Microservices Promise
- Independent deployment
- Technology flexibility
- Team autonomy
- Isolated failures
The Hidden Costs
What nobody tells you upfront:
- Network complexity - Remote calls fail in ways function calls don't
- Data consistency - Transactions across services are hard
- Debugging - Tracing issues across 10 services
- DevOps overhead - Each service needs CI/CD, monitoring, logs
When Microservices Make Sense
✅ Large team (50+ developers) ✅ Multiple teams working on different features ✅ Diverse technology requirements ✅ Different scaling needs per feature ✅ Already have DevOps expertise
When to Stick with Monolith
✅ Small team (< 15 developers) ✅ New product still evolving ✅ Limited DevOps resources ✅ Tight timeline ✅ Simple deployment needs
The Modular Monolith
Best of both worlds for many teams:
src/
├── modules/
│ ├── auth/
│ │ ├── controllers/
│ │ ├── services/
│ │ ├── models/
│ │ └── routes.js
│ ├── orders/
│ ├── payments/
│ └── notifications/
├── shared/
│ ├── database/
│ ├── utils/
│ └── middleware/
└── app.js
Rules:
- Modules can only import from shared
- Modules communicate through defined interfaces
- Each module owns its database tables
If You Do Go Microservices
Service Communication
Synchronous (REST/gRPC)
- Simple request-response
- Tight coupling
- Cascading failures risk
// Order service calls Payment service
const payment = await fetch('http://payment-service/charge', {
method: 'POST',
body: JSON.stringify({ orderId, amount })
});
Asynchronous (Message Queue)
- Loose coupling
- Better resilience
- Eventual consistency
// Order service publishes event
await messageQueue.publish('order.created', {
orderId: order.id,
userId: order.userId,
total: order.total
});
// Payment service subscribes
messageQueue.subscribe('order.created', async (event) => {
await processPayment(event.orderId, event.total);
});
Database Per Service
Each service owns its data:
order-service → orders-db (PostgreSQL)
product-service → products-db (MongoDB)
search-service → elasticsearch
analytics-service → ClickHouse
API Gateway
Single entry point for clients:
Client → API Gateway → Auth Service
→ Order Service
→ Product Service
Migration Strategy
Don't rewrite. Extract gradually:
- Identify bounded contexts
- Extract one service at a time
- Start with lowest-risk module
- Keep monolith running during transition
- Use feature flags for rollback
Essential Infrastructure
You'll need:
- Container orchestration (Kubernetes)
- Service discovery
- Centralized logging (ELK/Loki)
- Distributed tracing (Jaeger)
- Metrics (Prometheus/Grafana)
- Message queue (RabbitMQ/Kafka)
If this list seems overwhelming, you might not be ready for microservices.
Final Advice
Start with a monolith. Extract services when you feel the pain. The best architecture is the one your team can actually operate.