Harmonizing Microservices with API Composition Pattern

Orçun Yılmaz
Sahibinden Technology
7 min readJan 10, 2024
This free-to-use image was taken from pexels: https://www.pexels.com/@markusspiske/

API Composition serves as an elevated framework for interacting with microservices, introducing a central orchestrator that encapsulates the microservices’ intricacies.

With the composer sitting in between, the request from the client first hits the composer, and the composer then talks to the relevant services to get the response. It then merges the responses before sending them to the client.

API Composition simply allows these services to collaborate interoperably.

image taken from: https://microservices.io/patterns/data/api-composition.html

Imagine we have a screen for an e-commerce platform where we make a bunch of REST calls to a bunch of microservices to get;

  • Campaign Banners
  • Interested Brands
  • Interested Goods
  • Closest Ads
  • Daily Discounted Goods

Let’s say, all of these elements are being handled by its own services, Campaign Service, InterestedBrand Service, InterestedGood Service, and ClosestAds Service.

Without the API Composition pattern sitting in between the clients and the services, the client (web, mobile, etc.) would have to make multiple API calls to microservices directly to get the information and render the interface. Then, they should handle the Loading Screen kind of cosmetic issues to make the latency smoother.

What To Do Instead?

Now, if we want to build an API Composition layer from scratch, we should decide how to make the calls to these microservices with an orchestrator.

In this example, we are going to use Threads-CompletableFuture to build the API Composition.

Hence, we should decide if we would like to make the calls synchronously or asynchronously.

Sync or Async

What happens if we choose synchronous calls?

Two synchronous tasks must be aware of one another, and one task must execute in some way that is dependent on the other, meaning that one service call should finish before another starts.

In other words, sync calls are kind of waiting in line till everything clears up, one by one.

In our example, calculating the total time would work like this:

Campaign Banners -> Interested Brands -> Interested Goods -> …

Interested Brand Service calls would have to wait until Campaign Banner Service finishes its job. Let’s say the Campaign Banner Service takes 1 second to finish, Interested Brands takes 2 seconds, Interested Goods takes 2 seconds, Closest Ads takes 1 second and Daily Discounted Ads takes 0.1 seconds.

In the Sync scenario, the final result would take 1 + 2 + 2 + 1 + 0.1 = 6.1 seconds to finish.

What happens if we choose asynchronous calls?

Asynchronous means calls are totally independent and neither one must consider the other in any way, either in the initiation or in the execution.

In our example, calculating the total time would work like this:

Campaign Banners ->

Interested Brands ->

Interested Goods ->

..

Every service will start working independently, without waiting for any other service to finish.

Since each one of them will start approximately at similar times, the final result will take the longest one and it would be around 2 seconds in our case.

Well, this is clear then. Async is the winner scenario since we want the fastest solution.

But, how to implement it?

Completable Future

CompletableFuture is used for asynchronous programming in Java.

Asynchronous programming is a means of writing non-blocking code by running a task on a separate thread from the main application thread and notifying the main thread about its progress, completion, or failure.

Know that the runAsync() and supplyAsync() methods execute their tasks in a separate thread.

If you want to run some background task asynchronously and don’t want to return anything from the task, then you can use CompletableFuture.runAsync() method.

SupplyAsync takes Supplier as an argument and returns the CompletableFuture<U> with the result value, which means it does not take any input parameters but it returns the result as output.

You might be wondering that — Well, I know that the runAsync() and supplyAsync() methods execute their tasks in a separate thread. But, we never created a thread, right?

Yes! CompletableFuture executes these tasks in a thread obtained from the global ForkJoinPool.commonPool().

But hey, you can also create a Thread Pool and pass it to runAsync() and supplyAsync() methods to let them execute their tasks in a thread obtained from your thread pool.

All the methods in the CompletableFuture API have two variants — One which accepts an Executor as an argument and one which doesn’t.

CompletableFuture.allOf() is used in scenarios when you have a List of independent futures that you want to run in parallel and do something after all of them are complete.

CompletableFuture.anyOf() as the name suggests, returns a new CompletableFuture which is completed when any of the given CompletableFutures complete, with the same result.

Now that we learned what is CompletableFuture, let’s dive into an example.

Hands-On Example

First, we need a configuration class where we arrange our Executors.

@EnableAsync
@Configuration
public class ExecutorConfiguration implements AsyncConfigurer {
@Bean
public ExecutorService eCommerceMetaExecutor() {
return ContextAwareCommonExecutors.newBlockingThreadPool(ECOMMERCE_META.name(), ECOMMERCE_META.getCoreThreadCount(), ECOMMERCE_META.getMaxThreadCount());
}

@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new AsyncExceptionHandler();
}
}

You see that an ECOMMERCE_META variable is being used. What is it?

It simply contains coreThreadCount and maxThreadCount kinds of variables.

public enum ExecutorPool {
ECOMMERCE_META(4, 8);

private final int coreThreadCount;
private final int maxThreadCount;

ExecutorPool(int coreThreadCount, int maxThreadCount) {
this.coreThreadCount = coreThreadCount;
this.maxThreadCount = maxThreadCount;
}

public int getCoreThreadCount() {
return coreThreadCount;
}

public int getMaxThreadCount() {
return maxThreadCount;
}
}

Okay, why ContextAwareCommonExecutors then?

