Microservices Communication Patterns: Sync, Async, and Event-Driven

Choose the right communication pattern between microservices for each use case. Covers synchronous REST/gRPC, asynchronous messaging, event-driven architecture, saga patterns, the tradeoffs between consistency and availability, and the decision framework that prevents you from over-engineering simple integrations.

This reference explains how microservices communicate using synchronous, asynchronous, and event-driven approaches. These patterns determine how services interact, what happens when communication fails, and how systems scale under load. Understanding these patterns is critical when designing service boundaries, handling failures, and implementing reliable distributed systems.

Synchronous Communication

Synchronous communication occurs when a service makes a request and waits for a response before continuing. This pattern is most common with HTTP APIs.

Making a Request

A client service sends an HTTP request to another service:

curl -X POST \
  http://user-service:8080/users \
  -H 'Content-Type: application/json' \
  -d '{"name": "Alice", "email": "[email protected]"}'

The receiving service must respond with a status code and body before the client proceeds. If the service does not respond within a timeout, the client may time out.

Handling Timeouts

When timeouts occur, services should fail fast rather than wait indefinitely:

curl --max-time 5 \
  http://order-service:8080/orders/12345

A timeout response looks like:

curl: (28) Operation timed out after 5001 milliseconds with 0 bytes received

The client must distinguish between a timeout and a service returning an error. A timeout indicates network or resource issues, while a 5xx error from the service means it handled the request but failed.

Error Responses

Services should return appropriate HTTP status codes:

A service returning a 500 error may include JSON in the body:

{
  "error": "Database connection failed",
  "code": "DB_CONNECTION_ERROR"
}

Deadlock Scenarios

Synchronous calls can cause deadlocks when services call each other in cycles. If service A calls B, and B calls A, both wait indefinitely.

Asynchronous Communication

Asynchronous communication uses message queues to decouple services. The sender places a message and continues without waiting for a response.

Message Queue Basics

Messages are placed in a queue by producers. Consumers process messages from the queue:

# Example of sending a message via a queue client
echo '{"userId": 123, "action": "create_account"}' | \
  rabbitmqctl publish -q user_events -e application/json

Consumer Processing

Consumers pull messages and process them. If processing fails, the message can be retried or moved to a dead-letter queue:

# Example of consuming from a queue
rabbitmqctl get -q user_events -c 10

Retry Logic

Messages that fail processing should be retried with exponential backoff. A failed message may be retried after 1 second, then 2 seconds, then 4 seconds, and so on.

Acknowledgment Patterns

Consumers must acknowledge messages when they are successfully processed:

# Manual acknowledgment in a queue consumer
rabbitmqctl get -q user_events --ack

If a message is not acknowledged, it remains in the queue and can be re-delivered.

Poison Messages

Messages that repeatedly fail processing are called poison messages. These should be moved to a dead-letter queue rather than being retried infinitely:

# Configuration for dead-letter exchange
rabbitmqctl set_policy --priority 1 \
  --pattern "^user_events$" \
  --definition '{"dead-letter-exchange":"dlx"}' \
  user_events

Event-Driven Communication

Event-driven communication uses events to signal state changes. Services react to events rather than calling each other directly.

Publishing Events

Services publish events when significant actions occur:

curl -X POST http://event-bus:8080/events \
  -H 'Content-Type: application/json' \
  -d '{
    "type": "user.created",
    "data": {
      "userId": 123,
      "name": "Alice"
    },
    "timestamp": "2023-05-15T10:00:00Z"
  }'

Event Subscribers

Other services subscribe to specific event types:

# Example of subscribing to events via HTTP webhook
curl -X POST http://event-bus:8080/subscriptions \
  -H 'Content-Type: application/json' \
  -d '{
    "event_type": "user.created",
    "url": "http://notification-service:8080/webhook"
  }'

Event Delivery Guarantees

Events should be delivered at least once. Duplicate events are acceptable but must not cause data corruption.

Event Schema Versioning

Events should include version information to handle schema changes:

{
  "type": "user.created.v2",
  "data": {
    "userId": 123,
    "name": "Alice",
    "email": "[email protected]"
  },
  "version": "2.0",
  "timestamp": "2023-05-15T10:00:00Z"
}

Failure Modes and Recovery

Synchronous Failures

Timeouts in synchronous calls cause immediate failure. The client must retry or handle the timeout gracefully.

A service that times out on a request should return a 504 Gateway Timeout:

HTTP/1.1 504 Gateway Timeout
Content-Type: application/json

{
  "error": "Request timed out",
  "service": "user-service"
}

Asynchronous Failures

Failed message processing in asynchronous systems may cause:

Consumers should implement circuit breaker patterns to avoid cascading failures.

Event Delivery Issues

Events can be lost or duplicated. Services must handle both cases:

Common Configuration Patterns

Retry Configuration

Services should configure retry behavior explicitly:

# Example retry configuration for HTTP calls
retry:
  max_attempts: 3
  backoff_seconds: 1
  jitter: true

Queue Backpressure

Queues should implement backpressure mechanisms to prevent memory exhaustion:

# Configure queue size limits
rabbitmqctl set_parameter --vhost / \
  queue_max_length 10000

Message TTL Configuration

Messages may have time-to-live settings:

# Set message expiration after 30 seconds
rabbitmqctl publish -q user_events \
  --expiration 30000 \
  '{"userId": 123}'

Communication Patterns in Practice

Combining Patterns

Real systems often use combinations of patterns. For example, a service might:

  1. Use synchronous calls for immediate operations
  2. Publish events for notifications
  3. Queue messages for background processing

Circuit Breaker Implementation

Implement circuit breakers to prevent cascading failures:

# Example: Circuit breaker configuration
curl -X POST http://circuit-broker:8080/circuits \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "user-service",
    "timeout": 5000,
    "failure_threshold": 3,
    "reset_timeout": 30000
  }'

Monitoring and Observability

All communication patterns should include monitoring:

Idempotency

Services must ensure idempotency where possible, especially in asynchronous systems:

# Example of idempotent operation
curl -X POST http://payment-service:8080/payments \
  -H 'Content-Type: application/json' \
  -H 'X-Request-ID: abc123' \
  -d '{"amount": 100, "reference": "order-123"}'

The X-Request-ID header allows services to detect and ignore duplicate requests.

This page was rewritten on 15 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.