How to Merge Requests in Spring Boot to Reduce DB Load and Boost Performance

This article explains how to combine multiple user queries into a single database request using a queue, ScheduledThreadPoolExecutor, and CompletableFuture in Spring Boot, demonstrating code implementations, handling Java 8 CompletableFuture timeout limitations, and showing performance gains through request merging under high concurrency.

Java Backend Technology
Java Backend Technology
Java Backend Technology
How to Merge Requests in Spring Boot to Reduce DB Load and Boost Performance

Preface

What is the benefit of merging requests? The diagram below shows three users (IDs 1, 2, 3) each requesting their basic information, which would normally trigger three separate database queries. Since database connections are a precious resource, we aim to minimize the number of connections.

Request merging illustration
Request merging illustration

By merging the three requests on the server side, we issue a single SQL query, then distribute the returned data to each user based on a unique request ID.

The same principle applies when the database is replaced by a remote service.

We change the approach as shown in the next diagram.

Server-side request merging flow
Server-side request merging flow

Technical Means

LinkedBlockingQueue

– a blocking queue ScheduledThreadPoolExecutor – scheduled task thread pool CompletableFuture – asynchronous result handling (Java 8 lacks a timeout mechanism, so a queue is used as a workaround)

Code Implementation

Query User Service

public interface UserService {<br/>    Map<String, Users> queryUserByIdBatch(List<UserWrapBatchService.Request> userReqs);<br/>}<br/><br/>@Service<br/>public class UserServiceImpl implements UserService {<br/>    @Resource<br/>    private UsersMapper usersMapper;<br/><br/>    @Override<br/>    public Map<String, Users> queryUserByIdBatch(List<UserWrapBatchService.Request> userReqs) {<br/>        List<Long> userIds = userReqs.stream().map(UserWrapBatchService.Request::getUserId).collect(Collectors.toList());<br/>        QueryWrapper<Users> queryWrapper = new QueryWrapper<>();<br/>        queryWrapper.in("id", userIds); // combine into one SQL<br/>        List<Users> users = usersMapper.selectList(queryWrapper);<br/>        Map<Long, List<Users>> userGroup = users.stream().collect(Collectors.groupingBy(Users::getId));<br/>        HashMap<String, Users> result = new HashMap<>();<br/>        userReqs.forEach(val -> {<br/>            List<Users> usersList = userGroup.get(val.getUserId());<br/>            if (!CollectionUtils.isEmpty(usersList)) {<br/>                result.put(val.getRequestId(), usersList.get(0));<br/>            } else {<br/>                result.put(val.getRequestId(), null); // no data<br/>            }<br/>        });<br/>        return result;<br/>    }<br/>}

Batch Request Merging Service

package com.springboot.sample.service.impl;<br/><br/>@Service<br/>public class UserWrapBatchService {<br/>    @Resource<br/>    private UserService userService;<br/><br/>    public static int MAX_TASK_NUM = 100;<br/><br/>    public static class Request {<br/>        String requestId;<br/>        Long userId;<br/>        CompletableFuture<Users> completableFuture;<br/>        // getters and setters omitted for brevity<br/>    }<br/><br/>    private final Queue<Request> queue = new LinkedBlockingQueue<>();<br/><br/>    @PostConstruct<br/>    public void init() {<br/>        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);<br/>        scheduledExecutorService.scheduleAtFixedRate(() -> {<br/>            int size = queue.size();<br/>            if (size == 0) return;<br/>            List<Request> list = new ArrayList<>();<br/>            System.out.println("Merged [" + size + "] requests");<br/>            for (int i = 0; i < size; i++) {<br/>                if (i < MAX_TASK_NUM) {<br/>                    list.add(queue.poll());<br/>                }<br/>            }<br/>            List<Request> userReqs = new ArrayList<>(list);<br/>            Map<String, Users> response = userService.queryUserByIdBatch(userReqs);<br/>            for (Request request : list) {<br/>                Users result = response.get(request.requestId);<br/>                request.completableFuture.complete(result);<br/>            }<br/>        }, 100, 10, TimeUnit.MILLISECONDS);<br/>    }<br/><br/>    public Users queryUser(Long userId) {<br/>        Request request = new Request();<br/>        request.requestId = UUID.randomUUID().toString().replace("-", "");<br/>        request.userId = userId;<br/>        CompletableFuture<Users> future = new CompletableFuture<>();<br/>        request.completableFuture = future;<br/>        queue.offer(request);<br/>        try {<br/>            return future.get();<br/>        } catch (InterruptedException | ExecutionException e) {<br/>            e.printStackTrace();<br/>        }<br/>        return null;<br/>    }<br/>}

Controller Call

@RequestMapping("/merge")<br/>public Callable<Users> merge(Long userId) {<br/>    return () -> userBatchService.queryUser(userId);<br/>}

High‑Concurrency Test

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

Test Results

Test result 1
Test result 1
Test result 2
Test result 2

Things to Note

Java 8 CompletableFuture does not provide a timeout mechanism.

The underlying SQL statement has length limits, so batch size must be capped (MAX_TASK_NUM) to avoid exceeding the limit.

Problem Solving

Using a Queue to Add Timeout for CompletableFuture

public class UserWrapBatchQueueService {<br/>    // similar structure as UserWrapBatchService but each Request holds a LinkedBlockingQueue<Users> for timeout<br/>    // queue.poll(3000, TimeUnit.MILLISECONDS) provides a 3‑second wait before giving up<br/>    // rest of the implementation follows the same merging logic<br/>}

Conclusion

Request merging and batch processing can dramatically reduce connection resource consumption for the called system (e.g., a database). The trade‑off is added waiting time before actual logic execution, making it unsuitable for low‑concurrency scenarios.

Source Code

https://gitee.com/apple_1030907690/spring-boot-kubernetes/tree/v1.0.5

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.

JavaconcurrencyBatch ProcessingSpring Bootrequest 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.