What Is the Migration from Spring Boot to Quarkus?
Migrating from Spring Boot to Quarkus means transforming an existing Java application built on the Spring ecosystem—with its dependency injection, Spring MVC, Spring Data JPA, and embedded Tomcat—to run on Quarkus, a Kubernetes-native Java stack that prioritizes fast startup, low memory footprint, and developer joy through live reload. Quarkus was designed from the ground up for containerized environments and serverless architectures, while still supporting familiar imperative and reactive programming models. The migration is not a simple "search and replace" operation; it involves rethinking the application's structure around standards like Jakarta EE (JAX-RS, CDI, Jakarta Persistence) and Quarkus-specific extensions such as Panache and RESTEasy Reactive.
Why Migrating to Quarkus Matters
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →The decision to move from Spring Boot to Quarkus carries tangible benefits that directly impact production costs, performance, and developer efficiency:
- Dramatically faster startup times — Quarkus applications routinely boot in milliseconds rather than seconds, which is critical for auto-scaling Kubernetes pods and serverless cold starts.
- Lower memory consumption — A typical Quarkus service uses 30–50% less RAM than an equivalent Spring Boot application, allowing higher workload density per cluster node.
- Native compilation via GraalVM — Quarkus offers first-class support for compiling Java bytecode into native binaries that start almost instantly and consume minimal resources, without the heavy reflection configuration headaches often encountered with Spring Native.
- Developer experience — With dev mode, continuous testing, and live reload (changes propagate in under a second), Quarkus dramatically shortens the inner development loop.
- Reactive by default — Quarkus uses a non-blocking I/O model on top of Netty/Eclipse Vert.x, yielding better throughput under concurrent load without requiring developers to adopt reactive programming unless they choose to.
- Standardization on Jakarta EE and MicroProfile — Moving to Quarkus aligns your stack with vendor-neutral specifications, reducing long-term lock-in.
Step-by-Step Migration Guide
1. Assess Your Current Spring Boot Application
Before writing any code, catalog every Spring Boot feature your application uses. Create a checklist covering: Spring MVC controllers, Spring Data JPA repositories, Spring Security configuration, Spring Cloud integrations, scheduled tasks, AOP aspects, exception handlers, and any third-party Spring Boot starters. Not every feature has a one-to-one Quarkus equivalent, so identifying gaps early lets you plan for alternatives or decide whether certain parts should remain as-is during a phased migration.
2. Bootstrap a New Quarkus Project
Use the Quarkus CLI or the code.quarkus.io website to generate a new project skeleton. Alternatively, you can manually set up the Maven POM. The essential dependencies for a RESTful microservice that mirrors a typical Spring Boot app are:
<!-- pom.xml (Quarkus) -->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>quarkus-migration</artifactId>
<version>1.0.0-SNAPSHOT</version>
<properties>
<quarkus.platform.version>3.17.0</quarkus.platform.version>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-bom</artifactId>
<version>${quarkus.platform.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>${quarkus.platform.version}</version>
<executions>
<execution>
<goals><goal>generate</goal></goal></goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Compare this to a typical Spring Boot POM. The Quarkus BOM (bill of materials) manages all extension versions consistently, eliminating dependency hell. Notice there is no embedded server dependency—Quarkus bundles its own optimized runtime.
3. Migrate REST Controllers to JAX-RS Endpoints
Spring Boot uses @RestController and @RequestMapping annotations. Quarkus relies on JAX-RS annotations (@Path, @GET, @POST, etc.) provided by RESTEasy Reactive. Here is a side-by-side comparison:
Spring Boot (original):
// Spring Boot Controller
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public ResponseEntity<UserDto> getUserById(@PathVariable Long id) {
UserDto user = userService.findById(id);
return ResponseEntity.ok(user);
}
@PostMapping
public ResponseEntity<UserDto> createUser(@RequestBody @Valid CreateUserRequest request) {
UserDto created = userService.create(request);
return ResponseEntity
.status(HttpStatus.CREATED)
.body(created);
}
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(UserNotFoundException ex) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse(ex.getMessage()));
}
}
Quarkus (migrated):
// Quarkus JAX-RS Resource
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.jboss.resteasy.reactive.RestResponse;
import org.jboss.resteasy.reactive.server.ServerExceptionMapper;
@Path("/api/users")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class UserResource {
private final UserService userService;
public UserResource(UserService userService) {
this.userService = userService;
}
@GET
@Path("/{id}")
public UserDto getUserById(@PathParam("id") Long id) {
return userService.findById(id);
}
@POST
public RestResponse<UserDto> createUser(@Valid CreateUserRequest request) {
UserDto created = userService.create(request);
return RestResponse.status(Response.Status.CREATED, created);
}
@ServerExceptionMapper(UserNotFoundException.class)
public RestResponse<ErrorResponse> handleNotFound(UserNotFoundException ex) {
return RestResponse.status(
Response.Status.NOT_FOUND,
new ErrorResponse(ex.getMessage())
);
}
}
Key changes: @RestController + @RequestMapping become @Path. @GetMapping becomes @GET. @PathVariable becomes @PathParam. @RequestBody is implied for non-GET methods consuming JSON. Exception handling moves from @ExceptionHandler to Quarkus-specific @ServerExceptionMapper (or a JAX-RS ExceptionMapper implementing jakarta.ws.rs.ext.ExceptionMapper). The RestResponse class is a Quarkus enhancement that bridges reactive and imperative styles seamlessly.
4. Convert Dependency Injection: From Spring Annotations to CDI
Spring Boot uses @Service, @Repository, and @Autowired. Quarkus uses CDI (Contexts and Dependency Injection) with @ApplicationScoped, @Inject, and qualifiers. The migration is mostly mechanical:
Spring Boot service:
@Service
@Transactional
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public UserDto findById(Long id) {
return userRepository.findById(id)
.map(UserDto::from)
.orElseThrow(() -> new UserNotFoundException(id));
}
}
Quarkus CDI service:
@ApplicationScoped
public class UserService {
private final UserRepository userRepository;
@Inject
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Transactional
public UserDto findById(Long id) {
return userRepository.findById(id)
.map(UserDto::from)
.orElseThrow(() -> new UserNotFoundException(id));
}
}
In Quarkus, constructor injection is preferred and does not require @Inject on the constructor if there is only one constructor—though adding it makes the intent explicit. Field injection (@Inject on a field) also works but is discouraged for testability. The @Transactional annotation moves from Spring's package to jakarta.transaction.Transactional (or you can use Panache's @WithSessionOnDemand for read-only operations).
5. Migrate Persistence: Spring Data JPA to Panache
This step requires the most conceptual change. Spring Data JPA relies on repository interfaces with method name conventions and @Query annotations. Quarkus offers Hibernate ORM with Panache, which provides two patterns: the active record pattern (entity classes with static methods) and the repository pattern (similar to Spring Data but implemented via PanacheRepository). Choose the repository pattern for a more familiar migration path.
Spring Data JPA (original):
// Spring Data JPA Entity
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String name;
// getters and setters...
}
// Spring Data JPA Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
List<User> findByNameContainingIgnoreCase(String name);
}
Quarkus Panache (migrated):
// Quarkus Panache Entity
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
@Entity
@Table(name = "users")
public class User extends PanacheEntity {
@Column(nullable = false, unique = true)
public String email;
@Column(nullable = false)
public String name;
// PanacheEntity provides id (Long), toString, etc.
// Public fields are acceptable in Panache (no getters/setters required)
}
// Quarkus Panache Repository
import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.List;
import java.util.Optional;
@ApplicationScoped
public class UserRepository implements PanacheRepository<User> {
public Optional<User> findByEmail(String email) {
return find("email", email).firstResultOptional();
}
public List<User> findByNameContainingIgnoreCase(String name) {
return find("name like ?1", "%" + name + "%").list();
}
// Alternatively, use Panache's simplified query methods:
public List<User> findActiveUsers() {
return list("active", true);
}
}
Important changes: The entity extends PanacheEntity (which provides a Long id field, toString(), and static CRUD methods) or PanacheEntityBase if you need a custom ID type. Fields can be public, eliminating boilerplate getters and setters. The repository implements PanacheRepository<T> instead of extending Spring's interfaces. Query methods use Panache's fluent find(), list(), count(), and delete() patterns, which accept both JPQL snippets and named parameters.
If you prefer to keep the entity as a plain JPA entity without extending Panache classes, you can still use PanacheRepositoryBase<T> or inject Panache static helpers—the flexibility is there.
6. Migrate Configuration: application.properties to application.properties
Quarkus uses the same application.properties file name and location (src/main/resources), but the property keys differ. Most Spring Boot properties have clear Quarkus equivalents:
Spring Boot application.properties:
spring.application.name=my-service
server.port=8080
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=appuser
spring.datasource.password=secret
spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
logging.level.com.example=DEBUG
Quarkus application.properties:
quarkus.application.name=my-service
quarkus.http.port=8080
quarkus.datasource.db-kind=postgresql
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/mydb
quarkus.datasource.username=appuser
quarkus.datasource.password=secret
quarkus.hibernate-orm.database.generation=validate
quarkus.hibernate-orm.dialect=org.hibernate.dialect.PostgreSQLDialect
quarkus.log.category.\"com.example\".level=DEBUG
Quarkus automatically detects the JDBC driver from quarkus.datasource.db-kind, so you do not need to specify the driver class explicitly. The logging category syntax uses escaped double quotes for dotted package names. For YAML fans, Quarkus supports application.yaml natively.
7. Replace Spring Security with Quarkus Security
If your Spring Boot application uses Spring Security, you will need to adopt Quarkus Security, which builds on Jakarta Security and provides extensions for OIDC, OAuth2, and basic authentication. A simple role-based access control migration looks like this:
Spring Boot security configuration:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults());
return http.build();
}
}
Quarkus security configuration:
# application.properties
quarkus.http.auth.basic=true
quarkus.http.auth.permission.public.paths=/api/public/*
quarkus.http.auth.permission.public.policy=permit
quarkus.http.auth.permission.admin.paths=/api/admin/*
quarkus.http.auth.permission.admin.policy=authenticated
quarkus.http.auth.permission.admin.roles=ADMIN
quarkus.http.auth.permission.authenticated.paths=/*
quarkus.http.auth.permission.authenticated.policy=authenticated
Quarkus encourages declarative security configuration via properties. For programmatic control, annotate endpoints with @RolesAllowed, @Authenticated, or @PermitAll from the jakarta.annotation.security package. Quarkus also integrates with OpenID Connect providers seamlessly through the quarkus-oidc extension.
8. Migrate Testing Code
Spring Boot tests use @SpringBootTest, @MockBean, and MockMvc. Quarkus tests use @QuarkusTest, @TestHTTPEndpoint, and @InjectMock for CDI-based mocking. REST endpoint testing uses io.rest-assured or a Quarkus-provided @TestHTTPResource URL.
Spring Boot test:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserControllerTest {
@Autowired
private TestRestTemplate restTemplate;
@MockBean
private UserService userService;
@Test
void testGetUser() {
UserDto mockUser = new UserDto(1L, "john@example.com", "John");
given(userService.findById(1L)).willReturn(mockUser);
ResponseEntity<UserDto> response = restTemplate.getForEntity(
"/api/users/1", UserDto.class
);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertEquals("john@example.com", response.getBody().getEmail());
}
}
Quarkus test:
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.mockito.InjectMock;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.Test;
@QuarkusTest
class UserResourceTest {
@InjectMock
UserService userService;
@Test
void testGetUser() {
UserDto mockUser = new UserDto(1L, "john@example.com", "John");
Mockito.when(userService.findById(1L)).thenReturn(mockUser);
RestAssured
.given()
.contentType(ContentType.JSON)
.when()
.get("/api/users/1")
.then()
.statusCode(200)
.body("email", equalTo("john@example.com"));
}
}
Quarkus tests run on the actual application server (no mock servlet container), giving you true integration test fidelity. @InjectMock creates a CDI-compatible mock that replaces the real service bean in the application context. Quarkus also supports continuous testing: run quarkus dev and tests execute automatically when you save changes.
9. Build, Run, and Deploy
Spring Boot builds produce executable JARs with embedded Tomcat. Quarkus offers multiple run modes:
- JVM mode —
./mvnw quarkus:devfor development with live reload;./mvnw packageproduces aquarkus-run.jarthat runs on the JVM with optimized startup. - Native mode —
./mvnw package -Pnativecompiles the application to a native binary using GraalVM (requires setting up GraalVM or using a container build via-Dquarkus.native.container-build=true). - Docker — Quarkus can generate a minimal Docker image automatically. Run
./mvnw package -Pnative -Dquarkus.native.container-build=true -Dquarkus.container-image.build=trueand Quarkus builds a native binary inside a container and packages it into a Docker image named after your application.
Dockerfile generated by Quarkus (native, distroless-based):
# Quarkus automatically generates an optimized Dockerfile
# You can find it in target/docker after a native build
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9
WORKDIR /work/
COPY target/*-runner /work/application
RUN chmod 775 /work/application
EXPOSE 8080
CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]
Compare this to a Spring Boot Docker image that requires a full JDK layer and typically weighs 250–400 MB. A Quarkus native image often weighs under 50 MB and starts in 0.01 seconds.
10. Handle Scheduled Tasks and Async Processing
Spring Boot uses @Scheduled and @Async with @EnableScheduling. Quarkus provides @Scheduled (from io.quarkus.scheduler) and @Asynchronous with io.quarkus.scheduler.Scheduled for cron-like tasks. For async processing, Quarkus uses io.smallrye.common.annotation.NonBlocking or Mutiny-based reactive programming.
// Quarkus Scheduled Task
import io.quarkus.scheduler.Scheduled;
import io.quarkus.scheduler.ScheduledExecution;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class ReportGenerator {
@Scheduled(every = "1h")
void generateHourlyReport(ScheduledExecution execution) {
// This runs every hour
System.out.println("Generating report at " + execution.getScheduledFireTime());
}
@Scheduled(cron = "0 0 6 * * ?")
void sendDailyDigest() {
// Runs at 6:00 AM daily
}
}
Best Practices for a Successful Migration
- Migrate incrementally — Do not attempt a big-bang rewrite. Use the strangler fig pattern: extract one REST endpoint at a time, deploy it as a Quarkus microservice behind the same reverse proxy, and route traffic gradually. Both Spring Boot and Quarkus services can coexist in production while you complete the migration.
- Leverage Quarkus dev mode — Run
quarkus:devconstantly during migration. The live reload, exception display, and built-in Dev UI (available at/q/dev) show all endpoints, configuration, and database schemas in real time, speeding up the adaptation process immensely. - Keep entities simple with Panache — If your domain model is straightforward, use
PanacheEntityand public fields. The reduction in boilerplate alone can shrink your codebase by 30%. For complex JPA mappings (composite keys, inheritance), usePanacheEntityBaseand retain full control. - Test continuously — Quarkus's continuous testing feature re-runs affected tests on every code change. Enable it with
quarkus.test.continuous-testing=truein dev mode, and you will catch migration bugs instantly. - Watch out for reflection and serialization — If you plan to build native images, annotate classes that use reflection, serialization, or dynamic proxies with
@RegisterForReflectionor configure them inreflect-config.json. Quarkus extensions handle most common cases (Jackson, Hibernate, REST endpoints) automatically, but custom reflection-heavy code needs explicit registration. - Revisit your dependency tree — Many Spring Boot starter dependencies pull in transitive libraries you may no longer need. Audit your Quarkus POM and remove unused extensions. The Quarkus extension list is fine-grained: include only what you use (e.g.,
quarkus-resteasy-reactive-jacksoninstead of a monolithic "web starter"). - Profile-aware configuration — Quarkus supports
application-{profile}.propertiesexactly like Spring Boot. Use%dev.quarkus.log.level=DEBUGto override properties for the dev profile, and%prod.quarkus.datasource.jdbc.max-size=20for production-specific tuning. - Monitor and compare — Before fully decommissioning the old Spring Boot service, run both versions in parallel and compare metrics: startup time, memory consumption, GC pauses, and throughput under load. Use these hard numbers to justify the migration investment and to tune Quarkus configuration.
Conclusion
Migrating from Spring Boot to Quarkus is a strategic investment that pays dividends in infrastructure costs, application responsiveness, and developer productivity. The process involves translating Spring idioms to their Quarkus equivalents across controllers, dependency injection, persistence, security, configuration, and testing—each step is manageable when approached incrementally. Quarkus preserves the best parts of the Java ecosystem (mature libraries, strong typing, rich tooling) while discarding the heavyweight runtime assumptions that burden cloud-native workloads. By following this step-by-step guide, you can methodically transform a traditional Spring Boot application into a fast, lightweight, and Kubernetes-native Quarkus service without sacrificing the stability and familiarity of the JVM ecosystem.