Home CompletableFuture, The Power of Asynchronous Programming & Which business case did we use?
Post
Cancel

CompletableFuture, The Power of Asynchronous Programming & Which business case did we use?

In the dynamic landscape of software development, mastering concurrency and asynchronous operations has become pivotal for building responsive and efficient applications. Within the realm of Java, CompletableFuture stands as a robust instrument, empowering developers to sculpt a competitive future for their software endeavors.

What is Java Concurrency

“Java Concurrency,” or concurrency in Java, refers to the practice of executing multiple tasks or threads simultaneously within a Java program. It is a fundamental concept in Java programming and plays a crucial role in developing efficient and responsive applications. Concurrency allows different parts of a program to run independently, making better use of available resources, such as multiple processor cores, and improving overall system performance.

Java provides several mechanisms and libraries to support concurrency, including threads, thread pools, and higher-level abstractions like the CompletableFuture class. These tools enable developers to create concurrent and parallel applications, perform tasks concurrently, and handle synchronization and communication between threads to avoid data races and ensure thread safety.

Concurrency is essential for various application scenarios, such as handling multiple user requests in web servers, parallelizing data processing tasks, and improving the responsiveness of graphical user interfaces (GUIs). Properly managing concurrency in Java applications is crucial to achieving efficient resource utilization and maintaining program correctness.

How to Use CompletableFuture?

  • Creating a CompletableFuture

1 - Using supplyAsync for a Computation:

1
2
3
4
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    // Perform a computation or task asynchronously
    return "Result of the computation";
});

2 - Creating an Incomplete CompletableFuture:

1
2
3
CompletableFuture<String> future = new CompletableFuture<>();
    // Later, complete or exceptionally complete the future manually
});

3 - Using runAsync for an Asynchronous Task without a Result:

1
2
3
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
    // Perform an asynchronous task with no return value
});
  • Defining a Task Using a Customized Executor
1
2
3
4
5
6
Executor executor = Executors.newFixedThreadPool(5);

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    // Starting an asynchronous process using a custom Executor
    return "This process runs in a private thread pool.";
}, executor);
  • Defining Tasks with CompletableFuture Chained Transactions
1
2
3
4
5
6
CompletableFuture<Integer> initialFuture = CompletableFuture.supplyAsync(() -> 5);

CompletableFuture<Integer> future = initialFuture.thenApplyAsync(result -> {
    // Chain transaction: Starting a new transaction using the result of the previous transaction
    return result * 2;
});

Our case & Solution

Case:

Within our organization, we are actively enhancing our in-house OCR application to deliver ongoing performance improvements. With the goal of empowering foreign trade companies, our users can swiftly generate work orders using a sample invoice file by simply submitting a request to this internally developed OCR application.

Nevertheless, the response time of our OCR service may vary depending on the complexity of the file being processed. To ensure a seamless user experience and prevent users from waiting indefinitely, we have developed a client service that communicates with the asynchronous OCR service. This client service not only manages requests but also interprets and maps the responses for a more meaningful user interaction.

Solution:

We’ve developed two APIs to facilitate this process. The first API accepts the user’s desired file as a multipart-file input and provides a unique conversation ID in return. This conversation ID is used to monitor the progress of the file’s OCR processing. This service is designed to operate asynchronously, forwarding requests to our in-house OCR service and managing both results and error scenarios effectively. The implementation of this functionality is accomplished with the assistance of CompletableFuture.

1
2
3
4
5
    @PostMapping("/scan")
    ResponseEntity<T> scan(@RequestParam("file") MultipartFile file);

    @PostMapping("/monitor")
    ResponseEntity<T> monitor(@RequestParam("conversationId") String conversationId);

Caching begins promptly upon receiving a request, utilizing Redis to store the status with the conversation-id key set to in_progress. Subsequently, upon receiving a successful response from the OCR service, the information associated with the conversation-id is retrieved from the cache. It is then updated to success, and the OCR service’s response is appended. In the event of an error, the status information stored in the cache as ‘in_progress’ is updated to failed for the respective conversation-id.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
    @Async
    public CompletableFuture<CacheableOcrResponse> asyncOperation(MultipartFile file, UUID uuid) {
        ...
        ..

        //Ocr service entity
        HttpEntity<OcrRequest> entity = new HttpEntity<>(OcrRequest.builder()
                .guid(String.valueOf(uuid))
                .document(base64Encoded)
                .build());

        //Perform a task asynchronously
        return CompletableFuture.supplyAsync(() -> {
            try {
                // insert data to cache instantly
                putOcrDataToCache(uuid, OcrStatus.IN_PROGRESS);

                // Ocr service call
                ResponseEntity<OcrResponse> responseEntity = customRestClient.exchange(
                        Constants.OCR_ENDPOINT_BASE + Constants.OCR_ENDPOINT_PATH,
                        HttpMethod.POST, entity, OcrResponse.class);

                OcrResponse response = responseEntity.getBody();
                logger.debug("Ocr Legacy Response: {}", response != null ? response : "Ocr Legacy Service did not return any response");

                return CacheableOcrResponse.builder()
                        .status(OcrStatus.IN_PROGRESS)
                        .response(response)
                        .build();
            } catch (Exception e) {
                logger.error("Error:" + e.getMessage());
                throw new BusinessException(OCR_READ_EXCEPTION);
            }
        }).thenApply(result -> {
            // Perform operations on the response using thenApply().
            // Checking whether the response from the OCR service is successful or not.
            if (result.getResponse() == null || result.getResponse().getData() == null) {
                putOcrDataToCache(uuid, null, OcrStatus.FAILED);
            } else {
                putOcrDataToCache(uuid, result.getResponse(), OcrStatus.SUCCESS);
            }

            return result;
        }).exceptionally(ex -> {
            //Handling the situation when any exception is occured
            logger.error("Error: " + ex.getMessage());
            putOcrDataToCache(uuid, OcrStatus.FAILED);
            return null;
        });
    }

The second API monitors the file reading progress using this conversation-id data. To achieve this, it queries Redis with the conversation-id provided in the request and subsequently delivers the response to the client.

Final Thoughts

Through this approach, the objective is to empower users to carry out additional tasks without being hindered by lengthy transactional processes.

In a project utilizing CompletableFuture, by continuing to explore the power of multi-threading and asynchronous programming, you can make your application faster, more scalable, and more reliable. By harnessing the flexibility and performance-enhancing features offered by CompletableFuture, you can achieve greater success while advancing your project into the future. Therefore, by continuing to fully leverage the potential of CompletableFuture, you can enhance efficiency and effectiveness in your software development processes.

This post is licensed under CC BY 4.0 by the author.