CompletableFuture is a powerful tool from the Java library that gives a convenient API for working with async tasks. Its key advantages:
- Lets you run tasks on separate threads without blocking the main thread.
- Supports building chains of dependent tasks.
- Makes it easier to run async operations and handle their results.
In the context of GraphQL, CompletableFuture allows us to:
- Handle requests in parallel (for example, fetch data from multiple sources).
- Avoid blocking server resources while waiting for external API responses.
Step 1: Project setup
To work with CompletableFuture in GraphQL you'll need:
- A Spring Boot app with Spring GraphQL already configured.
- Basic entities and Data Fetchers set up in your GraphQL project.
If you don't have a project yet, create a minimal structure:
spring init --dependencies=web,graphql,data-jpa,h2 demo-graphql
Add dependencies to pom.xml (if not already added):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-graphql</artifactId>
</dependency>
Step 2: Schema for async GraphQL
In schema.graphqls add a query that returns a list of users with their orders:
type User {
id: ID!
name: String!
orders: [Order]!
}
type Order {
id: ID!
description: String!
}
type Query {
users: [User]!
}
Step 3: Implementing asynchronous Data Fetchers
1. Simulating data access
For simplicity we'll create two Service components:
- One to get users (
UserService). - Another to get orders (
OrderService).
@Service
public class UserService {
public List<User> getUsers() {
// Simulate fetching data
return List.of(
new User(1L, "Alice"),
new User(2L, "Bob")
);
}
}
2. Defining Data Fetchers
In our GraphQL API we need to:
- Return the list of users.
- For each user, attach an async call to fetch their orders.
@Component
public class GraphQLDataFetcher {
private final UserService userService;
private final OrderService orderService;
public GraphQLDataFetcher(UserService userService, OrderService orderService) {
this.userService = userService;
this.orderService = orderService;
}
@QueryMapping
public List<User> users() {
return userService.getUsers();
}
@SchemaMapping(typeName = "User", field = "orders")
public CompletableFuture<List<Order>> orders(User user) {
// Asynchronous call to fetch orders
return orderService.getOrdersForUser(user.getId());
}
}
What's happening here?
- The
usersmethod returns the list of users. - For each user the
ordersmethod is invoked, which asynchronously loads orders for that specific user.
Step 4: Testing async behavior
1. Enabling GraphQL Playground
Add to application.properties:
spring.graphql.graphiql.enabled=true
spring.graphql.graphiql.path=/graphiql
Now you can test your GraphQL API via the web UI at http://localhost:8080/graphiql.
2. Running a query
Send the following query:
{
users {
id
name
orders {
id
description
}
}
}
Expected result:
{
"data": {
"users": [
{
"id": "1",
"name": "Alice",
"orders": [
{ "id": "1", "description": "Order 1 for user 1" },
{ "id": "2", "description": "Order 2 for user 1" }
]
},
{
"id": "2",
"name": "Bob",
"orders": [
{ "id": "1", "description": "Order 1 for user 2" },
{ "id": "2", "description": "Order 2 for user 2" }
]
}
]
}
}
Step 5: Testing async code
Unit tests for CompletableFuture
Create a simple test to verify async behavior:
@SpringBootTest
class OrderServiceTest {
@Autowired
private OrderService orderService;
@Test
void testGetOrdersForUser() throws Exception {
CompletableFuture<List<Order>> future = orderService.getOrdersForUser(1L);
// We block only in tests
List<Order> orders = future.get(); // Blocking only for tests
assertEquals(2, orders.size());
assertEquals("Order 1 for user 1", orders.get(0).getDescription());
}
}
Integration tests for GraphQL
Use MockMvc to test GraphQL queries:
@WebMvcTest(GraphQLDataFetcher.class)
class GraphQLDataFetcherTest {
@Autowired
private MockMvc mockMvc;
@Test
void testUsersQuery() throws Exception {
String query = "{ users { id, name, orders { id, description } } }";
mockMvc.perform(post("/graphql")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"query\": \"" + query + "\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.users").isArray());
}
}
Step 6: Practice and analysis
How to improve performance?
- Make sure heavy operations run asynchronously.
- Use an
ExecutorServiceto manage threads. - Add caching if data rarely changes.
Common mistakes:
- Blocking the main thread. Use
CompletableFuturecorrectly to avoid blocking. - Incorrect exception handling. Always handle errors with
exceptionallyto deal with failures. - Unclosed resources. Async operations can leave connections open. Make sure all resources are closed properly.
return CompletableFuture.supplyAsync(() -> {
// Your logic code
}).exceptionally(ex -> {
// Log errors
return Collections.emptyList();
});
Your GraphQL API is now asynchronous, performant, and ready to interact with external APIs or slow systems. You can be proud — you've taken a step toward building a high-performance app of the future!
GO TO FULL VERSION