For more than a decade, Spring Data JPA was the default choice for database access in enterprise Java. Its repository pattern, dynamic queries, and high-level abstractions changed how teams built data layers on top of Hibernate and relational databases. However, modern cloud-native systems face new demands. Instant cold startup, low memory footprint, ahead-of-time (AOT) compilation, and GraalVM native image execution require lean architectures. In 2026, the Eclipse Foundation delivered a major milestone for standard enterprise Java: Jakarta Data 1.0, released as part of the Jakarta EE 11 platform.
Jakarta Data standardizes the repository pattern across all compliant enterprise runtimes. It removes proprietary framework lock-in. It allows developers to write clean, declarative data access interfaces that work identically in Quarkus, Open Liberty, WildFly, Helidon, or standalone Java SE. More importantly, Jakarta Data introduces compile-time code generation. While traditional frameworks create dynamic proxies in memory at application startup, Jakarta Data generates plain Java implementation classes during the build step. In this comprehensive technical guide, we explore Jakarta Data 1.0 architecture, compare it directly with Spring Data JPA, examine code generation mechanics, implement DTO projections, and analyze production deployment strategies.
Table of Contents
- Architectural Foundations: Compile-Time vs Runtime Generation
- The Repository Hierarchy & Type-Safe Metamodels
- Declarative Querying & Java Record Projections
- Advanced Pagination: Offset vs Keyset Cursor Traversal
- Universal Polyglot Architecture & Runtime Portability
- Comparative Analysis: Jakarta Data 1.0 vs Alternatives
- Real-World Enterprise Use Cases
- Production Hardening: 3 Pitfalls and Concrete Fixes
- Frequently Asked Questions (FAQ)
- Companion Workshop Code
- Authoritative References
Architectural Foundations: Compile-Time vs Runtime Generation
To understand the power of Jakarta Data 1.0, you must first examine how data access frameworks operate under the hood. For years, Spring Data JPA relied heavily on runtime reflection and dynamic proxies. When your Spring Boot application boots, Spring scans your classpath for interfaces that extend JpaRepository. For every interface found, Spring creates a dynamic proxy using JDK dynamic proxies or CGLIB. It then inspects every method signature, parses method names into Abstract Syntax Trees (AST), generates JPQL queries in memory, and wires them into the Spring application context.

This dynamic runtime approach offers great convenience during prototyping. However, it incurs noticeable costs in production cloud environments:
- Memory Footprint: Dynamic proxies, method reflection metadata, and runtime AST query models consume significant heap space in memory.
- Startup Latency: Classpath scanning, reflection analysis, and proxy instantiation increase JVM cold start time. This harms auto-scaling serverless containers.
- Native Compilation Barriers: Ahead-of-time (AOT) compilers like GraalVM require exhaustive reflection configuration and proxy hints. Any missing hint leads to runtime crashes in native images.
Jakarta Data 1.0 takes a completely different path. It moves code synthesis from application startup time to the Java compilation step (build time). When you compile your project with Maven or Gradle, a standard Java annotation processor (such as hibernate-processor) inspects your repository interfaces.

The annotation processor produces concrete Java source files directly in your build directory (for example, EmployeeRepository_.java). These generated classes contain plain, direct Java code that interacts with the underlying persistence provider. There are zero dynamic proxies. There is zero runtime reflection. When the application starts, it simply loads standard bytecode. This design enables instantaneous cold starts and frictionless compilation into native GraalVM binaries, making it an ideal companion to our guides on GraalVM Native Image with Spring Boot 4 and High-Concurrency APIs with Java 25 Virtual Threads.
The Repository Hierarchy & Type-Safe Metamodels
The Jakarta Data specification defines a clean, modular repository hierarchy under the jakarta.data.repository package. This hierarchy gives developers full control over which data operations are exposed to the business layer.

