My previous article, was about running a JakartaEE/MicroProfile application with minimum changes. My purpose was to keep the Java code as standard as possible so that it can keep running on other implementations such as OpenLiberty, Payara, KumuluzEE, TomEE.

This article proposes an alternative: how to optimize your code for Quarkus? It turns out that Quarkus offers optimizations that simplify both code and configuration at the expense of portability. Let’s look at this in detail.
Technical context
The technical context of this article has changed a little compared to my previous article:
- Upgrade to Quarkus 1.2.1.Final (vs 1.0.1.Final)
- Upgrade to GraalVM Community Edition 19.3.1 with Java 11 support (vs 19.2.1 with Java 8 support)
- Upgrade to AdoptOpenJDK 11.0.6 (vs 11.0.5)
- Use of JAX-RS, JPA, CDI, MicroProfile Config, Health, Fault Tolerance, RestClient, and MicroProfile Messaging on top of Kafka (new)
- PostgreSQL database (instead of H2)
- CDI discovery mode set to annotated
- Use of Lombok to avoid boilerplate code.
The level complexity of the demo application used here can be qualified as simple but not negligible. Somewhere in between a “Hello World” and a production-ready application. If you want to know more about it, you can view the video of my Touraine Tech 2020 talk (in French).
Configuration optimization
Quarkus enables to centralize the configuration in a single application.properties file. So persistence.xml and microprofile-config.properties can be removed and their parameters transferred.
Removing persistence.xml
With a traditional JakartaEE application, JPA persistence is configured in a dedicated persistence.xml file. For instance:
<persistence-unit name="myUnitName" transaction-type="JTA">
<properties>
<property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQL95Dialect"/>
<property name="hibernate.show_sql" value="false"/>
<property name="hibernate.format_sql" value="false"/>
<property name="javax.persistence.schema-generation.database.action" value="drop-and-create"/>
<property name="javax.persistence.sql-load-script-source" value="META-INF/load.sql"/>
</properties>
</persistence-unit>
By the way, note that Quarkus has some JPA limitations:
- no jta-data-source support for configuring the datasource, Quarkus is limited to the default (and unnamed) datasource
- no provider support for configuring the JPA provider, Quarkus relies on Hibernate and does not enable to switch to EclipseLink or OpenJPA.
Refactoring persistence.xml in application.properties is rather concise:
quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.log.sql=true
As you can see, declaring the dialect is no longer required. It seems that Quarkus needs the dialect to be configured in persistence.xml but not in applications.properties.
I’ve encountered an issue while removing persistence.xml: I’ve not found any way to configure the JPA unit name in application.properties. In persistence.xml this can be configured with the persistent-unit element. There is no equivalent in application.properties: it seems that only the default (and unnamed) JPA persistence unit is supported. This is an important limitation which doesn’t fit the needs of complex applications.
Merging microprofile-config.properties with application.properties
With a traditional MicroProfile application, configuration is stored in microprofile-config.properties. I’ve copy-pasted the whole content to application.properties “as is” without any problem.
Removing reflection configuration file
Quarkus needs to identify classes subject to reflection and does it best to do automatically with code analysis. However, it sometimes needs help.
There are two ways to explicitly declare classes subject to reflection:
- in the code with the @RegisterForReflection annotation
- in a configuration file such as:
[
{
"name":"my.class.path.Here",
"allDeclaredConstructors":true,
"allPublicConstructors":true,
"allDeclaredMethods":true,
"allPublicMethods":true,
"allDeclaredFields":true,
"allPublicFields":true
}
]
This file must be declared in application.properties to generate the native image:
quarkus.native.additional-build-args=-H:ReflectionConfigurationFiles=reflection-config.json
The @RegisterForReflection annotation is not standard and using a configuration file makes sense to preserve code portability. For this article, @RegisterForReflection has been used since we’re no longer concerned with portability.
Code optimization
Removing JAX-RS application class
With a traditional JakartaEE application, a JAX-RS Application class is needed to define the ApplicationPath. For instance:
@ApplicationPath("api")
public class JAXRSConfiguration extends Application {}
This is supported by Quarkus, but not required. To simplify the code, I’ve removed it and replaced it by the following parameter in order to keep the same URL for the REST endpoints:
quarkus.resteasy.path=/my/restapi/path
Removing useless @Inject annotations
As described in Context and Dependency Injection, Quarkus doesn’t need @Inject with @ConfigProperty. This is true for all other CDI stereotypes.
So:
@Inject
@ConfigProperty(name = "my.param.name")
int myParam;
Can be simplified by:
@ConfigProperty(name = "my.param.name")
int myParam;
Removing private qualifier on CDI injected fields
Quarkus recommends not to use private members in their beans due to a GraalVM limitation:
- Quarkus DI uses introspection to access private fields
- GraalVM supports reflection, but all relevant properties must be registered for reflection explicitly. Moreover, those registrations result in a bigger native executable.
Removing private qualifier means switching to the Java default package access. Purists may say it’s a violation of the encapsulation principle. In reality, it’s not such a big deal with an appropriate package structure.
Using ORM with Panache
This change has been the most impactful. Panache provides an opinionated way to use JPA with strong consequences on the code.
To enable it, quarks-hibernate-orm-panache dependency must be declared instead of quarks-hibernate-orm in pom.xml.
Extending PanacheEntity
JPA entities must extend PanacheEntity:
@Entity
public class Payment extends PanacheEntity { ... }
Another option, not used here, is to extend PanacheEntityBase in order to explicitly control the definition of the JPA id.
Removing JPA id definition
Extending PanacheEntity provides a default id definition annotated with @GeneratedValue. In my initial code, the id was explicitly defined with an IDENTITY strategy:
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
So the id value was provided by the database itself. With @GeneratedValue, a SEQUENCE strategy is implicitly used and that makes a difference when importing initial data at application startup: the id values must be provided in the INSERT SQL statements. I had to change my import SQL statements accordingly.
Making JPA Entity fields public
This is probably the less intuitive change for a traditional Java developer: to simplify the code, Panache recommends to declare Entity fields as public. For instance:
@Entity
public class PosRef extends PanacheEntity {
@Column(unique = true)
public String posId;
public String location;
public boolean active;
}
This seems a major violation of the principle of encapsulation! Don’t panic, under the hood Quarkus makes use of field access rewrite: it generates accessors and rewrites every field access to use them. Explicit accessors can also be provided for specific needs.
Replacing JPA queries by method calls on Panache Entities
Panache recommends to implement queries as static methods on JPA entities. Several convenient default methods are provided by default: listAll(), count(), findById(), persist() … And specific ones can be coded at will.
A few words about performance
Since performance is a key topic of Quarkus, we cannot terminate this article without mentioning some figures.
In the following tab are measured some relevant
Startup time
In the following tab, we measure the same application using 3 different technologies:
- A traditional JakartaEE/MicroProfile/JVM stack
- Quarkus in JVM mode
- Quarkus in native mode
As in my previous articles, startup time, memory footprint and response times of the 2 first transactions are measured to differentiate them.
Traditional stack | Quarkus JVM | Quarkus Native | |
Startup time | 4,6s | 1,8s | 0,26s |
Memory footprint after startup (RSS) | 261Mb | 113Mb | 41Mb |
Response time of 2 first request | 87ms;11ms | 142ms;16ms | 19ms;9ms |
In terms of trends, these results are very comparable to my previous article. We can observe that Quarkus performance promises are met in terms of start-up and response time. So optimizing the code does not decrease the performance.
But keep in mind that these figures are not indicative of a real system in production. In particular, scalability and robustness are not measured (maybe another article).
FYI, building a native image is a heavy process, it took more than 13 minutes and all resources of my laptop for a modest application.
Conclusion
In the end, we end up with a significantly simplified configuration and code:
- 3 configuration files removed: persistence.xml (JPA configuration moved to application.properties), microprofile-config.properties (merged in application.properties), reflection-config.json (replaced by @RegisterForReflection)
- concise JPA configuration (just 2 lines)
- one @ApplicationPath class file removed (replaced by a parameter)
- about 10 @Inject annotations removed
- about 10 private qualifiers removed
- about 20 accessors removed in JPA entities
- about 6 EntityManager injections removed
- about 10 JPA NamedQueries definitions removed
Clearly, Panache offers a simplified way to use JPA: most usual queries are pre-defined and there is no longer need for Repository/DAO classes, query definitions, use of the EntityManager.
One other big factor of simplification, already mentioned in my previous article is the ease of test.
To conclude, if code and configuration portability is not a concern, Quarkus can simplify your life!
Hope this help …
Leave a Reply