How to Merge Concurrent Requests in Spring Boot and Save Database Connections

This article explains how to batch multiple user‑info requests on the server side, merge them into a single SQL query using a blocking queue and ScheduledThreadPoolExecutor, and return the results individually, thereby reducing database connection usage and improving performance under high concurrency.

Java Backend Technology
Java Backend Technology
Java Backend Technology
How to Merge Concurrent Requests in Spring Boot and Save Database Connections

Introduction

What is the benefit of merging requests? When three users (IDs 1, 2, 3) each query their basic information, the server would normally issue three separate database calls, wasting valuable connection resources.

By merging the requests, only one SQL query is sent to the database, and the results are grouped and returned to each user based on a unique request ID.

Replacing the database with a remote service follows the same principle.

Technical Approach

LinkedBlockingQueue

– a blocking queue ScheduledThreadPoolExecutor – scheduled thread pool CompletableFuture – future without built‑in timeout (later replaced by a queue)

Code Implementation

Query Service Interface

public interface UserService {
    Map<String, Users> queryUserByIdBatch(List<UserWrapBatchService.Request> userReqs);
}

Service Implementation

@Service
public class UserServiceImpl implements UserService {
    @Resource
    private UsersMapper usersMapper;

    @Override
    public Map<String, Users> queryUserByIdBatch(List<UserWrapBatchService.Request> userReqs) {
        List<Long> userIds = userReqs.stream()
            .map(UserWrapBatchService.Request::getUserId)
            .collect(Collectors.toList());
        QueryWrapper<Users> queryWrapper = new QueryWrapper<>();
        queryWrapper.in("id", userIds);
        List<Users> users = usersMapper.selectList(queryWrapper);
        Map<Long, List<Users>> userGroup = users.stream()
            .collect(Collectors.groupingBy(Users::getId));
        HashMap<String, Users> result = new HashMap<>();
        userReqs.forEach(val -> {
            List<Users> usersList = userGroup.get(val.getUserId());
            if (!CollectionUtils.isEmpty(usersList)) {
                result.put(val.getRequestId(), usersList.get(0));
            } else {
                result.put(val.getRequestId(), null);
            }
        });
        return result;
    }
}

Batch Request Service

@Service
public class UserWrapBatchService {
    @Resource
    private UserService userService;
    public static int MAX_TASK_NUM = 100;
    public static class Request {
        String requestId;
        Long userId;
        CompletableFuture<Users> completableFuture;
        // getters and setters omitted for brevity
    }
    private final Queue<Request> queue = new LinkedBlockingQueue<>();
    @PostConstruct
    public void init() {
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
        executor.scheduleAtFixedRate(() -> {
            int size = queue.size();
            if (size == 0) return;
            List<Request> list = new ArrayList<>();
            System.out.println("Merged [" + size + "] requests");
            for (int i = 0; i < size; i++) {
                if (i < MAX_TASK_NUM) list.add(queue.poll());
            }
            List<Request> userReqs = new ArrayList<>();
            for (Request r : list) userReqs.add(r);
            Map<String, Users> response = userService.queryUserByIdBatch(userReqs);
            for (Request r : list) {
                Users result = response.get(r.requestId);
                r.completableFuture.complete(result);
            }
        }, 100, 10, TimeUnit.MILLISECONDS);
    }
    public Callable<Users> merge(Long userId) {
        return () -> {
            Request req = new Request();
            req.requestId = UUID.randomUUID().toString().replace("-", "");
            req.userId = userId;
            CompletableFuture<Users> future = new CompletableFuture<>();
            req.completableFuture = future;
            queue.offer(req);
            return future.get();
        };
    }
}

High‑Concurrency Test

public class TestBatch {
    private static int threadCount = 30;
    private static final CountDownLatch COUNT_DOWN_LATCH = new CountDownLatch(threadCount);
    private static final RestTemplate restTemplate = new RestTemplate();
    public static void main(String[] args) {
        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                COUNT_DOWN_LATCH.countDown();
                try { COUNT_DOWN_LATCH.await(); } catch (InterruptedException e) { e.printStackTrace(); }
                for (int j = 1; j <= 3; j++) {
                    int param = new Random().nextInt(4);
                    if (param <= 0) param++;
                    String response = restTemplate.getForObject(
                        "http://localhost:8080/asyncAndMerge/merge?userId=" + param,
                        String.class);
                    System.out.println(Thread.currentThread().getName() + " param " + param + " response " + response);
                }
            }).start();
        }
    }
}

Key Points to Note

Java 8 CompletableFuture lacks a timeout mechanism.

SQL length limits require batch size control (MAX_TASK_NUM).

Conclusion

Merging requests and processing them in batches can dramatically reduce connection usage for the target system (database or RPC service). The trade‑off is added latency before the actual logic runs, making this pattern unsuitable for low‑concurrency scenarios.

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.

Batch ProcessingCompletableFutureSpring BootJava concurrencyrequest merging
Java Backend Technology
Written by

Java Backend Technology

Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!

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.