Microservices Best Practices for Production Systems

By Oleksandr Andrushchenko — Published on
0 Likes
0 Dislikes
Microservices Best Practices for Production Systems
Microservices Best Practices for Production Systems

Running microservices in production requires more than splitting an application into independently deployed services. The architecture introduces network failures, partial availability, distributed data, asynchronous workflows, deployment coordination, and significantly more operational state.

Building Microservices
Building Microservices

Successful systems use microservices to create clear ownership and independent change while deliberately controlling the distributed complexity that follows. The most important practices focus on service boundaries, communication, data ownership, resilience, observability, deployments, and capacity rather than the number of services.

Table of Contents

Design Services for Independent Change

The primary architectural value of microservices is independent change. A service should own a meaningful business capability and allow its implementation, schema, capacity, and deployment schedule to evolve without requiring unrelated services to change at the same time.

If every release requires coordinated deployments across five services, the architecture has distributed the code without creating meaningful autonomy.

Keep Service Boundaries Cohesive

Services should be organized around cohesive business capabilities rather than database tables, framework layers, or arbitrary size targets.

Poor decomposition

Order Service
    |
    +--> Order Status Service
    +--> Order Item Service
    +--> Order Validation Service
    +--> Order Address Service


Cohesive boundary

Order Service
    |
    +-- Create order
    +-- Validate order
    +-- Modify order
    +-- Cancel order
    +-- Manage order lifecycle

The first design creates network calls between responsibilities that frequently participate in the same business operation. The second keeps strongly related behavior together and uses remote communication only when responsibility genuinely crosses a domain boundary.

Service size is therefore a consequence of cohesion, not a target. A larger service with one clear responsibility is often healthier than several tiny services that cannot operate independently.

Domain-Driven Design provides useful tools for identifying these boundaries through bounded contexts, aggregates, and business capabilities. For a deeper explanation, see Defining Service Boundaries with Domain-Driven Design.

Assign Clear Ownership

Every service should have clear ownership of its code, APIs, business rules, data, operational dashboards, alerts, and production behavior. Ambiguous ownership creates shared dependencies that nobody can safely evolve.

Ownership should also apply to business state:

State Authoritative Service Other Services
Order lifecycle Ordering Consume APIs or events
Available inventory Inventory Request reservations
Payment state Payments Observe payment outcomes
Shipment state Fulfillment Consume delivery updates

Other services can replicate information for queries or local decisions that tolerate staleness, but they should not become alternative writers of authoritative state.

Minimize Runtime Coupling

Independent deployments provide limited value when services remain tightly coupled at runtime. Every synchronous dependency adds latency and another component that must be healthy for the current operation to succeed.

Communication style should therefore be chosen according to business semantics. Immediate decisions may require request-response communication, while work that can happen later can often be decoupled through messaging.

Keep Synchronous Paths Short

A user-facing request should depend synchronously only on services required to produce its immediate result.

Poor critical path

Client
  |
  v
Service A
  |
  v
Service B
  |
  v
Service C
  |
  v
Service D
  |
  v
Service E


Better

Client
  |
  v
Service A
 /     \
v       v
B       C

Non-critical work
      |
      v
 Message Broker
 /      |       \
v       v        v
D       E        F

Consider a chain where each downstream service independently has 99.9% availability. If five services must all succeed, the theoretical combined availability is approximately 99.5% before accounting for the caller, network, gateway, database, or other dependencies.

Latency behaves similarly. Even fast services become slow when requests pass through long sequential dependency chains.

REST and gRPC are appropriate when an immediate result is required; messaging is usually preferable when downstream processing can proceed independently. More about these trade-offs can be found here: REST vs gRPC vs Messaging Between Microservices.

Use Asynchronous Communication Intentionally

Messaging can reduce temporal coupling and absorb traffic bursts, but it does not make distributed workflows automatically reliable. Consumers must handle duplicate delivery, delayed messages, retries, ordering constraints, poison messages, and schema evolution.

Events should describe facts that have already happened:

{
  "event_id": "evt_82914",
  "event_type": "OrderConfirmed",
  "version": 1,
  "occurred_at": "2026-08-12T15:42:10Z",
  "order_id": "ord_7281",
  "customer_id": "cus_381"
}

A consumer should not need access to the producer's internal database to interpret the event. Published schemas are contracts and should evolve with the same care as synchronous APIs.

Asynchronous systems should also expose operational metrics such as queue depth, oldest-message age, consumer lag, processing latency, retry rate, and dead-letter volume. A healthy broker does not mean the business workflow is keeping up.

Design Data for Service Autonomy

Services should own their persistent state instead of treating a shared database as a universal integration layer. This prevents one service from bypassing another service's business rules and allows schemas to evolve independently.

