The Dark Side of Microservices: When They Make Your System Worse
Think microservices are always the answer? Learn about the dark side of microservices architecture and when to stick with a monolith.
Everyone's talking about microservices, but nobody's talking about when they're the wrong choice. Here's what they don't tell you about microservice architecture.
The Promise vs Reality
The Marketing
# How it's sold
services:
auth:
# "Independent deployment"
users:
# "Scale independently"
orders:
# "Team autonomy"
payments:
# "Technology freedom"
The Reality
# What you actually get
services:
auth:
depends_on: [users, db, cache, queue]
users:
depends_on: [auth, events, cache]
orders:
depends_on: [users, payments, inventory]
payments:
depends_on: [users, auth, orders]
# + 20 more interdependent services
Hidden Costs
1. Operational Complexity
// Monolith logging
console.log('User created:', user);
// Microservices logging
interface LogMessage {
service: string;
timestamp: string;
traceId: string;
spanId: string;
parentSpanId: string;
level: string;
message: string;
metadata: Record<string, any>;
}
class DistributedLogger {
async log(message: LogMessage) {
await this.enrichWithTracing(message);
await this.sendToLogAggregator(message);
await this.alertIfNeeded(message);
await this.correlateWithMetrics(message);
}
}
2. Debugging Nightmare
// Monolith error tracking
try {
await createOrder(orderData);
} catch (error) {
console.error('Order creation failed:', error);
}
// Microservices error tracking
interface ErrorContext {
traceId: string;
service: string;
timestamp: string;
path: string[];
failedService?: string;
failureReason?: string;
retryCount: number;
}
class DistributedErrorTracker {
async trackError(error: Error, context: ErrorContext) {
// Find which service actually failed
await this.identifyFailureSource(context);
// Track error propagation
await this.updateErrorPath(context);
// Notify relevant teams
await this.notifyTeams(context);
// Update service health metrics
await this.updateHealthMetrics(context);
// Trigger circuit breakers if needed
await this.evaluateCircuitBreakers(context);
}
}
Real World Problems
1. Distributed Transactions
// Monolith transaction
async function createOrder(orderData: OrderData) {
const transaction = await db.beginTransaction();
try {
const order = await db.orders.create(orderData);
await db.inventory.update(orderData.items);
await db.payments.process(orderData.payment);
await transaction.commit();
return order;
} catch (error) {
await transaction.rollback();
throw error;
}
}
// Microservices "transaction"
class OrderOrchestrator {
async createOrder(orderData: OrderData) {
const sagaId = uuid();
try {
// Start saga
await this.sagaLog.start(sagaId);
// Create order
const order = await this.orderService.create(orderData);
await this.sagaLog.step(sagaId, 'ORDER_CREATED');
// Update inventory
try {
await this.inventoryService.reserve(orderData.items);
await this.sagaLog.step(sagaId, 'INVENTORY_RESERVED');
} catch (error) {
await this.orderService.cancel(order.id);
throw error;
}
// Process payment
try {
await this.paymentService.process(orderData.payment);
await this.sagaLog.step(sagaId, 'PAYMENT_PROCESSED');
} catch (error) {
await this.inventoryService.release(orderData.items);
await this.orderService.cancel(order.id);
throw error;
}
// Complete saga
await this.sagaLog.complete(sagaId);
return order;
} catch (error) {
await this.sagaLog.fail(sagaId);
throw new Error(`Order creation failed: ${error.message}`);
}
}
}
2. Service Discovery
// Monolith: Simple function call
const user = userService.getUser(userId);
// Microservices: Complex service discovery
class ServiceDiscovery {
private registry: Map<string, ServiceInstance[]> = new Map();
async findService(name: string): Promise<ServiceInstance> {
const instances = await this.registry.get(name);
if (!instances?.length) {
throw new Error(`Service ${name} not found`);
}
// Load balancing
const instance = this.loadBalancer.select(instances);
// Health check
if (!await this.healthCheck(instance)) {
await this.removeInstance(name, instance);
return this.findService(name);
}
return instance;
}
async call(service: string, method: string, data: any) {
const instance = await this.findService(service);
const circuit = await this.circuitBreaker.get(service);
if (!circuit.isAllowed()) {
throw new Error(`Circuit breaker open for ${service}`);
}
try {
const result = await this.makeRequest(instance, method, data);
circuit.recordSuccess();
return result;
} catch (error) {
circuit.recordFailure();
throw error;
}
}
}
When to Stay Monolithic
1. Team Size
// Small team reality
interface TeamCapacity {
developers: number;
devops: number;
qa: number;
}
function shouldConsiderMicroservices(team: TeamCapacity): boolean {
return (
team.developers > 20 &&
team.devops >= 2 &&
team.qa >= 3
);
}
2. Business Complexity
interface BusinessMetrics {
monthlyUsers: number;
dailyTransactions: number;
dataVolume: number; // GB
deploymentFrequency: number; // per week
}
function needsMicroservices(metrics: BusinessMetrics): boolean {
return (
metrics.monthlyUsers > 100000 ||
metrics.dailyTransactions > 10000 ||
metrics.dataVolume > 1000 ||
metrics.deploymentFrequency > 10
);
}
Cost Comparison
Infrastructure Costs
interface InfrastructureCosts {
compute: number;
storage: number;
network: number;
monitoring: number;
maintenance: number;
}
// Monolith
const monolithCosts: InfrastructureCosts = {
compute: 1000, // 2-3 servers
storage: 200, // Single database
network: 100, // Simple networking
monitoring: 200, // Basic monitoring
maintenance: 500 // One system
};
// Microservices
const microservicesCosts: InfrastructureCosts = {
compute: 5000, // Multiple services
storage: 1000, // Multiple databases
network: 1000, // Complex networking
monitoring: 2000, // Distributed monitoring
maintenance: 3000 // Multiple systems
};
Best Practices
1. Start Monolithic
// Begin with clean architecture
class OrderService {
private readonly repository: OrderRepository;
private readonly paymentService: PaymentService;
async createOrder(data: OrderData): Promise<Order> {
// Clean interfaces make future splitting easier
const order = await this.repository.create(data);
await this.paymentService.process(order);
return order;
}
}
2. Modular Monolith
// Organize by bounded contexts
src/
├── orders/
│ ├── domain/
│ ├── application/
│ └── infrastructure/
├── payments/
│ ├── domain/
│ ├── application/
│ └── infrastructure/
└── shared/
├── domain/
└── infrastructure/
Warning Signs
1. Communication Overhead
// Before microservices: Simple method call
const result = await orderService.createOrder(data);
// After microservices: Complex communication
const result = await axios.post(
`${await serviceDiscovery.resolve('orders')}/api/v1/orders`,
data,
{
headers: {
'X-Trace-ID': generateTraceId(),
'X-Service-Name': 'web-api',
'Authorization': `Bearer ${await getServiceToken()}`
},
timeout: 5000,
retry: 3
}
);
2. Data Consistency Issues
// Monolith: ACID transactions
await db.transaction(async (tx) => {
await tx.orders.create(orderData);
await tx.inventory.update(itemData);
await tx.payments.process(paymentData);
});
// Microservices: Eventually consistent
await eventBus.publish('OrderCreated', orderData);
// Hope all services eventually process it correctly
// Handle compensation if something fails
// Deal with duplicate events
// Handle out-of-order events
Conclusion
Consider microservices when:
- Team is large enough
- Business complexity demands it
- Infrastructure budget exists
- Operational expertise available
Stay monolithic when:
- Small team
- Simple business domain
- Budget constraints
- Limited operational resources
Remember: Microservices are not a goal, they're a tool.