Quarkus #

When microservices and containers became the standard of modern architecture, the weaknesses of the traditional JVM started feeling more real: startup times of several seconds, large memory consumption even for small services, and warm-up time before JIT compilation reaches optimal performance. Quarkus was designed by Red Hat specifically to answer these challenges. Instead of doing reflection and class loading at runtime like typical Java frameworks, Quarkus moves most of the framework work to compile time — dependency injection, configuration, and annotation metadata are processed at build time, not when the application first starts. The results are dramatic: startup in milliseconds, much lower memory consumption, and with GraalVM Native Image, a Java application can be compiled into a native binary that runs without a JVM at all. Quarkus also doesn’t introduce new APIs — it uses existing industry standards: Jakarta EE (JAX-RS, CDI, JPA) and MicroProfile (Config, Health, Metrics, OpenAPI).

The Quarkus Philosophy: Shift Left #

The term “shift left” in the Quarkus context means moving as much work as possible from runtime to build time. This is the fundamental difference from how traditional Java frameworks work.

flowchart TD
    subgraph TRADITIONAL[Traditional Frameworks — Runtime Heavy]
        A1[Startup] --> B1[Scan classpath]
        B1 --> C1[Process annotations]
        C1 --> D1[Create proxies]
        D1 --> E1[Wire dependencies]
        E1 --> F1[Ready to serve requests]
        note1["⏱ Startup: 5-15 seconds\n💾 Memory: 200-500 MB"]
    end

    subgraph QUARKUS[Quarkus — Build-Time Optimization]
        A2[Build Time] --> B2[Scan classpath]
        B2 --> C2[Process annotations]
        C2 --> D2[Create proxies]
        D2 --> E2[Generate optimized bytecode]

        A3[Runtime Startup] --> F3[Load pre-computed metadata]
        F3 --> G3[Ready to serve requests]
        note2["⏱ Startup: 0.3-1 second\n💾 Memory: 50-150 MB"]
    end

JVM Mode vs Native Mode #

Quarkus can run in two modes with different tradeoffs:

JVM ModeNative Mode
Build commandmvn packagemvn package -Pnative
Startup time~0.5–2 seconds~10–50 milliseconds
Initial memory~100–200 MB~30–80 MB
Peak throughputVery high (JIT)High (AOT, no JIT warm-up)
Build timeFast (~10 seconds)Long (2–5 minutes, needs GraalVM)
DebuggingNormalLimited
Best forLong-running servicesServerless, CLI, short-lived containers
flowchart TD
    A{"Service runs\ncontinuously?"} -- Yes --> B{"Is startup time\ncritical?"}
    A -- No --> NATIVE["Native Mode\nServerless / FaaS"]

    B -- Yes --> NATIVE
    B -- No --> C{"Is developer\nexperience a priority?"}

    C -- Yes --> JVM["JVM Mode\neasier to debug"]
    C -- No --> D{"Is memory footprint\ncritical?"}

    D -- Yes --> NATIVE
    D -- No --> JVM

Project Setup #

The easiest way to create a new Quarkus project is through code.quarkus.io or the CLI:

# Install the Quarkus CLI
curl -Ls https://sh.jbang.dev | bash -s - trust add https://repo1.maven.org/maven2/io/quarkus/quarkus-cli/
curl -Ls https://sh.jbang.dev | bash -s - app install --fresh --verbose quarkus@quarkusio

# Create a new project
quarkus create app com.example:online-store \
  --extension='rest,rest-jackson,hibernate-orm-panache,jdbc-postgresql,smallrye-openapi'

# Enter the directory and run dev mode (hot reload)
cd online-store
quarkus dev
# or: ./mvnw quarkus:dev

Alternative with Maven:

mvn io.quarkus.platform:quarkus-maven-plugin:3.11.0:create \
  -DprojectGroupId=com.example \
  -DprojectArtifactId=online-store \
  -Dextensions="rest,rest-jackson,hibernate-orm-panache,jdbc-postgresql"

