Communication Patterns
Learn communication patterns for distributed systems: synchronous vs asynchronous, messaging patterns, and event-driven architecture.
What is a communication pattern?
A communication pattern defines how software components exchange requests, messages, and events. It answers four questions before a protocol or product is chosen:
- Does the sender wait for an answer?
- Does one consumer or many consumers receive the work?
- Can the system buffer a traffic burst?
- What happens when delivery or processing fails?
The pattern is an architectural decision, not a library choice. HTTP can carry asynchronous callbacks, and Kafka can support request-reply, but those tools do not remove the need to choose latency, coupling, delivery, and consistency behavior deliberately.
Caller waits
Request-response
Use when the caller needs an immediate result. The contract is direct, but downstream latency and failure become part of the caller's path.
One worker
Work queue
Use when work can finish later. The queue absorbs bursts and lets workers scale independently, but handlers must tolerate retries.
Many consumers
Event stream
Use when several capabilities react to a fact that already happened. Replay and fan-out improve flexibility while making ordering and schema evolution explicit.
Start with the workload, not the technology
A design becomes clearer when the requirements are expressed as decisions. An interactive price lookup needs a response in the current request. Thumbnail generation can be queued. An OrderCreated event can be retained and consumed by inventory, billing, analytics, and notifications.
Change the constraints below and trace how the recommended path changes.
Choose the communication shape
Change the workload constraints. The path updates to show the simplest pattern that satisfies them.
Synchronous path
Request-response
The caller waits for one service to return a result.
Producer
Creates intent
Request-response
Worker
Handles work
End-to-end
Latency
Higher
Coupling
None
Buffering
Understand the failure boundary
Every remote interaction crosses a failure boundary. The sender can time out while the receiver still completes the operation, an acknowledgment can be lost, or a broker can redeliver a message after a consumer restarts.
The central distinction is between delivery and business effect:
- A transport can promise at-most-once or at-least-once delivery.
- The application decides whether repeated delivery repeats the business operation.
- An idempotency key lets several attempts converge on one durable effect.
Use the lab to create duplicate deliveries and observe the result.
Inject a duplicate delivery
Separate the broker's delivery promise from the application's business effect.
Current run
2 attempts · 2 effects
Step 1
Choose the delivery contract
One message, three guarantees
Step 2
Configure failure handling
Step 3
Trace every attempt
Every attempt applies
Broker
2 deliveries
Payment handler
No deduplication
Ledger
2 charges
Compose patterns for real workflows
Production systems rarely use one communication shape everywhere. They combine patterns so each step gets the behavior it needs.
1 Synchronous
Validate the order
Check price, inventory policy, and identity while the user is waiting. Return a clear success or rejection.
2 Atomic boundary
Commit intent and outbox
Store the order and an outgoing event record in one database transaction so state and communication cannot diverge.
3 Queue
Process durable work
Run payment capture, fulfillment, and email with bounded retries, idempotency keys, and dead-letter handling.
4 Event stream
Publish business facts
Let analytics, recommendations, and audit consumers react independently without joining the checkout latency budget.
Recognize the reusable messaging shapes
Competing consumers
Point-to-point queue
Each work item is handled by one available consumer. Use it for image processing, email delivery, and other jobs where workers can share a backlog.
Independent reactions
Publish-subscribe
Each subscriber receives the event through its own subscription. Use it when several domains react to the same business fact.
Async response
Request-reply with correlation
A request and later response share a correlation ID. Use it for long-running work when the caller cannot keep a connection open.
Multi-step transaction
Saga
Local transactions advance a workflow; compensating actions address partial failure. Use it when one database transaction cannot span every service.
Build resilience into the contract
Retries, timeouts, and dead-letter queues are not generic switches. They change load and correctness, so each one needs a bounded policy.
1 Process
Attempt
Validate the message, check its idempotency key, and perform the smallest durable unit of work.
2 Transient failure
Back off
Retry only errors that can plausibly recover. Add jitter and cap both the delay and total attempts.
3 Persistent failure
Quarantine
Move exhausted messages to a dead-letter queue with the payload, error, attempt count, and correlation context.
4 Operator action
Recover
Fix the cause, replay deliberately, and verify that idempotency protects already completed effects.
Guardrails that belong in the design
- Timeouts: derive them from the end-to-end latency budget, not a framework default.
- Retry budgets: prevent one dependency failure from multiplying traffic across the system.
- Circuit breakers: stop calls that are unlikely to succeed and preserve capacity for recovery.
- Backpressure: slow producers or shed optional work before queues grow without bound.
- Observability: carry correlation and causation IDs across synchronous and asynchronous hops.
Match technology to the chosen contract
Choose the semantic shape first, then select a technology that supports its operational needs.
Direct interaction
HTTP or gRPC
Good for user-facing commands, service queries, and low-latency internal calls. Protect every dependency with deadlines and cancellation.
Durable work
RabbitMQ or SQS
Good for task queues, delayed processing, and competing consumers. Define acknowledgment, retry, and dead-letter behavior explicitly.
Retained events
Kafka or Pulsar
Good for replayable event streams, high-throughput pipelines, and multiple consumer groups. Partitioning defines ordering and parallelism.
WebSockets and server-sent events solve a different problem: keeping a delivery channel open to a client. They are often fed by queues or streams behind the gateway rather than replacing them.
Review the decision before implementation
For every remote interaction, write down:
- The caller's latency budget and whether it can proceed without a result.
- The number of consumers and whether each needs its own copy.
- The ordering scope, retention period, and acceptable consistency delay.
- The timeout, retry budget, idempotency key, and poison-message policy.
- The overload behavior when consumers fall behind.
- The correlation fields and metrics needed to reconstruct one business operation.
The strongest design is usually a deliberate combination: synchronous communication for the smallest immediate decision, a queue for durable work, and an event stream for independent reactions.