DEV Community

Bharath ram
Bharath ram

Posted on

Response Streams: A Beginner’s Guide

Understanding Response Streams: A Beginner's Guide with Java Implementation

Main Takeaway: Response streams let you receive and process data incrementally as it arrives, improving latency, memory efficiency, and user experience in modern web applications.

1. Introduction

Imagine you request a large PDF download or AI-generated content—waiting for the entire payload to arrive before seeing anything causes frustrating delays. Response streams solve this by sending partial data chunks to the client as soon as they're ready, yielding faster time to first byte and smoother user experiences.

Think of it like watching a video on YouTube. You don't wait for the entire video to download before you start watching—it streams to you piece by piece, and you can start enjoying the content immediately.

2. What Are Response Streams?

A response stream breaks a server's full payload into small chunks and transmits them incrementally. Instead of buffering the entire response in memory, the client or server processes each chunk as it arrives, enabling:

  • Immediate data processing
  • Reduced memory overhead
  • Real-time progress updates

In our Java example, we use Server-Sent Events (SSE) to demonstrate this concept. SSE is a web standard that allows a server to push data to a web page in real-time.

3. Why Use Streaming?

  • Lower Latency: Users see data sooner, improving perceived performance
  • Memory Efficiency: Large files or big API results don't need full buffering
  • Progressive UX: Show loaders, partial content, or real-time logs

4. Enough Reading, Let's Write Some Code

We are going to build a system where there are 2 servers connected in a pipeline:

  • Server A → which is the source. The streaming starts from here
  • Server B → which acts as a proxy, i.e., receives the stream from Server A and streams the response again to a Client

For simple illustration, we will create an HTTP server with 4 endpoints:

  • /stream-source → initiate streaming
  • /stream-processor → receives a stream and forwards it again (proxy)
  • /stream-destination → end response is shown
  • /test-pipeline → demo page to test all endpoints

Setting Up the Basic Server

public class StreamingServer {
    private static final String PREDEFINED_STRING = 
        "Hello from Java streaming API! This is a test message that will be streamed in chunks.";
    private static final int PORT = 8080;

