Microservices Best Practices: A Guided Track
A practical track on service boundaries, data ownership, communication, resilience, observability and deployment for teams running microservices.
Microservices split an application into small, independently deployable services organised around business capabilities. Done well, they let teams release independently and scale components separately. Done poorly, they produce a distributed monolith: all the coupling of the old system plus network failures. The practices below focus on the decisions that most often separate the two outcomes.
Learning goals
- Draw service boundaries around business capabilities.
- Apply data ownership and choose communication styles deliberately.
- Build resilience into every remote call.
- Establish the observability and delivery foundations that microservices require.
Week 1: Boundaries and ownership
Good boundaries minimise the need for services to change together. Use business capabilities or bounded contexts from domain-driven design rather than technical layers. A "database service" or "validation service" is a warning sign.
Test a boundary with these questions:
- Can one team own it end to end, including data and on-call?
- Do most changes stay within one service?
- Can it be deployed without coordinating with others?
If a typical feature requires edits in four services, the boundaries are wrong, or the system is too fragmented for the team size. As a rule of thumb, you need enough engineers and operational maturity per service to justify its existence.
Exercise: take a feature backlog of 20 recent tickets and count how many services each would touch under your proposed boundaries.
Week 2: Data and communication
Each service owns its data. Other services access it only through its API or events, never through shared tables. Shared databases reintroduce coupling that prevents independent change.
Choose communication style per interaction:
| Style | Use for | Cost | |---|---|---| | Synchronous HTTP or gRPC | Queries needing an immediate answer | Runtime coupling; availability multiplies | | Asynchronous events | Notifying others of facts | Eventual consistency, harder tracing | | Asynchronous commands via queue | Work that can be deferred | Needs idempotency, retries, dead letters |
Availability compounds: five synchronous dependencies each at 99.9 percent give roughly 99.5 percent combined. Minimise synchronous call chains.
For multi-service business transactions, avoid distributed two-phase commit. Use sagas: a sequence of local transactions with compensating actions on failure.
ReserveStock -> ChargePayment -> CreateShipment
on payment failure: ReleaseStock
Exercise: design a saga for placing an order, listing compensations and how each step is made idempotent.
Week 3: Resilience
Assume every remote call will fail or be slow. For each one define:
- Timeouts, always, since the default is often infinite.
- Retries with exponential backoff and jitter, limited by a retry budget so retries do not amplify an outage.
- Circuit breakers to stop calling a failing dependency and allow recovery.
- Bulkheads to isolate resource pools, so one slow dependency cannot exhaust all threads.
- Fallbacks or degraded modes, such as serving cached recommendations.
Make operations idempotent so retries are safe, using idempotency keys on writes.
Exercise: inject latency into one dependency in a test environment and observe what fails. Fix the first cascading failure you find.
Week 4: Observability and delivery
You cannot debug what you cannot see across processes.
- Structured logs with a correlation id propagated across services.
- Metrics for the golden signals: latency, traffic, errors, saturation.
- Distributed tracing to follow a request across services.
- Service level objectives tied to user experience, with alerts on burn rate.
On the delivery side, each service needs its own automated pipeline, health checks and safe rollout such as canary or blue-green. Version APIs and keep changes backward compatible, using consumer-driven contract tests to detect breakage before deployment.
Exercise: trace one user request end to end and write down every service it touched, its latency contribution and what you could not see.
Anti-patterns
- Distributed monolith: services deploy together or call each other in long chains.
- Shared database across services.
- Too many tiny services for a small team, producing operational overhead exceeding benefit.
- No platform support: each team reinventing deployment and monitoring.
- Starting with microservices before understanding the domain, so boundaries are guessed and costly to move.
Key takeaways
- Draw boundaries around business capabilities, with team ownership of code and data.
- Never share databases; communicate through APIs and events.
- Minimise synchronous chains and use sagas instead of distributed transactions.
- Apply timeouts, bounded retries, circuit breakers and idempotency to every remote call.
- Invest in tracing, metrics, logging and automated delivery before scaling out service count.
- Count the operational cost honestly; many systems are better served by a modular monolith.