Server‑Side Pagination in Vaadin 24+ Grids with Spring Data JPA
Add efficient server‑side pagination to a Vaadin 24+ Grid by wiring a Spring Data JPA repository through a DataProvider. The guide walks through entity setup, DataProvider implementation, Grid configuration, sorting translation, lazy‑load handling, and verification steps.
22 Jan 2026, 19:02 UTC

Problem Statement
Displaying thousands of rows in a Vaadin Grid can exhaust client memory and slow the UI. Server‑side pagination lets the Grid request only the visible page from the database, keeping the client lean. This article shows how to wire a Spring Data JPA repository into a Vaadin 24+ Grid so that navigation and sorting are handled by the server.
Prerequisites
- Spring Boot 3.x with Java 17+
- Vaadin 24+ (Flow) starter:
vaadin-spring-boot-starter - Spring Data JPA starter:
spring-boot-starter-data-jpa - Database driver (e.g., H2, PostgreSQL)
- Entity with at least one lazy‑loaded relationship (to illustrate the N+1 problem)
1. Define the Entity and Repository
Below is a minimal Customer entity with a lazy collection of Order objects. The repository extends PagingAndSortingRepository to provide pagination and sorting out of the box.
import jakarta.persistence.*;
import java.util.List;
@Entity
public class Customer {
@Id @GeneratedValue
private Long id;
private String name;
private String email;
@OneToMany(mappedBy = "customer", fetch = FetchType.LAZY)
private List orders;
// getters & setters omitted for brevity
}
@Repository
public interface CustomerRepository extends PagingAndSortingRepository<Customer, Long> {
}
2. Create a DataProvider that Uses Paging
The Vaadin DataProvider interface requires two callbacks: fetch() and size(). We implement them by delegating to the repository. The DataProvider is stateless, so it can be a Spring bean and reused.
@Component
public class CustomerDataProvider implements DataProvider<Customer, Void> {
private final CustomerRepository repository;
public CustomerDataProvider(CustomerRepository repository) {
this.repository = repository;
}
@Override
public int size(Query<Customer, Void> query) {
// Total count needed for the paginator
return (int) repository.count();
}
@Override
public Stream<Customer> fetch(Query<Customer, Void> query) {
int offset = query.getOffset();
int limit = query.getLimit();
Pageable pageable = PageRequest.of(offset / limit, limit, mapSort(query.getSortOrder()));
return repository.findAll(pageable).stream();
}
private Sort mapSort(List<SortOrder> sortOrders) {
List<Sort.Order> orders = sortOrders.stream()
.map(s -> new Sort.Order(s.getDirection().isAscending() ? Sort.Direction.ASC : Sort.Direction.DESC, s.getSorted()))
.toList();
return Sort.by(orders);
}
}
Key points:
PageRequest.of(page, size, sort)converts the Grid’s offset/limit into a JPAPageable.- Sorting is translated from Vaadin’s
SortOrderto Spring Data’sSort. - The
size()method returns the total number of rows so that Vaadin can render the paginator.
3. Wire the DataProvider into the Grid
Inject the CustomerDataProvider into the UI component and configure the Grid. Set a fixed page size (e.g., 25) and enable the built‑in paginator. The Grid will call the DataProvider whenever the user navigates or sorts.
@Route("customers")
public class CustomerView extends VerticalLayout {
private final Grid<Customer> grid = new Grid<>(Customer.class, false);
public CustomerView(CustomerDataProvider provider) {
grid.setColumns("id", "name", "email");
grid.setPageSize(25);
grid.setPaginatorEnabled(true);
grid.setDataProvider(provider);
add(grid);
}
}
Because the Grid’s setDataProvider method expects a DataProvider, we can pass the Spring bean directly.
4. Avoid Lazy‑Loading Pitfalls
If the Customer entity contains lazy relationships (e.g., orders), the Grid may try to render them after the JPA session has closed, causing a LazyInitializationException. Two common solutions:
- Use
@EntityGraphon the repository method:@EntityGraph(attributePaths = {"orders"}) Page<Customer> findAll(Pageable pageable); - Project to a DTO that contains only the fields needed for the Grid:
public interface CustomerDto { Long getId(); String getName(); String getEmail(); } Page<CustomerDto> findAllProjectedBy(Pageable pageable);
Using DTO projections reduces the amount of data transferred and eliminates N+1 queries.
5. Verify the Setup
- Run the application:
./mvnw spring-boot:run - Open
http://localhost:8080/customersin a browser. - Check that the first 25 rows are displayed and the paginator shows the total page count.
- Click the Next button and confirm that the next 25 rows appear without a full page reload.
- Sort the Name column ascending and verify that the rows are reordered on the server side.
- Open the browser console and look for any
LazyInitializationExceptionmessages; if present, enable@EntityGraphor switch to a DTO.
6. Recovery Options
- Fallback to client‑side pagination: If the server cannot provide enough memory to handle the DataProvider, you can temporarily use
grid.setItems(entities)with a pre‑loaded list. This is only suitable for very small datasets. - Increase page size cautiously: A page size of 200+ rows can still strain the server if each row contains many columns or large text fields. Profile the JPA query time and adjust accordingly.
- Use caching: Wrap the repository call in a cache (e.g., Caffeine) if the underlying data changes infrequently. This reduces database load but adds complexity.
Limitations
- The example assumes a simple
Customerentity. Complex inheritance or composite keys require additional configuration. - Vaadin’s built‑in paginator only supports simple page size changes; dynamic page size selection requires custom UI.
- When using projections, you cannot access lazy relationships unless they are explicitly projected.
Conclusion
By wiring a Spring Data JPA repository into a Vaadin Grid via a stateless DataProvider, you enable efficient server‑side pagination and sorting. The Grid’s built‑in paginator eliminates client‑side memory pressure, while careful handling of lazy relationships prevents runtime exceptions. Follow the verification steps to confirm correct behavior and use the recovery options if your data set or environment imposes constraints.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.