Practical Microservices Development Patterns: Sync vs. Async
Practical Microservices Development Patterns: Sync vs. Async
Let’s look at some of the best practices in microservice architecture in regards to communication, and implementation approaches using Ballerina programming language.
Join the DZone community and get the full member experience.Join For Free
When deciding to develop a microservice architecture, one of the main questions that come to mind is if we should follow a synchronous or an asynchronous approach for our service communication. The answer to that question will mainly be driven by the nature of our service operations and also the performance characteristics we require from the system. The general properties for each communication style are as follows.
- Better for read-heavy service operations
- Generally provides immediate data consistency
- Potential for moderate scaling
- Tight coupling between services
- Simpler to design
- Requires special constructs such as circuit breakers to avoid cascading failures due to high load
- Better for write-heavy service operations
- Generally provide eventual data consistency
- Potential for higher scaling
- Loose coupling between services
- Comparatively more complicated to design
- Naturally resilient to traffic bursts and failures
As we can see from the above properties, there is no single approach that is suitable for all scenarios. The software system needs to be modeled closer to the majority of the requirements matched in each approach. There can also be scenarios where a hybrid approach will need to be followed in order to satisfy different requirements in the system. The Ballerina programming language has core features built-in that can be used to build microservices in either of the two patterns. In the following sections, we will look at the features and technologies provided by Ballerina in supporting these methodologies.
The most popular synchronous communication technique is using REST with HTTP. Ballerina contains rich support in creating HTTP-based services, and clients, which has inbuilt features such as data binding, mutual SSL, multipart MIME support, and out-of-the-box filters and interceptors for technologies such as BasicAuth, JWT, and OAuth.
In Ballerina, the language contains a service abstraction, which is used to model both synchronous and asynchronous communication-based services. For example, we write Ballerina services for both HTTP and WebSocket based communication. The differentiation of the communication technique is done using the way we implement the resources inside services. Let’s take a look at how a simple HTTP service is implemented using Ballerina.
A Ballerina service is created using the service construct, and its type is decided by the listener that is associated with it. In the code in Listing 1, it shows a Ballerina service which is associated with an HTTP listener, thus making it an HTTP service. Individual resource functions in the service represent distinct sub-contexts of the base path of the service. The caller parameter in the resource represents the HTTP client or the caller who has initialized the request. This object is used in communicating back to the caller, as we see here with the respond remote method invocation.
Listing 1: Ballerina HTTP Service Example
Ballerina services and clients have observability features built-in. Since the language is aware of network operations, it can do automated observability. For more information on this aspect, read this article on Rethinking Programming: Automated Observability.
Listing 2 shows a Ballerina HTTP client in action. This is part of an e-commerce solution that implements a virtual shopping cart. Here, we have data binding operations of Ballerina demonstrated, where the record type Delivery is automatically mapped through the JSON payload received to the service. The Ballerina language follows a structural type system, which makes it possible to have a network-friendly type system in order to convert data to and from the network.
Listing 2: Ballerina HTTP Client Example
The Ballerina language is also built upon a sequence diagram concept, where its constructs and operations are naturally represented in a sequence diagram. Figure 1 shows the auto-generated sequence diagram of Listing 2.
Figure 1: Sequence Diagram View of HTTP Client Example
The above scenario represents the caller, our service, and one remote endpoint, which is the service we are communicating with. Let’s take a look at a more advanced scenario of a flow that contains client requests to multiple remote services.
Figure 2: Sequence Diagram View of Multiple Clients Example
The scenario in Figure 2 shows how the synchronous network calls are being made to the reportService and lookupService. The sequence diagram view is a convenient approach to tracking how communication is done at a higher level in the system.
Ballerina’s network communication follows a specialized concurrency model in implementing non-blocking I/O with a synchronous programming model. This allows Ballerina to produce reactive microservices quickly without any special intervention by the developer. More information on this aspect can be found in this article on Reactive Microservices.
The communication resiliency features are used to mitigate issues that we would generally see in distributed systems. Microservices that operate synchronously are more susceptible to issues such as losing data in a temporary outage of a service or the system failing in high load scenarios. In an asynchronous communication approach, we generally use a message broker when connecting internal microservices together. The message broker acts as a buffer area, where it stores messages until someone consumes it. In this way, any temporary outages of services do not affect the overall system functionality, where it will simply result in a delay in service, and no data loss. This is the same for high traffic bursts: the message queues in the broker will simply store the requests, where the consumption of the message can be done with the processing capability of the system and the system will be normalized eventually.
We implement specific resiliency strategies in order to reduce the effect of these issues on synchronous systems. Ballerina supports the following in relation to HTTP communication.
The HTTP REST services are generally used in the services that are at the edge of the system, where the external clients interact. The HTTP protocol is a widely supported transport in many programming languages and platforms, and thus used by users frequently. The same is true with JSON as a data exchange format. This combination is generally considered a more verbose approach for communication. If we need to optimize for performance and latency, protocols such as gRPC provide a binary data exchange mechanism, which is primarily used for service-to-service communication.
In a microservice architecture, the internal service communication can be fine-tuned by using a binary protocol such as gRPC. Let’s take a look at how our e-commerce backend system can be updated to follow this pattern.
Figure 3: Microservice Architecture with HTTP and gRPC Communication
As shown in Figure 3, the outermost service which interacts with the client (website) is the Admin service. In this case, this service acts as an orchestrator service when communicating with other microservices that make up the overall system. The website to the Admin service communication is done using HTTP, whereas the Admin service to internal microservices is done using gRPC in order to optimize their communication.
Listing 3: Ballerina gRPC Service Example
In Listing 3, in place of the HTTP listener, we have used a gRPC listener in order to make it a gRPC service. And also, its resource functions are now adapted to work as gRPC service functions. Ballerina supports a wide range of gRPC communication modes such as unary blocking, non-blocking, and server/client/bi-directional streaming.
In the next section, let’s take a look at how asynchronous messaging can be used with microservices, and the features Ballerina have in supporting this approach.
Asynchronous communication is mainly used in scenarios where most of our operations are commands to be executed via microservices. That is, it will be a one-way message going to a target service where we don’t expect anything in return; at least not something immediately anyway. If we have a scenario of a user action that requires an immediate response, then an asynchronous communication approach is not suitable. We can implement request/response scenarios by way of implementing a synchronous wrapper around the asynchronous operations, where we would use a combination of correlation IDs and polling for data. However, this is complicated and can be error-prone when implementing at a higher scale. So in the case of having a high number of request/response scenarios, an asynchronous communication strategy would not be useful. Any benefit we probably get from asynchronous communication would be overshadowed by the complexity we add to emulate the synchronous operations.
Figure 4: Microservice Architecture with Asynchronous Communication
Figure 4 shows a scenario of implementing an asynchronous OCR processing system. Here, the OCR operation is considered to be a computationally expensive task. In order to scale the system and to have a better user experience, the tasks are submitted as image data and with an email address. The software system will process the request asynchronously and notify the user of the result through an email. This provides a better overall user experience, rather than having the possibility of timing out a synchronous request if the system is overloaded with requests. The software system also has the ability to process the requests by utilizing its resources to the maximum extent without dropping any user requests.
In the above scenario, we use Azure cloud services for message queuing, its blob storage service for image storage, and its computer vision service for the OCR. Also, the overall deployment is done using Kubernetes, where we can dynamically scale the worker instances according to the CPU load that is put on the running instances. Ballerina contains seamless deployment operations for Docker and Kubernetes in order to expose Ballerina services as services in Kubernetes, and configure aspects such as HPA (Horizontal Pod Autoscaler) using code annotations.
Along with many connectors for cloud services, Ballerina also contains support for message brokers such as NATS, Kafka, and RabbitMQ. Also, it has support for WebSockets, which can be used in place of polling logic when querying for the status of an asynchronous operation.
The above system uses a polling approach by worker instances to query requests from the shared queue. We can further simplify this by using a serverless approach. This involves deploying an Azure Function to listen to the queue and be automatically triggered when messages are available in the queue. In this manner, the scaling of the functions will also be done seamlessly by the cloud environment. In a similar manner, Ballerina’s AWS Lambda functionality can be used in an AWS environment.
In this article, we have gone through the pros and cons of using synchronous and asynchronous communication when designing a microservice architecture. Generally, we cannot conclude that one approach is better than the other, but rather, the communication strategy depends on the type of operations done in the system. The communication method must match naturally to the business domain you are designing. If you are constantly finding workarounds to get your use case done, for example, wrapping an asynchronous flow to make it synchronous, then it’s a good sign that you should change your approach. We also looked at the features supported by the Ballerina language in supporting both these programming models.
For more information on writing microservices in Ballerina, check out the following resources:
Opinions expressed by DZone contributors are their own.