Quarkus Guide
Applies to: Quarkus 3.x, Java 17+, Cloud-Native Microservices, GraalVM Native, Kubernetes
Core Principles
-
Kubernetes-Native: Designed for containers, fast startup, low memory footprint
-
Developer Joy: Live reload via Dev Services, Dev UI, continuous testing
-
Unified Reactive/Imperative: Choose per-endpoint, both coexist in one app
-
Build-Time Optimization: Extensions process metadata at build time, not runtime
-
GraalVM First-Class: Native compilation with millisecond startup and minimal RSS
-
Standards-Based: MicroProfile, Jakarta EE, Vert.x, SmallRye under the hood
Guardrails
Project & Dependencies
-
Use Quarkus BOM for version management (never pin individual Quarkus artifact versions)
-
Add extensions via ./mvnw quarkus:add-extension rather than editing pom.xml manually
-
Run ./mvnw quarkus:dev for live reload; never restart manually during development
-
Use Dev Services (auto-provisioned containers) for databases, Kafka, Redis in dev/test
-
Keep application.properties profile-aware: %dev. , %test. , %prod. prefixes
CDI (Contexts and Dependency Injection)
-
Use @ApplicationScoped for stateless services (one instance per app)
-
Use @RequestScoped only when you need per-request state
-
Prefer constructor injection or @Inject fields (constructor preferred for testability)
-
Avoid @Dependent scope unless you need a new instance per injection point
-
Use @Startup for beans that must initialize eagerly at boot
-
Register beans for native with @RegisterForReflection only when reflection is required
-
Produce CDI beans via @Produces methods for third-party objects
RESTEasy Reactive Resources
-
Use RESTEasy Reactive (quarkus-resteasy-reactive ), not classic RESTEasy
-
Return Uni<T> or Multi<T> for non-blocking I/O; plain types for blocking is acceptable
-
Keep resource classes thin: validate input, delegate to service, map response
-
Use @Valid on request parameters for Bean Validation
-
Annotate with @Produces(APPLICATION_JSON) and @Consumes(APPLICATION_JSON)
-
Use @Path versioning: /api/v1/resource
-
Add OpenAPI annotations (@Operation , @APIResponse ) on every endpoint
-
Secure endpoints with @RolesAllowed , @Authenticated , or @PermitAll
Panache ORM
-
Active Record (extends PanacheEntity ): good for simple CRUD entities
-
Repository (implements PanacheRepository<T> ): good for complex queries, better testability
-
Use Uni<T> return types with hibernate-reactive-panache for non-blocking DB access
-
Add custom finder methods on entity or repository (e.g., findByEmail )
-
Use @WithTransaction on service methods that write, not on resources
-
Set quarkus.hibernate-orm.database.generation=validate and manage schema via Flyway
-
Always provide @PrePersist / @PreUpdate for audit timestamps
Configuration
-
Use application.properties with profile prefixes: %dev. , %test. , %prod.
-
Inject config values with @ConfigProperty(name = "key", defaultValue = "fallback")
-
Group related config with @ConfigMapping interfaces
-
Never hardcode secrets; use ${ENV_VAR:default} placeholder syntax
-
Enable Dev Services for local containers: quarkus.devservices.enabled=true
Error Handling
-
Create domain exceptions: ResourceNotFoundException , DuplicateResourceException
-
Register JAX-RS @Provider classes implementing ExceptionMapper<T>
-
Return structured error bodies: { title, status, detail, timestamp }
-
Map ConstraintViolationException to 400 with per-field error details
-
Log at WARN for client errors, ERROR for unexpected failures
Native Build Compatibility
-
Avoid runtime reflection (use @RegisterForReflection when unavoidable)
-
Avoid dynamic proxies and runtime bytecode generation
-
Test native build regularly: ./mvnw verify -Pnative
-
Use @BuildStep extensions for build-time processing
-
Prefer compile-time DI and annotation processors (MapStruct, Panache)
-
Use quarkus.native.container-build=true to build without local GraalVM
Project Structure
myproject/ ├── src/main/java/com/example/ │ ├── resource/ # JAX-RS endpoints (thin controllers) │ │ ├── UserResource.java │ │ └── AuthResource.java │ ├── service/ # Business logic │ │ └── UserService.java │ ├── repository/ # Panache repositories (optional if using active record) │ │ └── UserRepository.java │ ├── entity/ # JPA entities with Panache │ │ └── User.java │ ├── dto/ # Request/response records │ │ ├── UserRequest.java │ │ └── UserResponse.java │ ├── mapper/ # MapStruct mappers (compile-time, CDI-scoped) │ │ └── UserMapper.java │ ├── exception/ # Domain exceptions + ExceptionMappers │ │ ├── ResourceNotFoundException.java │ │ ├── DuplicateResourceException.java │ │ └── ExceptionMappers.java │ └── health/ # MicroProfile Health checks │ └── DatabaseHealthCheck.java ├── src/main/resources/ │ ├── application.properties # Config with profile prefixes │ └── db/migration/ # Flyway SQL migrations │ └── V1__create_users.sql ├── src/main/docker/ │ ├── Dockerfile.jvm │ └── Dockerfile.native ├── src/test/java/com/example/ │ ├── resource/ # @QuarkusTest integration tests │ │ └── UserResourceTest.java │ └── service/ # Unit tests with @InjectMock │ └── UserServiceTest.java └── pom.xml
-
resource/ -- Thin REST endpoints; input validation and response mapping only
-
service/ -- All business logic; transactional boundaries live here
-
repository/ -- Data access (skip if active record pattern suffices)
-
entity/ -- JPA entities; keep domain logic minimal, push to service
-
dto/ -- Java records for request/response; use Bean Validation annotations
-
mapper/ -- MapStruct interfaces with componentModel = "cdi"
-
exception/ -- Domain exceptions and JAX-RS ExceptionMapper providers
-
health/ -- MicroProfile HealthCheck implementations
CDI Pattern
@ApplicationScoped public class UserService {
@Inject
UserRepository userRepository;
@Inject
UserMapper userMapper;
@WithTransaction
public Uni<UserResponse> createUser(UserRequest request) {
return userRepository.existsByEmail(request.email())
.flatMap(exists -> {
if (exists) {
return Uni.createFrom().failure(
new DuplicateResourceException("User", "email", request.email()));
}
User user = userMapper.toEntity(request);
return userRepository.persist(user).map(userMapper::toResponse);
});
}
public Uni<UserResponse> getUserById(Long id) {
return userRepository.findById(id)
.onItem().ifNull().failWith(
() -> new ResourceNotFoundException("User", "id", id))
.map(userMapper::toResponse);
}
}
RESTEasy Resource Pattern
@Path("/api/v1/users") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @Tag(name = "Users", description = "User management APIs") public class UserResource {
@Inject
UserService userService;
@POST
@Operation(summary = "Create a new user")
@APIResponse(responseCode = "201", description = "User created")
public Uni<Response> createUser(@Valid UserRequest request) {
return userService.createUser(request)
.map(user -> Response
.created(URI.create("/api/v1/users/" + user.id()))
.entity(user).build());
}
@GET
@Path("/{id}")
@RolesAllowed({"USER", "ADMIN"})
@SecurityRequirement(name = "jwt")
@Operation(summary = "Get user by ID")
public Uni<UserResponse> getUserById(@PathParam("id") Long id) {
return userService.getUserById(id);
}
}
Panache Entity Pattern
@Entity @Table(name = "users") public class User extends PanacheEntity {
@Column(nullable = false, unique = true)
public String email;
@Column(nullable = false)
public String name;
@Column(name = "created_at", updatable = false)
public LocalDateTime createdAt;
@PrePersist
void onCreate() {
createdAt = LocalDateTime.now();
}
// Panache active record queries
public static Uni<User> findByEmail(String email) {
return find("email", email).firstResult();
}
public static Uni<List<User>> findActive() {
return list("active", true);
}
}
DTO Records with Validation
public record UserRequest( @NotBlank(message = "Email is required") @Email(message = "Invalid email format") String email,
@NotBlank(message = "Password is required")
@Size(min = 8, max = 100, message = "Password must be 8-100 characters")
String password,
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100)
String name
) {}
public record UserResponse( Long id, String email, String name, String role, boolean active, LocalDateTime createdAt ) {}
Exception Mapper Pattern
@Provider public class ResourceNotFoundMapper implements ExceptionMapper<ResourceNotFoundException> {
@Override
public Response toResponse(ResourceNotFoundException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(Map.of(
"title", "Resource Not Found",
"status", 404,
"detail", e.getMessage(),
"timestamp", Instant.now().toString()))
.build();
}
}
Configuration
Application
quarkus.application.name=myproject quarkus.http.port=8080
Reactive datasource
quarkus.datasource.db-kind=postgresql quarkus.datasource.username=${DB_USERNAME:postgres} quarkus.datasource.password=${DB_PASSWORD:postgres} quarkus.datasource.reactive.url=vertx-reactive:postgresql://localhost:5432/mydb
JDBC (for Flyway migrations)
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/mydb
Hibernate
quarkus.hibernate-orm.database.generation=validate quarkus.hibernate-orm.log.sql=true
Flyway
quarkus.flyway.migrate-at-start=true quarkus.flyway.locations=db/migration
Dev Services
%dev.quarkus.datasource.devservices.enabled=true %dev.quarkus.datasource.devservices.image-name=postgres:15-alpine
Logging
quarkus.log.level=INFO quarkus.log.category."com.example".level=DEBUG %prod.quarkus.log.console.json=true
JWT
mp.jwt.verify.publickey.location=publicKey.pem mp.jwt.verify.issuer=https://example.com
OpenAPI / Swagger
quarkus.smallrye-openapi.path=/api-docs quarkus.swagger-ui.always-include=true
Health
quarkus.smallrye-health.root-path=/health
Testing Overview
Standards
-
Use @QuarkusTest for integration tests (full CDI + HTTP stack)
-
Use @InjectMock to mock CDI beans in integration tests
-
Use @TestSecurity(user = "test", roles = "ADMIN") to bypass auth in tests
-
Use REST-assured for HTTP endpoint assertions
-
Use @QuarkusTestResource with Testcontainers for DB integration tests
-
Test native with ./mvnw verify -Pnative (runs @NativeImageTest classes)
-
Use @DisplayName for readable test names describing behavior
Integration Test
@QuarkusTest @DisplayName("UserResource") class UserResourceTest {
@Test
@DisplayName("POST /api/v1/users - should create user")
void shouldCreateUser() {
given()
.contentType(ContentType.JSON)
.body(new UserRequest("test@example.com", "Password1!", "Test"))
.when()
.post("/api/v1/users")
.then()
.statusCode(201)
.body("id", notNullValue())
.body("email", equalTo("test@example.com"));
}
@Test
@TestSecurity(user = "admin", roles = "ADMIN")
@DisplayName("GET /api/v1/users - admin can list users")
void adminCanListUsers() {
given()
.when()
.get("/api/v1/users")
.then()
.statusCode(200);
}
}
Commands
Create project
mvn io.quarkus.platform:quarkus-maven-plugin:3.6.0:create
-DprojectGroupId=com.example
-DprojectArtifactId=myproject
-Dextensions="resteasy-reactive-jackson,hibernate-reactive-panache,reactive-pg-client"
Dev mode (live reload + Dev Services)
./mvnw quarkus:dev
Run tests
./mvnw test
Build JVM jar
./mvnw package
Build native binary
./mvnw package -Pnative
Build native in container (no local GraalVM)
./mvnw package -Pnative -Dquarkus.native.container-build=true
Verify native (integration tests against native binary)
./mvnw verify -Pnative
List available extensions
./mvnw quarkus:list-extensions
Add extension
./mvnw quarkus:add-extension -Dextensions="openapi"
Build container image
./mvnw package -Dquarkus.container-image.build=true
Deploy to Kubernetes
./mvnw package -Dquarkus.kubernetes.deploy=true
Lint / format
./mvnw compile # Runs annotation processors mvn checkstyle:check # If checkstyle configured
Best Practices Summary
Do
-
Use Dev Services for automatic local containers
-
Use Panache for simpler data access patterns
-
Use reactive types (Uni , Multi ) for I/O-bound operations
-
Use @WithTransaction on service methods (not resources)
-
Configure for native compilation early; test regularly
-
Use health checks (@Liveness , @Readiness , @Wellness )
-
Use profile-based configuration (%dev. , %test. , %prod. )
-
Use MapStruct (compile-time) over reflection-based mappers
Do Not
-
Mix blocking calls in reactive pipelines (use @Blocking annotation if needed)
-
Use runtime reflection without @RegisterForReflection
-
Hardcode configuration values (use @ConfigProperty or @ConfigMapping )
-
Use synchronous JDBC APIs with reactive datasources
-
Ignore native build compatibility until deployment time
-
Put business logic in resource classes
Advanced Topics
For detailed patterns and examples, see:
- references/patterns.md -- Reactive pipelines, Hibernate Reactive, testing strategies, GraalVM native, messaging, security, observability
External References
-
Quarkus Documentation
-
Quarkus Extensions
-
Panache Guide
-
RESTEasy Reactive Guide
-
SmallRye JWT
-
Native Compilation
-
Dev Services
-
MicroProfile Health