    public static void main(String[] args) throws IOException {
        HttpServer server = HttpServer.create(new InetSocketAddress(PORT), 0);

        // Create our streaming pipeline endpoints
        server.createContext("/stream-source", new StreamSourceHandler());
        server.createContext("/stream-processor", new StreamProcessorHandler());
        server.createContext("/stream-destination", new StreamDestinationHandler());
        server.createContext("/test-pipeline", new TestPipelineHandler());

        // Use thread pool for handling multiple requests
        server.setExecutor(Executors.newFixedThreadPool(10));
        server.start();

        System.out.println("Java Streaming Server started on port " + PORT);
        System.out.println("Test at: http://localhost:" + PORT + "/test-pipeline");
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 1: The Stream Source (/stream-source)

This is where our streaming journey begins. The source breaks down a message into individual words and streams them one by one:

static class StreamSourceHandler implements HttpHandler {
    @Override
    public void handle(HttpExchange exchange) throws IOException {
        // Set SSE headers for streaming
        exchange.getResponseHeaders().set("Content-Type", "text/event-stream;charset=UTF-8");
        exchange.getResponseHeaders().set("Cache-Control", "no-cache");
        exchange.getResponseHeaders().set("Connection", "keep-alive");
        exchange.getResponseHeaders().set("Transfer-Encoding", "chunked");
        exchange.sendResponseHeaders(200, 0);

        try (OutputStream os = exchange.getResponseBody()) {
            String[] words = PREDEFINED_STRING.split(" ");

            // Stream each word with a delay
            for (String word : words) {
                String chunk = "data: " + word + "\n\n";
                os.write(chunk.getBytes());
                os.flush(); // Immediately send the chunk

                try {
                    Thread.sleep(200); // 200ms delay between words
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }

            // Signal end of stream
            os.write("data: [END]\n\n".getBytes());
            os.flush();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Key Points:

  • text/event-stream tells the browser this is an SSE stream
  • os.flush() ensures each chunk is sent immediately
  • Each data chunk follows SSE format: data: content\n\n

Step 2: The Stream Processor (/stream-processor)

This acts as our proxy server. It receives the stream from the source, processes each chunk, and forwards it:

static class StreamProcessorHandler implements HttpHandler {
    @Override
    public void handle(HttpExchange exchange) throws IOException {
        // Set up streaming response headers
        exchange.getResponseHeaders().set("Content-Type", "text/event-stream;charset=UTF-8");
        exchange.getResponseHeaders().set("Cache-Control", "no-cache");
        exchange.getResponseHeaders().set("Connection", "keep-alive");
        exchange.getResponseHeaders().set("Transfer-Encoding", "chunked");
        exchange.sendResponseHeaders(200, 0);

        try (OutputStream os = exchange.getResponseBody()) {
            // Connect to the source stream
            URL url = new URL("http://localhost:" + PORT + "/stream-source");
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod("GET");
            connection.setRequestProperty("Cache-Control", "no-cache");

            try (InputStream inputStream = connection.getInputStream()) {
                byte[] buffer = new byte[1024];
                int bytesRead;

                // Process stream chunk by chunk
                while ((bytesRead = inputStream.read(buffer)) != -1) {
                    String chunk = new String(buffer, 0, bytesRead);
                    String[] lines = chunk.split("\n");

                    for (String line : lines) {
                        if (line.startsWith("data: ")) {
                            String data = line.substring(6); // Remove "data: " prefix
                            String processedChunk = "data: PROCESSED: " + data + "\n\n";
                            os.write(processedChunk.getBytes());
                            os.flush(); // Forward immediately
                        }
                    }
                }
            }
        } catch (Exception e) {
            String error = "data: ERROR: " + e.getMessage() + "\n\n";
            exchange.getResponseBody().write(error.getBytes());
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Key Points:

  • Connects to /stream-source as a client
  • Processes each incoming chunk by adding "PROCESSED: " prefix
  • Forwards processed data immediately without waiting for the complete stream

Step 3: The Stream Destination (/stream-destination)

This is the final consumer in our pipeline, representing where the processed data ultimately goes:

static class StreamDestinationHandler implements HttpHandler {
    @Override
    public void handle(HttpExchange exchange) throws IOException {
        // Set up streaming response headers
        exchange.getResponseHeaders().set("Content-Type", "text/event-stream;charset=UTF-8");
        exchange.getResponseHeaders().set("Cache-Control", "no-cache");
        exchange.getResponseHeaders().set("Connection", "keep-alive");
        exchange.getResponseHeaders().set("Transfer-Encoding", "chunked");
        exchange.sendResponseHeaders(200, 0);

        try (OutputStream os = exchange.getResponseBody()) {
            os.write("data: FINAL DESTINATION RECEIVED:\n\n".getBytes());
            os.flush();

            // Connect to the processor stream
            URL url = new URL("http://localhost:" + PORT + "/stream-processor");
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod("GET");
            connection.setRequestProperty("Cache-Control", "no-cache");

            try (InputStream inputStream = connection.getInputStream()) {
                byte[] buffer = new byte[1024];
                int bytesRead;

                while ((bytesRead = inputStream.read(buffer)) != -1) {
                    String chunk = new String(buffer, 0, bytesRead);
                    String[] lines = chunk.split("\n");

                    for (String line : lines) {
                        if (line.startsWith("data: ")) {
                            String data = line.substring(6);
                            String finalChunk = "data: FINAL: " + data + "\n\n";
                            os.write(finalChunk.getBytes());
                            os.flush();
                        }
                    }
                }
            }
        } catch (Exception e) {
            String error = "data: FINAL ERROR: " + e.getMessage() + "\n\n";
            exchange.getResponseBody().write(error.getBytes());
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Test Pipeline (/test-pipeline)

A simple HTML page to test all endpoints:

static class TestPipelineHandler implements HttpHandler {
    @Override
    public void handle(HttpExchange exchange) throws IOException {
        String html =
            "<h2>Java Streaming API Test Pipeline</h2>\n" +
            "<p>Test the streaming endpoints:</p>\n" +
            "<ul>\n" +
            "    <li><a href=\"/stream-source\" target=\"_blank\">1. Stream Source</a> - Original stream</li>\n" +
            "    <li><a href=\"/stream-processor\" target=\"_blank\">2. Stream Processor</a> - Processes and forwards</li>\n" +
            "    <li><a href=\"/stream-destination\" target=\"_blank\">3. Stream Destination</a> - Final destination</li>\n" +
            "</ul>\n" +
            "<p>Open each link in a new tab to see the streaming in action.</p>\n";

        exchange.getResponseHeaders().set("Content-Type", "text/html");
        exchange.sendResponseHeaders(200, html.getBytes().length);

        try (OutputStream os = exchange.getResponseBody()) {
            os.write(html.getBytes());
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

5. How to Test the Streaming Pipeline

  1. Compile and run the Java program
  2. Open your browser to http://localhost:8080/test-pipeline
  3. Click each link in separate tabs to see:
    • Source: Words appearing every 200ms
    • Processor: Same words with "PROCESSED:" prefix
    • Destination: Final words with "FINAL:" prefix

6. Real-World Applications

This streaming architecture pattern is used in:

  • Chat applications: Messages stream in real-time
  • Live sports updates: Scores update continuously
  • Log monitoring: Server logs stream to dashboards
  • AI content generation: Text appears as it's generated
  • File uploads: Progress updates stream in real-time
  • IoT data: Sensor readings flow continuously

7. Key Benefits Demonstrated

  1. Low Latency: Each word appears immediately, not after the complete message
  2. Memory Efficiency: No need to buffer the entire message at any stage
  3. Composable Architecture: Each component (source → processor → destination) can be modified independently
  4. Real-time Processing: Data is processed as it flows through the pipeline

8. Learning Takeaways

  • Streaming is about flow, not storage - Data moves through your system like water through pipes
  • Each component has a single responsibility - Source generates, processor transforms, destination consumes
  • flush() is crucial - It ensures immediate transmission of chunks
  • SSE format matters - The data: content\n\n format enables browser streaming
  • Error handling - Always handle connection failures gracefully

This Java streaming server demonstrates powerful concepts that scale to enterprise systems handling millions of events per second. The principles remain the same—you're just moving more data through bigger pipes!


Try experimenting: Change the delay time, add new processing steps, or create multiple processors in parallel. The best way to learn streaming is to play with the flow of data!

Top comments (0)