Updated September 30, 2026: revised the Spring comparison, replaced the setup example, and added a migration checklist.
You’ve built services with Spring, wired beans, and added configuration. You can use much of this with Quarkus too, including what you know about dependency injection, transactions, HTTP, and running Java applications.
With Quarkus, some things happen differently from what you’re used to. The framework finds beans during the build, for example. You also need to check which settings can change when the application starts and which thread runs a request.
If your Spring service takes too long to become ready, I’d start with one endpoint. Measure startup and a request that does something the service normally does. Then try the same in Quarkus, with the same CPU and memory limits. You can decide from there whether a bigger migration is worth it. Even if you stay with Spring, you’ll know more about how your application runs.
Build time becomes part of the application model
Quarkus does quite a bit during the build. ArC, its CDI container, finds beans and checks injection points at this stage. If it can’t find a bean for an ordinary injection point, the build can fail before you deploy anything. You get this with a regular JVM application too. You don’t need a native executable for it. The CDI integration guide explains the steps.
This needs some attention if your application finds plugins or changes its set of beans at runtime. Have a look at what the framework needs to know during the build. Then check what you can still change when you start the application.
Spring also has ahead-of-time processing on the JVM. With Spring AOT, there are limits on changing bean definitions and the classpath at runtime. So be clear about what you’re comparing. A normal Spring Boot start, Spring AOT, Quarkus on the JVM, and a native executable are different ways to run an application.
Of course, your database connections, HTTP requests, and scheduled jobs still run when the application runs. Checking injection points during the build won’t tell you whether the database password is correct when you deploy.
For your comparison, measure from starting the process to getting a successful response from a request your service normally handles. Check process memory with the same load and resource limits too. And include the build time. Doing more during the build can help startup, but you also have to wait for that build to finish. How often you build and deploy is part of the decision.
Quarkus performance on the JVM
There are numbers you can look at already. In Holly Cummins’s March 2, 2026 benchmark post, the lightly tuned JVM comparison used Quarkus 3.31.3, Spring Boot 4.0.2, and Java 25.0.2. Quarkus handled about 19,100 transactions per second, compared with 7,100 for Spring Boot. Time from startup to the first response was about 2.9 seconds versus 6.7 seconds. Process memory (RSS) after the first request was 267 MiB versus 576 MiB.
That’s about 2.7 times the throughput for this JPA/Hibernate application. The application and benchmark scripts are public, so you can check the setup and try it yourself. I’d take these results as a reason to test Quarkus with your own service. They don’t promise the same improvement for every application.
Compare the whole development workflow
With Quarkus dev mode, you get live reload, Dev UI, and continuous testing in the same place. Add an extension that supports Dev Services, and Quarkus can also start and configure a database for development and tests. Most of these services need a working container runtime. Use Podman for the examples that need containers.
Spring has tools for this too. DevTools can restart the application when compiled files on the classpath change. There is also support for Testcontainers service connections. Include these when you compare the two frameworks.
I’d try something you do every day. Change a resource method, change a setting, and run the tests for it. See how much you have to do yourself and how long you wait for the result. You can then explain why you prefer one setup for your project.
Let’s try this with a small application. We only need an endpoint and some configuration, so you can run it without a database or container runtime.
Try one endpoint in dev mode and after packaging
For this example, we’re using Quarkus 3.39.1, Quarkus CLI 3.39.1, and JDK 25. Select JDK 25 and set JAVA_HOME so the build uses it. You’ll also need the Quarkus CLI to create the project and curl to send a request. The project comes with a Maven wrapper, so that takes care of Maven. The commands below use macOS/Linux shell syntax.
First, create an application with Quarkus REST. We use --no-code because we’ll add the two Java files ourselves:
quarkus create app com.example:hello-quarkus \
--platform-bom=3.39.1 \
--java=25 \
--extension=rest \
--no-code \
--no-dockerfiles
cd hello-quarkusCreate src/main/java/com/example/GreetingConfig.java. Create the directories too if they’re missing:
package com.example;
import io.smallrye.config.ConfigMapping;
@ConfigMapping(prefix = "greeting")
public interface GreetingConfig {
String message();
}With @ConfigMapping, you keep related settings in an interface. Our message() method reads greeting.message. You can add more typed values and nested groups as you need them. If you’ve used Spring’s @ConfigurationProperties, the idea should feel familiar. Both frameworks support typed configuration.
Create src/main/java/com/example/GreetingResource.java:
package com.example;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@ApplicationScoped
@Path("/hello")
public class GreetingResource {
private final GreetingConfig config;
public GreetingResource(GreetingConfig config) {
this.config = config;
}
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return config.message();
}
}There is only one constructor, so Quarkus uses it to inject our configuration. The Jakarta REST annotations give us an endpoint that returns plain text.
Put these values in src/main/resources/application.properties:
greeting.message=Hello from Quarkus
%dev.greeting.message=Hello from dev modeThe %dev value is used when the dev profile is active. It overrides the value above it. Quarkus also has rules for profiles and for which configuration source takes priority. You can find these in the configuration reference.
Start dev mode from the project directory:
./mvnw quarkus:devIn another terminal, call the endpoint:
curl -i http://localhost:8080/helloYou should get HTTP 200 with Hello from dev mode in the body. Now change the %dev value to Hello after editing, save the file, and send the request again. Once live reload has finished, you should get Hello after editing. If you still see the old value, give it a moment and repeat the request. You can also open Dev UI to look at the application.
Now stop dev mode with Ctrl+C. In the project directory, package and run the application:
./mvnw package
java -jar target/quarkus-app/quarkus-run.jarSend the same request again. This time you should get Hello from Quarkus. The packaged application uses the production profile by default, so our %dev value no longer applies. If you copy this default fast-jar package to another machine, take the whole target/quarkus-app directory. The runner JAR needs the other files in that directory.
Stop the process with Ctrl+C, then set the message when you start it again:
java -Dgreeting.message="Hello from deployment config" \
-jar target/quarkus-app/quarkus-run.jarYou should now get Hello from deployment config. Stop the application with Ctrl+C when you’re done.
We changed our own setting between runs without building the application again. Some Quarkus framework settings are fixed during the build, though. Check the documented phase for the setting you want to change, and try it with the packaged application too. A change that works in dev mode doesn’t tell you how it will work after packaging.
A reactive return type changes where your code runs
You can write both imperative and reactive resource methods with Quarkus REST. You do need to know which thread runs the method, because the return type can change that.
Our method returns a String, so it runs on a worker thread by default. A method returning Mutiny’s Uni normally runs on an event-loop thread. Annotations such as @Blocking, @NonBlocking, and @Transactional can change those defaults.
But returning a Uni doesn’t make your database driver or HTTP client nonblocking. If your Uni pipeline makes a blocking call on an event loop, other requests using that thread may have to wait. Follow the calls from your endpoint into the libraries it uses before you choose how to run it.
For the first migration, I’d keep an imperative endpoint as it is. Get the same behavior working in Quarkus first. You can then try reactive APIs or virtual threads with the load your application needs to handle.
Choose native compilation for a measured requirement
You can run Quarkus on a regular JVM, just as we did above. You still get its build-time processing. There is no need to set up a native-image toolchain before you can try Quarkus.
If cold starts or memory limits are a problem for your service, have a look at native executables. You’ll also need to look at the build cost, support for your libraries, and how you’ll find problems when the application runs. Reflection, resources loaded from the classpath, and classes loaded dynamically can need extra attention. The native-image guide explains what you need and how to build one.
Measure startup separately from how the application runs after it has been serving requests for a while. A service running for days has different needs from a short command or a function that often starts cold. You’ll need to try your own application to see the result. Our hello endpoint and a successful native build can’t tell you that.
A migration checklist you can use on one service
Start with a small service, or one endpoint you can test on its own. Before you move more of the application, go through these checks:
Write down the reason to change. Decide what you want to improve and measure it first. This can be time until the service is ready, memory under the expected load, or time to check a code change. Also think about what it costs your team to maintain another framework.
Capture the existing contract. Write down request and response bodies, status codes, validation errors, authentication, and authorization. Run the same checks from outside both applications. Include requests that should fail too.
Inventory Spring-specific behavior. Check security configuration, transaction boundaries, event listeners, scheduled jobs, and custom bean registration. Similar annotation names don’t mean the behavior is the same. Quarkus has selected Spring API compatibility extensions. Check what they support before you depend on them.
Separate build and deployment settings. Find the values you need to change between environments. Try those changes with the packaged application, including credentials and URLs for external services.
Follow the blocking calls. Check how each endpoint calls the database and other HTTP services. Make sure blocking calls run where blocking is allowed. Check transaction behavior before you change how requests run concurrently.
Exercise deployment and rollback. Check health and readiness, graceful shutdown, logs, metrics, and traces where you plan to deploy. Keep a way to send traffic back to the existing application. Try native compilation separately if it could help with your target.
If your service depends on many Spring integrations and already runs as you need it, there may be little reason to change framework. A new service with fewer dependencies can be much easier for a first try with Quarkus. You can use the same checklist to decide either way.
You can also use an AI coding agent with the Quarkus community’s Spring Boot to Quarkus migration skill. It tells the agent to look at your project and helps you choose between Spring compatibility extensions and a full move to Quarkus APIs. The agent must compile after each migration module that applies, then check the build, tests, and application startup. I’d give it the same contract checks from our checklist and use those to review the changes.
If you want to continue, try the Quarkus getting started guide and its Dev Services walkthrough. For more about keeping an API working for existing clients, have a look at Versioning APIs in Quarkus.
I’d encourage you to try Quarkus even if you’re quite happy with Spring. Take one endpoint from the build to a running application, change it, and see what happens. You’ll know more about how the framework builds and runs your code, and how much you have to do when you change it. Then you can decide what fits your application based on something you’ve tried yourself.




Its very opinionated. Though the Reactive stack support is fabulous in Quarkus, Spring has its own vibe and aura. Both constructs are good for developing Production grade apps. The caveat is Spring is way to matured with way bigger community. Quarkus and Spring shine in some form based on the needs. I like the config management of Quarkus and Spring overall developer experience seems natural.
Spring brought in modulith/API Versioning simplification/Native Executables/Spring AI etc. in 2025 so its a tough competition both frameworks to solve for its developer community.