The Quarkus project directory structure:

online-store/
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/example/
│   │   │       ├── model/          ← Panache entities
│   │   │       ├── resource/       ← JAX-RS resources (controllers)
│   │   │       └── service/        ← business logic
│   │   └── resources/
│   │       ├── application.properties   ← configuration
│   │       └── import.sql               ← initial data (optional)
│   └── test/
│       └── java/
│           └── com/example/
├── pom.xml
└── src/main/docker/
    ├── Dockerfile.jvm       ← JVM mode Docker image
    └── Dockerfile.native    ← native mode Docker image

The key pom.xml file:

<properties>
    <quarkus.platform.version>3.11.0</quarkus.platform.version>
    <maven.compiler.source>21</maven.compiler.source>
    <maven.compiler.target>21</maven.compiler.target>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.quarkus.platform</groupId>
            <artifactId>quarkus-bom</artifactId>
            <version>${quarkus.platform.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <!-- REST API with Jackson JSON -->
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-rest</artifactId>
    </dependency>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-rest-jackson</artifactId>
    </dependency>

    <!-- ORM with Panache (a Hibernate wrapper) -->
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-hibernate-orm-panache</artifactId>
    </dependency>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-jdbc-postgresql</artifactId>
    </dependency>

    <!-- Validation -->
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-hibernate-validator</artifactId>
    </dependency>

    <!-- Health checks and metrics (MicroProfile) -->
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-smallrye-health</artifactId>
    </dependency>

    <!-- OpenAPI / Swagger UI -->
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-smallrye-openapi</artifactId>
    </dependency>

    <!-- Testing -->
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-junit5</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>io.rest-assured</groupId>
        <artifactId>rest-assured</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

Configuration #

Quarkus uses src/main/resources/application.properties with MicroProfile Config support. Quarkus also natively supports .env files and environment variables.

# ===== Datasource =====
quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=postgres
quarkus.datasource.password=secret
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/onlinestore

# Connection pool (Agroal)
quarkus.datasource.jdbc.max-size=10
quarkus.datasource.jdbc.min-size=2

# ===== Hibernate ORM =====
quarkus.hibernate-orm.database.generation=validate
# none      → don't change the schema
# validate  → validate the schema (production)
# update    → update the schema (development)
# drop-and-create → reset the schema (test)

quarkus.hibernate-orm.log.sql=false

# ===== HTTP =====
quarkus.http.port=8080
quarkus.http.cors=true
quarkus.http.cors.origins=https://frontend.example.com,http://localhost:3000
quarkus.http.cors.methods=GET,POST,PUT,DELETE,OPTIONS

# ===== OpenAPI =====
quarkus.smallrye-openapi.info-title=Online Store API
quarkus.smallrye-openapi.info-version=1.0.0
mp.openapi.extensions.smallrye.operationIdStrategy=METHOD
quarkus.swagger-ui.always-include=true  # show Swagger UI in production (disable if not needed)

# ===== Logging =====
quarkus.log.level=INFO
quarkus.log.category."com.example".level=DEBUG

# ===== Native build =====
quarkus.native.additional-build-args=-H:ResourceConfigurationFiles=resources-config.json

Custom Configuration with @ConfigProperty #

import org.eclipse.microprofile.config.inject.ConfigProperty;

import jakarta.enterprise.context.ApplicationScoped;
import java.util.Optional;

@ApplicationScoped
public class AppConfig {

    // Required property — the app fails to start if it's missing
    @ConfigProperty(name = "app.name")
    String appName;

    // Property with a default value
    @ConfigProperty(name = "app.page-size", defaultValue = "20")
    int pageSize;

    // Optional property
    @ConfigProperty(name = "app.api-key")
    Optional<String> apiKey;

    // Property from an environment variable: APP_UPLOAD_DIR
    // (dots and hyphens in property names are converted to underscores)
    @ConfigProperty(name = "app.upload.dir", defaultValue = "/tmp/uploads")
    String uploadDir;

    public String getAppName() { return appName; }
    public int getPageSize() { return pageSize; }
    public Optional<String> getApiKey() { return apiKey; }
    public String getUploadDir() { return uploadDir; }
}
# application.properties
app.name=Online Store
app.page-size=25
app.upload.dir=/data/uploads
# app.api-key= (optional, no need to define it)

Profiles in Quarkus #

# Applies to all profiles
quarkus.datasource.db-kind=postgresql

# dev profile — active during quarkus dev
%dev.quarkus.datasource.db-kind=h2
%dev.quarkus.datasource.jdbc.url=jdbc:h2:mem:testdb
%dev.quarkus.hibernate-orm.database.generation=drop-and-create
%dev.quarkus.hibernate-orm.log.sql=true

# test profile — active during mvn test
%test.quarkus.datasource.db-kind=h2
%test.quarkus.datasource.jdbc.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
%test.quarkus.hibernate-orm.database.generation=drop-and-create

# prod profile — active in production
%prod.quarkus.datasource.jdbc.url=jdbc:postgresql://prod-db:5432/onlinestore
%prod.quarkus.hibernate-orm.database.generation=validate

Dependency Injection with CDI #

Quarkus uses CDI (Contexts and Dependency Injection) — the Jakarta EE standard — as its dependency injection mechanism.

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;

// @ApplicationScoped — one instance for the whole application (thread-safe!)
// @Singleton       — similar to ApplicationScoped, slightly lighter (no proxy)
// @RequestScoped   — one instance per HTTP request
// @SessionScoped   — one instance per HTTP session

@ApplicationScoped
public class ProductService {

    // Inject with @Inject — constructor injection is more recommended
    @Inject
    ProductRepository productRepository;

    // Constructor injection — more explicit and easier to test
    // (both are valid in Quarkus)
    private final NotificationService notificationService;

    public ProductService(NotificationService notificationService) {
        this.notificationService = notificationService;
    }

    public java.util.List<Product> getAllProducts() {
        return productRepository.listAll();
    }
}

Qualifiers and Alternatives #

import jakarta.enterprise.inject.Produces;
import jakarta.enterprise.inject.Alternative;
import jakarta.inject.Qualifier;

import java.lang.annotation.*;

// Custom qualifier definition
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE})
public @interface EmailProduction {}

