With Helidon 4.4.0 right around the corner, I’ve been spending some time playing with the latest milestone release (4.4.0-M2). If you haven’t been following along, Helidon 4 was a major milestone because it was the first framework built from the ground up on Java 21 virtual threads.
Now, with 4.4, we are seeing some really cool incubating features becoming more stable inlcuding Helidon Data Repositories, Helidon SE Declarative and Helidon AI. I am working on a sample project called Helidon-Eats to show how these work together. In this installment, I am looking at the first two.
The “No-Magic” Power of Helidon SE Declarative
If you’ve used Helidon MP (MicroProfile), or if you are more familiar with Spring Boot’s Inversion of Control approach (like me), then you’re used to the convenience of dependency injection and annotations. Helidon SE, on the other hand, has always focused on transparency and avoiding “magic.” While that’s great for performance, it usually meant writing more boilerplate code to register routes and manage services manually.
Helidon SE Declarative changes that. It gives you an annotation-driven model like MP, but here is the trick: it does everything at build-time. Using Java annotation processors, Helidon generates service descriptors during compilation. This means you get the clean, injectable code you want, but without the runtime reflection overhead that slows down startup and eats memory. Benchmarks have even shown performance gains of up to +295% over traditional reflection-based models on modern JDKs. (see this article)
Now, to be completely fair, I will say that I am not completely sold on the build-time piece yet. For example, the @GenerateBinding annotation (if I am not mistaken) causes an ApplicationBinding class to be generated at build time, and that lives in your target/classes directory. I found during refactoring that you have to be careful to mvn clean each time to make sure it stays in sync with your code, just doing a mvn compile or package could get you into trouble. And I am not sure I am happy with it not being checked into the source code repository. But, I’ll withhold judegment until I have worked with it a bit more!
Simplifying Persistence with Helidon Data
The Helidon Data Repository is another big addition. It’s a high-level abstraction that acts as a compile-time alternative to heavy runtime ORM frameworks. Instead of writing JDBC code, you define a Java interface, and Helidon’s annotation processor generates the implementation for you.
It supports standard patterns like CrudRepository and PageableRepository, which I used in this project to handle the recipe collection. The framework can even derive queries directly from your method names (like Spring Data does) – so a method like findById is automatically turned into the correct SQL at build-time.
The Backend: Oracle AI Database 26ai
For this sample, I’m using Oracle AI Database 26ai Free. I sourced some public domain recipe data from Kaggle that comes as a line-by-line JSON file (LDJSON).
Normally, if you want to store hierarchical JSON in relational tables, you have to write complex mapping logic in your application. But I wanted to try a more novel approach using JSON Relational Duality Views (DV).
Duality Views are a game-changer because they decouple how data is stored from how it is accessed. My data is stored in three normalized tables (RECIPE, INGREDIENT, and DIRECTION) which ensures ACID consistency and no data duplication. The database can surface this data to applications as a single, hierarchical JSON document. I am not using that feature in this post, but I will in the future!
GraphQL-Based View Creation and Loading
One of the coolest parts of Oracle AI Database 26ai is that you can define these views using a GraphQL-based syntax . The database engine automatically figures out the joins based on the foreign key relationships.
Here is how I defined the recipe_dv:
CREATE OR REPLACE JSON RELATIONAL DUALITY VIEW recipe_dv AS
recipe @insert @update @delete
{
recipeId: id,
recipeTitle: title,
description: description,
category: category,
subcategory: subcategory,
ingredients: ingredient @insert @update @delete
[
{
id: id,
item: item
}
],
directions: direction @insert @update @delete
[
{
id: id,
step: step
}
]
};
Isn’t that just the cleanest piece of SQL that deals with JSON that you’ve ever seen?
Because the view is “updatable” (@insert, @update), I used it to actually load the data. Instead of a complex ETL process, my startup script just reads the LDJSON file line-by-line and does a simple SQL insert directly into the view. The database engine takes that single JSON object and automatically decomposes it into rows for the three underlying tables.
Modeling the Service
On the Java side, I modeled the Recipe entity to handle the parent-child relationships using standard @OneToMany collections.
One detail I want to highlight is the use of @JsonbTransient. When you build a REST API, you often have internal metadata like database primary keys or sort ordinals that you don’t want messing up the JSON that the end user gets to see. By annotating those fields with @JsonbTransient, they are excluded from the final JSON response. This keeps the API response clean and focused only on the recipe data.
@Entity
@Table(name = "RECIPE")
public class Recipe {
@Id
@Column(name = "ID")
private Long recipeId;
private String recipeTitle;
@JsonbTransient
private Long internalId; // Hidden from the API
@OneToMany(mappedBy = "recipe")
private List<Ingredient> ingredients;
//...
}
In the repository object, you can use the method naming conventions to automatically create queries (like Spring Data) and you can also write your own JPQL (not SQL) queries, as I did in this case (also like Spring Data):
package com.github.markxnelson.helidoneats.recipes.model;
import java.util.Optional;
import io.helidon.data.Data;
@Data.Repository
public interface RecipeRepository extends Data.CrudRepository<Recipe, Integer> {
@Data.Query("SELECT DISTINCT r FROM Recipe r "
+ "LEFT JOIN FETCH r.ingredients "
+ "LEFT JOIN FETCH r.directions "
+ "WHERE r.recipeId = :recipeId")
Optional<Recipe> findByRecipeIdWithDetails(Integer recipeId);
}
Wiring and Startup
The configuration is handled in the application.yaml, where I point Helidon to the Oracle instance using syntax that again is very reminiscent of what I’d do in Spring Boot.
server:
port: 8080
host: 0.0.0.0
app:
greeting: "Hello"
data:
sources:
sql:
- name: "food"
provider.hikari:
username: "food"
password: "Welcome12345##"
url: "jdbc:oracle:thin:@//localhost:1521/freepdb1"
jdbc-driver-class-name: "oracle.jdbc.OracleDriver"
persistence-units:
jakarta:
- name: "recipe"
data-source: "food"
properties:
hibernate.dialect: "org.hibernate.dialect.OracleDialect"
jakarta.persistence.schema-generation.database.action: "none"
With Declarative SE, injecting the repository into my endpoint is simple. I just use @Service.Inject on the constructor, which allows me to keep my fields private final.
@Service.Singleton
@Http.Path("/recipe")
public class RecipeEndpoint {
private final RecipeRepository repository;
@Service.Inject
public RecipeEndpoint(RecipeRepository repository) {
this.repository = repository;
}
@Http.GET
@Http.Path("/{id}")
public Optional<Recipe> getRecipe(Long id) {
return repository.findById(id);
}
}
Finally, the Main class uses @Service.GenerateBinding. This tells the annotation processor to generate the “wiring” code that starts the server and initializes the service registry without needing to scan the classpath at runtime.
@Service.GenerateBinding
public class Main {
public static void main(String args) {
LogConfig.configureRuntime();
ServiceRegistryManager.start(ApplicationBinding.create());
}
}
In this context, the “service registry” is something in Helidon that keeps track of the services in the application and handles injection and so on. It’s a lot like the way Spring Boot scans for beans and wires/injects them where needed.
The Result
When you hit the service, you get a clean, well structured JSON response that masks all the complexity of the underlying three-table relational join.
Example Response for http://localhost:8080/recipe/22387:
{
"category": "Appetizers And Snacks",
"description": "I came up with this rhubarb salsa while trying to figure out what to do with an over-abundance of rhubarb...",
"directions":,
"ingredients": [
"2 cups thinly sliced rhubarb",
"1 small red onion, coarsely chopped",
"3 roma (plum) tomatoes, finely diced"
],
"recipeId": 22387,
"recipeTitle": "Tangy Rhubarb Salsa",
"subcategory": "Salsa"
}
Wrap Up
Helidon 4.4 is making the SE flavor feel a lot more like a high-productivity framework without sacrificing performance. By shifting the data transformation logic to the database with Duality Views and using build-time code generation for injection, we can build services that are both incredibly fast and easy to maintain.
Now, you may have noticed that I said “like Spring” a lot in this post – and that’s because of two things – I do happen to use Spring a lot more than I use Helidon, and I like it. So I am very happy that Helidon is looking more like Spring, it makes it a lot easier to switch between the two, and I think it lowers the barrier to entry for people who are coming from the Spring world.
Grab the code from the Helidon-Eats repo and let me know what you think – and stay tuned for the next steps as I explore Helidon AI!
