Type Safe Property Paths in Spring Boot 4.1

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.

github.com/danvega/type-safe-property-paths

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:

  1. Spring Boot 4.1.0 or later
  2. Any Spring Data module (JPA, JDBC, MongoDB, etc.)
  3. 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!

Subscribe to my newsletter.

Sign up for my weekly newsletter and stay up to date with current blog posts.

Weekly Updates
I will send you an update each week to keep you filled in on what I have been up to.
No spam
You will not receive spam from me and I will not share your email address with anyone.