Java
Asynchronous Programming
Future API
Concurrency
Java Map

Asynchronously populating a Java Map and returning it as a future

Master System Design with Codemia

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

Introduction

If you want to populate a Java Map asynchronously and return it as a future, the cleanest tool is usually CompletableFuture. The main design choice is whether each async task should mutate a shared map directly or whether each task should return a value that you combine afterward. In most cases, combining results after completion is simpler and safer than concurrent mutation.

Prefer composing results over mutating one shared map

A tempting approach is to create a shared ConcurrentHashMap and let several tasks write into it. That can work, but it couples task execution and shared state management tightly.

A cleaner pattern is:

  1. run async tasks independently
  2. let each task return a value
  3. combine the completed values into a map
  4. return the final map as a CompletableFuture<Map<...>>

This usually makes exception handling and testing easier.

Build the map with CompletableFuture.allOf

Here is a simple example:

java
1import java.util.HashMap;
2import java.util.Map;
3import java.util.concurrent.CompletableFuture;
4
5public class AsyncMapDemo {
6    public static CompletableFuture<Map<String, Integer>> loadMap() {
7        CompletableFuture<Integer> users = CompletableFuture.supplyAsync(() -> 42);
8        CompletableFuture<Integer> orders = CompletableFuture.supplyAsync(() -> 15);
9        CompletableFuture<Integer> invoices = CompletableFuture.supplyAsync(() -> 7);
10
11        return CompletableFuture.allOf(users, orders, invoices)
12            .thenApply(ignored -> {
13                Map<String, Integer> result = new HashMap<>();
14                result.put("users", users.join());
15                result.put("orders", orders.join());
16                result.put("invoices", invoices.join());
17                return result;
18            });
19    }
20
21    public static void main(String[] args) {
22        Map<String, Integer> result = loadMap().join();
23        System.out.println(result);
24    }
25}

Each async task is independent. The map is assembled only after all futures finish.

Use a shared map only when tasks truly stream partial results

If tasks must update a common map as they run, use a thread-safe implementation such as ConcurrentHashMap:

java
1import java.util.Map;
2import java.util.concurrent.CompletableFuture;
3import java.util.concurrent.ConcurrentHashMap;
4
5public class SharedMapDemo {
6    public static CompletableFuture<Map<String, Integer>> loadMap() {
7        Map<String, Integer> result = new ConcurrentHashMap<>();
8
9        CompletableFuture<Void> first = CompletableFuture.runAsync(() -> result.put("a", 1));
10        CompletableFuture<Void> second = CompletableFuture.runAsync(() -> result.put("b", 2));
11
12        return CompletableFuture.allOf(first, second)
13            .thenApply(ignored -> result);
14    }
15}

That is valid, but it should be a deliberate choice, not the default. Shared mutable state makes concurrency code harder to reason about.

Handle failures explicitly

One of the advantages of CompletableFuture is that failures propagate through the chain. You can handle them at the aggregation point:

java
1loadMap()
2    .thenAccept(System.out::println)
3    .exceptionally(ex -> {
4        ex.printStackTrace();
5        return null;
6    });

This is much cleaner than manually polling old-style Future objects.

Return the future instead of blocking early

If the method contract says it returns a future, keep it asynchronous all the way out:

java
public CompletableFuture<Map<String, Integer>> loadMapAsync() {
    return loadMap();
}

Avoid calling .get() or .join() inside the method unless the whole point is to block. Blocking too early throws away the flexibility that futures provide.

Common Pitfalls

The most common mistake is using a normal HashMap and mutating it from multiple async tasks without synchronization.

Another common issue is blocking on each future one by one instead of composing them, which reduces concurrency and makes the code harder to maintain.

People also return a completed map after scheduling async work but before waiting for all tasks to finish, which produces partially populated results.

Finally, if the map is only needed after all tasks complete, prefer combining independent futures over shared mutable state.

Summary

  • 'CompletableFuture is the usual tool for asynchronously building and returning a map.'
  • Prefer independent async tasks plus a final combine step over concurrent mutation of one shared map.
  • Use ConcurrentHashMap only when tasks truly need shared updates during execution.
  • Let the method return a CompletableFuture<Map<...>> instead of blocking early.
  • Handle exceptions at the future-composition layer rather than with manual polling.

Course illustration
Course illustration

All Rights Reserved.