Boost High-Concurrency Performance with Request Merging in Java

By introducing request merging and batch APIs, this article demonstrates how to improve high‑concurrency performance in Java backend services, detailing the design, data structures, scheduled execution, and code implementation while discussing trade‑offs and practical considerations.

ITFLY8 Architecture Home
ITFLY8 Architecture Home
ITFLY8 Architecture Home
Boost High-Concurrency Performance with Request Merging in Java

Introduction

In many projects we call third‑party APIs and also expose our own APIs (RPC, HTTP, etc.). Providing batch APIs can improve performance, especially under high concurrency.

Without Merged Requests

Before merging, each request is handled individually, which is simple but inefficient for high‑frequency endpoints such as a movie‑detail service.

Example: device registration interface.

@Reference(check = false)
private DeviceService deviceService;

/**
 * Register device
 *
 * @param productKey product key
 * @param deviceName device name
 * @return device ID
 */
public R<Long> registDevice(String productKey, String deviceName) {
    log.debug("Start registration: {}, {}", productKey, deviceName);
    DeviceRequestDto deviceCreateQuery = new DeviceRequestDto()
                    .setProductKey(productKey)
                    .setName(deviceName);
    Long deviceId = deviceService.createDevice(deviceCreateQuery);
    return deviceId != null ? R.ok(deviceId) : R.error(DEVICE_CREATE_ERROR);
}

Request Merging

Merging requests can boost performance but adds complexity, latency, and risk; it should be applied only after careful business analysis.

The idea: store incoming requests, periodically invoke a batch interface, then notify each original request.

Implementation

1. Batch Interface

First we need a batch‑creation API on the service side.

Method signature

/**
 * Batch create device interface
 *
 * @param deviceRequestDtoList input list
 * @return creation result
 */
R<List<DeviceCreateResp>> batchCreateDevice(List<DeviceCreateQuery> deviceList);

Parameter class

@Data
public class DeviceCreateQuery implements Serializable {
    /** product identifier */
    private String productKey;
    /** device name */
    private String name;
    /** request source, unique per batch */
    private String requestSource;
}

Response class

@Data
public class DeviceCreateResp implements Serializable {
    /** device ID */
    private Long deviceId;
    /** request source, unique per batch */
    private String requestSource;
}

2. Merging Single Requests

Blocking queue

private LinkedBlockingDeque<DeviceCreateRequest> deviceCreateQueue = new LinkedBlockingDeque<>();

Custom request structure

@Data
static class DeviceCreateRequest {
    /** product key */
    private String productKey;
    /** device name */
    private String deviceName;
    /** request source, must be unique */
    private String requestSource;
    /** CompletableFuture for the result */
    private CompletableFuture<Long> completedFuture;
}

Storing a request

public R<Long> registDevice(String productKey, String deviceName) {
    log.debug("Start registration: {}, {}", productKey, deviceName);
    // cache request ====== start
    CompletableFuture<Long> completedFuture = new CompletableFuture<>();
    DeviceCreateRequest deviceCreateRequest = new DeviceCreateRequest();
    deviceCreateRequest.setProductKey(productKey);
    deviceCreateRequest.setDeviceName(deviceName);
    deviceCreateRequest.setRequestSource(UUID.randomUUID().toString());
    deviceCreateRequest.setCompletedFuture(completedFuture);
    deviceCreateQueue.add(deviceCreateRequest);
    // cache request ====== end
    Long deviceId = null;
    try {
        deviceId = completedFuture.get();
    } catch (Exception e) {
        log.error("Device registration failed", e);
    }
    return deviceId != null ? R.ok(deviceId) : R.error(DEVICE_CREATE_ERROR);
}

3. Sending Batch Requests

A scheduled thread pool periodically drains the queue, builds a list of DeviceCreateQuery, calls the batch API, and completes the corresponding futures.

/** Blocking queue for cached requests */
private LinkedBlockingDeque<DeviceCreateRequest> deviceCreateQueue = new LinkedBlockingDeque<>();
/** Number of threads in the pool */
@Value("${iot.register.merge.device.request.num:100}")
private int createDeviceMergeNum;
/** Period between executions (ms) */
@Value("${iot.register.merge.device.request.period:30}")
private long createDeviceMergePeriod;

@Reference(check = false)
private DeviceService deviceService;

@PostConstruct
public void init() {
    ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(createDeviceMergeNum);
    scheduledExecutorService.scheduleAtFixedRate(() -> {
        // snapshot of the queue
        List<DeviceCreateRequest> questBak = new ArrayList<>();
        List<DeviceCreateQuery> deviceCreateQueryList = new ArrayList<>();
        int size = deviceCreateQueue.size();
        for (int i = 0; i < size; i++) {
            DeviceCreateRequest deviceCreateRequest = deviceCreateQueue.poll();
            if (Objects.nonNull(deviceCreateRequest)) {
                questBak.add(deviceCreateRequest);
                deviceCreateQueryList.add(buildDeviceCreateQuery(deviceCreateRequest));
            }
        }
        if (!deviceCreateQueryList.isEmpty()) {
            try {
                List<DeviceCreateResp> response = deviceService.batchCreateDevice(deviceCreateQueryList);
                Map<String, Long> collect = response.stream()
                        .collect(Collectors.toMap(DeviceCreateResp::getRequestSource, DeviceCreateResp::getDeviceId));
                // notify original threads
                for (DeviceCreateRequest deviceCreateRequest : questBak) {
                    deviceCreateRequest.getCompletedFuture().complete(collect.get(deviceCreateRequest.getRequestSource()));
                }
            } catch (Throwable throwable) {
                log.error("Batch device registration error", throwable);
                // notify with exception
                questBak.forEach(req -> req.getCompletedFuture().obtrudeException(throwable));
            }
        }
    }, 0, createDeviceMergePeriod, TimeUnit.MILLISECONDS);
}

Conclusion

Request merging is a practical technique for high‑concurrency scenarios; this article presents a simple Java implementation using BlockingDeque and CompletableFuture. Production systems often rely on mature frameworks such as Hystrix for more robust merging.

Diagram
Diagram
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.

high concurrencyJava concurrencyrequest mergingbatch API
ITFLY8 Architecture Home
Written by

ITFLY8 Architecture Home

ITFLY8 Architecture Home - focused on architecture knowledge sharing and exchange, covering project management and product design. Includes large-scale distributed website architecture (high performance, high availability, caching, message queues...), design patterns, architecture patterns, big data, project management (SCRUM, PMP, Prince2), product design, and more.

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.