Microservices Best Practices That Actually Matter in Production
Microservices are easy to describe and difficult to operate.
The basic diagram looks clean:
API Gateway
├── Orders
├── Payments
├── Inventory
└── Notifications
Production is where the hidden cost appears: network failures, duplicated messages, incompatible API changes, shared database coupling, retry storms, missing traces, synchronized deployments, and teams that technically own services but cannot change them independently.
The best microservices practices are therefore not about maximizing the number of services. They are about preserving independence, clear ownership, bounded failure, and operational visibility.
1. Give Every Service One Clear Business Responsibility
A service should represent a meaningful business capability, not a random technical layer.
Better boundaries:
orders
payments
inventory
billing
identity
Weak boundaries:
validation-service
helper-service
sql-service
common-service
Google Cloud's microservices guidance recommends independently owned services with explicit APIs and private data boundaries. The practical reason is simple: if changing one service requires coordinated edits across many others, the boundary is not really independent.
A useful question is:
> What business decision is this service responsible for?
If the answer is unclear, the service may be too small, too generic, or incorrectly split.
2. Assign One Owning Team
Every production service needs clear ownership.
The owner should be responsible for:
- code
- API/event contracts
- deployments
- dashboards
- alerts
- runbooks
- data migrations
- operational incidents
Shared ownership often becomes no ownership.
A service can have contributors from several teams, but someone must remain accountable for its production behavior.
3. Keep Service Data Private
One of the fastest ways to create a distributed monolith is allowing multiple services to read and write each other's tables.
Bad:
Orders ───────┐
Payments ─────┼──> shared business tables
Inventory ────┘
Prefer clear ownership:
Orders → order tables/schema
Payments → payment tables/schema
Inventory → inventory tables/schema
Google Cloud explicitly recommends that each service own its database schema and that other services communicate through public APIs instead of direct database access.
This does not require one database server per service. Multiple services can share the same physical cluster while owning separate schemas/tables and credentials.
The boundary is logical ownership, not infrastructure theater.
4. Do Not Leak Database Schemas Through APIs
A service's public contract should represent business concepts rather than exposing raw persistence models.
Avoid:
{
"orders_table_row": { ... }
}
Prefer stable business contracts:
{
"order_id": "ord_123",
"status": "confirmed",
"total": 1499
}
This allows the service to change indexes, columns, storage engines, or internal schema without forcing every consumer to change.
5. Keep APIs Backward Compatible
Independent deployment only works if producers and consumers can run different versions temporarily.
Safer API evolution usually means:
- add optional fields instead of changing existing meanings
- avoid removing fields immediately
- tolerate unknown response fields
- version genuinely breaking contracts
- test old and new consumers during migrations
The same rule applies to events.
A deployment should not require fifteen services to restart in a precise order.
6. Choose Synchronous Communication Deliberately
HTTP or gRPC is appropriate when the caller needs an immediate answer.
But every synchronous dependency adds another availability and latency dependency to the user request.
Client
↓
Orders
↓
Inventory
↓
Pricing
↓
Customer
If one dependency slows down, the whole chain slows down.
Use direct calls when they simplify a genuinely synchronous workflow. Do not turn every internal interaction into RPC by default.
7. Use Messaging for Work That Does Not Need an Immediate Response
Asynchronous messaging is useful when the caller can continue without waiting.
Example:
OrderConfirmed
├── send receipt
├── update analytics
├── notify warehouse
└── update loyalty points
AWS guidance highlights two major benefits: asynchronous systems can isolate failures and absorb traffic spikes because consumers process work according to their own capacity.
But messaging introduces new responsibilities:
- duplicates
- retries
- ordering
- dead-letter handling
- tracing
- schema evolution
Use it because the workflow benefits from decoupling—not because event-driven architecture sounds modern.
8. Make Consumers Idempotent
Distributed systems retry.
A message can be delivered twice. A client can retry after a timeout. A worker can crash after completing an action but before acknowledging it.
Therefore this event:
PaymentCaptured(payment_456)
must not capture the same payment twice.
Use mechanisms such as:
- idempotency keys
- unique constraints
- processed-event records
- compare-and-set state changes
Design for at-least-once delivery and make duplicate execution harmless where possible.
9. Put Timeouts on Every Remote Call
A request without a timeout can wait forever while consuming resources.
Every remote dependency should have an explicit deadline:
HTTP request
DB query
cache request
queue publish
external API
Timeouts should reflect the user-facing latency budget, not arbitrary defaults copied from a library.
A service with a 2-second request budget should not make three downstream calls that each wait 5 seconds.
10. Retry Only Errors That Are Actually Retryable
Retrying can improve resilience—or create an outage amplifier.
A retry policy should include:
retryable error classification
+ exponential backoff
+ jitter
+ maximum attempts
+ overall deadline
Do not retry:
- validation errors
- authentication failures
- permanent business conflicts
- unsupported requests
And never retry non-idempotent side effects blindly.
11. Avoid Retry Multiplication
Suppose:
frontend retries 3 times
API retries 3 times
service retries database 3 times
One original request can create many downstream attempts during an incident.
Choose where retries belong and keep budgets bounded across the call chain.
Often it is better for one layer to own retries while other layers fail quickly.
12. Use Circuit Breaking and Load Shedding Where They Solve a Real Failure Mode
If a dependency is clearly unhealthy, continuing to send unlimited traffic to it can worsen recovery.
A circuit breaker can temporarily stop calls and allow controlled probes.
Load shedding can reject lower-priority work when the service is saturated.
These patterns are useful when overload and cascading failure are realistic risks, but they should not be added automatically to every internal call.
Complex resilience logic can itself become hard to debug.
13. Preserve Correlation IDs Across Boundaries
A request may cross several services and queues.
Without correlation, debugging becomes a log-search scavenger hunt.
Propagate identifiers such as:
trace_id
request_id
correlation_id
event_id
through synchronous and asynchronous boundaries.
OpenTelemetry provides standard tracing, metrics, and logging instrumentation designed for distributed systems.
14. Instrument Dependencies, Not Just Services
A dashboard saying:
Orders API p95 = 1.8 s
is not enough.
You need to know where the time went:
API handler 20 ms
Inventory RPC 110 ms
Payment RPC 900 ms
Database query 700 ms
AWS Well-Architected guidance explicitly recommends application telemetry, dependency telemetry, and distributed tracing.
Monitor the boundaries where failure propagates.
15. Centralize Logs but Keep Them Structured
Centralized logs are useful only when they can be searched reliably.
Prefer structured fields:
{
"service": "payments",
"trace_id": "abc123",
"order_id": "ord_9",
"event": "payment_capture_failed",
"error_code": "provider_timeout"
}
Avoid relying entirely on free-form strings.
Also avoid logging secrets, credentials, raw payment data, or unnecessary personal information.
16. Define SLOs for User-Visible Behavior
Infrastructure health should connect to product behavior.
Examples:
99.9% checkout success rate
p95 order creation < 500 ms
99% async invoices complete < 2 min
An internal service can technically be “up” while still violating what users expect.
Use SLOs to decide which reliability work actually matters.
17. Autoscale on Meaningful Metrics
CPU is useful for some workloads and misleading for others.
Examples:
HTTP service → CPU / request rate / latency
queue worker → queue depth / oldest message age
WebSocket service → active connections
CPU-heavy worker → CPU
The metric should correlate with saturation.
Otherwise autoscaling can add replicas without relieving the real bottleneck.
18. Protect Shared Dependencies From Autoscaling
Scaling from 10 to 100 service replicas may create ten times more:
- DB connections
- cache connections
- broker connections
- outgoing API requests
Application autoscaling can overload dependencies even while the application layer appears healthy.
Connection pools, concurrency limits, admission control, and downstream capacity must be part of the scaling plan.
19. Use an API Gateway for Edge Concerns, Not Business Logic
An API gateway is useful for cross-cutting concerns such as:
- authentication
- routing
- TLS termination
- rate limiting
- request limits
- observability
AWS guidance also highlights central security and monitoring as core API-management benefits.
But do not turn the gateway into the place where business workflows live.
A gateway should route and enforce edge policy. Domain logic should remain with domain services.
20. Keep Internal Service Authentication Explicit
A private network does not automatically mean a trusted request.
Depending on risk, use:
- workload identity
- mTLS
- signed service credentials
- short-lived tokens
- scoped permissions
Service-to-service authorization should answer:
Who is calling?
What are they allowed to do?
Which tenant/resource is involved?
Do not assume any process inside the network can call every administrative endpoint.
21. Make Configuration External and Versioned
Do not bake environment-specific values into service binaries.
Keep operational configuration such as:
endpoints
feature flags
timeouts
queue names
rate limits
outside application code where practical.
Secrets deserve a dedicated secret-management mechanism, not normal config files or environment dumps in logs.
22. Use Health Checks Correctly
A liveness check answers:
> Should this process be restarted?
A readiness check answers:
> Should this instance receive traffic?
Do not make liveness depend on every downstream service. If the database is temporarily unavailable and all service instances restart continuously, the outage becomes worse.
Health checks should represent the exact condition the orchestrator can safely act on.
23. Deploy With Compatibility, Canary, and Rollback in Mind
A good deployment process includes:
- automated tests
- immutable artifacts
- health/readiness gates
- rolling or progressive rollout
- rollback path
- database migration compatibility
For risky changes, canary a small percentage of traffic and compare error rate/latency before expanding.
Independent services only help when deployments are independently safe.
24. Use Expand-and-Contract Database Migrations
Breaking schema changes are dangerous when multiple application versions may run simultaneously.
Instead of:
drop old column
→ deploy new code
prefer:
1. add new schema shape
2. deploy code that supports both
3. backfill/migrate data
4. switch reads/writes
5. verify
6. remove old shape later
Backward-compatible migrations reduce deployment coupling.
25. Do Not Create a Service for Every Team or Every Table
A microservice should exist because its boundary is useful, not because the org chart has another team.
Too many tiny services create:
- more network hops
- more deployment units
- more dashboards
- more API contracts
- more failure modes
A modular monolith is often healthier when boundaries are still evolving.
Extract services when independent scaling, ownership, isolation, or deployment genuinely justify the cost.
26. Choose Choreography vs Orchestration Per Workflow
For event-driven workflows, there are two common styles.
Choreography
Services react to events independently.
Useful when:
- coupling should remain low
- reactions are independent
- no central workflow state is needed
Orchestration
A coordinator explicitly controls a multi-step process.
Useful when:
- ordering matters
- compensating actions matter
- operators need one place to see workflow state
Google Cloud and AWS both document this trade-off. Neither is universally superior.
Choose based on the business process.
27. Avoid Shared “Common” Libraries That Recreate Tight Coupling
Shared libraries are useful for stable infrastructure concerns such as:
- telemetry
- authentication primitives
- protocol clients
They become harmful when business rules are copied into a shared package that every service must upgrade together.
If a business capability belongs to one domain, keep ownership there and expose a contract.
28. Document Failure Semantics in API Contracts
Consumers should know more than request and response fields.
Document:
- timeouts
- retryable status codes
- idempotency behavior
- eventual consistency
- ordering guarantees
- pagination
- rate limits
A robust API contract includes operational semantics, not just JSON schemas.
29. Test the System Under Partial Failure
Normal unit tests will not reveal many distributed-system problems.
Test scenarios such as:
dependency is slow
queue is unavailable
message is duplicated
DB becomes read-only
service restarts mid-request
network call times out after remote side effect
old and new service versions run together
The objective is not chaos for its own sake. It is verifying the failure behavior you claim to support.
30. Keep the Architecture as Simple as Possible
Microservices already create distributed-system complexity.
Do not add a service mesh, event bus, CQRS, event sourcing, orchestration engine, and five databases merely because those tools are associated with microservices.
Add each component only when it solves a demonstrated problem.
The best microservices architecture often has fewer moving parts than the architecture diagram you started with.
Production Checklist
- Every service owns a coherent business capability
- Every service has a clear owning team
- Data ownership is private and explicit
- APIs/events remain backward compatible during rollout
- Remote calls use appropriate timeouts
- Retries are bounded, jittered, and safe
- Consumers are idempotent
- Async messaging is used only where it benefits the workflow
- Correlation/tracing crosses service and queue boundaries
- Logs are structured and privacy-aware
- User-facing SLOs exist
- Autoscaling uses meaningful pressure signals
- Shared databases/brokers are protected from replica explosions
- Health checks represent correct liveness/readiness semantics
- Database migrations support mixed application versions
- Deployments can canary and roll back
- Service-to-service authorization is explicit where required
- Failure modes are tested
- New infrastructure is added only for a demonstrated need
Final Takeaway
Microservices succeed when services can change independently, fail predictably, and remain understandable in production.
The practices that matter most are boring but powerful: clear ownership, private data, stable contracts, timeouts, idempotency, bounded retries, meaningful observability, safe migrations, and deliberate communication patterns.
Get those fundamentals right before adding more infrastructure. A small set of well-owned services with disciplined boundaries will outperform a sprawling distributed architecture whose teams cannot explain how requests, data, and failures move through the system.
References
BUILDING A SAAS PRODUCT?
Explore Lofingo’s end-to-end SaaS platforms, multi-tenant cloud architecture, and high-concurrency backend engineering.
EXPLORE SAAS SERVICES →