// Production implementation
@ApplicationScoped
@EmailProduction
public class SmtpEmailService implements EmailService {
    @Override
    public void send(String to, String subject, String body) {
        // real SMTP implementation
        System.out.println("Email sent via SMTP to: " + to);
    }
}

// Mock implementation for development/test
@Alternative
@io.quarkus.arc.profile.IfBuildProfile("dev")
@ApplicationScoped
public class MockEmailService implements EmailService {
    @Override
    public void send(String to, String subject, String body) {
        System.out.println("[MOCK] Email to " + to + ": " + subject);
    }
}

REST Resources with JAX-RS #

Quarkus uses JAX-RS (Jakarta RESTful Web Services) as the standard for defining REST endpoints. The API is very similar to Spring MVC but uses different annotations.

package com.example.resource;

import com.example.model.Product;
import com.example.service.ProductService;

import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;

import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;

import java.net.URI;
import java.util.List;

@Path("/api/products")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Product", description = "Product data management")
public class ProductResource {

    @Inject
    ProductService productService;

    // GET /api/products
    @GET
    @Operation(summary = "Get all products")
    public List<Product> getAllProducts() {
        return productService.getAllProducts();
    }

    // GET /api/products/{id}
    @GET
    @Path("/{id}")
    @Operation(summary = "Get a product by ID")
    public Response getProduct(@PathParam("id") Long id) {
        return productService.findById(id)
            .map(product -> Response.ok(product).build())
            .orElse(Response.status(Response.Status.NOT_FOUND)
                .entity(new ErrorResponse("Product with ID " + id + " not found"))
                .build());
    }

