Boost Spring Boot Order Calls: Sync vs Async with ThreadPool & CompletableFuture

This article compares synchronous and various asynchronous approaches for invoking multiple third‑party services in a Spring Boot order‑creation workflow, demonstrating plain sequential calls, manual thread creation, thread‑pool execution, CompletionService handling, and CompletableFuture composition, and shows their performance impact through timing results.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Boost Spring Boot Order Calls: Sync vs Async with ThreadPool & CompletableFuture

Environment: springboot2.3.9.RELEASE

The typical order‑creation process involves three external calls: query user information, query inventory information, and query discount information.

Synchronous Sequential Calls

public boolean createOrder() {
    long start = System.currentTimeMillis();
    String userResult = restTemplate.getForObject("http://localhost:8080/users/{1}", String.class, new Object[]{1});
    String storageResult = restTemplate.getForObject("http://localhost:8080/storage/{1}", String.class, new Object[]{1});
    String discountResult = restTemplate.getForObject("http://localhost:8080/discount/{1}", String.class, new Object[]{1});
    // merge results
    System.out.println(Arrays.toString(new String[]{userResult, storageResult, discountResult}));
    System.out.println("Traditional time: " + (System.currentTimeMillis() - start) + " ms");
    return true;
}
@GetMapping("/create")
public Object create() {
    return os.createOrder();
}

Calling the interfaces one by one is very time‑consuming.

Multi‑Thread (Callable + Future)

public boolean createOrder2() {
    long start = System.currentTimeMillis();
    Callable<String> userCallable = () -> restTemplate.getForObject("http://localhost:8080/users/{1}", String.class, new Object[]{1});
    Callable<String> storageCallable = () -> restTemplate.getForObject("http://localhost:8080/storage/{1}", String.class, new Object[]{1});
    Callable<String> discountCallable = () -> restTemplate.getForObject("http://localhost:8080/discount/{1}", String.class, new Object[]{1});
    FutureTask<String> userTask = new FutureTask<>(userCallable);
    FutureTask<String> storageTask = new FutureTask<>(storageCallable);
    FutureTask<String> discountTask = new FutureTask<>(discountCallable);
    new Thread(userTask).start();
    new Thread(storageTask).start();
    new Thread(discountTask).start();
    try {
        String userResult = userTask.get();
        String storageResult = storageTask.get();
        String discountResult = discountTask.get();
        // merge results
        System.out.println(Arrays.toString(new String[]{userResult, storageResult, discountResult}));
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    }
    System.out.println("Multi‑thread time: " + (System.currentTimeMillis() - start) + " ms");
    return true;
}

The execution time is reduced, showing clear performance improvement, but directly creating threads is discouraged in production because high concurrency can lead to OOM errors.

ThreadPool (Callable + Future) to Prevent OOM

ThreadPoolExecutor pool = new ThreadPoolExecutor(5, 5, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(1000));
public boolean createOrder3() {
    long start = System.currentTimeMillis();
    List<Future<String>> results = new ArrayList<>(3);
    results.add(pool.submit(() -> restTemplate.getForObject("http://localhost:8080/users/{1}", String.class, new Object[]{1})));
    results.add(pool.submit(() -> restTemplate.getForObject("http://localhost:8080/storage/{1}", String.class, new Object[]{1})));
    results.add(pool.submit(() -> restTemplate.getForObject("http://localhost:8080/discount/{1}", String.class, new Object[]{1})));
    for (int i = 0, size = results.size(); i < size; i++) {
        try {
            System.out.println(results.get(i).get());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
    System.out.println("ThreadPool time: " + (System.currentTimeMillis() - start) + " ms");
    return true;
}

The timing is similar to the previous method; however, using Future still requires retrieving results sequentially, which can block even if other tasks finish earlier.

CompletionService (Asynchronous Tasks with Result Decoupling)

public boolean createOrder4() {
    long start = System.currentTimeMillis();
    CompletionService<String> cs = new ExecutorCompletionService<>(pool);
    cs.submit(() -> restTemplate.getForObject("http://localhost:8080/users/{1}", String.class, new Object[]{1}));
    cs.submit(() -> restTemplate.getForObject("http://localhost:8080/storage/{1}", String.class, new Object[]{1}));
    cs.submit(() -> restTemplate.getForObject("http://localhost:8080/discount/{1}", String.class, new Object[]{1}));
    for (int i = 2; i >= 0; i--) {
        try {
            System.out.println(cs.take().get());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
    System.out.println("CompletionService time: " + (System.currentTimeMillis() - start) + " ms");
    return true;
}

Using CompletionService allows results to be retrieved as soon as any task finishes, regardless of submission order; the take() method blocks until a task completes.

CompletableFuture (Async Composition, JDK 1.8)

public boolean createOrder5() {
    long start = System.currentTimeMillis();
    CompletableFuture<String> userFuture = CompletableFuture.supplyAsync(() ->
        restTemplate.getForObject("http://localhost:8080/users/{1}", String.class, new Object[]{1})
    );
    CompletableFuture<String> storageFuture = CompletableFuture.supplyAsync(() ->
        restTemplate.getForObject("http://localhost:8080/storage/{1}", String.class, new Object[]{1})
    );
    CompletableFuture<String> discountFuture = CompletableFuture.supplyAsync(() ->
        restTemplate.getForObject("http://localhost:8080/discount/{1}", String.class, new Object[]{1})
    );
    CompletableFuture<List<String>> result = CompletableFuture
        .allOf(userFuture, storageFuture, discountFuture)
        .thenApply(v -> {
            List<String> datas = new ArrayList<>();
            try {
                datas.add(userFuture.get());
                datas.add(storageFuture.get());
                datas.add(discountFuture.get());
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
            return datas;
        })
        .exceptionally(e -> {
            e.printStackTrace();
            return null;
        });
    try {
        System.out.println(result.get());
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    }
    System.out.println("CompletableFuture time: " + (System.currentTimeMillis() - start) + " ms");
    return true;
}

CompletableFuture provides powerful asynchronous programming capabilities, supporting synchronous, asynchronous, and task‑orchestration patterns; its API resembles JavaScript's Promise.

All methods demonstrated.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

ThreadPoolCompletableFutureSpring Bootasynchronous programmingJava concurrency
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.