These threads are aware of their own state as well as the state of the broader system, and we need that.

Here is the class:

public class ContextAwareCommonExecutors {
private static ExecutorsFactory executorsFactory;

static {
ExecutorsFactoryImpl<Context> factory = new ExecutorsFactoryImpl<>();
factory.setAdapter(ContextManager.getAdapter());

executorsFactory = factory;
}

private static ExecutorsFactory getFactory(){
return executorsFactory;
}

public static ExecutorService newBlockingThreadPool(String poolName, int coreThreads, int maxThreads, BlockingQueue<Runnable> workQueue) {
return getFactory().newBlockingThreadPool(poolName, coreThreads, maxThreads, workQueue);
}

public static ExecutorService newBlockingThreadPool(String poolName, int threadCount, BlockingQueue<Runnable> workQueue) {
return getFactory().newBlockingThreadPool(poolName, threadCount, workQueue);
}

public static ExecutorService newBlockingThreadPool(String poolName, int coreThreads, int maxThreads) {
return getFactory().newBlockingThreadPool(poolName, coreThreads, maxThreads);
}

public static ExecutorService newBlockingThreadPool(String poolName, int threadCount) {
return getFactory().newBlockingThreadPool(poolName, threadCount);
}

public static ScheduledExecutorService newScheduledThreadPool(String poolName, int threadCount) {
return getFactory().newScheduledThreadPool(poolName, threadCount);
}

public static ExecutorService newRejectingThreadPool(String poolName, int threadCount) {
return getFactory().newRejectingThreadPool(poolName, threadCount);
}
}

Now that we have all the classes we need, let’s try to write the orchestrator.

@Service
@RequiredArgsConstructor
public class EcommerceMetaOrchestratorServiceImpl implements EcommerceMetaOrchestratorService {

private final EcommerceService eCommerceService;

@Override
public EcommerceMeta getEcommerceMeta(EcommerceForm EcommerceForm) throws ExecutionException, InterruptedException {
CompletableFuture<DailyDiscountedGoods> dailyDiscountedGoods = eCommerceService.getDailyDiscountedGoodsAsync(EcommerceForm.getEcommerceClassifiedForm());
CompletableFuture<CampaignBanner> campaignBanner = eCommerceService.getBannerAsync();
CompletableFuture<InterestedGood> interestedGoods = eCommerceService.getInterestedGoodsAsync(EcommerceForm.getInterestedGoodsForm());
CompletableFuture<InterestedBrand> interestedBrands = eCommerceService.getInterestedBrandsAsync(EcommerceForm.getInterestedBrandForm());
CompletableFuture<ClosestAds> closestAds = eCommerceService.getClosestAdsAsync(EcommerceForm.gettClosestAdsForm());

CompletableFuture.allOf(dailyDiscountedGoods, campaignBanner, interestedGoods, interestedBrands,
closestAds).join();

List<EcommerceElement> EcommerceElements = new ArrayList<>();
EcommerceElements.add(dailyDiscountedGoods.get());
EcommerceElements.add(campaignBanner.get());
EcommerceElements.add(interestedGoods.get());
EcommerceElements.add(interestedBrands.get());
EcommerceElements.add(closestAds.get());

return new EcommerceMeta(EcommerceElements);
}
}

Looks smooth, right?

What about eCommerceService method calls? What do they do exactly?

Let’s look at the CampaignBanner call, for example.

    @Async(ExecutorName.ECOMMERCE_META)
@Override
public CompletableFuture<ShoppingBanner> getBannerAsync()
{
return CompletableFuture.completedFuture(getBanner());
}

We are simply using the “Async” annotation with our newly created Executor name.

In our case, it is just a simple connection that makes the connection between the variable name and our bean name that we created earlier in the config class (look for the first code block under the Hands-On Example title).

In other words, it simply looks for eCommerceMetaExecutor named bean.

    interface ExecutorName {
String ECOMMERCE_META = "eCommerceMetaExecutor";
}

Then, we return the “CompletableFuture.completedFuture()” method to make the service call in it.

Orchestrator simply makes the calls separately to the services, and in return, it collects them with the help of the allOf() method into a return object that is going to be returned to the clients, EcommerceMeta in our case.

With the Async approach, a significant acceleration in application can be achieved.

Summary

API Composition’s strength lies in its ability to simplify the complex symphony of microservices, providing a centralized layer that abstracts away intricacies. It enables the creation of composite services, where each note (microservice) contributes to a unified melody, optimizing performance and reducing latency by intelligently aggregating data.

The advantages of API Composition are evident in its promotion of modularity, reusability, and the customization of responses tailored to specific client needs. Clients interact with a single endpoint, shielding them from the underlying intricacies of multiple microservices. This not only streamlines development on the client side but also fosters collaboration and interoperability among microservices.

However, this architectural approach is not without its challenges. The introduction of an API Composition layer adds complexity, and careful consideration is required to mitigate potential issues such as a single point of failure, data consistency challenges, and development overhead.

In essence, API Composition is the pencil sketching of the blueprint for microservices orchestration. It empowers development teams to create a symphony of services that work together seamlessly, offering a flexible and efficient solution to the dynamic demands of modern software architecture.

P.S: Thanks to mumin ayyildiz for his valuable support to this article.

References:

--

--