Quarkus Patterns
REST Resource
@Path("/api/v1/products")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class ProductResource {
@Inject
ProductService productService;
@GET
@Path("/{id}")
public Product getProduct(@PathParam("id") Long id) {
return productService.findById(id)
.orElseThrow(() -> new NotFoundException("Product " + id + " not found"));
}
@POST
@Transactional
public Response createProduct(@Valid CreateProductRequest request) {
Product product = productService.create(request);
return Response.created(URI.create("/api/v1/products/" + product.id)).entity(product).build();
}
@GET
public List<Product> list(@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size) {
return Product.findAll().page(page, size).list();
}
}
Panache Entity (Active Record)
@Entity
@Table(name = "products")
public class Product extends PanacheEntity {
@NotBlank
public String name;
@Positive
public BigDecimal price;
@Enumerated(EnumType.STRING)
public ProductStatus status = ProductStatus.ACTIVE;
public static List<Product> findActive() {
return list("status", ProductStatus.ACTIVE);
}
public static Optional<Product> findBySlug(String slug) {
return find("slug", slug).firstResultOptional();
}
}
Panache Repository
@ApplicationScoped
public class ProductRepository implements PanacheRepository<Product> {
public List<Product> findByCategory(String category) {
return list("category = ?1 AND status = ?2", category, ProductStatus.ACTIVE);
}
public Page<Product> findPaged(int page, int size) {
return findAll().page(Page.of(page, size));
}
}
CDI Scopes & Injection
@ApplicationScoped // singleton per app
@RequestScoped // one per HTTP request
@Dependent // default — lifecycle of injecting bean
@SessionScoped // one per session (CDI sessions)
@ApplicationScoped
public class EmailService {
@Inject
@ConfigProperty(name = "app.smtp.host")
String smtpHost;
@Inject
Event<UserCreatedEvent> userCreatedEvent;
public void sendWelcome(User user) {
userCreatedEvent.fire(new UserCreatedEvent(user));
}
}
Reactive REST with Mutiny
@Path("/api/v1/orders")
public class OrderResource {
@Inject
OrderService orderService;
@GET
public Multi<Order> streamOrders() {
return orderService.streamAll();
}
@POST
public Uni<Response> placeOrder(@Valid PlaceOrderRequest request) {
return orderService.place(request)
.map(order -> Response.created(URI.create("/orders/" + order.id)).entity(order).build());
}
}
Configuration
@ConfigMapping(prefix = "app")
public interface AppConfig {
String name();
DatabaseConfig database();
FeatureFlags features();
interface DatabaseConfig {
String url();
int maxPoolSize();
}
interface FeatureFlags {
boolean newCheckout();
}
}
Health Checks
@Liveness
@ApplicationScoped
public class AppLivenessCheck implements HealthCheck {
@Override
public HealthCheckResponse call() {
return HealthCheckResponse.up("app-live");
}
}
@Readiness
@ApplicationScoped
public class DatabaseReadinessCheck implements HealthCheck {
@Inject
@ConfigProperty(name = "quarkus.datasource.jdbc.url")
String jdbcUrl;
@Override
public HealthCheckResponse call() {
// probe DB
return HealthCheckResponse.named("database").up().build();
}
}
application.properties
quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=${DB_USER}
quarkus.datasource.password=${DB_PASS}
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/mydb
quarkus.hibernate-orm.database.generation=validate
quarkus.http.port=8080
quarkus.log.level=INFO
quarkus.log.category."com.example".level=DEBUG
# Native image
quarkus.native.container-build=true
quarkus.native.builder-image=quay.io/quarkus/ubi-quarkus-mandrel-builder-image:jdk-21
Build Commands
# Dev mode with hot reload
./mvnw quarkus:dev
# JVM build
./mvnw package
# Native build (requires GraalVM or Docker)
./mvnw package -Pnative
./mvnw package -Pnative -Dquarkus.native.container-build=true
Key Rules
- Use
@Transactional on resource methods or service methods that mutate — not on Panache entity static methods
- Prefer
PanacheEntity (Active Record) for simple domains; PanacheRepository when you need to decouple
- All Panache operations outside a transaction throw — always wrap mutations
- Use
Uni/Multi for reactive endpoints; don't block the event loop with synchronous I/O in reactive mode
- Native image: register reflection classes in
@RegisterForReflection; test native before shipping