Overview
As we commented in previous articles the need for efficient and flexible communication among components within microservices architectures is more crucial than ever. Asynchronous market services take the spotlight, reshaping the way specific components interact within these architectures. By embracing asynchronous communication, a multitude of benefits are unlocked, including enhanced scalability and improved user experience. In this artible, we explore the different approaches to achieving asynchronous communication within microservices, such as event-driven communication and asynchronous API calls. let´s delve into the intricacies and advantages of leveraging asynchronous microservices.
Why we need asynchronous microservices
- Asynchronous Communication: Allows tasks to be initiated without waiting for completion, increasing system efficiency.
- Improved Performance: Prevents performance hindrance that could occur when an application waits for a task to complete.
- Decoupling of Services: Improves system modularity and allows services to operate independently, enhancing resilience. Decoupling client and service is also a better architectural approach
- Better User Experience: Ensures immediate responses and makes the system more responsive, improving the user experience.
- Event-based Asynchronous Communication: Enables efficient, indirect communication between services, reducing system complexity.
- Asynchronous API calls: Changes the way services are called, increasing flexibility and adaptability.
- Long Running Jobs : In the same line that better user experience, async pattern improves the communication without blocking caller threads
How we can implement asynchronous microservices
At this article will delve into various techniques for achieving asynchronous communication within microservices. One of these techniques is event-based asynchronous communication, where the client application indirectly triggers tasks by sending messages or events. We will explore the intricacies of this approach and delve into the mechanisms used, such as queues or message brokers, to facilitate seamless communication between services. Additionally, we will explore the concept of asynchronous API calls, which alters the way services are invoked, ensuring synchronous behavior while maintaining the flexibility of microservices.
event based communication
One option for achieving synchronous communication within microservices is through event-based communication. In this approach, the client application, which could be a web app, a mobile app, or a service, does not directly make a call to another API or service. Instead, it raises an event that is consumed by one or more consuming applications or services. These consuming applications or services pick up the event and perform the necessary tasks. This communication is asynchronous because the client application continues with its processing without being aware of the consumer’s actions.
The event represents an action or a task that is part of an overall transaction. For example, on an e-commerce website, when a customer places an order, multiple events may be raised. These events could include placing the order for processing, updating the order history, or updating stock levels in the background. Different microservices would pick up these events and carry out the corresponding tasks. It’s worth noting that these tasks may complete after the overall transaction is finished. This asynchronous nature of the communication allows background tasks related to the transaction to run without hindering the overall transaction. The communication for these tasks is handled in a unified way.
At a technical level, events are raised as messages and placed onto message queues. The message brokers are responsible for delivering these messages to the consumers, which are the services that carry out the tasks. Multiple consumers can consume the same event, picking up the same message from the queue and performing different tasks based on that event. This decoupling of the client and service is ideal for a microservices architecture. The client is not aware of the service, and the service is not tightly coupled to the client. The client raises the event, and the service listens for the event on a message broker, consuming it and performing its tasks.
This decoupling allows for significant changes to be made to the client or service applications without breaking the system, as long as the event (i.e., the message format) remains the same. Event-based systems also provide flexibility in terms of queuing patterns that control how events are consumed. For instance, an event may trigger multiple consumers to perform different tasks, or multiple consumers may compete to process a single event.
Main Features
- In event-based communication, the client raises an event instead of making a direct call to another API or service.
- The event is consumed by one or more consuming applications or services.
- Client continues processing without being aware of the consumer’s actions.
- Events represent actions or tasks within an overall transaction.
- Multiple events can be raised within a transaction, and different microservices pick up these events to perform corresponding tasks.
- Microservices can complete their tasks after the overall transaction is finished, allowing for background tasks related to the transaction to run.
- Events are raised as messages and placed onto message queues.
- Message brokers deliver these messages to the consuming services.
- Multiple consumers can consume the same event and perform different tasks based on it.
- Changes to the client or service applications can be made without breaking the system, as long as the event/message format remains the same.
Main Event-Based Patterns
There are several event-based patterns that we can apply to our microservice architecture, let´s review a couple of them:
Competing Workers Pattern
It is designed to efficiently process events by distributing the workload among multiple worker instances, or competing workers, in a concurrent manner.
In this pattern, when an event is raised or published, multiple worker instances compete to consume and process the event. The event is typically stored in a message queue, and the workers actively listen to the queue, waiting for events to become available.
When a worker instance receives an event from the queue, it becomes the owner of that event and starts processing it. This ownership ensures that only one worker processes each event, preventing duplication of work. Other workers are aware that the event is being processed and will not attempt to consume it.
The competing workers pattern offers several benefits. First, it enables parallel processing of events, improving overall system throughput and reducing processing time. By distributing the workload among multiple workers, the system can handle a higher volume of events and scale effectively.
Additionally, this pattern provides fault tolerance and high availability. If a worker instance fails or becomes unresponsive, another worker can take over the processing of the event, ensuring that events are not lost and the system continues to function.
To implement the competing workers pattern effectively, it is important to consider factors such as load balancing, ensuring fair distribution of events among workers, and handling potential concurrency issues, such as event ordering or race conditions. Advanced message queuing systems or event streaming platforms are often employed to manage the message queues and facilitate reliable event delivery to competing workers.
By leveraging the competing workers pattern, event-based microservices can efficiently handle large volumes of events, achieve parallel processing, and ensure fault tolerance and scalability in a distributed architecture
Fanout Pattern
The fanout pattern is another important approach in event-based microservices architecture. It is designed to enable the broadcasting of events to multiple consumers or subscribers, allowing for parallel processing and decoupled communication between microservices.
In the fanout pattern, when an event is published, it is broadcasted to multiple subscribers who have expressed interest in receiving that particular event. Each subscriber receives its own copy of the event and processes it independently.
The pattern derives its name from the concept of a fan spreading out or dispersing the airflow. Similarly, the fanout pattern spreads events to multiple consumers, ensuring that each subscriber receives the event and can act upon it accordingly.
This pattern is particularly useful in scenarios where multiple microservices or components need to react to the same event simultaneously or perform different tasks based on the same event. It allows for parallel processing and enables each consumer to independently handle the event without being aware of other subscribers or affecting their processing.
To implement the fanout pattern effectively, a publish-subscribe system or message broker is typically used. The publisher publishes the event to a topic or exchange, and the message broker takes care of distributing the event to all subscribed consumers. The subscribers can dynamically subscribe or unsubscribe to events based on their specific interests and requirements.
The fanout pattern offers several advantages. It enables loose coupling between microservices, as publishers and subscribers are decoupled and unaware of each other’s existence. It allows for scalability, as new subscribers can be added without impacting the publishers or existing subscribers. It also enhances flexibility, as different subscribers can process events differently based on their specific needs.
However, it’s important to consider potential challenges with the fanout pattern, such as ensuring event ordering consistency across different subscribers and managing the overall system load and performance when dealing with a large number of subscribers.
ASYNC API CALLS
Async Api Calls offers a simpler solution compared to the event-based approach. Let´s review it
Sync Approach and its drawbacks
Traditionally, when making a call from a client application, such as a web app, service, or API, to another API, the request-response pattern is commonly used. This pattern involves the client formulating and making the call, while the service receives the request and carries out the corresponding task. Once the task is completed, the service sends a response back to the client.
However, this synchronous approach poses a challenge as the client application is compelled to wait for the response. This waiting period in the synchronous approach can lead to inefficiencies and delays in the client application’s execution. It creates a dependency on the response time of the service, which can impact overall system performance. Additionally, if the service encounters any delays or issues, it directly affects the client’s waiting time.
ASync adoption
By adopting an asynchronous approach, the client application can initiate a call to the service without waiting for an immediate response. Instead, it receives an acknowledgement (such as a confirmation or identifier) that the request has been received and is being processed. This frees up the client to perform other operations or tasks, enhancing its responsiveness and efficiency.
Once the service completes the requested task, it can send the response back to the client using the provided acknowledgement or identifier. This decoupling of execution allows the client application to proceed independently, improving overall system performance and responsiveness.
Main features
- Asynchronous API calls with request acknowledgement using callbacks offer a simpler way of achieving synchronous communication.
- In this approach, the call is made similar to a synchronous call, but instead of waiting for the response, a callback address is registered with the service.
- The callback address is where the actual response will be sent once the task is complete.
- The output of the call is registered as a callback with the service, allowing the service to execute the task and provide the response.
- Third-party solutions exist for achieving background task execution in a synchronous manner.
- Hangfire is a popular framework for .NET that enables firing background tasks as fire-and-forget jobs in a synchronous way.
- Hangfire operates in the background and provides dashboards for monitoring the ongoing background tasks.