Mastering Asynchronous Responses in Spring: CompletableFuture vs ResponseBodyEmitter

This guide explains how to implement high‑performance asynchronous APIs in Spring Web using CompletableFuture and ResponseBodyEmitter, covering controller and service code, configuration, request lifecycle, client handling, Nginx settings, and practical recommendations for choosing the right approach.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Mastering Asynchronous Responses in Spring: CompletableFuture vs ResponseBodyEmitter

Introduction

When building high‑performance web applications, asynchronous response mechanisms are key to improving throughput and user experience. This article explores two common Spring Web async techniques— CompletableFuture and ResponseBodyEmitter —and provides a complete practical guide, including front‑end interaction, Nginx configuration, and connection management.

1. Using CompletableFuture for Async Endpoints

Controller Code

@RestController
public class AsyncController {
    @Autowired
    private AsyncService asyncService;

    @GetMapping("/async")
    public CompletableFuture<String> async() {
        return asyncService.doAsyncTask();
    }
}

Service Layer Code

@Service
public class AsyncService {
    @Async
    public CompletableFuture<String> doAsyncTask() {
        try {
            Thread.sleep(3000); // simulate long‑running task
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return CompletableFuture.completedFuture("Task completed!");
    }
}

Enable Async Support

@Configuration
@EnableAsync
public class AsyncConfig {
    @Bean
    public Executor taskExecutor() {
        return Executors.newCachedThreadPool();
    }
}

Explanation

@Async: Executes the method in a separate thread.

CompletableFuture: Container for the async result; Spring suspends the request until completion.

@EnableAsync: Turns on Spring’s async method support.

taskExecutor(): Provides a thread pool to avoid creating a new thread for each request.

Spring MVC CompletableFuture Response Flow

Server‑side processing

Client sends a request to DispatcherServlet.

Controller returns a CompletableFuture. Spring detects the return type, suspends the request, and waits for the async task.

The async task runs in the thread pool defined by @Async and TaskExecutor.

When the task finishes, Spring serializes the CompletableFuture result (JSON or plain text) and sends it back.

After the response is sent, the HTTP connection is closed.

Client experience

The client receives a normal HTTP response; no special handling is required, though the response time may be slightly longer.

Benefits of CompletableFuture

Increases system throughput and concurrency by releasing the servlet thread while the task runs.

Provides a non‑blocking programming model and supports chaining methods such as .thenApply() and .thenCompose().

Improves user experience with shorter perceived latency for parallel tasks.

Works well with @Async to decouple business logic.

Allows graceful error handling and timeout control, e.g.:

return asyncService.doAsyncTask()
    .orTimeout(5, TimeUnit.SECONDS)
    .exceptionally(ex -> "Task failed: " + ex.getMessage());

2. Using ResponseBodyEmitter for Streaming Push

ResponseBodyEmitter

enables the server to send data in chunks within a single HTTP request, suitable for long‑living connections or real‑time updates.

Controller Code

@RestController
public class StreamController {
    @Autowired
    private EmitterManager emitterManager;

    @GetMapping("/stream/{id}")
    public ResponseBodyEmitter stream(@PathVariable String id) {
        ResponseBodyEmitter emitter = new ResponseBodyEmitter();
        emitterManager.register(id, emitter);
        Executors.newSingleThreadExecutor().submit(() -> {
            try {
                for (int i = 0; i < 10; i++) {
                    if (emitterManager.shouldStop(id)) break;
                    emitter.send("Progress: " + i * 10 + "%
", MediaType.TEXT_PLAIN);
                    Thread.sleep(1000);
                }
                emitter.complete();
            } catch (Exception e) {
                emitter.completeWithError(e);
            }
        });
        return emitter;
    }

    @PostMapping("/stop/{id}")
    public ResponseEntity<Void> stop(@PathVariable String id) {
        emitterManager.stop(id);
        return ResponseEntity.ok().build();
    }
}

Explanation

ResponseBodyEmitter: Allows multiple data chunks to be sent in one request.

emitter.send(): Sends each chunk; the client receives data in real time.

emitter.complete(): Marks the end of the stream and closes the connection.

emitter.completeWithError(): Closes the connection on error.

EmitterManager: Manages active emitter sessions and supports external stop signals.

Key Considerations

Default timeout is 30 seconds; can be changed via the constructor.

Async thread must explicitly call complete() or completeWithError().

Core Features

Non‑blocking asynchronous response.

Supports multiple writes per request.

Compatible with Servlet 3.0+ async processing.

Can be combined with @ResponseBody or @RestController.

Front‑end Consumption

Using fetch with a ReadableStream:

fetch('/progress/stream')
  .then(response => {
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    function read() {
      reader.read().then(({ done, value }) => {
        if (done) return;
        console.log(decoder.decode(value));
        read();
      });
    }
    read();
  });

Alternatively, EventSource can be used for text/event-stream responses:

const source = new EventSource('/sse/stream');
source.onmessage = event => console.log('Received:', event.data);

Nginx Configuration for Streaming

location /stream {
    proxy_pass http://localhost:8080;
    proxy_buffering off;            # disable buffering for real‑time push
    proxy_read_timeout 3600s;       # long‑connection timeout
    chunked_transfer_encoding on;   # enable chunked transfer
}

EmitterManager and EmitterSession

@Component
public class EmitterManager {
    private final Map<String, EmitterSession> sessions = new ConcurrentHashMap<>();

    public void register(String id, ResponseBodyEmitter emitter) {
        EmitterSession session = new EmitterSession(id, emitter);
        sessions.put(id, session);
        emitter.onCompletion(() -> cleanup(id));
        emitter.onTimeout(() -> cleanup(id));
        emitter.onError(t -> cleanup(id));
    }

    public void stop(String id) {
        EmitterSession session = sessions.get(id);
        if (session != null) {
            session.getStopFlag().set(true);
            session.getEmitter().complete();
            cleanup(id);
        }
    }

    public boolean shouldStop(String id) {
        EmitterSession session = sessions.get(id);
        return session != null && session.getStopFlag().get();
    }

    public ResponseBodyEmitter getEmitter(String id) {
        EmitterSession session = sessions.get(id);
        return session != null ? session.getEmitter() : null;
    }

    private void cleanup(String id) {
        sessions.remove(id);
    }
}

public class EmitterSession {
    private final String id;
    private final ResponseBodyEmitter emitter;
    private final AtomicBoolean stopFlag;
    private final Instant connectedAt;

    public EmitterSession(String id, ResponseBodyEmitter emitter) {
        this.id = id;
        this.emitter = emitter;
        this.stopFlag = new AtomicBoolean(false);
        this.connectedAt = Instant.now();
    }
    public String getId() { return id; }
    public ResponseBodyEmitter getEmitter() { return emitter; }
    public AtomicBoolean getStopFlag() { return stopFlag; }
    public Instant getConnectedAt() { return connectedAt; }
}

3. Comparison: CompletableFuture vs ResponseBodyEmitter

Comparison Diagram
Comparison Diagram

**Usage Recommendations**

Use CompletableFuture for single‑result asynchronous operations such as remote calls, database queries, or CPU‑bound tasks.

Choose ResponseBodyEmitter when you need to push incremental data, stream progress updates, or implement server‑sent events.

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.

JavaspringAsynchronousCompletableFutureResponseBodyEmitter
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

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.