Communication Between Microservices: How to Avoid Common Problems

By: Thorben
  |  March 11, 2024
Communication Between Microservices: How to Avoid Common Problems

In one of the previous posts, I showed you how to build a microservice with Java EE and JBoss Forge. But building one microservice is obviously not enough. The overall idea of this architectural style is to implement an application as a system of services. In the beginning, that seems like an easy task. As long as each service solves only one problem and doesn’t need to call other services, you can keep its complexity low, and it will be easy to understand.

But if you think back to all the applications you’ve built during your career as a software developer, were there any applications that required the implementation of several independent tasks which didn’t interact with each other?

In an ideal world, that should be the case. But in my experience, that only rarely happens. Sure, most applications have several tasks that are independent of the rest of the application. Examples for that are a nightly job that imports data or the product catalog of an online store. But there are also tasks that are more complex, and that can’t be easily implemented in an independent service which doesn’t use any other parts of the system.

In these cases, your microservices need to communicate with each other. But that sounds a lot easier than it seems. As soon as your services need to interact with each other, you can’t any longer ignore that you’re building a distributed system.

Problems of distributed systems

Distributed systems introduce a lot of challenges that you can most often ignore when you build a monolith. Some of them are performance, fault tolerance and monitoring.

Performance

As long as you build a monolith, you don’t need to put too much thought into how your modules communicate with each other. The main reason for that is that in-process function calls are incredibly fast. But that changes in a distributed system when you need to replace the in-process calls with remote calls. These calls are a lot slower. So, you need to think carefully about when and how you’re using them. In the best case, you don’t use them at all.

Fault Tolerance

Fault tolerance is another thing that becomes necessary when you’re building a distributed system.

In a monolith, all parts of your application are either available or not. That is one of the often named disadvantages of a monolith. Whenever one part of your application breaks, it affects your whole application. But it also reduces the complexity of your system. It can’t happen that one part of your application is up and performing well while another part is down.

With a system of distributed microservices, you need to prepare for this situation. Independently deployed services also fail independently. So, you need to implement your communication in a fault tolerant way so that the downtime of one service doesn’t affect other services.

Logging and Monitoring

Other challenges that you need to face in a distributed environment are monitoring and logging. As long as you deploy your system as one big monolith, you just need to monitor one application, and you find all log files in one place. In a distributed system, these tasks become a lot harder.

You now need to monitor multiple services at once, and these services might even use different technologies. So, the selection of a good monitoring tool becomes important. And when you want to analyze something in your log files, you need to check the log files of multiple services and track one user request through multiple systems.

So, how do you handle these challenges?

Tools like Retrace can help you solve the logging and monitoring challenges. But that’s not the case for performance and fault tolerance. You need to address these problems in your application design. The obviously best approach to do that is to design your services so that they don’t depend on each other.

Avoiding communication between microservices

I know, at the beginning of this post, we agreed that the world isn’t perfect and that some services depend on each other. The important question is: What do they depend on? Does service A depend on the data that service B provides or does it require B to perform a specific operation?

If it only depends on the data, you should consider replicating that data to service A to avoid the remote call. That also allows you to transform and store the data in a way that’s optimal for service A. So, you might even get more benefits than just the avoided remote call.

But as always, you don’t get that for free. Replicating data introduces a new challenge. You need to update the replicated data. That’s an easy task if the data is static or if service A can work with slightly outdated data so that you can do the replication asynchronously.

But the smaller the time frame in which you need to replicate your data, the more complicated it gets. And it should be obvious that you don’t want to perform a synchronous call from service B to service A. All remote calls that are carried out while processing a user request slow down your system and introduce another source of failure.

So, you might be able to avoid remote calls when your service just depends on the data provided by another microservice. But what about all the cases in which you need to call service B to trigger the business logic it contains? You can’t avoid the communication in these situations. So, you need to implement it as efficient as possible.

Implementing asynchronous communication

You should prefer asynchronous communication for all remote calls. They don’t block any resources while you’re waiting for the response and you can even execute multiple calls in parallel. That can provide huge performance improvements because you just need to wait until the slowest service answered your request.

As always, there are several ways to implement an asynchronous communication between two services. One of them is an asynchronous REST call.

Asynchronous calls with JAX-RS

Java EE supports asynchronous REST calls for quite a while, now. It’s defined by the JAX-RS specification, and you can use it with all spec compliant implementations. That’s one of the benefits of Java EE. You can run the same code on different servers, like Wildfly, Payara, WebSphere or TomEE.