The interface hierarchy consists of three core levels:
DataRepository<T, K>: The root marker interface. It defines no operations. It simply designates a class as a Jakarta Data repository. You extend this interface when you want to expose only custom, explicitly declared query methods without automatic CRUD methods.BasicRepository<T, K>: ExtendsDataRepository. It provides essential lookup and inspection operations, includingfindById(K id),existsById(K id), andcount().CrudRepository<T, K>: ExtendsBasicRepository. It adds full lifecycle mutation operations, includinginsert(T entity),update(T entity),save(T entity),delete(T entity), anddeleteAll().
Static Metamodel Generation
In addition to repository classes, the annotation processor generates static metamodel interfaces for every entity. For example, for an Employee entity, the processor generates _Employee.java:
package com.webcodein.data;
import jakarta.annotation.Generated;
import jakarta.data.metamodel.SortableAttribute;
import jakarta.data.metamodel.TextAttribute;
@Generated("org.hibernate.processor.HibernateProcessor")
public interface _Employee {
String ID = "id";
String DEPARTMENT = "department";
String SALARY = "salary";
String ACTIVE = "active";
SortableAttribute<Employee> id = new SortableAttributeRecord<>(ID);
TextAttribute<Employee> department = new TextAttributeRecord<>(DEPARTMENT);
SortableAttribute<Employee> salary = new SortableAttributeRecord<>(SALARY);
SortableAttribute<Employee> active = new SortableAttributeRecord<>(ACTIVE);
}
This static metamodel provides compile-time property verification. If a developer accidentally renames a database column or makes a typo in a sorting expression, the Java compiler immediately halts the build. You catch errors during mvn compile rather than during production deployment.
Declarative Querying & Java Record Projections
Jakarta Data 1.0 provides intuitive declarative query annotations that eliminate boilerplate database logic. Unlike Spring Data, which parses method names at runtime, Jakarta Data emphasizes explicit, parameter-driven queries.
Parameter-Based Queries with @Find and @By
The primary query annotation in Jakarta Data is @Find. You combine @Find with the @By parameter annotation to match entity attributes directly:
package com.webcodein.data;
import jakarta.data.repository.By;
import jakarta.data.repository.CrudRepository;
import jakarta.data.repository.Find;
import jakarta.data.repository.Query;
import jakarta.data.repository.Repository;
import java.util.List;
@Repository
public interface EmployeeRepository extends CrudRepository<Employee, Long> {
// Parameter-based query matching department and active status
@Find
List<Employee> activeInDepartment(@By("department") String department, @By("active") boolean active);
// Custom query with positional parameter binding
@Query("SELECT e FROM Employee e WHERE e.salary > ?1 ORDER BY e.lastName ASC")
List<Employee> findHighEarners(double minSalary);
// DTO projection query returning Java 25 record summaries
@Query("SELECT new com.webcodein.data.DepartmentSummary(e.department, count(e), avg(e.salary)) " +
"FROM Employee e GROUP BY e.department ORDER BY e.department ASC")
List<DepartmentSummary> summarizeDepartments();
}
Immutable DTO Projections with Java Records
Modern enterprise applications must avoid exposing managed entity objects directly to controllers or network callers. Exposing entities invites the dreaded N+1 query problem and unwanted lazy loading exceptions, as detailed in our analysis of Eliminating the Hibernate 7 & JPA N+1 Query Problem. Jakarta Data pairs seamlessly with Java 25 records to create zero-overhead DTO projections:
package com.webcodein.data;
/**
* Immutable DTO projection representing aggregated department statistics.
*/
public record DepartmentSummary(String department, long employeeCount, double averageSalary) {
public DepartmentSummary {
if (department == null || department.isBlank()) {
throw new IllegalArgumentException("Department name cannot be null or blank");
}
if (employeeCount < 0) {
throw new IllegalArgumentException("Employee count cannot be negative");
}
if (averageSalary < 0) {
throw new IllegalArgumentException("Average salary cannot be negative");
}
}
}
When the query executes, the persistence provider instantiates the DepartmentSummary record directly using constructor expressions. No entity snapshots are attached to the persistence context. No dirty tracking occurs. This guarantees maximum database read performance and minimal garbage collection pressure.
Advanced Pagination: Offset vs Keyset Cursor Traversal
Paginating large enterprise datasets is a frequent source of performance degradation in cloud applications. Jakarta Data 1.0 introduces built-in support for two distinct pagination mechanisms: traditional offset pagination and modern keyset cursor pagination.

