Java concurrency
CompletionService
ExecutorService
asynchronous programming
thread management

When should I use a CompletionService over an ExecutorService?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

ExecutorService and CompletionService both help you run tasks asynchronously in Java, but they solve different coordination problems. ExecutorService is the basic execution mechanism, while CompletionService adds a completion-order result queue for cases where the order in which tasks finish matters more than the order in which they were submitted.

Core Sections

Start with ExecutorService when simple futures are enough

ExecutorService manages the thread pool and returns Future objects when you submit tasks.

java
1import java.util.ArrayList;
2import java.util.List;
3import java.util.concurrent.*;
4
5public class ExecutorOnlyExample {
6    public static void main(String[] args) throws Exception {
7        ExecutorService pool = Executors.newFixedThreadPool(3);
8        List<Future<String>> futures = new ArrayList<>();
9
10        for (int i = 1; i <= 3; i++) {
11            int id = i;
12            futures.add(pool.submit(() -> {
13                Thread.sleep(id * 100L);
14                return "task-" + id;
15            }));
16        }
17
18        for (Future<String> future : futures) {
19            System.out.println(future.get());
20        }
21
22        pool.shutdown();
23    }
24}

This is fine when you either:

  • want results in submission order
  • wait for everything anyway
  • have similarly sized tasks so the ordering does not matter much

The downside is that a slow early future can block processing even when later tasks have already completed.

Use CompletionService when you want results as soon as tasks finish

CompletionService wraps an executor and provides a queue of completed tasks. That lets you consume results in completion order rather than submission order.

java
1import java.util.concurrent.*;
2
3public class CompletionServiceExample {
4    public static void main(String[] args) throws Exception {
5        ExecutorService pool = Executors.newFixedThreadPool(3);
6        CompletionService<String> completion = new ExecutorCompletionService<>(pool);
7
8        completion.submit(() -> { Thread.sleep(400); return "slow"; });
9        completion.submit(() -> { Thread.sleep(100); return "fast"; });
10        completion.submit(() -> "instant");
11
12        for (int i = 0; i < 3; i++) {
13            Future<String> done = completion.take();
14            System.out.println(done.get());
15        }
16
17        pool.shutdown();
18    }
19}

This is the right tool when partial results are useful immediately and you do not want slow tasks to delay handling of fast ones.

Real scenarios where CompletionService helps

CompletionService becomes valuable when:

  • task durations vary significantly
  • you want the first successful answer from several sources
  • you want to process results incrementally as workers finish
  • you are building a fan-out and fan-in workflow where latency matters

A common pattern is to return the first successful result and cancel the rest.

java
1import java.util.List;
2import java.util.concurrent.*;
3
4public class FirstSuccessExample {
5    public static void main(String[] args) throws Exception {
6        ExecutorService pool = Executors.newFixedThreadPool(4);
7        CompletionService<String> completion = new ExecutorCompletionService<>(pool);
8
9        List<Callable<String>> tasks = List.of(
10            () -> { Thread.sleep(500); return "A"; },
11            () -> { Thread.sleep(200); return "B"; },
12            () -> { throw new RuntimeException("failed"); }
13        );
14
15        for (Callable<String> task : tasks) {
16            completion.submit(task);
17        }
18
19        String winner = null;
20        for (int i = 0; i < tasks.size(); i++) {
21            try {
22                winner = completion.take().get();
23                break;
24            } catch (ExecutionException ex) {
25                // keep waiting for a successful result
26            }
27        }
28
29        pool.shutdownNow();
30        System.out.println(winner);
31    }
32}

Doing this cleanly with only a List<Future<?>> is awkward because the finished task is not easy to discover without polling every future.

CompletionService is not a replacement for the executor

This is an important conceptual point: CompletionService does not replace ExecutorService. It uses an executor underneath. The comparison is really about whether the raw executor API is enough, or whether you also need completion-order orchestration.

If you need richer task composition, chained asynchronous logic, or transformations, CompletableFuture may be a better fit than either bare futures or CompletionService. But for queue-style consumption of completed tasks, CompletionService is still a strong and simple abstraction.

Common Pitfalls

  • Using CompletionService when you only need to wait for all tasks adds extra abstraction without much benefit.
  • Iterating a List<Future<?>> in submission order when task durations vary can waste latency that CompletionService would avoid.
  • Forgetting to cancel or shut down remaining work after early success can leave background tasks running unnecessarily.
  • Ignoring timeout handling when waiting on completed tasks can still leave the orchestration vulnerable to stalls.
  • Treating CompletionService as a different execution engine instead of as a coordination layer on top of an executor makes the API relationship harder to understand.

Summary

  • 'ExecutorService is the base tool for running asynchronous tasks.'
  • 'CompletionService is better when you need completed results in the order they finish, not the order they were submitted.'
  • It is especially useful for variable-duration tasks, first-success races, and incremental result processing.
  • It still relies on an underlying executor rather than replacing it.
  • Choose the simplest concurrency abstraction that matches the coordination behavior your code actually needs.

Course illustration
Course illustration

All Rights Reserved.