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:
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);
}
});
};
}
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<T>
Server-Sent Events (SSE) / Reactive
Read this article and more on my website: https://www.saadelattar.me/article/spring-response-streams
Top comments (0)