Traditional Offset Pagination with PageRequest
Traditional pagination divides a result set into pages using an offset and a limit. In Jakarta Data 1.0, you create page requests using PageRequest.ofPage(pageNumber).size(pageSize). Sorting instructions are passed via a separate, type-safe Order parameter:
package com.webcodein.data;
import jakarta.data.Order;
import jakarta.data.Sort;
import jakarta.data.page.Page;
import jakarta.data.page.PageRequest;
import jakarta.data.repository.By;
import jakarta.data.repository.CrudRepository;
import jakarta.data.repository.Find;
import jakarta.data.repository.Repository;
@Repository
public interface ProductRepository extends CrudRepository<Product, Long> {
@Find
Page<Product> byCategory(@By("category") String category,
PageRequest pageRequest,
Order<Product> order);
}
In your service or test code, you invoke the repository cleanly:
PageRequest pageRequest = PageRequest.ofPage(1).size(20);
Order<Product> order = Order.by(Sort.asc("price"));
Page<Product> page = productRepository.byCategory("Hardware", pageRequest, order);
List<Product> items = page.content();
long totalRows = page.totalElements();
long totalPages = page.totalPages();
Keyset Cursor Traversal for High-Scale Feeds
While offset pagination works well for administrative dashboards with small data volumes, it degrades drastically on large datasets. Executing OFFSET 50000 LIMIT 50 forces the database to read 50,050 rows from disk, sort them in memory, and discard the first 50,000 rows. Furthermore, if records are inserted or deleted while a user navigates between pages, rows shift, resulting in duplicate or skipped records.
Jakarta Data 1.0 solves this problem with keyset cursor pagination via CursoredPage<T>. Instead of jumping to an arbitrary row offset, the cursor stores the boundary key (such as the ID or timestamp) of the last retrieved item. The subsequent request uses an indexed WHERE id > :lastSeenId query. This achieves O(1) constant-time lookup performance, regardless of whether you are reading page 1 or page 10,000.
Universal Polyglot Architecture & Runtime Portability
One of the greatest benefits of Jakarta Data is true vendor neutrality. Developers no longer need to rewrite their repository layers when switching between runtimes or databases.