Database-per-service is primarily an ownership principle. Multiple services can still use the same managed database cluster when isolation requirements allow it, provided credentials and schemas prevent cross-service writes.

Keep Transactions Local

Strong ACID consistency should normally protect invariants inside one service boundary. Cross-service operations should not automatically become distributed database transactions.

For example, Inventory can atomically reserve stock:

BEGIN;

SELECT available_quantity
FROM inventory
WHERE sku = 'SKU-42'
FOR UPDATE;

UPDATE inventory
SET available_quantity = available_quantity - 2,
    reserved_quantity = reserved_quantity + 2
WHERE sku = 'SKU-42'
  AND available_quantity >= 2;

INSERT INTO reservations (
    reservation_id,
    order_id,
    sku,
    quantity,
    status
)
VALUES (
    'res_481',
    'ord_7281',
    'SKU-42',
    2,
    'reserved'
);

COMMIT;

The same transaction should not directly modify Order or Payment tables owned by other services.

Cross-service workflows frequently require eventual consistency, sagas, compensating actions, and query-specific projections. More about these patterns can be found here: Managing Data Across Multiple Services.

Make Distributed Workflows Recoverable

Distributed operations should be designed as durable state machines, not sequences that assume every step succeeds immediately.

pending
   |
   +--> inventory_reserved
   |          |
   |          +--> payment_authorized --> confirmed
   |          |
   |          +--> payment_failed ------> compensating
   |                                         |
   |                                         v
   |                                  inventory_released
   |                                         |
   |                                         v
   |                                      failed
   |
   +--> inventory_rejected ----------------> failed

Intermediate states make recovery possible after crashes or temporary dependency outages. A worker can inspect durable state and continue instead of relying on an in-memory call chain.

Database updates and event publication also need reliable coordination. A common solution is to commit the business change and an outbox record in one transaction, then publish the event asynchronously.

For a deeper explanation of this failure window and its implementation, see Transactional Outbox Pattern for Reliable Messaging.

Build for Partial Failure

In a distributed system, one dependency can be slow while the rest of the platform remains healthy. Production services should therefore assume that remote calls fail independently and protect themselves from propagating those failures.

Reliability mechanisms should have explicit limits. Timeouts without retry policies, retries without idempotency, or circuit breakers without recovery behavior can create new failure modes instead of reducing them.

Timeouts, Retries, and Idempotency

Every remote operation should have a deadline derived from the end-to-end latency budget. Infinite or excessively large timeouts allow slow dependencies to consume workers, connections, memory, and request capacity.

Retries should be reserved for transient failures and should normally use exponential backoff with jitter.

import asyncio
import random

import httpx


async def get_inventory(product_id: str) -> dict:
    timeout = httpx.Timeout(connect=0.3, read=0.7, write=0.5, pool=0.3)

    async with httpx.AsyncClient(timeout=timeout) as client:
        for attempt in range(3):
            try:
                response = await client.get(
                    f"http://inventory-service/v1/items/{product_id}"
                )
                response.raise_for_status()
                return response.json()

            except (httpx.TimeoutException, httpx.NetworkError):
                if attempt == 2:
                    raise

                # Jitter prevents many callers from retrying together.
                delay = (0.1 * (2 ** attempt)) + random.uniform(0, 0.05)
                await asyncio.sleep(delay)

    raise RuntimeError("unreachable")

Retries increase load during incidents, so attempts must remain bounded. Side-effecting operations require idempotency before retries can be considered safe.

A timeout on CreatePayment does not prove that payment creation failed. The operation may have succeeded while the response was lost. Stable idempotency keys allow retries to represent the same logical operation instead of creating duplicate effects.

Partial failures require additional patterns such as circuit breakers, fallbacks, and graceful degradation. For a broader treatment, see Handling Partial Failures in Production Systems.

Protect Services from Overload

Failure protection is also capacity protection. A service operating at saturation can produce rapidly increasing latency before it completely fails.

Each service should have explicit limits for:

  • incoming request concurrency
  • worker concurrency
  • database connection pools
  • outbound connection pools
  • queue consumer concurrency
  • request and message sizes
  • retry volume

Load shedding is often safer than accepting unlimited work. Rejecting excess traffic quickly allows healthy requests to complete instead of allowing every request to time out slowly.

Backpressure is equally important for asynchronous workloads. If consumers can process 5,000 messages per second while producers continuously generate 8,000, the queue is not absorbing a temporary burst; it is accumulating permanent debt.

Producer rate:       8,000 msg/s
Consumer capacity:   5,000 msg/s
---------------------------------
Backlog growth:      3,000 msg/s

