Request Merging in Java: Concept, Pros & Cons, and Implementation with ScheduledExecutorService
This article explains the concept of request merging for high‑concurrency web services, outlines its advantages and drawbacks, and provides a complete Java implementation using ScheduledExecutorService, a memory queue, and a generic BatchCollapser utility with usage examples.
In web projects, each HTTP request is normally processed individually, which can cause excessive I/O under high concurrency; merging multiple requests into a single batch request can reduce the number of interactions and improve performance.
The main benefit of request merging is that it aggregates several calls, allowing a timed or count‑based delay before sending a single batch request, thereby decreasing I/O overhead. The downside is the introduced latency, making it unsuitable for time‑critical APIs.
The implementation uses a ScheduledExecutorService together with an in‑memory LinkedBlockingDeque to collect incoming requests. When the queue size reaches a configured count threshold or a time interval expires, the accumulated requests are drained and processed by a user‑provided BatchHandler .
package com.leilei.support;
import lombok.extern.log4j.Log4j2;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
/**
* @author lei
* @desc Request merging utility class
*/
@Log4j2
public class BatchCollapser
{
private static final Map
BATCH_INSTANCE = new ConcurrentHashMap<>();
private static final ScheduledExecutorService SCHEDULE_EXECUTOR = Executors.newScheduledThreadPool(1);
private final LinkedBlockingDeque
batchContainer = new LinkedBlockingDeque<>();
private final BatchHandler
, R> handler;
private final int countThreshold;
private BatchCollapser(BatchHandler
, R> handler, int countThreshold, long timeThreshold) {
this.handler = handler;
this.countThreshold = countThreshold;
SCHEDULE_EXECUTOR.scheduleAtFixedRate(() -> {
try {
this.popUpAndHandler(BatchHandlerType.BATCH_HANDLER_TYPE_TIME);
} catch (Exception e) {
log.error("pop-up container exception", e);
}
}, timeThreshold, timeThreshold, TimeUnit.SECONDS);
}
public void addRequestParam(T event) {
batchContainer.add(event);
if (batchContainer.size() >= countThreshold) {
popUpAndHandler(BatchHandlerType.BATCH_HANDLER_TYPE_DATA);
}
}
private void popUpAndHandler(BatchHandlerType handlerType) {
List
tryHandlerList = Collections.synchronizedList(new ArrayList<>(countThreshold));
batchContainer.drainTo(tryHandlerList, countThreshold);
if (tryHandlerList.isEmpty()) return;
try {
R handle = handler.handle(tryHandlerList, handlerType);
log.info("Batch execution result:{}", handle);
} catch (Exception e) {
log.error("batch execute error, transferList:{}", tryHandlerList, e);
}
}
public static
BatchCollapser
getInstance(BatchHandler
, R> batchHandler, int countThreshold, long timeThreshold) {
Class jobClass = batchHandler.getClass();
if (BATCH_INSTANCE.get(jobClass) == null) {
synchronized (BatchCollapser.class) {
BATCH_INSTANCE.putIfAbsent(jobClass, new BatchCollapser<>(batchHandler, countThreshold, timeThreshold));
}
}
return BATCH_INSTANCE.get(jobClass);
}
public interface BatchHandler
{
R handle(T input, BatchHandlerType handlerType);
}
public enum BatchHandlerType {
BATCH_HANDLER_TYPE_DATA,
BATCH_HANDLER_TYPE_TIME,
}
}To use the utility, a service implements BatchCollapser.BatchHandler , obtains a singleton instance via BatchCollapser.getInstance with desired thresholds (e.g., 20 requests or 5 seconds), and adds request parameters through addRequestParam . A scheduled method simulates incoming requests and demonstrates the merging behavior.
package com.leilei.support;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.List;
@Service
public class ProductService implements BatchCollapser.BatchHandler
, Integer> {
private BatchCollapser
batchCollapser;
@PostConstruct
private void postConstructorInit() {
// Merge when 20 requests are collected or every 5 seconds
batchCollapser = BatchCollapser.getInstance(this, 20, 5);
}
@Override
public Integer handle(List
input, BatchCollapser.BatchHandlerType handlerType) {
System.out.println("Handler type:" + handlerType + ", batch params:" + input);
return input.stream().mapToInt(x -> x).sum();
}
@Scheduled(fixedDelay = 300)
public void generateRequest() {
Integer requestParam = (int) (Math.random() * 100) + 1;
batchCollapser.addRequestParam(requestParam);
System.out.println("Current request param:" + requestParam);
}
}A simple Product data class is also shown for completeness.
@Data
public class Product {
private Integer id;
private String notes;
}The provided code is a demonstration; developers can extend it, adjust thresholds, and integrate it into real services to alleviate server pressure during high‑traffic periods.
Java Architect Essentials
Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.