Preventing Memory Bloat: Server-Side Pagination in Vaadin with Spring Data JPA
Stop loading entire database tables into your JVM. Learn how to implement server-side pagination in Vaadin using CallbackDataProvider and Spring Data JPA to keep your application scalable.
11 Dec 2025, 21:01 UTC

The Memory Trap of Client-Side Lists
When building an admin dashboard or a reporting tool, the instinct is often to fetch a list of entities from a database and pass them directly into a Vaadin Grid. While this works for a few hundred rows, it creates a critical failure point as your data grows. Loading 10,000 records into the JVM heap for every active user session quickly leads to OutOfMemoryError crashes and sluggish UI responsiveness.
The solution is to shift the pagination logic from the application memory to the database engine. By using a CallbackDataProvider, the Vaadin Grid only requests the specific slice of data currently visible in the user's browser viewport, leveraging SQL LIMIT and OFFSET clauses to keep the memory footprint constant regardless of the total dataset size.
Connecting the Grid to Spring Data Pageable
Vaadin's Grid does not automatically know how to talk to a database; it requires a DataProvider to act as the bridge. For server-side pagination, the CallbackDataProvider is the most effective tool. It requires two primary functions: one to fetch a specific range of items and one to return the total count of available items.
The total count is not optional. Without it, the Grid cannot calculate the height of the scrollbar, and the user will have no visual indication of how much data exists beyond the current view.
Implementation Example
Assuming a Spring Data JPA repository ProductRepository that extends JpaRepository, here is how to configure the Grid for lazy loading.
// In your View class
Grid<Product> grid = new Grid<>(Product.class);
// Define the data provider
CallbackDataProvider<Product, Void> dataProvider = DataProvider.fromCallbacks(
query -> {
// Convert Vaadin query to Spring Data Pageable
// query.getOffset() provides the starting index
// query.getLimit() provides the number of items requested
PageRequest pageRequest = PageRequest.of(
(int) (query.getOffset() / query.getLimit()),
(int) query.getLimit()
);
return productRepository.findAll(pageRequest).stream();
},
query -> {
// Return total count for scrollbar calculation
return (int) productRepository.count();
}
);
grid.setDataProvider(dataProvider);
Performance Tuning and Diagnostics
To ensure the pagination is actually happening at the database level and not just in the JVM, you must verify the generated SQL. Enable SQL logging in your application.properties:
spring.jpa.show-sql=true
logging.level.org.hibernate.SQL=DEBUG
When you scroll the Grid, you should see SELECT statements containing LIMIT ? OFFSET ?. If you see a query that returns all rows, your DataProvider is likely calling findAll() without a Pageable argument.
Handling Sorting and Filtering
A common mistake is implementing client-side sorting on a lazy-loaded Grid. If you sort only the visible 50 rows, the rest of the 10,000 rows remain unsorted in the database. To maintain consistency, you must pass the query.getSortOrders() from the Vaadin query into the Spring Data Sort object. This ensures the database performs the sort before returning the specific page of data.
The Trade-off: The "Deep Paging" Problem
While server-side pagination solves memory issues, it introduces a database performance risk known as Deep Paging. As the OFFSET value increases (e.g., requesting page 1,000 of a million records), the database must still scan through all preceding rows before discarding them to return the requested slice.
If your users frequently jump to the end of massive datasets, consider implementing a "seek-based" pagination (using a WHERE id > last_seen_id clause) instead of offset-based pagination. This keeps query times constant regardless of how deep the user scrolls.
Verification Checklist
- Network Tab: Open the browser inspector. Scrolling should trigger small XHR requests returning JSON chunks, not one massive initial payload.
- Heap Monitor: Use a tool like VisualVM to ensure memory usage remains stable as you increase the total record count in the database.
- SQL Logs: Confirm that every scroll action triggers a new query with a dynamic
OFFSET.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.