Thinking about moving a Spring Boot app to Quarkus 3? You’re not alone. Quarkus has earned a reputation for blazing-fast startup times, low memory usage, and killer developer experience—especially when you go native with GraalVM. In this guide, we’ll walk through a pragmatic migration path from Spring Boot to Quarkus 3, with code examples and tips that help you avoid the potholes.
Why Quarkus 3 is worth your time
- Faster startup and lower RSS: great for containers, FaaS, and high-density microservices
- Native image support out of the box
- Excellent dev experience with hot reload via
quarkus dev
- First-class integration with Jakarta EE 10, MicroProfile, and the CNCF ecosystem
- Spring API compatibility extensions to ease migration
Tip: Treat your migration as a refactor, not a rewrite. Move feature-by-feature, verify, and iterate. You can even run parts side-by-side during the transition.
Prep work: set yourself up for success
Before you touch Quarkus, do a quick health check of your Spring Boot app.
- Align on Java and Jakarta:
- Use Java 17+ (Quarkus 3 and Spring Boot 3 both support it).
- If you’re still on Spring Boot 2 (javax.), upgrade to Spring Boot 3 first to adopt jakarta.. It makes the Quarkus move much smoother.
- Inventory your dependencies:
- Note down web stack (Spring MVC/WebFlux), data access (JPA, JDBC, Mongo), security (Spring Security), messaging, scheduling, etc.
- Identify any heavy runtime proxies/AOP and exotic reflection—these need attention for native images.
- Choose a migration strategy:
- Big bang for small apps.
- Incremental for larger apps: migrate a slice (e.g., a bounded context), keep the API contract stable, and route traffic progressively.
Create a Quarkus 3 baseline project
Use Maven (Gradle is supported too), and start with a clean Quarkus project. You can use the CLI or the Maven plugin.
# Quarkus CLI (recommended)
quarkus create app com.example:hello-quarkus \
-x 'resteasy-reactive,hibernate-orm-panache,jdbc-postgresql'
# Or with Maven directly
mvn io.quarkus.platform:quarkus-maven-plugin:3.8.3:create \
-DprojectGroupId=com.example \
-DprojectArtifactId=hello-quarkus \
-Dextensions="resteasy-reactive,hibernate-orm-panache,jdbc-postgresql"
Make sure your pom.xml
imports the Quarkus platform BOM:
io.quarkus.platform
quarkus-bom
3.8.3
pom
import
Run the dev mode to verify it’s all good:
./mvnw quarkus:dev
Your options: Spring compatibility vs idiomatic Quarkus
Quarkus offers Spring API compatibility extensions to ease migration (e.g., quarkus-spring-di
, quarkus-spring-web
, quarkus-spring-data-jpa
, quarkus-spring-security
). They don’t cover 100% of Spring, but they let you move faster.
- Option A: Add Spring compatibility and keep much of your code as-is while you migrate piece-by-piece.
- Option B: Go idiomatic Quarkus using Jakarta REST (JAX-RS), CDI (ArC), config mappings, and Panache.
You can even mix: start with compatibility, then refactor to idiomatic Quarkus for long-term simplicity.
Add extensions with the CLI:
quarkus ext add spring-di,spring-web,spring-data-jpa,spring-security
Web layer: from @RestController to JAX-RS
A simple Spring Boot controller:
// Spring Boot
@RestController
@RequestMapping("/api/hello")
class HelloController {
@GetMapping
String hello(@RequestParam(defaultValue = "world") String name) {
return "Hello " + name;
}
}
The idiomatic Quarkus equivalent using RESTEasy Reactive (JAX-RS):
// Quarkus JAX-RS
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
@Path("/api/hello")
@Produces(MediaType.TEXT_PLAIN)
public class HelloResource {
@GET
public String hello(@QueryParam("name") @DefaultValue("world") String name) {
return "Hello " + name;
}
}
If you prefer to keep Spring annotations during migration, include quarkus-spring-web
and keep your @RestController
for now. Just remember compatibility isn’t full-featured.
Dependency injection: @Autowired to CDI @Inject
Quarkus uses CDI with build-time injection. Replace @Autowired
with @Inject
and use CDI scopes such as @ApplicationScoped
, @Singleton
, or @RequestScoped
.
// Spring
@Service
public class Greeter {
public String greet(String name) { return "Hello " + name; }
}
// Quarkus
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class Greeter {
public String greet(String name) { return "Hello " + name; }
}
And inject it:
@Inject Greeter greeter;
Rule of thumb: Avoid lazy-initialization assumptions. Quarkus favors build-time wiring and fast startup, so design components to be ready at start.
Configuration: from @ConfigurationProperties to @ConfigMapping
Spring-style configuration:
// Spring
@ConfigurationProperties(prefix = "app")
public class AppConfig {
private String greeting;
// getters/setters
}
Quarkus equivalent using SmallRye Config’s mapping:
// Quarkus
import io.smallrye.config.ConfigMapping;
@ConfigMapping(prefix = "app")
public interface AppConfig {
String greeting();
}
Inject and use it:
@Inject AppConfig config;
...
return config.greeting() + " " + name;
Properties with profiles in Quarkus:
# src/main/resources/application.properties
app.greeting=Hello
quarkus.http.port=8080
%dev.quarkus.http.port=8081
%test.quarkus.http.test-port=0
Prefer YAML? Add the quarkus-config-yaml
extension and use application.yaml
.
Data access: Spring Data JPA to Panache or JPA
Quarkus gives you two good routes:
- Keep your Spring Data repositories with
quarkus-spring-data-jpa
. - Embrace Panache for a leaner repository pattern and convenience methods.
Panache entity example:
// Quarkus Panache
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.Entity;
@Entity
public class Book extends PanacheEntity {
public String title;
public String author;
}
Panache repository example:
import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class BookRepository implements PanacheRepository {
}
Resource with transaction:
import jakarta.transaction.Transactional;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Response;
import static jakarta.ws.rs.core.Response.Status;
@Path("/api/books")
@Consumes("application/json")
@Produces("application/json")
public class BookResource {
@POST
@Transactional
public Response add(Book book) {
book.persist();
return Response.status(Status.CREATED).entity(book).build();
}
@GET
public List list() {
return Book.listAll();
}
}
Database configuration:
quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=postgres
quarkus.datasource.password=postgres
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/app
quarkus.hibernate-orm.database.generation=update
Development convenience: with Dev Services, Quarkus can spin up a PostgreSQL container automatically if you don’t set quarkus.datasource.jdbc.url
for dev/test.
Observability: Actuator to Quarkus endpoints
Actuator has equivalents in Quarkus:
- Health:
quarkus-smallrye-health
→/q/health
- Metrics:
quarkus-micrometer
→/q/metrics
(Micrometer registry support) - OpenAPI:
quarkus-smallrye-openapi
→/q/openapi
and/q/swagger-ui
(in dev/test)
Add dependencies:
io.quarkus
quarkus-smallrye-health
io.quarkus
quarkus-micrometer
io.quarkus
quarkus-smallrye-openapi
You also get a Dev UI at /q/dev
in dev mode—super handy for checking health, config, and endpoints.
Security mapping
If you’re on Spring Security, you can either:
- Use
quarkus-spring-security
for basic annotation compatibility, or - Move to Quarkus Security and OIDC extensions for standards-based auth.
A simple role-protected resource with Quarkus:
import io.quarkus.security.Authenticated;
import io.quarkus.security.runtime.annotations.RolesAllowed; // or jakarta.annotation.security.RolesAllowed
@Path("/api/admin")
@Authenticated
public class AdminResource {
@GET
@RolesAllowed("admin")
public String onlyAdmins() {
return "Top secret";
}
}
Configure your auth provider (Basic, OIDC, JWT) via Quarkus security extensions.
Testing: from @SpringBootTest to @QuarkusTest
Quarkus uses JUnit 5 with @QuarkusTest
. RestAssured works great for HTTP tests.
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
@QuarkusTest
class HelloResourceTest {
@Test
void testHelloEndpoint() {
given()
.queryParam("name", "devs")
.when()
.get("/api/hello")
.then()
.statusCode(200)
.body(is("Hello devs"));
}
}
If you use a database, Dev Services will auto-provision containers for tests—no manual Testcontainers wiring required for common cases.
Build, run, and go native
Usual development cycle:
# hot reload dev mode
./mvnw quarkus:dev
# JVM build
./mvnw package
# Native build (requires GraalVM or container build)
./mvnw package -Dnative
# or build in a container:
./mvnw package -Dnative -Dquarkus.native.container-build=true
Produce a container image with Quarkus’ container-image support:
./mvnw package -Dquarkus.container-image.build=true \
-Dquarkus.container-image.group=myorg \
-Dquarkus.container-image.tag=1.0.0
Common gotchas and how to avoid them
- javax vs jakarta:
- Quarkus 3 uses Jakarta EE 10 (jakarta.*). Upgrade your imports.
- AOP and proxies:
- Spring AOP patterns may not translate directly. Prefer CDI interceptors and extensions, or use build-time friendly alternatives.
- Configuration injection:
- Replace
@Value("${...}")
with@ConfigProperty
or@ConfigMapping
for type-safe, native-friendly config. - Scheduling:
- Use
quarkus-scheduler
’s@Scheduled
. It’s lightweight and production-ready. - Reflection in native images:
- Avoid dynamic proxies and reflection-heavy libraries. If unavoidable, configure reflection in
reflect-config.json
or via extension configs. - Auto-configuration expectations:
- Quarkus does build-time augmentation instead of runtime auto-config. Be explicit with extensions and config.
Migration mindset: Prefer Quarkus-native patterns for long-term maintainability, even if you use Spring compatibility extensions initially.
A step-by-step migration recipe
- Upgrade your Spring Boot app to Boot 3 (Jakarta) and Java 17+.
- Create a Quarkus 3 project with the closest matching extensions.
- Migrate configuration:
- Move essentials from
application.yml/properties
. - Use
@ConfigMapping
for complex configs.
- Web layer:
- Keep
@RestController
usingquarkus-spring-web
or move to JAX-RS.
- DI:
- Replace
@Autowired
with@Inject
; add CDI scopes.
- Persistence:
- Either keep Spring Data repositories with
quarkus-spring-data-jpa
or refactor to Panache.
- Security and observability:
- Map Actuator to
/q/*
endpoints; wire up security with Quarkus extensions.
- Tests:
- Convert to
@QuarkusTest
and leverage Dev Services.
- Build and benchmark:
- Validate memory and startup time improvements (JVM and native).
- Refactor off compatibility extensions where it makes sense.
Useful links
- Quarkus Guides: https://quarkus.io/guides
- RESTEasy Reactive (JAX-RS): https://quarkus.io/guides/resteasy-reactive
- Hibernate ORM with Panache: https://quarkus.io/guides/hibernate-orm-panache
- Spring DI compatibility: https://quarkus.io/guides/spring-di
- Spring Web compatibility: https://quarkus.io/guides/spring-web
- Spring Data JPA compatibility: https://quarkus.io/guides/spring-data-jpa
- SmallRye Health: https://quarkus.io/guides/smallrye-health
- OpenAPI/Swagger UI: https://quarkus.io/guides/openapi-swaggerui
- Config mappings: https://quarkus.io/guides/config-mappings
- CLI tooling: https://quarkus.io/guides/cli-tooling
Final thoughts
Migrating from Spring Boot to Quarkus 3 doesn’t have to be a grind. Start small, lean on the Spring compatibility extensions to reduce risk, and gradually adopt idiomatic Quarkus for the best performance and long-term simplicity. Once you flip the switch to native, the payoff in startup time and resource usage is often dramatic—perfect for cloud-native workloads. Happy migrating!