After 10 minutes:
3,000 × 600 = 1,800,000 queued messages

Autoscaling can help when additional instances increase real processing capacity, but it cannot solve a bottleneck in a saturated database or downstream provider.

Make Services Operable

A service is not production-ready merely because it starts successfully. It must be possible to determine whether it is healthy, understand why it is failing, deploy it safely, estimate its capacity, and recover it without relying on undocumented knowledge.

Operational consistency across services is especially valuable. Standardized health endpoints, telemetry, deployment conventions, configuration management, and runbooks reduce the cognitive cost of operating a large service fleet.

Observability and SLOs

Logs, metrics, and traces answer different questions. Metrics show whether behavior is abnormal, traces show where time and failures occur across service boundaries, and structured logs provide detailed event context.

At minimum, synchronous services should measure:

  • request rate
  • success and failure rate
  • p50, p95, and p99 latency
  • timeout rate
  • dependency latency and errors
  • CPU, memory, connections, and worker saturation

Asynchronous services should additionally measure queue age, consumer lag, processing throughput, retry volume, and dead-letter count.

Alerts should correspond to user-visible or business-impacting conditions rather than every infrastructure fluctuation. Service Level Objectives (SLOs) provide a useful boundary for deciding when reliability has degraded enough to require action.

Distributed traces should propagate across HTTP, gRPC, and messaging boundaries using a consistent correlation context:

trace_id = 92b8f64...

API Gateway
   |
   v
Order Service ---------------------- 180 ms
   |
   +--> Inventory Service ----------  72 ms
   |
   +--> Pricing Service ------------  41 ms
   |
   +--> PostgreSQL -----------------  18 ms
   |
   +--> Outbox Commit --------------  12 ms

Without cross-service correlation, debugging often becomes manual timestamp matching across independent logs.

Safe Deployments

Independent deployment requires contracts and schemas that tolerate mixed versions. During a rolling deployment, old and new instances can serve traffic simultaneously while consumers may upgrade hours or days later.

Database migrations should therefore follow compatibility-safe sequences. For example, renaming a column should not require every application instance to switch atomically.

Unsafe:

1. Rename old_column -> new_column
2. Deploy application

Old instances fail between steps.


Safer expand-and-contract:

1. Add new_column
2. Deploy code compatible with both columns
3. Backfill data
4. Move reads/writes to new_column
5. Verify old usage is gone
6. Remove old_column later

API and event contracts need similar compatibility. Removing fields or changing their meaning should require an explicit migration strategy rather than assuming every consumer upgrades together.

More about API contract evolution can be found here: API Versioning and Backward Compatibility.

Deployments should also support rapid rollback or forward fixes, readiness checks, gradual traffic shifts where justified, and automatic detection of elevated error rates or latency.

Production Design Example

Consider an e-commerce platform composed of Ordering, Inventory, Payments, Fulfillment, Notifications, and Reporting services. The goal is to process orders reliably without turning every checkout into a long synchronous transaction.

The architecture separates immediate customer-facing work from durable asynchronous processing and assigns authoritative state to each domain.

Reliable Order Processing

                         Client
                           |
                           v
                      API Gateway
                           |
                           v
                     Order Service
                      /         \
                     /           \
              Pricing API    Inventory API
                     \           /
                      \         /
                       v       v
                    Validate Order
                           |
                  Order + Outbox Commit
                           |
                           v
                      OrderCreated
                           |
                           v
                    Message Broker
                    /            \
                   v              v
             Payment Service   Analytics
                   |
            PaymentAuthorized
                   |
                   v
               Order Saga
                   |
             OrderConfirmed
              /          \
             v            v
       Fulfillment    Notifications

The synchronous checkout path contains only dependencies required to accept the order. Every remote call has a deadline, and retryable side effects use idempotency keys.

Ordering owns the order state, Inventory owns reservations, Payments owns payment state, and Fulfillment owns shipment state. No service writes directly to another service's database.

Order creation and the OrderCreated outbox record commit atomically. A publisher delivers the event to the broker, and consumers tolerate duplicate delivery.

Payment processing can continue independently from the original HTTP request. The order remains in a durable pending state until required outcomes are known.

If payment authorization fails after inventory reservation, the saga requests inventory release and transitions the order through an explicit compensating state. A worker crash does not lose workflow progress because the state is persisted.

Operational dashboards separate synchronous and asynchronous health:

Component Important Signals Example Failure
API Gateway Request rate, 5xx rate, p99 latency, throttling Ingress saturation
Order Service Checkout latency, errors, DB pool usage Database saturation
Inventory Reservation latency, rejection rate, lock contention Hot inventory records
Message Broker Queue depth, oldest-message age Consumer throughput below producer rate
Payments Authorization latency, provider errors, retries Payment provider degradation
Order Saga Pending duration, failed steps, compensation rate Workflows stuck between services

