Jakarta Data
In the previous lessons we wrote PostRepository by hand: every method is a JPQL query and an EntityManager call. Jakarta Data 1.0 is a new Jakarta EE 11 specification that removes this repetitive code. You declare only a repository interface (the object the application uses to read and save entities), and the server creates the implementation itself at startup. Spring Data has similar repositories; now they are a standard.
The server feature
In Open Liberty the specification is enabled by the data-1.0 feature. It is already part of webProfile-11.0 from our server.xml, so there is nothing to add. You can see it in the log at startup:
[AUDIT ] CWWKF0012I: The server installed the following features: [appAuthentication-3.1, appSecurity-6.0, cdi-4.1, concurrent-3.1, data-1.0, ...]
If your project lists individual features instead of a profile, add the line <feature>data-1.0</feature> to featureManager. The API is already there too: the jakarta.data package is part of the jakarta.jakartaee-api dependency, version 11.
The repository interface
Create the file src/main/java/com/example/blog/Posts.java:
// src/main/java/com/example/blog/Posts.java
package com.example.blog;
import jakarta.data.repository.CrudRepository;
import jakarta.data.repository.Repository;
@Repository(dataStore = "java:app/env/blogUnit")
public interface Posts extends CrudRepository<Post, Long> {
}@Repository tells the server: create an implementation and make it a CDI bean. In CrudRepository<Post, Long> the first type is the entity from the JPA lesson, the second is the type of its @Id. The interface has no body, yet it already has methods: the parent interfaces provide them.
The link to the database
The dataStore parameter says how the repository reaches the database. We use our blog persistence unit from persistence.xml — then the repository and PostRepository see the same table. To do that, the persistence unit gets a JNDI name. Add an annotation to RestApplication:
// src/main/java/com/example/blog/RestApplication.java
package com.example.blog;
import jakarta.persistence.PersistenceUnit;
import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;
@ApplicationPath("/api")
@PersistenceUnit(name = "java:app/env/blogUnit", unitName = "blog")
public class RestApplication extends Application {
}@PersistenceUnit on a class declares the reference java:app/env/blogUnit to the blog unit. The java:app namespace is shared by the whole application, so declaring the reference once is enough.
Built-in methods
| Method | From | What it does |
|---|---|---|
findById(id) | BasicRepository | Finds by key, returns an Optional |
findAll() | BasicRepository | All rows as a Stream |
save(post) | BasicRepository | Inserts a new one or updates an existing one |
deleteById(id), delete(post) | BasicRepository | Deletes |
insert(post) | CrudRepository | Insert only, an error for an existing one |
update(post) | CrudRepository | Update only |
CrudRepository extends BasicRepository. If you do not need separate insert and update, extend BasicRepository.
Injecting it into a resource
To keep the existing API intact, we add a second version next to it — /api/v2/posts. Create src/main/java/com/example/blog/PostV2Resource.java:
// src/main/java/com/example/blog/PostV2Resource.java
package com.example.blog;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.time.LocalDate;
import java.util.List;
@Path("/v2/posts")
@Produces(MediaType.APPLICATION_JSON)
public class PostV2Resource {
@Inject
Posts posts;
@GET
public List<Post> list() {
return posts.findAll().toList();
}
@GET
@Path("/{id}")
public Post show(@PathParam("id") long id) {
return posts.findById(id).orElseThrow(() -> new PostNotFoundException(id));
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Post create(@Valid NewPost data) {
return posts.insert(new Post(data.title(), data.body(), LocalDate.now()));
}
}The repository is injected with @Inject, like any bean. Writing methods run in a transaction on their own, and inside a @Transactional method they join the one already open.
Trying it out
curl http://localhost:8080/blog/api/v2/posts
curl -i -H "Content-Type: application/json" -d '{"title":"Jakarta Data","body":"Repositories without boilerplate."}' http://localhost:8080/blog/api/v2/posts
curl http://localhost:8080/blog/api/posts/3[{"body":"The first post of the blog.","createdAt":"2026-10-01","id":1,"title":"Hello, Jakarta EE"},{"body":"How a request reaches our code.","createdAt":"2026-10-02","id":2,"title":"Servlets and filters"}]
HTTP/1.1 200 OK
X-Powered-By: Blog on Jakarta EE
Content-Type: application/json
Content-Length: 99
{"body":"Repositories without boilerplate.","createdAt":"2026-10-04","id":3,"title":"Jakarta Data"}
{"body":"Repositories without boilerplate.","createdAt":"2026-10-04","id":3,"title":"Jakarta Data"}
A post created through the repository is also visible in the old /api/posts API: both work with the same table. A request to /api/v2/posts/99 returns 404 with {"error":"Post 99 not found","status":404} — the PostNotFoundException mapper from the REST chapter handles it.
Common mistake
Writing @Repository without dataStore. Then the repository looks for the default data source java:comp/DefaultDataSource, which our server.xml does not have, and the very first request fails with a 500 error:
CWWKD1077E: The blog#blog.war application artifact has one or more repository interfaces (com.example.blog.Posts) that use or default to the java:comp/DefaultDataSource data store. This data store must be configured in the server configuration by defining a dataSource element that has an identifier of DefaultDataSource.
Set dataStore to the persistence unit reference as shown above, or define a data source with id="DefaultDataSource" in server.xml.