Your code compiles. The IDE is happy. Tests pass. Then in production, a user tries to sort a column and the whole thing blows up. If you've used Spring Data before, you've probably run into this. A mistyped property name hides as a string until runtime, when it surfaces as a cryptic exception. Spring Boot 4.1 introduces type-safe property paths in Spring Data Commons, and it fixes this problem for good.
📦 Get the Code
Follow along with the complete working example.
The Problem with Stringly Typed Property References
In previous versions of Spring Data, whenever you needed to reference a property on an entity (for sorting, building criteria queries, or constructing composite paths) you passed in a plain String. Something like this:
Sort.by("lastName")
That looks fine. But what if you accidentally typed "last_name" or "lasName"? The compiler wouldn't catch it. Your IDE wouldn't flag it. Your unit tests might not exercise that specific path. The bug would sit quietly until it hit production and threw an InvalidPersistentPropertyPath exception.
This is what developers call "stringly typed" programming. You're using strings where a type-safe reference would be much safer. Spring Data Commons (which ships with Spring Boot 4.1) now generates type-safe property path references for your entities, so you can write Person_.lastName instead of "lastName". If you misspell it, the code won't compile.
This feature works across Spring Data projects: Spring Data JPA, Spring Data JDBC, Spring Data MongoDB, and others. For this tutorial, I'll use Spring Data JDBC.
Setting Up the Project
Head over to start.spring.io and create a new project with these settings:
- Language: Java
- Build: Maven
- Spring Boot: 4.1.0
- Java: 17 or later
- Dependencies: Spring Web, Spring Data JDBC, PostgreSQL Driver, Docker Compose Support
The Docker Compose Support dependency is a nice convenience. It generates a compose.yaml file that spins up a PostgreSQL container automatically when you start your Spring Boot application. No need to install or configure Postgres on your local machine.
Schema and Configuration
Create a schema.sql file in src/main/resources:
DROP TABLE IF EXISTS person;
CREATE TABLE person (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
first_name VARCHAR(255),
last_name VARCHAR(255),
city VARCHAR(255),
country VARCHAR(255)
);
In your application.yaml, tell Spring to always run the schema initialization (since we're using a real database, not an embedded one):
spring:
sql:
init:
mode: always
This drops and recreates the person table every time the app starts. That's fine for a demo but not something you'd do in production.
The Domain Model
Create a Person record mapped to the person table:
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Embedded;
import org.springframework.data.relational.core.mapping.Table;
@Table
public record Person(
@Id Long id,
String firstName,
String lastName,
@Embedded.Nullable Address address
) {}
The @Embedded.Nullable annotation tells Spring Data JDBC that the Address fields live in the same table, not in a separate one.
Now create the Address record:
public record Address(String city, String country) {}
The Repository
import org.springframework.data.repository.ListCrudRepository;
import org.springframework.data.repository.ListPagingAndSortingRepository;
public interface PersonRepository extends
ListCrudRepository<Person, Long>,
ListPagingAndSortingRepository<Person, Long> {
}
The ListPagingAndSortingRepository gives us a findAll(Sort sort) method, which is exactly what we need to demonstrate sorting with type-safe property paths.
Seeding Some Data
To have something to query, create a DataLoader class that implements CommandLineRunner:
import org.springframework.boot.CommandLineRunner;
import org.springframework.data.jdbc.core.JdbcAggregateTemplate;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class DataLoader implements CommandLineRunner {
private final PersonRepository repository;
private final JdbcAggregateTemplate template;
public DataLoader(PersonRepository repository, JdbcAggregateTemplate template) {
this.repository = repository;
this.template = template;
}
@Override
public void run(String... args) throws Exception {
repository.saveAll(List.of(
new Person(null, "Dan", "Vega", new Address("Cleveland", "US")),
new Person(null, "DaShaun", "Carter", new Address("Atlanta", "US")),
new Person(null, "Josh", "Long", new Address("San Francisco", "US")),
new Person(null, "Cora", "Iberkleid", new Address("New York", "US")),
new Person(null, "Michael", "Coté", new Address("Amsterdam", "NL"))
));
// Demo methods will go here
}
}
Sorting: The Old Way vs. The New Way
The Old Way
Here's how you've always sorted results in Spring Data:
public void simpleSortOldWay() {
List<Person> sorted = repository.findAll(Sort.by("last_name"));
sorted.forEach(System.out::println);
}
See the problem? The property on our Person record is lastName, but we typed "last_name". The compiler is perfectly happy. The IDE doesn't complain. But at runtime, Spring Data throws:
InvalidPersistentPropertyPath: No property last_name found. Did you mean lastName?
At least the error message is helpful. But this is the kind of bug that slips through code review and testing if nobody happens to exercise that particular sort.
The New Way
With Spring Boot 4.1, Spring Data generates a Person_ class (note the underscore) that contains type-safe references to each property. Now you can write:
public void simpleSortNewWay() {
List<Person> sorted = repository.findAll(Sort.by(Person_.lastName));
sorted.forEach(System.out::println);
}
When you type Person_., your IDE shows you all available properties: id, firstName, lastName, address. If you try to reference a property that doesn't exist, your code won't compile. No more runtime surprises.
Criteria Queries: No More String-Based Column Names
The Old Way
When building criteria queries with JdbcAggregateTemplate, you'd reference column names as strings:
public void criteriaQueryOldWay() {
List<Person> results = template.findAll(
Query.query(Criteria.where("lastName").is("Vega")),
Person.class
);
results.forEach(System.out::println);
}
If you mistype "lastName" as "last_name" or "lasName", you'll get a runtime error like: Column person.lastName does not exist. Again, nothing catches this before your code runs.
The New Way
Replace the string with the generated property reference:
public void criteriaQueryNewWay() {
List<Person> results = template.findAll(
Query.query(Criteria.where(Person_.lastName).is("Vega")),
Person.class
);
results.forEach(System.out::println);
}
Same result, but now the property reference is checked at compile time.
Composite Paths: Nested Properties Made Safe
This is where things get even more interesting. When you have embedded objects, the old approach required chaining strings to build a path.
The Old Way
public void compositePathOldWay() {
List<Person> results = template.findAll(
Query.query(Criteria.where("address.country").is("US")),
Person.class
);
results.forEach(System.out::println);
}
You're relying on two assumptions: that "address" is the correct property name and that "country" is correct within that nested type. Mistype either one and you get a runtime exception: Missing from clause entry for table address.
The New Way
Using PropertyPath.of, you can construct type-safe nested paths:
public void compositePathNewWay() {
List<Person> results = template.findAll(
Query.query(Criteria.where(
PropertyPath.of(Person_.address, Address_.country)
).is("US")),
Person.class
);
results.forEach(System.out::println);
}
Now you're using Person_.address to navigate to the address, then Address_.country to access the country. Your IDE guides you with autocompletion, and a typo becomes a compile error rather than a production incident.
When you run this, everyone with a US address shows up. Michael Coté, based in Amsterdam, is correctly excluded.
How the Generated Classes Work
You might be wondering where Person_ and Address_ come from. Spring Data Commons generates these classes at compile time using annotation processing. When you're on Spring Boot 4.1 (which pulls in Spring Data Commons 2026), these underscore-suffixed classes are created automatically for any class annotated with @Table or used as an entity in your Spring Data repositories.
You don't need to configure anything extra. As long as you're on the right version, the generated classes are available in your IDE with full autocompletion support.
Why This Matters
This feature addresses a real pain point that every Spring Data user has encountered at some point. Here's why it's a welcome addition:
Compile-time safety. Typos in property names are caught before your code ever runs. This is exactly the kind of thing we expect from a type-safe language like Java.
Better IDE support. Autocompletion means you don't have to remember exact property names. Your IDE shows you what's available.
Works across Spring Data projects. Because this lives in Spring Data Commons, it works whether you're using JPA, JDBC, MongoDB, or any other Spring Data module.
Nested path support. Composite paths with embedded objects are particularly error-prone with strings. The PropertyPath.of approach makes these much safer and more readable.
Getting Started
To use type-safe property paths in your own project, you need:
- Spring Boot 4.1.0 or later
- Any Spring Data module (JPA, JDBC, MongoDB, etc.)
- Java 17 or later
That's it. No additional dependencies or configuration required. Start replacing your string-based property references with the generated underscore classes, and enjoy the peace of mind that comes with compile-time checking.
If you want to explore the Spring Boot 4.1 release highlights, there are plenty more features worth checking out, including Spring gRPC support and HTTP client SSRF mitigation.
Happy Coding!
Related Articles
Lazy JDBC Connections in Spring Boot 4.1: One Property, Massive Performance Win
Spring Boot 4.1 adds lazy JDBC connection fetching via spring.datasource.connection-fetch. See how it cuts connection hold time from 2 seconds to 15ms.
Spring Data AOT Repositories: Faster Startup and Build-Time Query Validation
Learn how Spring Data AOT Repositories move query processing from runtime to compile time, reducing startup times and memory usage, plus how to validate repository methods at build time.
Query By Example in Spring Data JPA: A Clean Approach to Dynamic Queries
Learn how to simplify your Spring Data JPA queries using Query By Example (QBE) and create dynamic, type-safe queries without the boilerplate code.