    // GET /api/products/search?keyword=laptop
    @GET
    @Path("/search")
    public List<Product> search(@QueryParam("keyword") String keyword,
                                 @QueryParam("page") @DefaultValue("0") int page,
                                 @QueryParam("size") @DefaultValue("20") int size) {
        return productService.searchByName(keyword, page, size);
    }

    // POST /api/products
    @POST
    @Operation(summary = "Create a new product")
    public Response createProduct(@Valid ProductRequest request) {
        Product product = productService.create(request);
        // Return 201 Created with a Location header to the new resource
        return Response.created(URI.create("/api/products/" + product.id))
            .entity(product)
            .build();
    }

    // PUT /api/products/{id}
    @PUT
    @Path("/{id}")
    public Response updateProduct(@PathParam("id") Long id,
                                    @Valid ProductRequest request) {
        return productService.update(id, request)
            .map(product -> Response.ok(product).build())
            .orElse(Response.status(Response.Status.NOT_FOUND).build());
    }

    // DELETE /api/products/{id}
    @DELETE
    @Path("/{id}")
    public Response deleteProduct(@PathParam("id") Long id) {
        boolean success = productService.delete(id);
        return success
            ? Response.noContent().build()
            : Response.status(Response.Status.NOT_FOUND).build();
    }
}

// Request DTO
public class ProductRequest {
    @jakarta.validation.constraints.NotBlank(message = "Name must not be empty")
    @jakarta.validation.constraints.Size(min = 3, max = 100)
    public String name;

    @jakarta.validation.constraints.NotNull
    @jakarta.validation.constraints.DecimalMin("0.01")
    public java.math.BigDecimal price;

    @jakarta.validation.constraints.Min(0)
    public int stock;
}

// Error response DTO
public record ErrorResponse(String message) {}

Database Access with Panache #

Panache is Quarkus’s abstraction over Hibernate ORM that eliminates boilerplate. There are two approaches: Active Record (an entity that can query itself) and Repository.

The Active Record Pattern #

package com.example.model;

import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.*;
import jakarta.validation.constraints.*;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;

@Entity
@Table(name = "products")
public class Product extends PanacheEntity {
    // PanacheEntity already provides the `id` field (Long, auto-generated)
    // and all static query methods

    @NotBlank
    @Size(max = 100)
    @Column(nullable = false, length = 100)
    public String name;

    @NotNull
    @DecimalMin("0.01")
    @Column(nullable = false, precision = 15, scale = 2)
    public BigDecimal price;

    @Min(0)
    @Column(nullable = false)
    public int stock;

    @Column(nullable = false)
    public boolean active = true;

    @Column(name = "created_at")
    public LocalDateTime createdAt;

    // Lifecycle callback
    @PrePersist
    public void preSave() {
        createdAt = LocalDateTime.now();
    }

    // ===== Query Methods — defined inside the entity =====

    // Get all active products, ordered by name
    public static List<Product> allActive() {
        return list("active = true ORDER BY name");
    }

    // Search by name (partial, case-insensitive)
    public static List<Product> searchByName(String keyword) {
        return list("lower(name) like lower(?1)", "%" + keyword + "%");
    }

    // Low-stock products
    public static List<Product> lowStock(int threshold) {
        return list("stock <= ?1 AND active = true ORDER BY stock asc", threshold);
    }

    // Count active products
    public static long countActive() {
        return count("active = true");
    }

    // Bulk stock update (more efficient than updating one by one)
    public static long addStockToAll(int amount) {
        return update("stock = stock + ?1 WHERE active = true", amount);
    }
}

The Repository Pattern #

package com.example.repository;

import com.example.model.Product;
import io.quarkus.hibernate.orm.panache.PanacheRepository;

import jakarta.enterprise.context.ApplicationScoped;
import java.util.List;