And as we’re talking about microservices, you should also take a look at the MicroProfile initiative and some of its optimized application server distributions, like Wildfly Swarm, Payara Micro, WebSphere Liberty or TomEE.

OK, so how do you implement an asynchronous REST endpoint with JAX-RS and how do you call it?

Let’s start with the REST endpoint.

[adinserter block=”33″]

Implementing an asynchronous REST endpoint

The JAX-RS specification makes the implementation of an asynchronous REST endpoint very easy. It just takes an additional @Suspended annotation and an AsyncResponse method parameter to turn a synchronous REST endpoint into an asynchronous one.

The container injects a suspended AsyncResponse object as a method parameter. The object is bound to the processing of the active request, and you can use it within your method to resume the request as soon as a result is available.

@Stateless
@Path("/books")
public class BookEndpoint {
	@Resource
	ManagedExecutorService exec;
	
	@GET
	@Path("/async")
	public void async(@Suspended AsyncResponse response) {
		response.setTimeout(5, TimeUnit.SECONDS);
		
		String firstThread = Thread.currentThread().getName();
		log.info("First thread: "+firstThread);
		
		exec.execute(new Runnable() {
			
			@Override
			public void run() {
				String secondThread = Thread.currentThread().getName();
				log.info("Second thread: "+secondThread);
				
				// do something useful ...
				
				// resume request and return result
				response.resume(Response.ok("Some result ...").build());
			}
		});
	}
}

OK, as you can see in the code snippet, there are a few other things you should do to implement an asynchronous REST endpoint. First of all, you should define a timeout after which the request gets canceled, and the client receives an HTTP 503 Service Unavailable response. You can do that by calling the setTimeout of the injected AsyncResponse object.

And you should also use a ManagedExecutorService to execute the logic of your REST endpoint in a separate thread. The ManagedExecutorService is part of JSR 236: Concurrency Utilities for Java EE. It utilizes a managed thread pool within a Java EE application server and provides a safe way to run your code within a separate thread.

These are the most important things you need to do to implement an asynchronous REST endpoint. You now just need to add your business logic to the run method and to call the REST endpoint asynchronously.

Implementing an asynchronous REST client

As you’ve seen, JAX-RS provides full support to implement asynchronous REST endpoints. So, it’s no surprise that it’s the same on the client side. But you might ask yourself why you need to do anything when you already implemented the endpoint asynchronously. The reason for that is that the HTTP call is still blocking.

But don’t worry the implementation of an asynchronous client call is simple. You can implement it in almost the same way as a synchronous call. The only thing special about it is that you need to call the async method on the Invocation.Builder.

Client client = ClientBuilder.newBuilder().build();
WebTarget webTarget = client.target("http://localhost:8080/bookStore/rest/books/async");
Invocation.Builder request = webTarget.request();
AsyncInvoker asyncInvoker = request.async();
Future futureResp = asyncInvoker.get();
log.info("Do something while server process async request ...");
Response response = futureResp.get(); //blocks until client responds or times out
String responseBody = response.readEntity(String.class);
log.info("Received: "+responseBody);

The call of the get method returns a Future object. You can use it to wait for the request to finish and to retrieve the result. That’s all you need to do to implement an asynchronous REST call.

Summary

The implementation of a microservice might seem easy in the beginning. Its smaller size and the focus on one specific task reduces its complexity and makes it a lot simpler to understand than the typical monolith.

But that quickly changes when you have to implement multiple services that depend on each other. The distributed nature of the system adds a lot of technical complexity. You should, therefore, try to avoid any dependencies between the services and implement them as independent as possible.

But you can’t always do that. You sometimes need to call another service to trigger its business logic.

In these cases, you need to design your services and infrastructure and service so that you can handle the additional complexity. Monitoring tool, like Retrace, can help you to collect the required information from all systems. And by implementing the communication in an asynchronous way, you can minimize the performance impact of remote calls.

One of the options to implement an asynchronous communication between your services are asynchronous REST endpoints. As you’ve seen in this post, JAX-RS provides good support for that, and you can implement them in almost the same way as you implement any synchronous REST call and endpoint.

Messaging provides another option to implement an asynchronous communication between your services. But that provides other challenges and benefits. I will get into more details about it in another post.

Improve Your Code with Retrace APM

Stackify's APM tools are used by thousands of .NET, Java, PHP, Node.js, Python, & Ruby developers all over the world.
Explore Retrace's product features to learn more.

Learn More

Want to contribute to the Stackify blog?

If you would like to be a guest contributor to the Stackify blog please reach out to [email protected]