In one of previous articles we saw that Google Guava provides a listenable futures which are able to invoke callback methods. But it's not theirs single feature.
Data Engineering Design Patterns

Looking for a book that defines and solves most common data engineering problems? I wrote
one on that topic! You can read it online
on the O'Reilly platform,
or get a print copy on Amazon.
I also help solve your data engineering problems 👉 contact@waitingforcode.com 📩
In this article we'll see how to transform future tasks into one common result. The first part will explain this concept while the second one will show how to implement it.
Chaining and transforming tasks in Google Guava
Google Guava introduces listenable futures but it's not the only new feature. It also provides a way to transform results from future objects. The transformation is achieved thanks to transform method from com.google.common.util.concurrent.Futures class. To simplify the workflow, this method takes a ListenableFuture tasks in argument and return the final result after the computation of all tasks results.
The transformation can be useful when we must to divide some request to be able to get the result quicker. For example, imagine the situation when you need to make 2 separate request to get the object understandable by your application. And these two calls must be make one after another, in ordered way. This situation can be resolved with transform method which will return the final object after receiving the responses of the first request.
Example of transform in Google Guava
To see how transform works, there are a simple test case:
public class FuturesTest { @Test public void test() { long start = System.currentTimeMillis(); String itemName = "black wheels"; String itemName2 = "glass"; ListeningExecutorService pool = MoreExecutors.sameThreadExecutor(); ListenableFuture<String> firstResult = pool.submit(new ApiCaller(3000, itemName)); ListenableFuture<String> secondResult = pool.submit(new ApiCaller(6000, itemName2)); final Map<String, String> threads = new HashMap<String, String>(); /** * Transform method can take the 3rd argument, an instance of Executor. If you want to execute given * Function in the same thread as the caller thread, you can omit this argument. If you want * to use an executor, you have to specify it. * Consider this case when you want to do not disturb the calling thread. */ Function<String, CarItem> function1 = new Function<String, CarItem>() { @Override public CarItem apply(String item) { // imagine that you make to query your database and make some conversions before getting the expected CarItemObject threads.put("no-executor", Thread.currentThread().getName()); return new CarItem(item); } }; Function<String, CarItem> function2 = new Function<String, CarItem>() { @Override public CarItem apply(String item) { // imagine that you make to query your database and make some conversions before getting the expected CarItemObject threads.put("with-executor", Thread.currentThread().getName()); return new CarItem(item); } }; ListenableFuture<CarItem> car = Futures.transform(firstResult, function1); ListenableFuture<CarItem> carExecutor = Futures.transform(secondResult, function2, Executors.newCachedThreadPool()); long end = System.currentTimeMillis(); int executionTime = Math.round((end - start)/1000); assertTrue("Execution time should be 9 seconds (first Future executed within 6 seconds, the second within 3 seconds) but is "+executionTime + " seconds", executionTime == 9); assertTrue("Car item name should be '"+itemName+"' but was '"+car.get().getName()+"'", car.get().getName().equals(itemName)); assertTrue("Car item name should be '"+itemName2+"' but was '"+carExecutor.get().getName()+"'", carExecutor.get().getName().equals(itemName2)); String curThreadName = Thread.currentThread().getName(); assertTrue("Transformation without Executor should be made in calling thread ("+Thread.currentThread().getName()+")", curThreadName.equals(threads.get("no-executor"))); assertTrue("Transformation with Executor should be made in thread pool but it wasn't", !threads.get("with-executor").equals(curThreadName)); } } class CarItem { private String name; public CarItem(String name) { this.name = name; } public String getName() { return this.name; } } class ApiCaller implements Callable<String> { private int timeSleep; private String result; public ApiCaller(int timeSleep, String result) { this.timeSleep = timeSleep; this.result = result; } @Override public String call() throws Exception { Thread.sleep(this.timeSleep); return this.result; } }
In this article we saw how to transform the computation of two future tasks into one result. This feature can be useful when you have to deal with two future tasks where the final result can be calculated only when the first task is executed before the second.
Consulting

With nearly 16 years of experience, including 8 as data engineer, I offer expert consulting to design and optimize scalable data solutions.
As an O’Reilly author, Data+AI Summit speaker, and blogger, I bring cutting-edge insights to modernize infrastructure, build robust pipelines, and
drive data-driven decision-making. Let's transform your data challenges into opportunities—reach out to elevate your data engineering game today!
👉 contact@waitingforcode.com
đź”— past projects