@ApplicationScoped
public class ProductRepository implements PanacheRepository<Product> {
    // PanacheRepository provides: findById, listAll, persist, delete, count, etc.

    public List<Product> searchByName(String keyword) {
        return list("lower(name) like lower(?1)", "%" + keyword + "%");
    }

    public List<Product> lowStock(int threshold) {
        return list("stock <= ?1 AND active = true", threshold);
    }

    // Pagination
    public List<Product> allWithPagination(int page, int size) {
        return findAll()
            .page(page, size)
            .list();
    }

    public long totalPages(int pageSize) {
        return findAll().pageCount(pageSize);
    }
}

Services with Transactions #

package com.example.service;

import com.example.model.Product;
import com.example.repository.ProductRepository;
import com.example.resource.ProductRequest;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.NotFoundException;

import java.util.List;
import java.util.Optional;

@ApplicationScoped
public class ProductService {

    @Inject
    ProductRepository productRepository;

    // @Transactional — opens a transaction at the start, commits at the end (or rolls back on exception)
    // Methods without @Transactional don't need a transaction (read-only is safe)
    public List<Product> getAllProducts() {
        return productRepository.listAll();
    }

    public Optional<Product> findById(Long id) {
        return productRepository.findByIdOptional(id);
    }

    public List<Product> searchByName(String keyword, int page, int size) {
        if (keyword == null || keyword.isBlank()) {
            return productRepository.allWithPagination(page, size);
        }
        return productRepository.searchByName(keyword);
    }

    @Transactional
    public Product create(ProductRequest request) {
        Product product = new Product();
        product.name = request.name;
        product.price = request.price;
        product.stock = request.stock;
        productRepository.persist(product);
        return product;
    }

    @Transactional
    public Optional<Product> update(Long id, ProductRequest request) {
        return productRepository.findByIdOptional(id).map(product -> {
            product.name = request.name;
            product.price = request.price;
            product.stock = request.stock;
            // No need to call persist/update — Hibernate detects changes
            // on managed entities automatically (dirty checking)
            return product;
        });
    }

    @Transactional
    public boolean delete(Long id) {
        return productRepository.deleteById(id);
    }

    @Transactional
    public void decreaseStock(Long id, int amount) {
        Product product = productRepository.findByIdOptional(id)
            .orElseThrow(() -> new NotFoundException("Product not found: " + id));

        if (product.stock < amount) {
            throw new IllegalStateException(
                "Insufficient stock. Available: " + product.stock + ", requested: " + amount);
        }

        product.stock -= amount;
    }
}

Global Exception Handling #

import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import jakarta.validation.ConstraintViolationException;

// @Provider registers this class as a JAX-RS provider (no other annotation needed)
@Provider
public class ValidationExceptionMapper
        implements ExceptionMapper<ConstraintViolationException> {

    @Override
    public Response toResponse(ConstraintViolationException exception) {
        var errors = exception.getConstraintViolations().stream()
            .collect(java.util.stream.Collectors.toMap(
                cv -> cv.getPropertyPath().toString(),
                cv -> cv.getMessage()
            ));

        var body = java.util.Map.of(
            "status", 400,
            "message", "Validation failed",
            "errors", errors
        );

        return Response.status(Response.Status.BAD_REQUEST)
            .entity(body)
            .build();
    }
}

@Provider
public class NotFoundExceptionMapper
        implements ExceptionMapper<jakarta.ws.rs.NotFoundException> {

    @Override
    public Response toResponse(jakarta.ws.rs.NotFoundException exception) {
        return Response.status(Response.Status.NOT_FOUND)
            .entity(java.util.Map.of("message", exception.getMessage()))
            .build();
    }
}

@Provider
public class GenericExceptionMapper implements ExceptionMapper<Exception> {

    @Override
    public Response toResponse(Exception exception) {
        // Log the error for internal tracking
        java.util.logging.Logger.getLogger(getClass().getName())
            .severe("Unhandled exception: " + exception.getMessage());

        return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
            .entity(java.util.Map.of("message", "An internal error occurred"))
            .build();
    }
}

