Spring Boot to Quarkus Migration Guide
Status: Final
Version: 1.0
Last Updated: 2026-01-13
Purpose
This guide provides a comprehensive mapping for developers migrating from Spring Boot to Quarkus. It covers framework equivalents, code patterns, configuration changes, and common pitfalls to ensure a smooth transition.
Quick Reference Table
| Spring Boot Concept | Quarkus Equivalent | Notes |
|---|---|---|
| Spring Context | CDI Container (Arc) | Build-time optimized CDI |
| Spring Bean | CDI Bean | Use @ApplicationScoped, @RequestScoped |
| @Component, @Service, @Repository | @ApplicationScoped | CDI scope annotations |
| @Autowired | @Inject | Standard JSR-330 injection |
| @Configuration + @Bean | CDI @Produces | Producer methods |
| @Value | @ConfigProperty | MicroProfile Config |
| @RestController | @Path | JAX-RS resource |
| @GetMapping, @PostMapping | @GET, @POST | JAX-RS annotations |
| ResponseEntity | Response | JAX-RS Response |
| @RequestBody | Method parameter with @Consumes | Implicit in JAX-RS |
| @PathVariable | @PathParam | JAX-RS path parameters |
| @RequestParam | @QueryParam | JAX-RS query parameters |
| JpaRepository | PanacheRepository | Active record or repository pattern |
| @PreAuthorize | @RolesAllowed | Standard security annotation |
| Spring Security | Quarkus Security | SmallRye JWT for tokens |
| @Async | @RunOnVirtualThread or Uni | Virtual threads or reactive |
| @Scheduled | @Scheduled | Quarkus Scheduler |
| ApplicationEventPublisher | Event |
CDI events |
| @EventListener | @Observes | CDI event observers |
| @SpringBootTest | @QuarkusTest | Quarkus test framework |
| MockMvc | RestAssured | HTTP client for testing |
| application.yml | application.properties | Properties format preferred |
Dependency Injection
Spring Boot Pattern
@Service
class UserService @Autowired constructor(
private val userRepository: UserRepository,
@Value("\${app.admin.email}") private val adminEmail: String
) {
// ...
}
Quarkus Pattern
@ApplicationScoped
class UserService @Inject constructor(
private val userRepository: UserRepository,
@ConfigProperty(name = "app.admin.email") private val adminEmail: String
) {
// ...
}
Key Changes
- Replace
@Service/@Componentwith@ApplicationScoped(singleton) or@RequestScoped - Replace
@Autowiredwith@Inject(standard CDI) - Replace
@Valuewith@ConfigProperty(MicroProfile Config) - Use
@Dependentfor per-injection-point lifecycle (default CDI scope)
REST APIs
Spring Boot Pattern
@RestController
@RequestMapping("/api/users")
class UserController @Autowired constructor(
private val userService: UserService
) {
@GetMapping("/{id}")
fun getUser(@PathVariable id: Long): ResponseEntity<UserDTO> {
val user = userService.findById(id)
return ResponseEntity.ok(user)
}
@PostMapping
fun createUser(@RequestBody @Valid request: CreateUserRequest): ResponseEntity<UserDTO> {
val user = userService.create(request)
return ResponseEntity.status(HttpStatus.CREATED).body(user)
}
}
Quarkus Pattern
@Path("/api/users")
@ApplicationScoped
class UserResource @Inject constructor(
private val userService: UserService
) {
@GET
@Path("/{id}")
@Produces(MediaType.APPLICATION_JSON)
fun getUser(@PathParam("id") id: Long): Response {
val user = userService.findById(id)
return Response.ok(user).build()
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
fun createUser(@Valid request: CreateUserRequest): Response {
val user = userService.create(request)
return Response.status(Response.Status.CREATED).entity(user).build()
}
}
Key Changes
- Replace
@RestControllerwith@Path+@ApplicationScoped - Replace
@GetMapping/@PostMappingwith@GET/@POST - Replace
@PathVariablewith@PathParam - Replace
@RequestParamwith@QueryParam - Replace
@RequestBodywith method parameter (implicit with@Consumes) - Replace
ResponseEntitywith JAX-RSResponse - Add
@Produces/@Consumesannotations for content types - Use
Response.ok(),Response.status()builders
Data Access
Spring Boot Pattern
interface UserRepository : JpaRepository<User, Long> {
fun findByEmail(email: String): Optional<User>
@Query("SELECT u FROM User u WHERE u.tenantId = :tenantId")
fun findByTenant(@Param("tenantId") tenantId: Long): List<User>
}
@Service
class UserService @Autowired constructor(
private val userRepository: UserRepository
) {
@Transactional
fun createUser(user: User): User {
return userRepository.save(user)
}
}
Quarkus Pattern
@ApplicationScoped
class UserRepository : PanacheRepository<User> {
fun findByEmail(email: String): Optional<User> {
return find("email", email).firstResultOptional()
}
fun findByTenant(tenantId: Long): List<User> {
return list("tenantId", tenantId)
}
}
@ApplicationScoped
class UserService @Inject constructor(
private val userRepository: UserRepository
) {
@Transactional
fun createUser(user: User): User {
userRepository.persist(user)
return user
}
}
Key Changes
- Replace
JpaRepository<Entity, ID>withPanacheRepository<Entity> - Replace
.save()with.persist() - Replace
.findById()with.findByIdOptional() - Replace query methods with Panache query syntax:
find(),list(),stream() - Replace
@QueryJPQL with Panache query methods - Use
@Transactionalfromjakarta.transactionpackage (not Spring) - Entities no longer need getters/setters (Kotlin data classes work great)
Alternative: Active Record Pattern
@Entity
class User : PanacheEntity() {
lateinit var email: String
lateinit var name: String
var tenantId: Long = 0
companion object {
fun findByEmail(email: String): Optional<User> {
return find("email", email).firstResultOptional()
}
}
}
// Usage:
val user = User()
user.email = "test@example.com"
user.persist()
val found = User.findByEmail("test@example.com")
Security
Spring Boot Pattern
@Service
class TenantService {
@PreAuthorize("hasRole('ADMIN')")
fun deleteTenant(tenantId: Long) {
val currentUser = SecurityContextHolder.getContext().authentication.principal as UserDetails
// ...
}
}
Quarkus Pattern
@ApplicationScoped
class TenantService @Inject constructor(
private val securityIdentity: SecurityIdentity
) {
@RolesAllowed("ADMIN")
fun deleteTenant(tenantId: Long) {
val currentUser = securityIdentity.principal.name
// ...
}
}
Key Changes
- Replace
@PreAuthorizewith@RolesAllowed - Inject
SecurityIdentityinstead of usingSecurityContextHolder - Use
securityIdentity.principalfor user info - Use
securityIdentity.hasRole()for role checks - Use
securityIdentity.rolesto get all roles - Replace Spring Security JWT with SmallRye JWT
Configuration
Spring Boot Pattern (application.yml)
spring:
datasource:
url: jdbc:postgresql://localhost:5432/esg
username: postgres
password: secret
jpa:
hibernate:
ddl-auto: validate
show-sql: true
app:
admin:
email: admin@example.com
server:
port: 8080
logging:
level:
com.example: DEBUG
Quarkus Pattern (application.properties)
# Database
quarkus.datasource.db-kind=postgresql
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/esg
quarkus.datasource.username=postgres
quarkus.datasource.password=secret
# Hibernate
quarkus.hibernate-orm.database.generation=validate
quarkus.hibernate-orm.log.sql=true
# Application properties
app.admin.email=admin@example.com
# HTTP
quarkus.http.port=8080
# Logging
quarkus.log.level=INFO
quarkus.log.category."com.example".level=DEBUG
# Profiles
%dev.quarkus.datasource.password=dev-secret
%test.quarkus.datasource.password=test-secret
%prod.quarkus.datasource.password=${DB_PASSWORD}
Key Changes
- Use
application.propertiesformat (YAML supported but properties preferred) - Replace
spring.*properties withquarkus.*equivalents - Use profile prefixes (
%dev,%test,%prod) instead of separate files - Environment variables use same name with
_instead of.(automatic mapping) - Build-time properties affect compilation, runtime properties affect execution
Async Processing
Spring Boot Pattern
@Service
class NotificationService {
@Async
fun sendEmail(to: String, subject: String, body: String): CompletableFuture<Boolean> {
// Send email
return CompletableFuture.completedFuture(true)
}
}
@Service
class EventPublisher @Autowired constructor(
private val applicationEventPublisher: ApplicationEventPublisher
) {
fun publishEvent(event: UserCreatedEvent) {
applicationEventPublisher.publishEvent(event)
}
}
@Component
class EventListener {
@EventListener
fun handleUserCreated(event: UserCreatedEvent) {
// Handle event
}
}
Quarkus Pattern
@ApplicationScoped
class NotificationService {
@RunOnVirtualThread
fun sendEmail(to: String, subject: String, body: String): Boolean {
// Send email (blocking call is OK with virtual threads)
return true
}
// Or use reactive approach:
fun sendEmailReactive(to: String, subject: String, body: String): Uni<Boolean> {
return Uni.createFrom().item(true)
}
}
@ApplicationScoped
class EventPublisher @Inject constructor(
private val userCreatedEvent: Event<UserCreatedEvent>
) {
fun publishEvent(event: UserCreatedEvent) {
userCreatedEvent.fire(event) // Synchronous
// Or: userCreatedEvent.fireAsync(event) // Asynchronous
}
}
@ApplicationScoped
class EventListener {
fun handleUserCreated(@Observes event: UserCreatedEvent) {
// Handle event synchronously
}
fun handleUserCreatedAsync(@ObservesAsync event: UserCreatedEvent) {
// Handle event asynchronously
}
}
Key Changes
- Replace
@Asyncwith@RunOnVirtualThread(for blocking I/O) orUni<T>(reactive) - Replace
CompletableFuturewithUni<T>for reactive programming - Replace
ApplicationEventPublisherwith CDIEvent<T> - Replace
@EventListenerwith@Observes(sync) or@ObservesAsync(async) - Virtual threads enable millions of concurrent operations with minimal overhead
Scheduled Jobs
Spring Boot Pattern
@Component
class ScheduledTasks {
@Scheduled(cron = "0 0 2 * * ?")
fun dailyCleanup() {
// Run at 2 AM daily
}
@Scheduled(fixedRate = 60000)
fun everyMinute() {
// Run every minute
}
}
Quarkus Pattern
@ApplicationScoped
class ScheduledTasks {
@Scheduled(cron = "0 0 2 * * ?")
fun dailyCleanup() {
// Run at 2 AM daily
}
@Scheduled(every = "60s")
fun everyMinute() {
// Run every minute
}
}
Key Changes
- Replace
@Componentwith@ApplicationScoped - Cron expressions remain the same
- Use
everyparameter instead offixedRatefor intervals - Import from
io.quarkus.schedulerpackage - Configuration:
quarkus.scheduler.enabled=true(enabled by default)
Testing
Spring Boot Pattern
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest @Autowired constructor(
private val mockMvc: MockMvc,
private val userRepository: UserRepository
) {
@Test
@Transactional
fun `should create user`() {
mockMvc.perform(
post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("""{"name":"John","email":"john@example.com"}""")
).andExpect(status().isCreated)
val user = userRepository.findByEmail("john@example.com")
assertNotNull(user)
}
}
Quarkus Pattern
@QuarkusTest
class UserResourceTest @Inject constructor(
private val userRepository: UserRepository
) {
@Test
@TestTransaction
fun `should create user`() {
given()
.contentType(ContentType.JSON)
.body("""{"name":"John","email":"john@example.com"}""")
.`when`()
.post("/api/users")
.then()
.statusCode(201)
val user = userRepository.findByEmail("john@example.com")
assertTrue(user.isPresent)
}
}
Key Changes
- Replace
@SpringBootTestwith@QuarkusTest - Replace
MockMvcwithRestAssured(static imports fromio.restassured.RestAssured.*) - Replace
@Transactional(test) with@TestTransactionfor automatic rollback - Use
given().when().then()pattern for REST API testing - Quarkus starts once for all tests (faster execution)
- Use
@QuarkusTestResourcefor containers (DevServices often handle this automatically)
Validation
Spring Boot Pattern
@RestController
class UserController {
@PostMapping("/api/users")
fun createUser(@Valid @RequestBody request: CreateUserRequest,
errors: BindingResult): ResponseEntity<*> {
if (errors.hasErrors()) {
return ResponseEntity.badRequest().body(errors.allErrors)
}
// ...
}
}
data class CreateUserRequest(
@field:NotBlank
@field:Email
val email: String,
@field:Size(min = 3, max = 100)
val name: String
)
Quarkus Pattern
@Path("/api/users")
class UserResource {
@POST
@Consumes(MediaType.APPLICATION_JSON)
fun createUser(@Valid request: CreateUserRequest): Response {
// Validation happens automatically
// If invalid, ConstraintViolationException is thrown
// JAX-RS ExceptionMapper converts it to 400 Bad Request
// ...
}
}
data class CreateUserRequest(
@field:NotBlank
@field:Email
val email: String,
@field:Size(min = 3, max = 100)
val name: String
)
Key Changes
- Validation annotations remain the same (Hibernate Validator)
- No need for
BindingResultparameter - validation is automatic ConstraintViolationExceptionthrown on validation failure- Create
ExceptionMapperto customize validation error responses:
@Provider
class ValidationExceptionMapper : ExceptionMapper<ConstraintViolationException> {
override fun toResponse(exception: ConstraintViolationException): Response {
val errors = exception.constraintViolations.map {
mapOf("field" to it.propertyPath.toString(), "message" to it.message)
}
return Response.status(Response.Status.BAD_REQUEST)
.entity(mapOf("errors" to errors))
.build()
}
}
Fault Tolerance
Spring Boot Pattern
@Service
class ExternalApiService {
@Retryable(
value = [IOException::class],
maxAttempts = 3,
backoff = Backoff(delay = 1000)
)
fun callExternalApi(): String {
// Call external API
}
@Recover
fun recoverFromFailure(e: IOException): String {
return "Fallback response"
}
}
Quarkus Pattern
@ApplicationScoped
class ExternalApiService {
@Retry(
maxRetries = 3,
delay = 1000,
delayUnit = ChronoUnit.MILLIS,
retryOn = [IOException::class]
)
@Fallback(fallbackMethod = "recoverFromFailure")
fun callExternalApi(): String {
// Call external API
}
private fun recoverFromFailure(): String {
return "Fallback response"
}
}
Key Changes
- Replace Spring Retry with MicroProfile Fault Tolerance
- Replace
@Retryablewith@Retry - Replace
@Recoverwith@Fallback - Additional fault tolerance patterns available:
@Timeout,@CircuitBreaker,@Bulkhead
Message Queues
Spring Boot Pattern
@Service
class MessageProducer @Autowired constructor(
private val rabbitTemplate: RabbitTemplate
) {
fun sendMessage(message: ValidationRequest) {
rabbitTemplate.convertAndSend("validation-queue", message)
}
}
@Service
class MessageConsumer {
@RabbitListener(queues = ["validation-queue"])
fun processMessage(message: ValidationRequest) {
// Process message
}
}
Quarkus Pattern
@ApplicationScoped
class MessageProducer @Inject constructor(
@Channel("validation-queue") private val emitter: Emitter<ValidationRequest>
) {
fun sendMessage(message: ValidationRequest) {
emitter.send(message)
}
}
@ApplicationScoped
class MessageConsumer {
@Incoming("validation-queue")
@Blocking
fun processMessage(message: ValidationRequest) {
// Process message
}
}
Configuration (application.properties):
mp.messaging.outgoing.validation-queue.connector=smallrye-rabbitmq
mp.messaging.outgoing.validation-queue.queue.name=validation-queue
mp.messaging.incoming.validation-queue.connector=smallrye-rabbitmq
mp.messaging.incoming.validation-queue.queue.name=validation-queue
Key Changes
- Replace
RabbitTemplatewithEmitter<T>(SmallRye Reactive Messaging) - Replace
@RabbitListenerwith@Incoming - Add
@Blockingfor blocking message processing - Configure messaging in
application.propertiesinstead of Java config - Supports RabbitMQ, Kafka, AMQP, and more with same API
Common Pitfalls
1. Constructor Injection in Kotlin
Problem: CDI requires a no-arg constructor for proxying.
Solution: Use field injection or lateinit properties:
// Option 1: Field injection (recommended)
@ApplicationScoped
class UserService {
@Inject
lateinit var userRepository: UserRepository
}
// Option 2: Constructor injection with @Inject
@ApplicationScoped
class UserService @Inject constructor(
private val userRepository: UserRepository
)
2. Transaction Boundaries
Problem: @Transactional only works on CDI bean methods called from outside the bean.
Solution: Don't call @Transactional methods from within the same class:
@ApplicationScoped
class UserService {
@Transactional
fun createUser(user: User) {
// This works
}
fun doSomething() {
createUser(user) // This WON'T start a transaction (same class)
}
}
// Solution: Extract to separate service
@ApplicationScoped
class UserTransactionService {
@Transactional
fun createUser(user: User) { }
}
@ApplicationScoped
class UserService @Inject constructor(
private val txService: UserTransactionService
) {
fun doSomething() {
txService.createUser(user) // This WILL start a transaction
}
}
3. JAX-RS Response Bodies
Problem: Returning Kotlin data classes might not serialize correctly.
Solution: Ensure Jackson Kotlin module is configured (Quarkus handles this automatically with quarkus-resteasy-reactive-jackson).
4. Dev Services vs Production
Problem: Dev Services work in dev mode but not in production.
Solution: Configure production database in application.properties with profile:
%prod.quarkus.datasource.jdbc.url=jdbc:postgresql://prod-db:5432/esg
%prod.quarkus.datasource.username=${DB_USER}
%prod.quarkus.datasource.password=${DB_PASSWORD}
5. Panache and Transactions
Problem: Calling .persist() outside a transaction fails silently.
Solution: Always use @Transactional for write operations:
@ApplicationScoped
class UserService @Inject constructor(
private val userRepository: UserRepository
) {
@Transactional // Required!
fun createUser(user: User) {
userRepository.persist(user)
}
}
Maven Dependencies
Spring Boot
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
Quarkus
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-panache-kotlin</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-security</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt</artifactId>
</dependency>
</dependencies>
Tip: Use quarkus extension list to see all available extensions and quarkus extension add <extension-name> to add them.
Migration Checklist
- Update
pom.xml/build.gradleto use Quarkus BOM and extensions - Replace Spring annotations with CDI/JAX-RS equivalents
- Convert
@RestControllerclasses to JAX-RS@Pathresources - Update repository interfaces to use Panache
- Replace
.save()with.persist(),.findById()with.findByIdOptional() - Convert
application.ymltoapplication.propertieswithquarkus.*prefix - Update
@PreAuthorizeto@RolesAllowed - Inject
SecurityIdentityinstead of usingSecurityContextHolder - Replace
ApplicationEventPublisherwith CDIEvent<T> - Convert
@Asyncto@RunOnVirtualThreadorUni<T> - Update test classes from
@SpringBootTestto@QuarkusTest - Replace
MockMvcwithRestAssured - Update scheduled jobs to use Quarkus
@Scheduled - Configure message queues with SmallRye Reactive Messaging
- Test in dev mode:
./mvnw quarkus:dev - Test in production mode:
./mvnw packageandjava -jar target/quarkus-app/quarkus-run.jar - Build native image:
./mvnw package -Dnative
Next Steps
- Review Quarkus Features Guide for platform advantages
- Study Quarkus Security RBAC Implementation
- Read Testing Strategy for Quarkus test patterns
- Check Quarkus Deployment Guide for deployment options
Additional Resources
- Quarkus Documentation
- Quarkus for Spring Developers
- CDI Specification
- JAX-RS Specification
- MicroProfile Specifications
Cross-References
- Related: Quarkus Features Guide
- Related: Testing Strategy
- Related: Quarkus Security RBAC Implementation
- Related: Quarkus Deployment Guide
Change Log
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0 | 2026-01-13 | Ralph Agent | Initial migration guide for Spring Boot to Quarkus conversion |