- Published on
Tight Coupling Between Services — When Changing One Service Breaks Five Others
- Authors

- Name
- Sanjeev Sharma
- @webcoderspeed1
Introduction
Microservices are supposed to be independently deployable. If you have to deploy three services together to ship a feature, you don't have microservices — you have a distributed monolith with all the downsides of distribution and none of the benefits. Tight coupling between services is the most common mistake teams make after splitting a monolith.
- What Tight Coupling Looks Like
- Fix 1: Async Events Instead of Sync Calls
- Fix 2: Define Stable Service Contracts
- Fix 3: Circuit Breakers on All Service Calls
- Fix 4: Avoid Circular Dependencies with Dependency Inversion
- Service Coupling Checklist
- Conclusion
What Tight Coupling Looks Like
❌ Distributed monolith:
User API → synchronously calls → Order Service
Order Service → synchronously calls → Inventory Service
Order Service → synchronously calls → Payment Service
Payment Service → synchronously calls → User API (circular!)
Problems:
- Chain of synchronous calls: one service down = all down
- Deploy order matters (Payment must deploy before Order)
- API contract changes in User API break Order and Payment
- Latency stacks: 50ms + 80ms + 60ms + 40ms = 230ms minimum
- Circular dependency makes reasoning about the system impossible
Fix 1: Async Events Instead of Sync Calls
// ❌ Synchronous chain — fragile
class OrderService {
async createOrder(data: CreateOrderDto) {
// Synchronously call 3 other services
const user = await userServiceClient.getUser(data.userId) // 50ms
const inventory = await inventoryClient.reserve(data.items) // 80ms
const payment = await paymentClient.charge(data.total) // 200ms
// If ANY of these fail, the whole order fails
// If payment service is slow, user waits 330ms+ just for coordination overhead
return this.ordersRepo.create({ ...data, payment })
}
}
// ✅ Async events — resilient
class OrderService {
async createOrder(data: CreateOrderDto) {
// Only do what ORDER service owns
const order = await this.ordersRepo.create({
...data,
status: 'pending',
})
// Publish event — don't wait for downstream services
await eventBus.publish('order.created', {
orderId: order.id,
userId: data.userId,
items: data.items,
total: data.total,
})
return order // Return immediately, 5ms response time
// Inventory, payment, notifications handle asynchronously
}
}
Fix 2: Define Stable Service Contracts
// ❌ Services share internal data structures — any field rename breaks everyone
// In user-service: returns full internal User model
interface User {
id: string
internalDbId: number // internal detail leaked!
firstName: string
lastName: string
emailAddress: string // later renamed to email
passwordHash: string // security sensitive!
createdTimestamp: Date // later renamed to createdAt
}
// ✅ Publish a stable public API contract (versioned)
// user-service public API v1 — never changes without versioning
interface UserPublicV1 {
id: string
fullName: string // stable name
email: string // stable name
createdAt: string // ISO 8601 — stable format
// Internal fields never exposed
}
// API versioning
app.get('/v1/users/:id', getV1User) // stable forever
app.get('/v2/users/:id', getV2User) // new version when contract must change
// Old version deprecated, not removed, for 6+ months
app.get('/v1/users/:id', async (req, res) => {
res.setHeader('Deprecation', 'version="v1", date="2026-09-01"')
res.setHeader('Sunset', 'Tue, 01 Sep 2026 00:00:00 GMT')
return getV1User(req, res)
})
Fix 3: Circuit Breakers on All Service Calls
import CircuitBreaker from 'opossum'
// Every service-to-service call gets a circuit breaker
const userServiceBreaker = new CircuitBreaker(
(userId: string) => userServiceClient.getUser(userId),
{
timeout: 3000, // fail after 3s
errorThresholdPercentage: 50, // open after 50% error rate
resetTimeout: 30000, // try again after 30s
}
)
userServiceBreaker.fallback((userId: string) => {
// Return cached data or a degraded response — don't fail the whole request
return { id: userId, fullName: 'Unknown User', email: '' }
})
// Now order-service can degrade gracefully if user-service is down
const user = await userServiceBreaker.fire(userId)
Fix 4: Avoid Circular Dependencies with Dependency Inversion
❌ Circular:
Order Service → calls → User Service → calls → Order Service
✅ Fix via events:
Order Service → publishes 'order.created' event
User Service → subscribes to 'order.created', updates own data
No circular call — both services are autonomous
Service Coupling Checklist
- ✅ Services communicate via events/messages for non-query operations
- ✅ Each service owns its own data — no service reads another's database directly
- ✅ Service APIs are versioned — breaking changes need a new version, not an edit
- ✅ All inter-service calls have circuit breakers with fallback behavior
- ✅ No deploy requires coordinating multiple services at the same time
- ✅ Any service can be deployed, rolled back, or restarted independently
Conclusion
Tight coupling in microservices is worse than tight coupling in a monolith — at least the monolith is in the same process and shares memory. Distributed tight coupling combines the complexity of a distributed system with the rigidity of a monolith. The fix is to communicate through events for operations, define stable versioned contracts for queries, and add circuit breakers so one service's failure doesn't cascade to all callers. Each service should be deployable and restartable without touching any other.