Health Checks with MicroProfile Health #

import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
import org.eclipse.microprofile.health.Liveness;
import org.eclipse.microprofile.health.Readiness;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

// Liveness — is the application still alive? (restart if it fails)
@Liveness
@ApplicationScoped
public class LivenessCheck implements HealthCheck {

    @Override
    public HealthCheckResponse call() {
        return HealthCheckResponse.up("app-liveness");
    }
}

// Readiness — is the application ready to receive requests?
// (remove from the load balancer if it fails, don't restart)
@Readiness
@ApplicationScoped
public class DatabaseReadinessCheck implements HealthCheck {

    @Inject
    jakarta.inject.Provider<javax.sql.DataSource> dataSourceProvider;

    @Override
    public HealthCheckResponse call() {
        try {
            javax.sql.DataSource ds = dataSourceProvider.get();
            try (var conn = ds.getConnection();
                 var stmt = conn.createStatement()) {
                stmt.execute("SELECT 1");
                return HealthCheckResponse.named("database-readiness")
                    .up()
                    .withData("database", "PostgreSQL")
                    .withData("status", "connected")
                    .build();
            }
        } catch (Exception e) {
            return HealthCheckResponse.named("database-readiness")
                .down()
                .withData("error", e.getMessage())
                .build();
        }
    }
}

The health endpoints are available automatically:

# Liveness check — Kubernetes livenessProbe
curl http://localhost:8080/q/health/live

# Readiness check — Kubernetes readinessProbe
curl http://localhost:8080/q/health/ready

# All health checks
curl http://localhost:8080/q/health

# Example response
{
  "status": "UP",
  "checks": [
    { "name": "app-liveness", "status": "UP" },
    { "name": "database-readiness", "status": "UP", "data": { "database": "PostgreSQL" } }
  ]
}

Testing #

Quarkus provides @QuarkusTest, which runs the full application in test mode, and REST-Assured for testing HTTP endpoints.

import io.quarkus.test.junit.QuarkusTest;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.Test;

import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;

// @QuarkusTest — starts the full application with the %test configuration
@QuarkusTest
public class ProductResourceTest {

    @Test
    public void getAllProducts_withoutData_returnsEmptyArray() {
        given()
            .when().get("/api/products")
            .then()
            .statusCode(200)
            .contentType(ContentType.JSON)
            .body("", hasSize(0));
    }

    @Test
    public void createProduct_withValidData_returns201() {
        String body = """
            {
                "name": "Gaming Laptop",
                "price": 15000000,
                "stock": 5
            }
            """;

        given()
            .contentType(ContentType.JSON)
            .body(body)
            .when().post("/api/products")
            .then()
            .statusCode(201)
            .header("Location", containsString("/api/products/"))
            .body("name", equalTo("Gaming Laptop"))
            .body("id", greaterThan(0));
    }

    @Test
    public void createProduct_emptyName_returns400() {
        String body = """
            { "name": "", "price": 100000, "stock": 1 }
            """;

        given()
            .contentType(ContentType.JSON)
            .body(body)
            .when().post("/api/products")
            .then()
            .statusCode(400)
            .body("errors", hasKey("name"));
    }

    @Test
    public void getProduct_nonexistentId_returns404() {
        given()
            .when().get("/api/products/999")
            .then()
            .statusCode(404);
    }
}

Mock Beans for Testing #

import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.mockito.InjectMock;
import org.mockito.Mockito;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.Optional;

@QuarkusTest
public class ProductResourceMockTest {

    // InjectMock replaces the bean with a Mockito mock
    @InjectMock
    ProductService productService;

