Quarkus Features for Service Diagnostics and Certificate Renewal
The patterns behind modern Java. From Quarkus to AI inside the JVM
Write boring code. Ship exciting systems.
Quarkus Features for Service Diagnostics and Certificate Renewal
See where existing framework capabilities can reduce custom application code, with tested examples and clear deployment limits.
Y’all liked my first hidden Quarkus features article apparently. I followed it with another collection around Quarkus 3.31 and 3.32, and, well, I had enough left for another article. When I went back through what I’ve already covered here, I found smaller features that fit into those same applications. We’ve spent time on REST-client timeouts and retries, for example. But when a request fails, there is another setting that can help you find the code that made the call.
You have the exception, and you know which remote service was involved. You still need to find the place in your own application that started the request, though, and the exception may leave you reading through quite a bit of client code. Before adding another wrapper around the client, you could enable a setting that captures that call site for you. Start there, with the client that is already giving you trouble, and see whether the extra information makes the failure easier to understand.
You can try these examples in an existing Quarkus application. I’ve linked the earlier Main Thread tutorials where they give you the rest of the application or explain the setup in more detail. Several of the examples only need a change in application.properties, so you can also try one without following the whole article from beginning to end.
The examples were checked with Quarkus 3.39.5 on Temurin JDK 25, using packaged JVM applications. The certificate check used a replacement certificate with the same private key.
1. Let a failed REST call take you back to your code
In my REST-client article on timeouts, retries, and redaction, I used a carrier service for the outbound HTTP calls. You ask for tracking information, the carrier takes too long or returns an error, and your service has to decide what to send back to its caller. That already gives you a few things to configure. You need to decide how long to wait and whether to try again before you have much reason to look at the stack trace.
Now suppose several parts of your application use that client. When one call fails, you want to know which part made it. Quarkus can capture the stack when you call the client method and use it if the call throws an exception. You can enable that for the client you are checking:
quarkus.rest-client.carrier-api.capture-stacktrace=trueThe carrier-api part comes from the interface’s @RegisterRestClient(configKey = "carrier-api"). I’ve kept the configuration name from the earlier article, so this is one more property for that same client. The setting belongs to the current Quarkus REST Client. The REST Client reference describes this property and the global setting if you want to enable it for all clients.
Start with one failing call and compare the exception before and after enabling this. Keep the exception cause and any suppressed exceptions when you log it, so the extra information is there for you to read. Capturing stacks has a cost, and the setting is disabled by default. Starting with one client lets you check both the information you get and the effect on your application before deciding whether to leave it enabled.
And yes, you still need the timeouts and error handling from the earlier tutorial. Those settings control how your application handles the request. With this extra setting, you can also follow a failure back to the code that made it.
2. A call inside the same bean can still be intercepted
Suppose you have a service with a public method that calls another method on the same object. The second method has @Transactional on it. If you’ve used a framework where that internal call skips interception, you may already be thinking about moving the method into another bean. Then you could inject that bean and make the call through a proxy.
Quarkus supports intercepted self-invocation, so that internal call can still run through the transaction interceptor. ArC, the CDI implementation in Quarkus, can apply an interceptor when a managed bean calls one of its own intercepted methods. The CDI reference describes this as a Quarkus feature. The CDI specification itself does not guarantee this behavior.
We can check for an active transaction with a small class. It needs the Narayana JTA extension, which is normally already present in an application using Hibernate ORM. If your application doesn’t have it, run quarkus extension add narayana-jta from the module directory to add it.
package org.acme.delivery;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.transaction.Status;
import jakarta.transaction.Transactional;
import io.quarkus.narayana.jta.QuarkusTransaction;
@ApplicationScoped
public class TransactionProbe {
public boolean check() {
return insideTransaction();
}
@Transactional
boolean insideTransaction() {
return QuarkusTransaction.getStatus() == Status.STATUS_ACTIVE;
}
}Inject TransactionProbe into a resource or a Quarkus test and call check() without an existing transaction. The expected result is true, because the call to insideTransaction() triggers the transaction interceptor. I kept the example to a status check so you can try this without adding an entity and a database. We only need to know whether a transaction is active inside that second method. The Quarkus transaction API gives us that status.
Use the CDI-managed instance and keep the intercepted method non-private. If you create it with new TransactionProbe(), the container won’t provide the same behavior. You also need to account for this Quarkus-specific feature if the code must run on another CDI implementation. Within a Quarkus application, though, you can keep the two methods in one bean when that fits your design. You don’t need a second bean just to have this call intercepted.
3. Choose an implementation when you start the application
I’ve already written about using qualifiers when several beans implement the same interface. With a qualifier, you say which implementation you want at an injection point. Sometimes you want to make that choice in the deployment configuration instead. Let’s stay with the carriers for a moment. Your application supports two of them, but a particular installation only uses one. You can include both implementations in the package and choose which one to get with a setting supplied at startup.
Quarkus provides @LookupIfProperty for this. The annotation controls whether a bean is available for programmatic lookup through Instance<T>. You can keep these types together in DeliveryRouter.java, in the package shown below. The two implementations just return their names, which lets you see which one was chosen without making a request to a carrier:
package org.acme.delivery;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Instance;
import io.quarkus.arc.lookup.LookupIfProperty;
interface Carrier {
String name();
}
@ApplicationScoped
@LookupIfProperty(name = "delivery.carrier", stringValue = "standard")
class StandardCarrier implements Carrier {
@Override
public String name() {
return "standard";
}
}
@ApplicationScoped
@LookupIfProperty(name = "delivery.carrier", stringValue = "express")
class ExpressCarrier implements Carrier {
@Override
public String name() {
return "express";
}
}
@ApplicationScoped
public class DeliveryRouter {
private final Instance<Carrier> carriers;
public DeliveryRouter(Instance<Carrier> carriers) {
this.carriers = carriers;
}
public String selectedCarrier() {
return carriers.get().name();
}
}Set delivery.carrier=standard in application.properties, then inject DeliveryRouter where you need to use it. You can choose the other implementation when starting the packaged application:
java -Ddelivery.carrier=express -jar target/quarkus-app/quarkus-run.jarThe constructor receives Instance<Carrier>. If you inject Carrier directly, you still have an ambiguous injection point, because these annotations apply when you get a bean through programmatic lookup. They don’t remove the other implementation from ordinary bean resolution. And for get() to return a bean, you need exactly one matching candidate. An unknown value gives this example no candidate, while two matching implementations would make the lookup ambiguous.
So, set the carrier property explicitly and check the selection in a startup test. Quarkus also has @LookupUnlessProperty for the inverse condition, described in the programmatic lookup section of the CDI reference. For this application, choosing a carrier when the process starts is enough. If you need to switch while it is running, you also need to decide how configuration changes and the lifecycle of the selected beans should work.
4. Find out which configuration your build actually used
In the configuration tutorial, I spent quite a bit of time on where values come from. Once you use profiles and deployment settings, the values in application.properties only tell you part of it. This gets harder when somebody else built the application. You may have the same source code and still need to find out which values Quarkus actually read during that build.
Quarkus can write those values to a report. Enable it before building:
quarkus.config-tracking.enabled=trueRun your normal production build from the application module, for example ./mvnw package in a Maven project. By default, Quarkus writes the configuration properties it reads and their values to:
.quarkus/quarkus-prod-config-dumpKeep this report with the build it describes. If a later build behaves differently, you can compare the values as well as the source revision and dependency versions. You can also use the Maven plugin’s track-config-changes goal to check values against an earlier report. The configuration reference explains how that works and how to change the report location.
Of course, you should check what is in that file before sharing it with the build. Quarkus lets you exclude selected properties with quarkus.config-tracking.exclude, and you can also have selected values hashed. Review the report before publishing or committing it. It contains configuration read during the build, so you still need runtime diagnostics to check a value supplied to an already-built application.
When two builds from same commit behave differently, check this report early. You can open the files and compare what Quarkus read, even if nobody remembers exactly how the build environment was configured last week.
5. Give health and metrics their own listener
You probably add health endpoints fairly early. You add the extension, open /q/health, and then continue with the application. Later, you put the service behind an ingress or reverse proxy and need to decide how people and other systems can reach it. That includes deciding whether the operational endpoints should be available through the same route as the business API.
Quarkus can serve management endpoints through a separate HTTP server. If your local application already has smallrye-health, you can start with these settings:
quarkus.management.enabled=true
quarkus.management.host=localhost
quarkus.management.port=9000Enable it before packaging, because quarkus.management.enabled is a build-time setting. Start the application, then check readiness on the management listener:
curl http://localhost:9000/q/health/readyThe business API can continue on port 8080 while health uses port 9000. If you have quarkus-micrometer-registry-prometheus installed, its /q/metrics endpoint also uses the separate server. The management interface guide lists the endpoints that support this and explains how to configure their paths and security.
I used localhost here so the local example listens on loopback. For a container deployment, you need an address that the probes and monitoring system can reach, along with the appropriate network rules. Keep the management port out of a public service unless you intend to expose it, and configure management authentication when required. A separate port by itself does not make the endpoint private.
You’ll need to update the deployment configuration as well. The readiness probe may still call the old port, and the monitoring configuration may still expect metrics there. Change those callers to use the management listener. Then you can decide separately who can reach the business API and who can reach the operational endpoints.
6. Pick up a replacement certificate without restarting
In my earlier TLS article, I covered how to configure secure connections. The mTLS follow-up then covered how to decide which clients should be allowed in. Once that is running, you also need a way to replace the certificate. Your certificate-management system may already have written the renewed certificate to disk, but the running server still needs to start using it.
The Quarkus TLS registry can reload certificates at a configured interval. If you already have a valid certificate and its matching private key at the paths below, you can give the HTTP server this named TLS configuration:
quarkus.tls.http.key-store.pem.0.cert=tls/server.crt
quarkus.tls.http.key-store.pem.0.key=tls/server.key
quarkus.tls.http.reload-period=30s
quarkus.http.tls-configuration-name=httpHere http is the configuration name. The last property tells the HTTP server to use that configuration. The reload period tells the registry when to reload the files, after which the registry sends a certificate-update event that the Quarkus HTTP server handles. Supported clients, including Quarkus REST Client, can also handle these updates when you configure them through the registry. The TLS registry reference describes which components support this.
With thirty seconds, you don’t have to wait very long to check a replacement locally. Choose an interval that fits your deployment when you configure the service there. Your certificate-management system still has to get and install the new certificate, and it needs to publish the certificate and key together so a reload cannot read a mismatched pair.
Check this with a packaged application, because dev mode has additional behavior when files change. First, note its process ID and the certificate serial you get on a fresh TLS connection. Replace the files through your certificate deployment mechanism, wait for a reload interval, and open another connection. You want to see the replacement certificate while the same process is still running. Existing TLS sessions don’t get a new certificate just because the files changed.
A reload in dev mode could make it look as though you’ve checked what will happen after deployment. With the packaged service, you can check that the certificate changes while the process keeps running. Once you’ve verified that, you can use certificate renewal without having to restart the process for each replacement.
7. Re-run augmentation after distributing the application
I’ve covered JAR tree shaking and Leyden AOT caching here already. This time, suppose you distribute a Quarkus application for somebody else to install. They need to change a setting that was fixed during the build, and they already have your packaged application. Quarkus has a packaging option for that situation too.
By the time somebody runs the JAR, a build-time setting has normally already been used to prepare the application. Quarkus calls that phase augmentation. It generates code and other output using the extensions and configuration available during the build. You can run that phase again with different build-time configuration if the package still contains the required deployment dependencies. This is called re-augmentation.
Choose mutable-jar to keep those dependencies in the package. From an existing Maven module, run:
./mvnw package -Dquarkus.package.jar.type=mutable-jarDistribute the whole target/quarkus-app directory, including lib/deployment, and keep its name and layout. Renaming the copied directory broke re-augmentation on Quarkus 3.39.5 because Quarkus still looked for files under quarkus-app. From the distributed quarkus-app directory, you can run re-augmentation with:
java -Dquarkus.launch.rebuild=true -jar quarkus-run.jarSupply the changed build-time settings to this command through the appropriate configuration source. Quarkus generates the augmentation output again and exits. Then start the application separately with java -jar quarkus-run.jar. The re-augmentation guide shows this with a database-driver example. Both required driver extensions must already be included in the original build for that example to work.
Now, keeping those deployment dependencies makes the package larger. The installer also needs a writable location for the generated output. Re-augmentation applies to JVM applications; you can’t use it to change an existing native executable. Check the configuration included in the package as well. The guide warns that Quarkus properties supplied through build-tool configuration can be kept in the package, including sensitive values.
Consider this for a product where you don’t know all the required build-time settings when you release it. You can compile the source and distribute the application, while the person installing it makes those remaining choices and runs augmentation again. If you control both the build and the deployment of a service, a normal rebuild may be easier to manage. It depends on who needs to make the configuration choice and when they can make it.
Before adding another helper
The carrier client from the earlier tutorial already has its timeout policy. You can now add the call-site information and check whether it helps you understand a failed request. The same goes for the TLS example: once the secure connection works, you can check how the running process starts using a replacement certificate. Starting with an application you already know makes these smaller changes easier to follow, because you have something to compare them with.
If you only try one, choose a problem you already have in your own repository. Enable call-site capture for the client that is difficult to debug and compare the next failure with what you could see before. Then you can decide whether you still need that extra wrapper you were about to write. There may be a good reason to keep it, of course. But check what Quarkus already gives you before taking on more code to maintain.
Thanks for reading The Main Thread! Subscribe for free to receive new posts and support my work.
Thank you for reading.
If this piece was useful, sharing it is the simplest way to support the work and help others find it.
Thoughts and corrections are always welcome. I read every comment.
If this email was forwarded to you, you can subscribe to receive future issues directly.
Write boring code. Ship exciting systems.
© 2026 Markus Eisele
🇩🇪