Building a Full-Featured Demo with webforJ and Spring Boot

In my time working with documentation and coding for customers I have built quite the number of demo apps myself, so by now I know what the shortcuts look like. The dataset is always small, with authentication and advanced features "coming soon" or hardcoded in instead of properly implemented. Filtering works fast, because coincidentally there are only five rows to filter. All of that isn't to say those demos are bad, after all they serve their purpose, but I wanted to see how efficiently I could build a demo that doesn't cut corners while still being small and easy to understand.
The webforJ Bookstore is my attempt at that. It's a book inventory manager built on webforJ and Spring Boot with live table filtering, colored genre chips, a data-bound edit drawer, and Spring Security handling who can do what. This post covers the pieces I found most worth writing about.

The bookstore at a glance
Here's what's in the app:
- Browse, search, and sort a book inventory in a live
Table - Add and edit books using a slide-out
Drawerwith automatic data binding and validation - Create and manage genres with custom colors, displayed as colored chips in each row
- Log in as a regular user or an admin, with Spring Security protecting views based on role
Two accounts ship with the app for testing:
| Username | Password | Role |
|---|---|---|
user | password | User |
admin | admin | User + Admin |
Connecting the table to Spring Data
The core of the inventory is a Table<Book> fed by a SpringDataRepository. webforJ's SpringDataRepository wraps a standard Spring Data JpaRepository and gives the Table component a way to page, sort, and filter rows without loading everything into memory at once.
Setting it up in BookService just takes one method:
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class BookService {
private final BookRepository bookRepository;
public SpringDataRepository<Book, String> getFilterableRepository() {
return new SpringDataRepository<>(bookRepository);
}
}
And in InventoryView, plugging it into the table takes two lines:
repository = bookService.getFilterableRepository();
bookTable.setRepository(repository);
From there, the table handles its own lifecycle perfectly. Sorting, scrolling, and refreshing after a save all go through the repository.
Live filtering with JPA Specifications
The search bar and genre filter both update the table in real time, no matter the size of the dataset. Each time the user types in the search bar or picks a genre, the view builds a JPA Specification and hands it to the repository:
Specification<Book> searchSpec = (root, query, cb) -> {
query.distinct(true);
Predicate predicate = cb.conjunction();
if (!term.isEmpty()) {
String escaped = escapeLikePattern(term);
predicate = cb.and(predicate, cb.or(
cb.like(cb.lower(root.get("title")), "%" + escaped + "%", '\\'),
cb.like(cb.lower(root.get("author")), "%" + escaped + "%", '\\')));
}
if (genre != null) {
predicate = cb.and(predicate, cb.isMember(genre, root.get("genres")));
}
return predicate;
};
repository.setFilter(searchSpec);
repository.commit();
repository.commit() is the trigger that tells the Table to re-fetch its data. The JPA Specification runs as a proper SQL query, so filtering against a large dataset stays fast.
Make sure to use escapeLikePattern. Without it, your search field is an open door into your database, and we don't want that, even in a demo.