This architecture does not eliminate failures. It makes failures bounded, observable, retryable, and recoverable without requiring the entire platform to succeed as one distributed transaction.

Common Mistakes

Production microservice failures frequently result from excessive distribution, unclear ownership, or reliability mechanisms that were added without considering their system-wide effects.

Mistake Why It Causes Problems Better Approach
Creating services around tables or CRUD entities Related business operations become chatty distributed workflows with little independent value. Define boundaries around cohesive business capabilities.
Building long synchronous dependency chains Latency accumulates and one downstream failure can break the entire request path. Keep critical paths short and move non-immediate work to asynchronous processing.
Sharing writable databases between services Business ownership becomes ambiguous and schema changes require coordination. Give one service authoritative ownership of each business state.
Retrying every failure automatically Retries amplify load during incidents and repeat permanent failures unnecessarily. Retry only transient failures with bounded backoff, jitter, and deadlines.
Retrying side effects without idempotency Ambiguous failures can duplicate payments, reservations, or other operations. Define stable idempotency semantics before enabling retries.
Publishing events after database commits without coordination A crash can persist business state without publishing the corresponding event. Use a transactional outbox or another durable publication mechanism.
Assuming messaging provides exactly-once business execution Redelivery can duplicate side effects even when broker guarantees are strong. Design consumers and business operations to tolerate duplicates.
Ignoring intermediate workflow states Partial completion becomes difficult to recover and can leave business state ambiguous. Persist explicit pending, failed, and compensating states.
Autoscaling without identifying the bottleneck More service instances can overload a fixed database or downstream provider faster. Scale based on the constrained resource and end-to-end capacity model.
Allowing unbounded concurrency Traffic spikes exhaust connections, workers, memory, or downstream capacity. Apply concurrency limits, queues, backpressure, and load shedding.
Deploying breaking schemas and contracts atomically Rolling deployments temporarily run incompatible producer and consumer versions. Use backward-compatible expand-and-contract migrations.
Monitoring individual services without workflows Every component can appear healthy while an end-to-end business process is stuck. Measure business workflows, queue age, saga duration, and distributed traces.

Production Checklist

A microservice should not be considered production-ready until its boundaries, dependencies, failure behavior, observability, and recovery procedures are explicit.

  • Validate service boundaries: confirm that each service owns a cohesive business capability and can evolve independently.
  • Assign authoritative data ownership: ensure every important business state has exactly one service responsible for changing it.
  • Minimize synchronous dependencies: remove downstream calls that are not required to produce the immediate result.
  • Set explicit deadlines: derive remote-call timeouts from the end-to-end request latency budget.
  • Bound retries: retry only transient failures with limited attempts, backoff, and jitter.
  • Protect side effects with idempotency: make duplicate requests and messages safe before introducing retries.
  • Coordinate state and events reliably: prevent database commits from becoming disconnected from message publication.
  • Persist workflow progress: make long-running operations recoverable after worker or service restarts.
  • Control concurrency: configure worker, connection, request, and consumer limits according to downstream capacity.
  • Implement backpressure: prevent producers and retries from permanently exceeding sustainable processing capacity.
  • Propagate trace context: preserve correlation across HTTP, gRPC, queues, and background workers.
  • Define service-level indicators: measure latency, errors, saturation, queue age, and workflow completion instead of infrastructure health alone.
  • Use compatibility-safe deployments: allow old and new API, event, and database versions to coexist during rollout.
  • Test partial failures: exercise slow dependencies, duplicate messages, broker outages, database saturation, and failed compensation.
  • Maintain recovery procedures: document how to replay events, drain queues, repair projections, resume workflows, and roll back unsafe releases.

Conclusion

Production microservices succeed when service independence is greater than the distributed complexity introduced to achieve it. Clear boundaries, explicit data ownership, short dependency paths, reliable asynchronous workflows, bounded failure handling, and strong operational visibility are more important than service count or infrastructure sophistication.

The architecture should make partial failure expected rather than exceptional. Services need to degrade safely, preserve recoverable state, evolve contracts independently, and expose enough telemetry to understand both individual components and complete business workflows.

Key Takeaway

Optimize microservices for independent ownership and predictable failure. Keep boundaries cohesive, transactions local, synchronous paths short, asynchronous processing durable, retries bounded, contracts compatible, and every important production workflow observable and recoverable.

Author

Enjoyed this article?

Support Oleksandr Andrushchenko

Buy me a coffee

This helps Oleksandr Andrushchenko continue creating useful content

Related articles

Comments (0)