TL;DR
- Microservices only pay off when they remove a real scaling or coordination bottleneck.
- Bounded contexts, outbox, idempotency, and observability are usually more valuable than any service-mesh slide.
- I prefer fewer, better-bounded services over a large number of thin services with unclear ownership.
- Principal-level microservices work is as much about team boundaries and failure containment as it is about APIs and code.
Context
Microservices create leverage when teams need independent deployability, targeted scaling, or clearer domain isolation. They create pain when they are adopted to feel modern or when a modular monolith would have solved the real problem more cheaply.
The question I care about is not “should we use microservices?” It is “what coupling are we removing, and what new operational complexity are we willing to own?”
The Approach
1. Start with bounded contexts, not technical slicing
I want each service to have:
- clear domain ownership
- its own write model
- explicit upstream and downstream dependencies
- a contract that can evolve without weekly coordination
2. Use sync for user-critical commands, async for propagation
My common pattern is:
- accept the user-critical command
- persist source-of-truth data
- publish a domain event
- let other services react independently
That keeps the latency-sensitive path simple while still allowing other domains to update without becoming part of one giant distributed transaction.
3. Outbox before dual-write pain arrives
If a service writes to a database and emits an event, I do not want those two actions drifting apart.
The outbox pattern gives me:
- transactional safety at write time
- asynchronous publication afterward
- cleaner replay and recovery behavior
4. Use sagas and compensations instead of distributed transactions
For multi-step workflows, I design:
- compensating actions
- explicit state transitions
- timeout handling
- replay-safe commands
I assume retries and partial failure are normal, not exceptional.
5. Make resilience and observability part of the architecture
The system is incomplete without:
- trace propagation
- per-service SLIs and SLOs
- retry and timeout policy
- queue-depth or consumer-lag visibility
- dead-letter and replay strategy
6. Avoid the distributed monolith trap
The anti-pattern I look for is a fleet of services that:
- deploy together
- share databases
- require every feature to touch many repos
- fail together anyway
That is not service architecture maturity. It is monolith complexity plus network overhead.
Trade-offs
- More services improve isolation, but they also multiply deployment, observability, and contract-management cost.
- Async workflows decouple teams well, but they are harder to debug without trace and event lineage.
- Outbox and saga patterns improve safety, but they add implementation and operational overhead.
- A modular monolith is often the better starting point until the team and domain boundaries are real.
References
- Related: Microservices Patterns
- Related: Monorepo Architecture
- Related: Event-Driven Architecture
- Related: Consistency: At-most / Exactly-once
- Microservices.io Patterns