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-extensionrather than editing pom.xml manually - •Run
./mvnw quarkus:devfor live reload; never restart manually during development - •Use Dev Services (auto-provisioned containers) for databases, Kafka, Redis in dev/test
- •Keep
application.propertiesprofile-aware:%dev.,%test.,%prod.prefixes
CDI (Contexts and Dependency Injection)
- •Use
@ApplicationScopedfor stateless services (one instance per app) - •Use
@RequestScopedonly when you need per-request state - •Prefer constructor injection or
@Injectfields (constructor preferred for testability) - •Avoid
@Dependentscope unless you need a new instance per injection point - •Use
@Startupfor beans that must initialize eagerly at boot - •Register beans for native with
@RegisterForReflectiononly when reflection is required - •Produce CDI beans via
@Producesmethods for third-party objects
RESTEasy Reactive Resources
- •Use RESTEasy Reactive (
quarkus-resteasy-reactive), not classic RESTEasy - •Return
Uni<T>orMulti<T>for non-blocking I/O; plain types for blocking is acceptable - •Keep resource classes thin: validate input, delegate to service, map response
- •Use
@Validon request parameters for Bean Validation - •Annotate with
@Produces(APPLICATION_JSON)and@Consumes(APPLICATION_JSON) - •Use
@Pathversioning:/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 withhibernate-reactive-panachefor non-blocking DB access - •Add custom finder methods on entity or repository (e.g.,
findByEmail) - •Use
@WithTransactionon service methods that write, not on resources - •Set
quarkus.hibernate-orm.database.generation=validateand manage schema via Flyway - •Always provide
@PrePersist/@PreUpdatefor audit timestamps
Configuration
- •Use
application.propertieswith profile prefixes:%dev.,%test.,%prod. - •Inject config values with
@ConfigProperty(name = "key", defaultValue = "fallback") - •Group related config with
@ConfigMappinginterfaces - •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
@Providerclasses implementingExceptionMapper<T> - •Return structured error bodies:
{ title, status, detail, timestamp } - •Map
ConstraintViolationExceptionto 400 with per-field error details - •Log at WARN for client errors, ERROR for unexpected failures
Native Build Compatibility
- •Avoid runtime reflection (use
@RegisterForReflectionwhen unavoidable) - •Avoid dynamic proxies and runtime bytecode generation
- •Test native build regularly:
./mvnw verify -Pnative - •Use
@BuildStepextensions for build-time processing - •Prefer compile-time DI and annotation processors (MapStruct, Panache)
- •Use
quarkus.native.container-build=trueto build without local GraalVM
Project Structure
code
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 withcomponentModel = "cdi" - •
exception/-- Domain exceptions and JAX-RS ExceptionMapper providers - •
health/-- MicroProfile HealthCheck implementations
CDI Pattern
java
@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
java
@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
java
@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
java
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
java
@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
properties
# 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
@QuarkusTestfor integration tests (full CDI + HTTP stack) - •Use
@InjectMockto 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
@QuarkusTestResourcewith Testcontainers for DB integration tests - •Test native with
./mvnw verify -Pnative(runs@NativeImageTestclasses) - •Use
@DisplayNamefor readable test names describing behavior
Integration Test
java
@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
bash
# 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
@WithTransactionon 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
@Blockingannotation if needed) - •Use runtime reflection without
@RegisterForReflection - •Hardcode configuration values (use
@ConfigPropertyor@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