Jakarta Data in Jakarta EE 12 M2: From Repositories to a Unified Data Access Model
Jakarta Data in Jakarta EE 12 M2 extends the EE 11 repository model with stateful operations, unified querying, and SQL/NoSQL alignment for domain-centric data access.
Join the DZone community and get the full member experience.
Join For FreeEnterprise Java persistence has been expanding its scope over the last few releases, slowly but deliberately moving away from the idea that persistence is synonymous with relational databases. With Jakarta EE 11, that shift became explicit through the introduction of Jakarta Data, a specification that standardizes application-level data access across both SQL and NoSQL databases. Jakarta EE 12 M2 builds on that foundation, not by changing direction, but by completing ideas that were intentionally deferred in the previous release.
Jakarta Data did not replace Jakarta Persistence. Instead, it introduced a new abstraction layer, focused on how applications use data rather than how data is stored. This distinction is subtle but fundamental. Jakarta Persistence remains an ORM specification, deeply rooted in relational concepts, SQL semantics, and persistence contexts. Jakarta Data, by contrast, targets a higher level: the repository, where domain logic meets data access.
The inspiration for this model is not accidental. The repository concept in Jakarta Data closely aligns with the Domain-Driven Design definition popularized by Eric Evans, which defines a repository as a collection-like abstraction that mediates between the domain and the data source. Rather than exposing tables, documents, or queries directly, repositories express intent in domain terms.
At a high level, Jakarta Data provides a familiar, Jakarta-based programming model that allows Java developers to interact with relational and NoSQL databases consistently, while preserving the specific strengths of each underlying store. The goal is not to flatten differences, but to consolidate common patterns that appear across persistence technologies into a single, standard API.
To make this concrete, Jakarta Data defines repository abstractions that eliminate significant boilerplate while remaining explicit and type-safe. These repositories fall into two broad categories.
Types of Repositories in Jakarta Data
The first category consists of built-in repository interfaces. At the root of this hierarchy is DataRepository, which is extended by more specialized interfaces such as CrudRepository and BasicRepository. These interfaces provide ready-made support for common data access operations while remaining extensible.
@Repository
public interface CarRepository extends BasicRepository<Car, Long> {
List<Car> findByType(CarType type);
Optional<Car> findByName(String name);
}
Once defined, the repository can be injected and used directly by application services without additional plumbing:
@Inject
CarRepository repository;
Car ferrari = Car.id(10L)
.name("Ferrari")
.type(CarType.SPORT);
repository.save(ferrari);
This style will feel familiar to developers coming from other ecosystems, but Jakarta Data is careful not to force this approach. Real-world domains often do not map cleanly to one-repository-per-entity designs, especially when operations span multiple aggregates or represent domain actions rather than CRUD semantics.
That leads to the second category: custom repository interfaces.
Custom repositories allow developers to define domain-centric operations explicitly, using lifecycle annotations such as @Insert, @Update, @Delete, and @Save. These repositories act as a bridge between domain language and persistence mechanics, without inheriting assumptions from generic repository hierarchies.
@Repository
public interface Garage {
@Insert
Car park(Car car);
}
@Inject
Garage garage;
Car ferrari = Car.id(10L)
.name("Ferrari")
.type(CarType.SPORT);
garage.park(ferrari);
This approach tends to resonate strongly with DDD practitioners, as repository methods reflect domain actions rather than data access patterns. Jakarta Data deliberately supports both styles, allowing teams to choose the one that best fits their domain model rather than enforcing a single architectural shape.
Beyond the repositories themselves, Jakarta Data also standardizes several cross-cutting concerns that previously required ad hoc solutions. Pagination is a good example. Jakarta Data supports both offset-based and cursor-based pagination, recognizing that different data stores and workloads have different trade-offs. Offset pagination is simple and familiar, computing pages relative to a fixed offset. Cursor-based pagination, on the other hand, is designed to reduce missed or duplicated results by querying relative to observed values, which is particularly useful for large or frequently changing datasets.
Stateful Repository Operations in Jakarta EE 12
Jakarta Data intentionally avoids competing with frameworks on convenience features that depend on runtime enhancement or framework-specific infrastructure. Its focus is standardization, portability, and architectural clarity at the platform level.
Jakarta EE 12 M2 is where one of the most important deferred capabilities begins to land: stateful repository operations.
In Jakarta EE 11, repositories are stateless by design. Developers explicitly control when entities are saved or updated. Jakarta EE 12 introduces stateful repositories, which are aware of a persistence context, as defined by Jakarta Persistence. These repositories manage entity state transitions automatically and expose lifecycle annotations that mirror familiar JPA operations.
@Repository
public interface Products extends DataRepository<Product, String> {
@Persist
void add(Product product);
@Merge
Product merge(Product product);
@Remove
void remove(Product product);
@Refresh
void reload(Product product);
@Detach
void detach(Product product);
}
Each of these annotations maps to a well-defined persistence operation:
| Annotation | Meaning |
|---|---|
@Persist |
Inserts a new entity into the data store, immediately or on flush |
@Merge |
Merges entity state, inserting if the entity does not exist |
@Remove |
Deletes the entity from the data store |
@Refresh |
Reloads entity state, discarding unsaved changes |
@Detach |
Detaches the entity from the persistence context |
A key design decision here is exclusivity: a repository is either stateless or stateful. Lifecycle annotations for one model cannot be mixed with those of another. This avoids ambiguous semantics and makes repository behavior explicit.
Another important evolution in Jakarta EE 12 is the tighter integration with Jakarta Query. What was previously known as Jakarta Data Query has been moved into Jakarta Query, which defines the minimum common behavior for querying across SQL and NoSQL databases. This separation clarifies responsibilities: Jakarta Data focuses on repositories, while Jakarta Query focuses on querying.
In addition to string-based queries, Jakarta Data also supports dynamic query construction through restrictions. Restrictions provide a fluent, type-safe way to build queries programmatically, without embedding query strings.
Given a repository like this:
@Repository
public interface Countries extends CrudRepository<Country, String> {
@Find
List<Country> filter(Restriction<Country> filter);
}
Developers can express complex queries fluently:
List<Country> result1 =
countries.filter(Restrict.all(_Country.code.equalTo("MN")));
List<Country> result2 =
countries.filter(Restrict.all(
_Country.code.notNull(),
_Country.code.in("FI", "FR", "GR"),
_Country.code.notEqualTo("FR")));
List<Country> result3 =
countries.filter(Restrict.any(
_Country.code.equalTo("CO"),
_Country.code.equalTo("MY")));
For cases where a declarative query is clearer, Jakarta Data repositories can also use @Query with the Jakarta Common Query Language (JCQL). JCQL is defined by Jakarta Query and is conceptualized as a subset of JPQL, deliberately constrained to be implementable across diverse data stores.
@Repository
public interface BookRepository extends BasicRepository<Book, UUID> {
@Query("where title like :titlePattern")
List<Book> booksMatchingTitle(String titlePattern);
@Query("where author.name = :author order by title")
List<Book> findByAuthorSortedByTitle(String author);
}
This design ensures that developers familiar with JPQL feel immediately at home, while still allowing the same query model to apply to non-relational databases.
Jakarta Data’s evolution in Jakarta EE 12 M2 does not radically change the specification’s direction. Instead, it completes the picture that began in Jakarta EE 11. Repositories move from purely stateless abstractions to persistence-context-aware components. Querying is clarified and centralized. Dynamic and declarative approaches coexist. Most importantly, the specification continues to focus on consolidating what SQL and NoSQL databases have in common, without erasing what makes them different.
Jakarta Data is not about hiding databases. It is about giving enterprise Java developers a consistent, domain-centric way to work with data, regardless of where it resides. Jakarta EE 12 is where that vision starts to feel whole.
Opinions expressed by DZone contributors are their own.
Comments