    @BeforeEach
    public void setup() {
        Product laptop = new Product();
        laptop.id = 1L;
        laptop.name = "Laptop";
        laptop.price = new java.math.BigDecimal("15000000");

        Mockito.when(productService.getAllProducts()).thenReturn(List.of(laptop));
        Mockito.when(productService.findById(1L)).thenReturn(Optional.of(laptop));
        Mockito.when(productService.findById(99L)).thenReturn(Optional.empty());
    }

    @Test
    public void getAllProducts_returnsMockData() {
        given()
            .when().get("/api/products")
            .then()
            .statusCode(200)
            .body("size()", equalTo(1))
            .body("[0].name", equalTo("Laptop"));
    }
}

Building Native Images #

# Make sure GraalVM is installed with native-image
# Export GRAALVM_HOME and JAVA_HOME to GraalVM

# Build a native image (takes ~2-5 minutes)
./mvnw package -Pnative

# Or with a container (no local GraalVM needed — Docker must be running)
./mvnw package -Pnative -Dquarkus.native.container-build=true

# Run the native binary
./target/online-store-1.0-SNAPSHOT-runner

# Build a Docker image for native
docker build -f src/main/docker/Dockerfile.native -t online-store:native .
docker run -i --rm -p 8080:8080 online-store:native

Real startup time comparison:

# JVM mode
java -jar target/quarkus-app/quarkus-run.jar
→ Quarkus 3.x.x started in 0.852s

# Native mode
./target/online-store-runner
→ Quarkus 3.x.x started in 0.018s  ← 47x faster

# Spring Boot (JVM)
java -jar target/spring-boot-app.jar
→ Started Application in 3.421s
Native compilation has limitations: some Java features that rely on reflection (like custom serialization, dynamic proxies, or class loading) require additional configuration (reflect-config.json). Third-party libraries that aren’t yet “Quarkus-aware” may not be compatible with native compilation.

When to Use Quarkus and When Not To #

USE QUARKUS WHEN:
  ✓ Serverless or FaaS — millisecond startup times are critical
  ✓ Container-dense environments — small memory footprint = more pods
  ✓ Small microservices deployed and restarted frequently
  ✓ The team is already familiar with Jakarta EE / MicroProfile
  ✓ You need native compilation for binaries that run without a JVM
  ✓ Kubernetes-native — health check, metrics, and config integration is very easy
  ✓ Dev mode with very fast live coding (instant hot reload)

CONSIDER ALTERNATIVES WHEN:
  ✗ The team is more familiar with Spring — Spring Boot has a larger ecosystem
  ✗ Many third-party libraries that aren't Quarkus-compatible yet
  ✗ You need native compilation but use a lot of reflection → complex configuration
  ✗ Large monolithic applications — Quarkus's advantages are less noticeable
  ✗ The team needs a very wide community — Spring is still larger

Summary #

  • Quarkus moves framework work to build time — dependency injection, annotation processing, and configuration are resolved at compilation, not startup. The result is millisecond startup and smaller memory.
  • JVM mode vs Native mode is a throughput vs startup/memory tradeoff — use JVM mode for long-running services, Native mode for serverless and frequently restarted containers.
  • Quarkus uses industry standards: JAX-RS for REST, CDI for dependency injection, Hibernate/Panache for ORM, MicroProfile for config/health/metrics — no new proprietary APIs to learn.
  • Panache eliminates Hibernate boilerplate — choose Active Record for simplicity (Product.list(...)) or the Repository pattern for cleaner separation of concerns.
  • @Transactional on service methods that write — Hibernate automatically detects changes on managed entities (dirty checking) and executes SQL when the transaction commits.
  • MicroProfile Health (@Liveness, @Readiness) integrates directly with Kubernetes probes — no additional configuration needed in deployment YAML.
  • @QuarkusTest with REST-Assured is the standard combination for integration tests — the application runs fully with the %test profile pointing to an in-memory H2 database.
  • quarkus dev is the best dev mode in the Java ecosystem — code changes are applied instantly without restarts, and the Dev UI is available at http://localhost:8080/q/dev.

← Previous: Play Framework   Next: Hibernate →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact