Boost Performance: Using DataLoader in Spring Boot for Efficient Batch Processing

This article explains how to integrate the Java‑DataLoader library into a Spring Boot 3.5.0 application, covering dependency setup, entity and repository definitions, service methods, DataLoader configuration, testing, contextual loading, and custom two‑level caching to achieve high‑performance batch data fetching.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Boost Performance: Using DataLoader in Spring Boot for Efficient Batch Processing

1. Introduction

DataLoader is a lightweight Java 11 port of Facebook's DataLoader library. It serves as a core component of the data layer, providing batch loading and caching to reduce communication overhead, especially useful for GraphQL where the N+1 query problem is common.

2. Core Features

Simple, generic API with fluent coding style

Lambda‑based batch load function definition

Requests are queued for batch processing

Load calls can be placed anywhere in the code

Each load returns a CompletableFuture<V> Multiple loaders can be created simultaneously

Automatic caching of loaded values

Cache entries can be cleared individually

Cache can be pre‑filled to avoid unnecessary loads

Custom cache‑key extraction via lambda

Batch futures resolve in order of request insertion

Partial error handling for batch failures

Batching and caching can be disabled via configuration

Custom CacheMap<K,V> and ValueCache<K,V> implementations are supported

High test coverage

3. Practical Example

3.1 Add Dependency

<dependency>
  <groupId>com.graphql-java</groupId>
  <artifactId>java-dataloader</artifactId>
  <version>6.0.0</version>
  <scope>compile</scope>
</dependency>

3.2 Entity Definition

@Entity
@Table(name = "q_user")
public class User {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  private String name;
  private Integer age;
  private String email;
  private String sex;
}

3.3 Repository Interface

public interface UserRepository extends JpaRepository<User, Long> { }

3.4 Service Layer

@Service
public class UserService {
  private final UserRepository userRepository;
  public UserService(UserRepository userRepository) {
    this.userRepository = userRepository;
  }
  public CompletableFuture<List<User>> queryUserByIds(List<Long> ids) {
    return CompletableFuture.supplyAsync(() -> userRepository.findAllById(ids));
  }
}

3.5 DataLoader Configuration

@Component
public class UserDataLoader {
  private final UserService userService;
  public UserDataLoader(UserService userService) {
    this.userService = userService;
  }
  public DataLoader<Long, User> createUserLoader() {
    BatchLoader<Long, User> userBatchLoader = ids -> {
      return userService.queryUserByIds(ids).thenApply(users -> {
        Map<Long, User> userMap = users.stream()
            .collect(Collectors.toMap(User::getId, user -> user));
        return ids.stream()
            .map(userMap::get)
            .collect(Collectors.toList());
      });
    };
    return DataLoaderFactory.newDataLoader(userBatchLoader);
  }
}

3.6 Basic Test

@Resource
private UserDataLoader loader;

@Test
public void test1() {
  DataLoader<Long, User> dataLoader = loader.createUserLoader();
  dataLoader.load(1L);
  dataLoader.load(2L).thenAccept(user -> {
    dataLoader.load(3L);
  });
  List<User> users = dataLoader.dispatchAndJoin();
  System.err.println(users);
}

When dispatchAndJoin is first called, the keys 1 and 2 are sent to the batch loader, which loads the two users. The thenAccept callback triggers a third load (key 3), causing the batch loader to process that key as well.

3.7 Contextual Loading

To pass additional information (e.g., security credentials) to the batch loader, implement BatchLoaderContextProvider and use BatchLoaderWithContext:

public DataLoader<Long, User> createUserLoaderContext() {
  DataLoaderOptions options = DataLoaderOptions.newOptions()
      .setBatchLoaderContextProvider(() -> "ADMIN")
      .build();
  BatchLoaderWithContext<Long, User> batchLoader = new BatchLoaderWithContext<Long, User>() {
    public CompletionStage<List<User>> load(List<Long> keys, BatchLoaderEnvironment env) {
      String role = env.getContext();
      System.err.println("role: %s".formatted(role));
      System.err.println("key contexts: %s".formatted(env.getKeyContexts()));
      return userService.queryUserByIds(keys);
    }
  };
  return DataLoaderFactory.newDataLoader(batchLoader, options);
}

3.8 Two‑Level Caching

DataLoader provides a first‑level cache (implemented by CacheMap) that stores CompletableFuture objects locally in the JVM, and an optional second‑level value cache ( ValueCache) that can be backed by external stores such as Redis or Memcached. By default the second‑level cache is a no‑op.

3.9 Custom Cache Implementation

public class PackCacheMap implements CacheMap<Long, User> {
  public static final Cache<Long, CompletableFuture<User>> CACHE = Caffeine.newBuilder()
      .expireAfterWrite(Duration.ofMillis(1000 * 5))
      .build();
  @Override
  public boolean containsKey(Long key) {
    return CACHE.getIfPresent(key) != null;
  }
  @Override
  public @Nullable CompletableFuture<User> get(Long key) {
    System.err.println("查询缓存【%s】".formatted(key));
    return CACHE.getIfPresent(key);
  }
  @Override
  public Collection<CompletableFuture<User>> getAll() {
    return CACHE.asMap().values();
  }
  @Override
  public @Nullable CompletableFuture<User> putIfAbsentAtomically(Long key, CompletableFuture<User> value) {
    System.err.println("缓存【%s】对象".formatted(key));
    return CACHE.asMap().putIfAbsent(key, value);
  }
  @Override
  public CacheMap<Long, User> delete(Long key) {
    CACHE.invalidate(key);
    return this;
  }
  @Override
  public CacheMap<Long, User> clear() {
    CACHE.invalidateAll();
    return this;
  }
  @Override
  public int size() {
    return CACHE.asMap().size();
  }
}

3.10 DataLoader with Custom Cache

public DataLoader<Long, User> createUserLoaderCache() {
  DataLoaderOptions options = DataLoaderOptions.newOptions()
      .setCacheMap(new PackCacheMap())
      .build();
  BatchLoader<Long, User> userBatchLoader = ids -> {
    return userService.queryUserByIds(ids).thenApply(users -> {
      Map<Long, User> userMap = users.stream()
          .collect(Collectors.toMap(User::getId, user -> user));
      return ids.stream()
          .map(userMap::get)
          .collect(Collectors.toList());
    });
  };
  return DataLoaderFactory.newDataLoader(userBatchLoader, options);
}

3.11 Cache‑Aware Unit Test

@Test
public void test3() throws Exception {
  DataLoader<Long, User> dataLoader = loader.createUserLoaderCache();
  dataLoader.load(1L);
  dataLoader.load(2L);
  List<User> users = dataLoader.dispatchAndJoin();
  System.err.println(users);
  dataLoader.load(1L).thenAccept(System.out::println);
  TimeUnit.SECONDS.sleep(6);
  dataLoader.load(1L);
  users = dataLoader.dispatchAndJoin();
  System.err.println(users);
}

The test demonstrates that the first‑level cache returns the same CompletableFuture for repeated loads of the same key, while the custom cache expires after five seconds, causing a fresh load on the second request.

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.

JavacachingSpring BootGraphQLDataLoaderBatch Loading
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.