Sitemap

Spring streaming response made easy

--

In certain scenarios, we need to retrieve large volumes of data, yet we often experience delays before the first pieces of the response are displayed. Fortunately, this is a well-known problem, and an effective solution exists.

TechStack

This project is built using:

  • Java 24.
  • Spring boot 3.5.5 with WebFlux.
  • Postgres (or any DB you want)

Use case

Simply, we need 1 million products (json objects) in the GET endpoint.

this is a simple SQL script to create and fill the products table:

-- Create table
CREATE TABLE IF NOT EXISTS products (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
price NUMERIC(10,2) NOT NULL
);

-- Insert 100,000 products
INSERT INTO products (name, price)
SELECT
'Product ' || i,
ROUND((RANDOM() * 1000)::numeric, 2)
FROM generate_series(1, 1000000) AS s(i);

Next the entity and JPA repository:

@Entity
@Table(name = "products")
public class Product {
@Id
private Long id;
private String name;
private Double price;

public Product() {
}

public Product(Long id, String name, Double price) {
this.id = id;
this.name = name;
this.price = price;
}

public Long getId() {
return id;
}

public String getName() {
return name;
}

public Double getPrice() {
return price;
}

@Override
public String toString() {
return "Product[" +
"id=" + id + ", " +
"name=" + name + ", " +
"price=" + price + ']';
}
}
import org.springframework.data.jpa.repository.JpaRepository;

public interface ProductRepository extends JpaRepository<Product, Long> {
}

Any database can be used here, I’m just used to postgres due to my daily work.

Traditional endpoints

Nothing fancy for our traditional endpoint, a simple find all GET:

@RestController
@RequestMapping("/products")
public class ProductController {

private final ProductRepository productRepository;

public ProductController(ProductRepository productRepository) {
this.productRepository = productRepository;
}

@GetMapping
public List<Product> getAll(){
return productRepository.findAll();
}
}

While using this endpoint, the client needs to wait before exploiting the first returned product, or even worse, the response size is too big to be handled properly:

Press enter or click to view image in full size

Streaming Response to the rescue

Using StreamingResponseBody, we can stream our products one by one and save some time :)

@GetMapping("/stream")
public StreamingResponseBody getAllStreamed(HttpServletResponse response){
response.setContentType("text/event-stream");
return outputStream -> {
productRepository.findAll()
.forEach(product -> {
try {
String json = new ObjectMapper().writeValueAsString(product) + "\n";
outputStream.write(json.getBytes());
outputStream.flush();
} catch (JsonProcessingException e) {
log.error("Error parsing product to json", e);
} catch (IOException e) {
log.error("Error writing object to stream", e);
}
});
};
}
Press enter or click to view image in full size

On another level

You may already noticed that we loop over the products and write one by one to the output stream, this can be improved even more by streaming data end-2-end from the data source to the client; For that purpose solutions like Spring Webflux and JPA streams exist. (The resources are just below :D)

Resources

Streaming with JPA Stream

Server-Sent Events (SSE) / Reactive

Read this article and more on my website: https://www.saadelattar.me/article/spring-response-streams

--

--

ELATTAR Saad
ELATTAR Saad

Written by ELATTAR Saad

Warm regards! I'm Saad, a software engineer. The most enjoyable part of any journey is what you share along the way, so code and document the journey!