Skip to content

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

  1. Replace @Service/@Component with @ApplicationScoped (singleton) or @RequestScoped
  2. Replace @Autowired with @Inject (standard CDI)
  3. Replace @Value with @ConfigProperty (MicroProfile Config)
  4. Use @Dependent for 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

  1. Replace @RestController with @Path + @ApplicationScoped
  2. Replace @GetMapping/@PostMapping with @GET/@POST
  3. Replace @PathVariable with @PathParam
  4. Replace @RequestParam with @QueryParam
  5. Replace @RequestBody with method parameter (implicit with @Consumes)
  6. Replace ResponseEntity with JAX-RS Response
  7. Add @Produces/@Consumes annotations for content types
  8. 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

  1. Replace JpaRepository<Entity, ID> with PanacheRepository<Entity>
  2. Replace .save() with .persist()
  3. Replace .findById() with .findByIdOptional()
  4. Replace query methods with Panache query syntax: find(), list(), stream()
  5. Replace @Query JPQL with Panache query methods
  6. Use @Transactional from jakarta.transaction package (not Spring)
  7. 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

  1. Replace @PreAuthorize with @RolesAllowed
  2. Inject SecurityIdentity instead of using SecurityContextHolder
  3. Use securityIdentity.principal for user info
  4. Use securityIdentity.hasRole() for role checks
  5. Use securityIdentity.roles to get all roles
  6. 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

  1. Use application.properties format (YAML supported but properties preferred)
  2. Replace spring.* properties with quarkus.* equivalents
  3. Use profile prefixes (%dev, %test, %prod) instead of separate files
  4. Environment variables use same name with _ instead of . (automatic mapping)
  5. 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

  1. Replace @Async with @RunOnVirtualThread (for blocking I/O) or Uni<T> (reactive)
  2. Replace CompletableFuture with Uni<T> for reactive programming
  3. Replace ApplicationEventPublisher with CDI Event<T>
  4. Replace @EventListener with @Observes (sync) or @ObservesAsync (async)
  5. 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

  1. Replace @Component with @ApplicationScoped
  2. Cron expressions remain the same
  3. Use every parameter instead of fixedRate for intervals
  4. Import from io.quarkus.scheduler package
  5. 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

  1. Replace @SpringBootTest with @QuarkusTest
  2. Replace MockMvc with RestAssured (static imports from io.restassured.RestAssured.*)
  3. Replace @Transactional (test) with @TestTransaction for automatic rollback
  4. Use given().when().then() pattern for REST API testing
  5. Quarkus starts once for all tests (faster execution)
  6. Use @QuarkusTestResource for 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

  1. Validation annotations remain the same (Hibernate Validator)
  2. No need for BindingResult parameter - validation is automatic
  3. ConstraintViolationException thrown on validation failure
  4. Create ExceptionMapper to 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

  1. Replace Spring Retry with MicroProfile Fault Tolerance
  2. Replace @Retryable with @Retry
  3. Replace @Recover with @Fallback
  4. 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

  1. Replace RabbitTemplate with Emitter<T> (SmallRye Reactive Messaging)
  2. Replace @RabbitListener with @Incoming
  3. Add @Blocking for blocking message processing
  4. Configure messaging in application.properties instead of Java config
  5. 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.gradle to use Quarkus BOM and extensions
  • Replace Spring annotations with CDI/JAX-RS equivalents
  • Convert @RestController classes to JAX-RS @Path resources
  • Update repository interfaces to use Panache
  • Replace .save() with .persist(), .findById() with .findByIdOptional()
  • Convert application.yml to application.properties with quarkus.* prefix
  • Update @PreAuthorize to @RolesAllowed
  • Inject SecurityIdentity instead of using SecurityContextHolder
  • Replace ApplicationEventPublisher with CDI Event<T>
  • Convert @Async to @RunOnVirtualThread or Uni<T>
  • Update test classes from @SpringBootTest to @QuarkusTest
  • Replace MockMvc with RestAssured
  • 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 package and java -jar target/quarkus-app/quarkus-run.jar
  • Build native image: ./mvnw package -Dnative

Next Steps

  1. Review Quarkus Features Guide for platform advantages
  2. Study Quarkus Security RBAC Implementation
  3. Read Testing Strategy for Quarkus test patterns
  4. Check Quarkus Deployment Guide for deployment options

Additional Resources


Cross-References


Change Log

Version Date Author Changes
1.0 2026-01-13 Ralph Agent Initial migration guide for Spring Boot to Quarkus conversion