Lazy Pagination in Vaadin Flow Grid with CallbackDataProvider
A step‑by‑step guide to turning a Vaadin Flow Grid into a lazy, paginated component that fetches only the rows the user needs, keeping server memory flat and ensuring accurate sorting and filtering.
08 Oct 2026, 23:22 UTC

Desired Outcome
Show a Grid that can display tens of thousands of rows without loading them all into server memory. The Grid should request only the slice of data currently visible, use database‑side paging, sorting, and filtering, and keep the scrollbar accurate.
Prerequisites
- Vaadin Flow project (Vaadin 14 LTS or newer). If you use Vaadin 23/24 remember the switch from
javax.*tojakarta.*and that Java 17 is the baseline. - A database with a large table (e.g., 100 000 rows) and a repository layer that can execute offset/limit queries (Spring Data
Pageable, JPAsetFirstResult, jOOQlimit, or plain JDBC). - Grid component bound to a domain entity
T.
Procedure
1. Create a CallbackDataProvider
Use DataProvider.fromCallbacks to build a provider that fetches only the requested page. The fetch callback receives a Query object exposing paging, sorting, and filtering information.
DataProvider<T, Filter> provider = DataProvider.fromCallbacks(
query -> fetchSlice(query),
query -> countAll(query)
);
Implement fetchSlice to translate Query into a database call:
private Stream<T> fetchSlice(Query<T, Filter> query) {
int offset = query.getOffset();
int limit = query.getLimit();
List<Sort.Order> orders = query.getSortOrders().stream()
.map(so -> new Sort.Order(so.getDirection(), so.getSorted()))
.collect(Collectors.toList());
Pageable pageable = PageRequest.of(offset / limit, limit, Sort.by(orders));
return repository.findAll(pageable, query.getFilter()).stream();
}
Replace repository.findAll with the appropriate method for your data layer. The key is using offset and limit to request only the needed rows.
2. Provide a Count Callback
The Grid needs the total number of matching rows to size the scrollbar. Return the count that matches the current filter but ignore paging.
private long countAll(Query<T, Filter> query) {
return repository.count(query.getFilter());
}
A cheap count query is essential; an expensive one will dominate database load. If your filter is complex, consider caching the count per filter value.
3. Bind the Provider to the Grid
grid.setDataProvider(provider);
4. Enable Sorting and Filtering
Grid columns must expose the property name that the backend can map to a database column. For sorting, Vaadin sends the column key; you must translate it in fetchSlice.
For filtering, wrap the provider:
ConfigurableFilterDataProvider<T, Filter, Void> cfProvider = provider.withConfigurableFilter();
cfProvider.setFilter(filterObject);
When the user changes a filter, call cfProvider.refreshAll() to trigger a new fetch.
5. Adjust Page Size (Optional)
The default page size is 50 rows. If you notice high server round‑trips, increase it:
grid.setPageSize(200);
Trade‑off: larger pages mean fewer requests but more data per request.
Expected Checks
- Scroll test: Open the view, scroll to the bottom, and observe repeated
GET /yourEndpoint?page=2&size=50requests (or equivalent) rather than one large payload. - Heap check: Monitor the server’s heap during scrolling; it should stay flat, not spike to the size of the full table.
- SQL log: Verify that
SELECT ... LIMIT 50 OFFSET 100or the equivalent is executed for each fetch, and thatORDER BYandWHEREclauses match the Grid’s sort/filter state. - Scrollbar accuracy: The Grid’s scrollbar should reach the last row and the total count displayed (if any) should match the database count.
Recovery Options
- Mis‑count leads to truncated data: Check the count callback. If it returns too low, rows will be missing; if too high, the Grid will request beyond the dataset.
- Offset mis‑mapping: Log
query.getOffset()andquery.getLimit()to ensure they match the database’sOFFSETandLIMITusage. - Performance hit from count query: Cache the count per filter combination or move the count to a lightweight view.
- Large page size causes latency: Reduce
grid.setPageSizeto a smaller value like 50 or 100. - Debugging with in‑memory provider: Temporarily replace the lazy provider with
grid.setItems(list)for a small test dataset to confirm that sorting/filtering works client‑side before re‑enabling lazy loading.
Limitations & Caveats
- All filtering logic must be implemented in the backend; Vaadin does not apply filters automatically.
- Sorting keys from the client must be validated before being used in SQL to avoid injection.
- Vaadin sessions live in the HTTP session, so never store the full result set in UI fields or session attributes.
- Different Vaadin releases have subtly different DataProvider APIs; always consult the version you are using.
Practical Verification Checklist
- Confirm the provider is built with
DataProvider.fromCallbacks. - Verify that the fetch callback uses
query.getOffset()andquery.getLimit(). - Check that the count callback ignores paging parameters.
- Open the view, scroll, and inspect network traffic for incremental requests.
- Monitor JVM heap during the test.
- Apply a filter, then sort a column, and verify the SQL log for correct
WHEREandORDER BYclauses. - Ensure the scrollbar reaches the last row and the count matches the database.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.