The Jakarta Data architecture spans four decoupled layers:
- Application Layer: Your service code interacts purely with
@Repositoryinterfaces, standard entities, and Java records. Your code contains zero framework-specific imports. - Specification Layer: The
jakarta.dataAPI defines standard lifecycle interfaces, annotations, and page abstractions. - Implementation Providers: Hibernate ORM 7.x, EclipseLink, and Jakarta NoSQL provide underlying persistence execution engines. For Hibernate 7.1 details, see our post on Hibernate 7.0 & Spring Data JPA 4: The Best New Features.
- Deployment Runtimes: Jakarta Data repositories run unchanged inside Quarkus, Open Liberty, Helidon, WildFly, or standalone batch workers. In Java SE, repositories can be backed directly by Hibernate’s lightweight
StatelessSession.
Comparative Analysis: Jakarta Data 1.0 vs Alternatives
The following matrix compares Jakarta Data 1.0 with other prominent data access frameworks in modern Java development:
| Feature / Dimension | Jakarta Data 1.0 | Spring Data JPA 4 | Micronaut Data | Quarkus Panache | jOOQ 3.x |
|---|---|---|---|---|---|
| Specification Type | Vendor-neutral standard (Jakarta EE 11) | Proprietary Spring framework | Proprietary Micronaut framework | Proprietary Quarkus extension | Commercial / Open-source library |
| Implementation Model | Compile-time code generation | Runtime dynamic proxies (CGLIB / JDK) | Compile-time AST transformation | Bytecode transformation at build time | Compile-time code generator CLI |
| Startup Overhead | Near zero (static classes) | Moderate to high (reflection scanning) | Near zero | Near zero | Near zero |
| GraalVM Native Support | Native first (zero reflection hints) | Requires Spring AOT hints | Native first | Native first | Native first |
| Polyglot Capabilities | SQL & NoSQL (Jakarta NoSQL) | SQL, MongoDB, Redis, Cassandra | SQL, Mongo, Cassandra | SQL, MongoDB | SQL relational only |
| Runtime Portability | Quarkus, Liberty, Helidon, Java SE | Spring Boot only | Micronaut only | Quarkus only | Any Java runtime |
Real-World Enterprise Use Cases
Use Case 1: Ultra-Low-Latency Financial Microservices with GraalVM
High-frequency payment processing and trading gateways require sub-millisecond cold starts and deterministic memory behavior. Using Spring Data JPA in serverless environments often incurs seconds of warmup latency while reflection metadata caches populate. By switching to Jakarta Data 1.0 with Quarkus or Helidon, financial engineering teams achieve sub-50ms cold starts in GraalVM native binaries. Memory utilization drops from 350 MB to under 40 MB per container, allowing massive density in Kubernetes clusters.
Use Case 2: Multi-Tenant Cloud SaaS on Open Liberty
Enterprise SaaS applications frequently host isolated database schemas for individual corporate tenants. Jakarta Data’s vendor-neutral repository interfaces allow service layers to remain completely isolated from underlying connection pooling and tenant routing logic. A single @Repository definition executes against dynamic schema contexts managed by Open Liberty and Jakarta EE transactions. This architecture pairs directly with the design principles in our tutorial on Clean Architecture: Implementing Hexagonal in Spring Boot 4.
Use Case 3: High-Throughput Batch Ingestion with Hibernate StatelessSession
Traditional JPA repositories attach entities to a stateful first-level persistence cache (the EntityManager). In high-volume batch processing (ingesting 500,000 sensor readings or transactional records), the persistence context quickly exhausts JVM heap space unless developers manually flush and clear the session. Jakarta Data repository implementations generated by hibernate-processor can operate directly on Hibernate’s StatelessSession. The stateless session bypasses the first-level cache entirely, performing direct, high-speed JDBC batch statements with constant memory overhead.
Production Hardening: 3 Pitfalls and Concrete Fixes
Pitfall 1: Expecting Spring-Style Method Name Parsing in Hibernate
Developers transitioning from Spring Data often attempt to write query methods derived purely from method names, such as findByDepartmentAndActiveTrue. While the Jakarta Data specification describes derived query syntax as optional, Hibernate’s Jakarta Data implementation (hibernate-processor) does not implement dynamic method name parsing. Attempting to compile this code causes the annotation processor to fail with a build error.
Anti-Pattern (Build Error):
// BAD: Fails compilation in Hibernate Jakarta Data processor!
@Repository
public interface EmployeeRepository extends CrudRepository<Employee, Long> {
List<Employee> findByDepartmentAndActiveTrue(String department);
}
Production-Ready Fix:
// GOOD: Use explicit @Find and @By parameter matching
@Repository
public interface EmployeeRepository extends CrudRepository<Employee, Long> {
@Find
List<Employee> activeInDepartment(@By("department") String department,
@By("active") boolean active);
}
Pitfall 2: Missing Annotation Processor in Maven Compiler Configuration
Because Jakarta Data relies on build-time code generation, omitting the annotation processor in your pom.xml prevents the generation of repository implementation classes (such as EmployeeRepository_). At runtime or test execution, your application will fail with ClassNotFoundException or injection errors.
Anti-Pattern (Missing Processor):
<!-- BAD: Compiler plugin without annotation processor path -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
</plugin>
Production-Ready Fix:
<!-- GOOD: Explicitly configure hibernate-processor in annotationProcessorPaths -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-processor</artifactId>
<version>7.1.0.Final</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
Pitfall 3: Falling into the Deep Offset Pagination Trap
Using traditional offset pagination (PageRequest.ofPage(1000).size(50)) on tables with millions of records forces the database storage engine to perform expensive sequential index scans and discard tens of thousands of rows. This spikes database CPU and saturates connection pools, as explored in our guide on Production HikariCP Tuning and Leak Detection.
Anti-Pattern (Heavy Database Discard Cost):
// BAD: Requesting deep offsets forces SQL 'OFFSET 50000 LIMIT 50'
Page<Product> page = productRepository.byCategory("Electronics",
PageRequest.ofPage(1000).size(50), Order.by(Sort.asc("id")));
Production-Ready Fix:
// GOOD: Use keyset cursor pagination with CursoredPage
@Repository
public interface ProductRepository extends CrudRepository<Product, Long> {
@Find
CursoredPage<Product> byCategoryCursored(@By("category") String category,
PageRequest pageRequest,
Order<Product> order);
}
Frequently Asked Questions (FAQ)
Is Jakarta Data replacing Jakarta Persistence (JPA)?
No. Jakarta Data does not replace JPA; it complements it. Jakarta Data sits on top of JPA as a higher-level repository abstraction, much like Spring Data JPA sits on top of Hibernate. JPA remains the standard specification for entity mapping, relational joins, table schemas, and low-level transaction control. Jakarta Data standardizes how developers query and persist entities without writing repetitive DAO code.
Can I use Jakarta Data inside a Spring Boot application?
Yes. While Spring Boot provides its own Spring Data starter, you can integrate Jakarta Data into Spring applications. Spring Framework 7 and Spring Boot 4 have begun exploring support for Jakarta Data repository interfaces. Alternatively, you can use Hibernate 7.1’s compile-time processor in any Spring Boot project and inject the generated repository classes as standard Spring beans.
How does Jakarta Data achieve instant startup in GraalVM native images?
Jakarta Data eliminates runtime dynamic proxies. In traditional frameworks, runtime proxies require complex reflection metadata and proxy configuration files for GraalVM SubstrateVM. Because Jakarta Data’s hibernate-processor generates standard Java classes during javac compilation, the ahead-of-time compiler analyzes concrete method invocations directly. This eliminates reflection overhead and prevents native compilation errors.
Does Jakarta Data support NoSQL databases?
Yes. Jakarta Data is designed as a polyglot data access specification. Through the Jakarta NoSQL specification, developers can use the same @Repository, @Find, and CrudRepository abstractions against MongoDB document stores, Apache Cassandra wide-column databases, and Neo4j graph stores without changing business code.
What is the difference between Page and CursoredPage in Jakarta Data?
Page<T> represents traditional offset-based pagination. It calculates total elements and total pages by executing an additional COUNT(*) query, making it suitable for classic UI pagination controls with page numbers. CursoredPage<T> represents keyset cursor-based pagination. It navigates relative to a specific record key, avoiding count queries and offset scans. It provides constant O(1) performance for infinite-scroll mobile feeds and high-volume data streams.
Companion Workshop Code
A complete, standalone companion workshop project for this guide is available in our public GitHub repository: Jakarta Data 1.0 vs Spring Data Workshop. The project demonstrates:
- Entity modeling with
EmployeeandProduct. - Compile-time annotation processing with Hibernate 7.1 (
hibernate-processor). - Immutable DTO projections using Java 25 records (
DepartmentSummary). - Type-safe parameter queries using
@Findand@By. - Complete 13-test automated test suite running against in-memory H2 with
StatelessSession.
# Clone the repository
git clone https://github.com/salahtobok/learn.webcodein.workshop.git
cd learn.webcodein.workshop/persistence/jakarta-data-1-0-vs-spring-data
# Run the 13-test verification suite on JDK 25
mvn clean test
Authoritative References
- Jakarta Data 1.0 Official Specification (Jakarta EE)
- Hibernate ORM 7.1 User Guide & Jakarta Data Implementation
- Hibernate 7.0 & Spring Data JPA 4: The Best New Features
- GraalVM Native Image with Spring Boot 4: Cloud-Native Java Guide
- High-Concurrency APIs with Java 25 Virtual Threads
- Clean Architecture: Implementing Hexagonal in Spring Boot 4
- Production HikariCP Tuning and Leak Detection in Spring Boot 4
