Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Overview

In this tutorial, we’ll look at how we can use the DeferredResult class in Spring MVC to perform asynchronous request processing.

Asynchronous support was introduced in Servlet 3.0 and, simply put, it allows processing an HTTP request in another thread than the request receiver thread.

DeferredResult, available from Spring 3.2 onwards, assists in offloading a long-running computation from an http-worker thread to a separate thread.

Although the other thread will take some resources for computation, the worker threads are not blocked in the meantime and can handle incoming client requests.

The async request processing model is very useful as it helps scale an application well during high loads, especially for IO intensive operations.

2. Setup

For our examples, we’ll use a Spring Boot application. For more details on how to bootstrap the application, refer to our previous article.

Next, we’ll demonstrate both synchronous and asynchronous communication using DeferredResult and also compare how asynchronous one scales better for high load and IO intensive use cases.

3. Blocking REST Service

Let’s start with developing a standard blocking REST service:

@GetMapping("/process-blocking")
public ResponseEntity<?> handleReqSync(Model model) { 
    // ...
    return ResponseEntity.ok("ok");
}

The problem here is that the request processing thread is blocked until the complete request is processed and the result is returned. In case of long-running computations, this is a sub-optimal solution.

To address this, we can make better use of container threads to handle client requests as we’ll see in the next section.

4. Non-Blocking REST Using DeferredResult

To avoid blocking, we’ll use callbacks-based programming model where instead of the actual result, we’ll return a DeferredResult to the servlet container.

@GetMapping("/async-deferredresult")
public DeferredResult<ResponseEntity<?>> handleReqDefResult(Model model) {
    LOG.info("Received async-deferredresult request");
    DeferredResult<ResponseEntity<?>> output = new DeferredResult<>();
    
    ForkJoinPool.commonPool().submit(() -> {
        LOG.info("Processing in separate thread");
        try {
            Thread.sleep(6000);
        } catch (InterruptedException e) {
        }
        output.setResult(ResponseEntity.ok("ok"));
    });
    
    LOG.info("servlet thread freed");
    return output;
}

Request processing is done in a separate thread and once completed we invoke the setResult operation on the DeferredResult object.

Let’s look at the log output to check that our threads behave as expected:

[nio-8080-exec-6] com.baeldung.controller.AsyncDeferredResultController: 
Received async-deferredresult request
[nio-8080-exec-6] com.baeldung.controller.AsyncDeferredResultController: 
Servlet thread freed
[nio-8080-exec-6] java.lang.Thread : Processing in separate thread

Internally, the container thread is notified and the HTTP response is delivered to the client. The connection will remain open by the container(servlet 3.0 or later) until the response arrives or times out.

5. DeferredResult Callbacks

We can register 3 types of callbacks with a DeferredResult: completion, timeout and error callbacks.

Let’s use the onCompletion() method to define a block of code that’s executed when an async request completes:

deferredResult.onCompletion(() -> LOG.info("Processing complete"));

Similarly, we can use onTimeout() to register custom code to invoke once timeout occurs. In order to limit request processing time, we can pass a timeout value during the DeferredResult object creation:

DeferredResult<ResponseEntity<?>> deferredResult = new DeferredResult<>(500l);

deferredResult.onTimeout(() -> 
  deferredResult.setErrorResult(
    ResponseEntity.status(HttpStatus.REQUEST_TIMEOUT)
      .body("Request timeout occurred.")));

In case of timeouts, we’re setting a different response status via timeout handler registered with DeferredResult.

Let’s trigger a timeout error by processing a request that takes more than the defined timeout values of 5 seconds:

ForkJoinPool.commonPool().submit(() -> {
    LOG.info("Processing in separate thread");
    try {
        Thread.sleep(6000);
    } catch (InterruptedException e) {
        ...
    }
    deferredResult.setResult(ResponseEntity.ok("OK")));
});

Let’s look at the logs:

[nio-8080-exec-6] com.baeldung.controller.DeferredResultController: 
servlet thread freed
[nio-8080-exec-6] java.lang.Thread: Processing in separate thread
[nio-8080-exec-6] com.baeldung.controller.DeferredResultController: 
Request timeout occurred

There will be scenarios where long-running computation fails due to some error or exception. In this case, we can also register an onError() callback:

deferredResult.onError((Throwable t) -> {
    deferredResult.setErrorResult(
      ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
        .body("An error occurred."));
});

In case of an error, while computing the response, we’re setting a different response status and message body via this error handler.

6. Conclusion

In this quick article, we looked at how Spring MVC DeferredResult eases the creation of asynchronous endpoints.

As usual, the complete source code is available over on Github.

Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.