Event-Driven Microservices Architecture: A Practical Guide for Software Developers
Explore how microservices communicate through events, messages, brokers, queues, and subscriptions. This tutorial covers event design, event-driven communication, eventual consistency, delivery guarantees, duplicate messages, idempotent consumers, ordering, retries, dead-letter queues, transactional outbox, schema evolution, .NET implementation, observability, and debugging. It concludes with a practical framework for designing production-ready event-driven architectures.
Lesson 3: Synchronous vs. Asynchronous Communication
Microservices can communicate either by waiting for an immediate response or by sending messages that are processed later. In this lesson, we compare synchronous and asynchronous communication and examine the practical trade-offs involved, including coupling, availability, responsiveness, and the way these choices affect the overall architecture.
Synchronous Communication
In a traditional microservice architecture, services often communicate synchronously through HTTP APIs. The calling service sends a request and waits for the receiving service to return a response. REST APIs are a common example of this approach because the caller cannot normally continue the operation that depends on the response until that response is received.
Imagine that a customer places an order. The Order Service might call the Payment Service directly:
Order Service
|
| POST /payments
v
Payment Service
|
| Payment Result
v
Order Service
In .NET, the call could look like this:
var response = await httpClient.PostAsJsonAsync(
"/payments",
new { OrderId = orderId, Amount = total });
The Order Service now depends on the Payment Service being available and responsive. If the Payment Service is slow, the Order Service waits. If it is unavailable, the request may fail, which means the Order Service must decide whether to retry, return an error, or use another recovery strategy.
Asynchronous Communication
With asynchronous communication, the sender does not directly wait for the receiving service to process the operation. Instead, it sends a message to a messaging system such as RabbitMQ, Apache Kafka, Azure Service Bus, or Amazon SQS. The consumer can process the message later.
The same order scenario can therefore look like this:
Order Service
|
| OrderPlaced
v
Message Broker
|
v
Payment Service
The Order Service publishes the OrderPlaced event and can continue its own work without waiting for the Payment Service to finish processing the payment. The broker holds and delivers the message according to its configuration, while the Payment Service processes it independently.
The Main Difference
The key difference is waiting for a response. In synchronous communication, the caller directly communicates with the receiver and normally waits for a result. In asynchronous communication, the producer sends a message and does not need to wait for the consumer to complete its work.
This creates an important change in the relationship between services. With REST, the Order Service needs to know how to reach the Payment Service. With messaging, the Order Service primarily needs to know how to publish the appropriate message.
Synchronous:
Order Service ----request----> Payment Service
<---response----
Asynchronous:
Order Service ----message----> Broker
|
+----> Payment Service
Coupling and Availability
Synchronous communication creates a form of runtime dependency between services. If the Order Service needs an immediate response from the Payment Service, both services must be available at that moment. This can become problematic when a system contains many services.
Asynchronous communication can reduce this dependency. The Order Service can publish an event even when the Payment Service is temporarily unavailable, provided the messaging infrastructure can store the message. The Payment Service can process the message when it becomes available again.
However, asynchronous communication does not eliminate failure. It changes where and when failure is handled. Instead of immediately receiving an HTTP error, the system might encounter failed message processing, retries, duplicate messages, or messages waiting in a queue.
When Should You Use Each Approach?
Synchronous communication is useful when the caller genuinely needs an immediate answer. For example, when a frontend requests customer information from a Customer Service, it normally needs the response before displaying the information.
Asynchronous communication is useful when work can happen independently or when multiple services need to react to the same business event. Sending notifications, processing background jobs, updating search indexes, and reacting to OrderPlaced are common examples.
A practical architecture often uses both approaches rather than choosing only one. A microservice might expose REST APIs for operations that require an immediate response while publishing events for other services that need to react asynchronously.
A Concrete Order Workflow
Consider the complete order process. The frontend can synchronously call the Order Service because it needs confirmation that the order was successfully created.
Customer
|
| HTTP
v
Order Service
After creating the order, the Order Service can asynchronously publish OrderPlaced. Payment, inventory, and notification processing can then happen independently.
Order Service
|
| OrderPlaced
v
Message Broker
|
+--> Payment Service
+--> Inventory Service
+--> Notification Service
This combination is common in real-world microservices. Synchronous communication is useful for immediate request-and-response interactions, while asynchronous communication is useful for independent, background, and event-driven workflows. In the next lesson, we will look at the infrastructure that makes asynchronous communication possible: message brokers and event-streaming platforms.