Get ahead
VMware offers training and certification to turbo-charge your progress.
Learn moreIf you've worked with data access in Java and especially with Spring Data for a while, then you are familiar with various Query and Update programming models. You write data access code. You refactor a property name. You run your tests. They fail. Your query strings? Still pointing to the old property name because strings don't refactor.
Sort.by("firstName", "lastName");
where("address.country").is(…);
Query construction often involves referencing domain properties by strings, whether for predicates, sorting, or path navigation. This approach is simple and intentionally lightweight. It requires no additional setup and integrates naturally into application code. For many use cases, it's a reasonable default. However, String-based property references come with limitations that become more apparent as applications evolve over time.
Using strings to refer to properties is a pragmatic design choice. They require no additional setup, no code generation, and no build process changes. However, this simplicity comes with inherent fragility, shifting some responsibility to runtime.
Property names expressed as strings are not validated by the compiler. Typographical errors, outdated names after refactoring, or mismatches between the domain model and query definitions will compile successfully and typically surface only when the code is executed. In some cases, tests can detect these issues early, but this is not always feasible.
Strings provide limited context for refactoring. IDEs can reliably update method and field references, but string literals lack semantic context, as they are effectively textual content. With a significant distance between execution points and query declarations, it becomes increasingly difficult for tools to associate them with the relevant domain types. In larger codebases, this can lead to subtle errors that are easy to miss.
These issues are not new, and they are not unique to Spring Data. They are a natural consequence of stringly-typed programming APIs.
The Java ecosystem offers several alternatives based on metamodels. These approaches represent domain models or database schemas in a structured, type-safe form.
Frameworks such as Querydsl and the JPA Metamodel Generator rely on annotation processing to generate metamodel classes. Their use in application code is rather convenient, as the following examples illustrate, reflecting the underlying domain model:
Querydsl
QPerson person = QPerson.person;
query.where(person.firstName.eq("John"));
JPA Criteria API
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Person> query = cb.createQuery(Person.class);
Root<Person> person = query.from(Person.class);
query.where(cb.equal(person.get(Person_.firstName), "John"));
Generated metamodels provide compile-time validation. At the same time, they typically introduce build-time code generation and often additional dependencies. When refactoring, the generated code must be regenerated consistently, which can become a source of friction.
These metamodels work best within their intended scope: The JPA metamodel integrates naturally with the Criteria API and Spring Data JPA’s Specification support. Outside of those contexts, additional integration layers are required. Spring Data provides adapters such as JpaSort (for JPA Metamodel path expressions) or QSort (for Querydsl order specifiers), but availability and feature coverage varies across modules.
Other libraries, such as jOOQ, focus more directly on the database schema. jOOQ's plugins can aid with generating a metamodel during the build reflecting tables and columns:
Result result =
create.select()
.from(PERSON)
.where(PERSON.FIRST_NAME.eq("John"))
.fetch();
Each of these approaches involves tradeoffs between safety, flexibility, and operational complexity.
Metamodel generation typically employs annotation processors or dedicated build plugins. In more mature projects where domain models change rather infrequently, repetitive generation yields the same generated metamodel code at the expense of build time.
IDE support for generated sources has evolved over time, leading to an improved overall experience, but behavior varies across environments and may require explicit configuration. None of these concerns is prohibitive, but each contributes to overall project complexity in areas that are rarely revisited. Thus, complexity leads to higher troubleshooting efforts in comparison to areas that are revisited rather often by project maintainers.
So far, we have discussed existing approaches, their adverse effects on maintainability, and how increased distance contributes to classes of potential bugs. In an ideal world, modern data access code should be able to leverage language primitives to reference properties for type- and refactoring-safety, and with Spring Data 2026.0.0-M1 we are proud to introduce first-class support for type-safe property references.
Let's go back to our very first example:
Sort.by("firstName", "lastName");
Now, let's transform that into a type-safe variant using method references:
Sort.by(Person::getFirstName, Person::getLastName)
One uses string literals. The other is code. Consider how naturally method references express the intent.
The compiler validates that getFirstName() exists on Person. Your IDE can navigate to the method definition. Refactoring tools work as expected. It's type-safe, concise, and does not require additional infrastructure.
For nested properties, composition feels similarly natural, although it reads slightly more verbose:
Sort.by(PropertyPath.of(Person::getAddress).then(Address::getCountry))
Each step in the path is validated by the type system. Invalid paths fail fast, typically surfaced through your IDE, but in any case at compile time instead of at runtime. In addition, promoting the concept of an owning type to the interface allows methods to enforce sensible constraints. For example, a sort definition can ensure that all properties originate from the same domain type:
Sort.by(Person::getFirstName, Person::getLastName)
With more context, the type system does the work:
Sort.by(Person::getFirstName, Order::getOrderDate)
// Compilation error: incompatible owner types as per:
// <T> Sort by(TypedPropertyPath<T, ?>... properties)
The benefit isn't just safety: It's clarity. The intent is immediately obvious, and the type system enforces correctness.
Kotlin offers first-class support for property references already. Spring Data gradually adopted Kotlin-friendly APIs by leveraging operator overloads for the div (/) operator for property path navigation. Such applications can use KProperty references in various Criteria implementations effectively for all methods accepting TypedPropertyPath embracing Kotlin's natural idioms:
Sort.by(Person::address)
Sort.by(Person::address / Address::city)
Type-safe property paths provide a unified abstraction while respecting the conventions of each language.
Existing string-based APIs remain unchanged and fully supported. They are and will remain an active design aspect of new functionality. Type-safe property paths come with a sweet-spot for usage and like every other approach also with limitations. For any arrangement requiring dynamic property names at runtime, it is totally fine to continue using strings with their aforementioned limitations (such as accepting a property name through a web request).
Type-safe property paths are strictly additive and can be introduced selectively wherever their benefits are most pronounced. For example, consider the following examples alongside their refined type-safe alternatives:
// Existing code
where("firstName").is("…")
where("address.country").is("…")
Sort.by("firstName", "lastName")
// Type-safe variants
where(Person::getFirstName).is("…")
where(PropertyPath.of(Person::getAddress).then(Address::getCountry)).is("…")
Sort.by(Person::getFirstName, Person::getLastName)
Adoption can be gradual, focusing on areas where refactoring safety or clarity is most valuable. From a performance perspective, method references are introspected once and cached for subsequent usage, resulting in an overall pleasant performance baseline.
We believe developers should spend their time building applications, not debugging property name typos. Your tools should work with you, not against you. The introduction of support for type-safe property paths represents more than just a set of new interfaces: It is an alignment with how modern Java and Kotlin developers expect to work. Languages provide powerful tools for type safety and refactoring, and our frameworks should leverage those tools rather than work around them.
Type-safe property paths eliminate a class of bugs, improve refactoring confidence, and make intent more explicit in code. They allow developers to rely on the compiler and less on convention and discipline. Moving forward, there are several more potential refinements possible to surface type-safe relationships within a query, but that will be part of an ongoing research effort.
We shipped type-safe property paths in Spring Data 2026.0.0-M1 allowing you to explore the new APIs and provide feedback. We are excited to see how you will use them in your projects and how they will evolve based on your feedback.