# Quarkus From Zero

> Learn the cloud-native Java framework built for fast startup and low memory: why Quarkus moves work to build time, its loved dev mode, REST APIs, build-time CDI, Hibernate with Panache, configuration, reactive programming with Mutiny, testing, and compiling to a native executable. The standards you know, made supersonic.


---

# Quarkus From Zero

Quarkus calls itself "supersonic subatomic Java," which sounds like marketing until you watch a Quarkus
app boot in tens of milliseconds and sip a fraction of the memory a traditional Java service needs. It
was built for a world that didn't exist when classic Java frameworks were designed: containers,
Kubernetes, serverless, and autoscaling - where slow startup and fat memory cost real money. If you've
done [Spring Boot](/guides/spring-boot-from-zero) or [Jakarta EE](/guides/jakarta-ee-from-zero), Quarkus
will feel familiar *and* faster - because it runs the same standards (CDI, JAX-RS, Hibernate,
MicroProfile) but optimizes them in a fundamentally different way.

The one idea that explains everything Quarkus does: **move work from runtime to build time.** Classic
frameworks scan, reflect, and wire everything when the app *starts*; Quarkus does as much of that as
possible when the app is *compiled*, so startup is nearly free - and so the app can even be compiled to a
native machine-code executable. We build that mental model first, then walk the pieces you'll actually
use, demystifying each.

> 📝 This assumes you know **Java** and will lean on concepts from [Jakarta EE](/guides/jakarta-ee-from-zero)
> (CDI, JAX-RS) and [Hibernate & JPA](/guides/hibernate-and-jpa-from-zero) (Panache simplifies these). If
> those are new, do them first - Quarkus is "the standards, optimized," so knowing the standards pays off.

## How to read this

Read in order - it builds one example (a small `Product` service) and adds a capability each phase. The
real magic to *feel* is dev mode (Phase 2) and native compilation (Phase 9). Phases carry difficulty badges.

## The phases

**Part 1 - The Quarkus way (🟢 Basic)**
1. **[What Quarkus Is & Why It's Fast](01-what-quarkus-is.md)** 🟢 - build-time vs runtime work, native images, and how it relates to Spring Boot / Jakarta EE.
2. **[Dev Mode & the Developer Experience](02-dev-mode-and-dx.md)** 🟢 - live reload, the Dev UI, continuous testing - the thing people fall in love with.
3. **[Building REST APIs](03-rest-apis.md)** 🟢 - JAX-RS endpoints (RESTEasy Reactive) and JSON, the standards you already know.

**Part 2 - A real application (🟡 Intermediate)**
4. **[CDI in Quarkus (ArC)](04-cdi-with-arc.md)** 🟡 - build-time dependency injection: the same `@Inject`, wired at compile time.
5. **[Persistence: Hibernate with Panache](05-persistence-with-panache.md)** 🟡 - Panache's active-record and repository patterns over Hibernate.
6. **[Configuration](06-configuration.md)** 🟡 - MicroProfile Config, `application.properties`, profiles, and injecting config.

**Part 3 - Going further (🔴 Advanced → 🟢)**
7. **[Reactive Quarkus with Mutiny](07-reactive-with-mutiny.md)** 🔴 - `Uni`/`Multi`, reactive vs imperative, and when each fits.
8. **[Testing Quarkus Apps](08-testing.md)** 🟡 - `@QuarkusTest`, continuous testing, and testing the native build.
9. **[Native Compilation & Containers](09-native-compilation.md)** 🔴 - GraalVM native images, the closed-world model, and container-first deployment.
10. **[Production & Where to Go Next](10-where-to-go-next.md)** 🟢 - health/metrics, Kubernetes-native, the extension ecosystem, and what to build.

> Quarkus isn't a rejection of Spring/Jakarta EE - it's the same ideas re-engineered for the container
> era. Knowing the standards (this guide assumes them) is exactly what makes Quarkus click.


---

# What Quarkus Is & Why It's Fast

If you've written Java for the web, you know the rhythm: hit run, go make coffee while the framework wakes up. Classic Java frameworks can take seconds to start and hold hundreds of megabytes before serving a single request. For two decades that was fine - you started the server once and it ran for months. Quarkus exists because that assumption stopped being true, and the speed isn't a trick or a tuning flag - it comes from one deliberate design decision that everything else falls out of.

This guide assumes you're comfortable with Java. If you've used Spring Boot or Jakarta EE, even better - Quarkus implements the same specs.

## The problem Quarkus solves

Classic Java frameworks were designed for a **long-running application server**: started once, kept alive for months. Spending ten seconds and 500 MB at boot to scan, reflect, and wire everything was a reasonable one-time tax.

Then deployment moved into **containers, Kubernetes, and serverless**, and the economics flipped.

⚠️ In the cloud, startup and memory are recurring costs, not one-time ones. Apps that **autoscale** start new instances constantly - every one pays the startup tax again. In **serverless**, a slow boot becomes a user-facing **cold start**. Memory is billed per replica, multiplied across every pod. The old "start once, run forever" assumption is gone.

📝 **Quarkus** - a cloud-native Java framework optimized for **fast startup** and **low memory use**, built for the world where those two things directly drive cost and responsiveness.

## The core idea: build-time over runtime

📝 **Build-time over runtime** - classic frameworks do their setup work (scanning classes, reading annotations, reflection, wiring objects, reading config) when the app **starts**. Quarkus does as much of that as possible at **build/compile time**, so the running app skips it entirely and goes straight to serving requests.

A classic framework's first seconds of startup are pure *discovery and bookkeeping* - scanning the classpath, reflecting on classes, building and wiring a model of how everything connects. None of it serves a request, and the answers don't change between runs.

💡 Quarkus's bet: work whose answer is the same every startup shouldn't be done at startup at all. A Quarkus build runs the scanning, reflection, and wiring during compilation and bakes the results in. Startup just executes a plan already computed.

```mermaid
flowchart TB
  subgraph Classic["Classic framework"]
    A1[Build: compile only] --> A2[Startup: scan + reflect + wire] --> A3[Serve requests]
  end
  subgraph Q["Quarkus"]
    B1[Build: compile + scan + reflect + wire] --> B2[Startup: execute baked plan] --> B3[Serve requests]
  end
```

*What just happened:* the heavy box moved. In the classic flow, scan-reflect-wire sits at **startup**, on the path users wait behind. In Quarkus it moved left into the **build**, happening once in CI, not every container boot - which is why a Quarkus app can answer requests in tens of milliseconds.

💡 This one choice also enables the next idea: if the framework already knows exactly which classes and methods the app uses, a compiler can throw away everything else and produce a tiny, self-contained executable. That's native compilation, and it only works *because* Quarkus resolved the wiring early.

## Native images with GraalVM

📝 **Native image** - a standalone machine-code executable for one OS/CPU, produced ahead of time by **GraalVM**. Instead of shipping `.class` files a JVM interprets and warms up, you ship a single binary that *is* your application. It boots in **milliseconds**, uses a fraction of the memory, and needs no JVM warmup.

Native compilation requires a **closed-world** assumption: the compiler must know, at build time, every class and method the program will ever touch, so it can compile those and discard the rest. A traditional framework discovers and wires things dynamically at startup, so the compiler can't be sure what's needed. Quarkus already resolved that wiring at build time, so it hands GraalVM a precise list. Build-time wiring and native compilation are two sides of the same coin.

⚠️ Native images aren't free (full trade-offs in Phase 9). Two to know now: the native build itself is **slow and memory-hungry** - minutes, not seconds - so you don't do it on every code change. And reflection the framework can't see at build time will fail at runtime unless registered explicitly.

💡 JVM mode is still excellent - you don't have to go native to benefit. A Quarkus app on a normal JVM still starts far faster and uses less memory than a classic framework. Many teams deploy in JVM mode and reach for native only where cold-start latency or memory cost truly matters.

## It runs the standards you may already know

📝 Quarkus **implements the same standard specifications** Jakarta EE and MicroProfile define:

- **CDI** for dependency injection (`@Inject`, `@ApplicationScoped`) - Phase 4.
- **JAX-RS** for REST endpoints (`@Path`, `@GET`) - Phase 3.
- **Hibernate ORM / Panache** for database access (`@Entity`) - Phase 5.
- **MicroProfile** for config, health checks, and metrics.

💡 Quarkus is "the standards, re-engineered," not a fresh API. What changed is the engine underneath - build-time instead of startup-time. Same contract you write against; a faster machine fulfilling it.

How does it compare? [Spring Boot](/guides/spring-boot-from-zero) is the dominant, hugely popular framework with an enormous ecosystem. [Jakarta EE](/guides/jakarta-ee-from-zero) is the vendor-neutral standard prized in long-lived enterprises. **Quarkus** implements the same kinds of standards but wins specifically on **startup time, memory footprint, and native compilation**.

⚠️ "Faster" is about a specific axis, not a verdict. Spring's ecosystem breadth or an existing Jakarta EE investment can easily outweigh boot speed for a given team. Choose on what your project actually optimizes for.

## Create a project

The fastest way is the **Quarkus CLI** (or [code.quarkus.io](https://code.quarkus.io) if you'd rather not install anything):

```bash
quarkus create app org.acme:hello-quarkus
cd hello-quarkus
```

*What just happened:* the CLI generated a complete, ready-to-run Quarkus project - a build file with the right dependencies, standard source layout, and a sample REST resource already in place.

Inside, the heart of a Quarkus web app is an ordinary class with standard JAX-RS annotations:

```java
package org.acme;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@Path("/hello")
public class GreetingResource {

    @GET
    public String hello() {
        return "Hello from Quarkus";
    }
}
```

*What just happened:* these are the **exact same** `jakarta.ws.rs` annotations from the Jakarta EE standard - `@Path("/hello")` maps this class's endpoints under `/hello`, `@GET` maps HTTP `GET` to `hello()`. No `main()`, no server-start code. You described *which URL runs which method*; Quarkus owns everything around it.

Now start it in **dev mode**:

```bash
quarkus dev
```

```console
__  ____  __  _____   ___  __ ____  ______
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/

INFO  hello-quarkus 1.0.0-SNAPSHOT on JVM started in 0.842s.
INFO  Profile dev activated. Live Coding activated.
INFO  Installed features: [cdi, rest, smallrye-context-propagation, vertx]
```

*What just happened:* the app started in well under a second - build-time wiring paying off even in plain JVM mode. `Installed features` lists the standard pieces Quarkus wired up. Open `http://localhost:8080/hello` and you'll see `Hello from Quarkus`.

That `Live Coding activated` note is a hint at something special about `quarkus dev` - Phase 2's story.

## Recap

- **The problem:** classic Java frameworks were built for long-running servers, where slow startup and high memory were one-time costs. In containers, Kubernetes, and serverless, those become *recurring* costs - autoscaling cold starts and per-replica memory bills.
- **The core idea - build-time over runtime:** classic frameworks scan, reflect, and wire at **startup**; Quarkus moves that work to **build time** so the running app skips it and starts in milliseconds. This single choice explains everything else.
- **Native images (GraalVM):** because the wiring is resolved at build time (closed-world), Quarkus can compile to a standalone native executable that boots in milliseconds with tiny memory and no JVM warmup. Trade-offs (slow build, reflection limits) come in Phase 9.
- **JVM mode is still fast:** you get much of the benefit without going native - Quarkus on a normal JVM still beats classic frameworks on startup and memory.
- **It runs the standards:** Quarkus implements CDI, JAX-RS, Hibernate/Panache, and MicroProfile - the same annotations as Jakarta EE, re-engineered around build-time. It's a faster engine for a contract you may already know, not a new API.
- **Straight comparison:** Spring Boot, Jakarta EE, and Quarkus are all solid; Quarkus's specific edge is startup time, memory, and native compilation in the cloud.

## Quick check

Lock in the one idea everything else builds on:

```quiz
[
  {
    "q": "What is the core design choice that makes Quarkus fast?",
    "choices": [
      "It rewrites your Java into a faster language at runtime",
      "It moves framework work like scanning, reflection, and wiring from startup time to build/compile time",
      "It skips dependency injection entirely to save time",
      "It caches HTTP responses so requests never hit your code"
    ],
    "answer": 1,
    "explain": "Quarkus does the scan/reflect/wire work at build time so the running app skips it and starts in milliseconds. This single choice also enables native compilation."
  },
  {
    "q": "Why can Quarkus compile to a GraalVM native image when classic frameworks struggle to?",
    "choices": [
      "Because Quarkus apps are smaller and have fewer classes",
      "Because GraalVM only works with the jakarta.* namespace",
      "Because Quarkus resolves wiring at build time, giving the compiler the closed-world knowledge of exactly which classes and methods are used",
      "Because native images don't need any of the framework's features"
    ],
    "answer": 2,
    "explain": "Native compilation needs a closed-world assumption - knowing at build time every class and method used. Quarkus already resolves its wiring at build time, so it can hand GraalVM that precise list."
  },
  {
    "q": "How does Quarkus relate to Jakarta EE and MicroProfile?",
    "choices": [
      "It replaces them with a brand-new set of proprietary annotations",
      "It implements the same standard specs (CDI, JAX-RS, Hibernate, MicroProfile) with a re-engineered, build-time engine underneath",
      "It only works inside a Jakarta EE application server",
      "It has nothing to do with them and uses no standards"
    ],
    "answer": 1,
    "explain": "Quarkus writes against the same standard annotations (e.g. @Path, @Inject) but implements them with a build-time engine. You keep the familiar contract; the machine fulfilling it is faster."
  }
]
```


---

# Dev Mode & the Developer Experience

Phase 1 built the mental model: Quarkus moves work from runtime to build time, which is why it boots in milliseconds. That same machinery has a second payoff you *feel* every day, and it's the reason most people who try Quarkus don't want to go back: the inner loop of edit-code-see-result, run hundreds of times a day.

The mental model for this phase: **Quarkus treats your running dev server as a live thing you talk to, not a build you keep restarting.** Java has a long reputation for the "change one line, wait thirty seconds" tax. Quarkus's dev experience is an attack on that tax - you start the app once and stay in flow, without ever stopping and starting it yourself.

Four pieces make that real: live reload, the Dev UI, continuous testing, and Dev Services.

## `quarkus dev` and live reload

```bash
quarkus dev
```

*What just happened:* you started Quarkus in **dev mode**. The app comes up on `http://localhost:8080` and is now *watching your source files* - dev mode is a first-class run mode of Quarkus itself, not a separate tool. (Maven: `./mvnw quarkus:dev`; Gradle: `./gradlew quarkusDev` - same behavior.)

> 📝 **Live reload happens on the *next request*, not on save.** Quarkus doesn't rebuild in the background when you change a file. It waits until the next time you hit the app - a refresh, a `curl`, an API call - and *then* recompiles and reloads the changed code before serving that request. The first request after an edit pays a tiny recompile cost; everything after is instant.

Nothing recompiles while you're still typing - the work happens exactly when you ask to *see* a result.

Say you have a resource that returns a greeting:

```java
package org.acme;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@Path("/products")
public class ProductResource {

    @GET
    public String list() {
        return "Product list coming soon";
    }
}
```

With `quarkus dev` running, hit it:

```bash
curl http://localhost:8080/products
```

```console
Product list coming soon
```

Now change the returned text to `"We have 3 products"` and **save**. Don't restart anything - run the same `curl` again:

```bash
curl http://localhost:8080/products
```

```console
We have 3 products
```

*What just happened:* between the two requests you changed Java source and never touched the server. On the second request Quarkus noticed the file was newer, recompiled `ProductResource`, and served the response - transparently. Edit, refresh, see it.

> 💡 This covers most config and dependency changes too - a new endpoint method, a tweaked `application.properties`, a new extension all get picked up on the next request. Cases needing a full restart (deep classpath surgery) are rare enough you'll forget restarting was ever a habit.

## The Dev UI: a console for your running app

Live reload keeps you in flow; the **Dev UI** lets you see inside the app while it runs:

```bash
http://localhost:8080/q/dev
```

> 📝 The **Dev UI** is a web console Quarkus serves *only in dev mode* - a live dashboard of every **extension**, your current **configuration**, the **CDI beans** wired up, your **REST endpoints**, plus interactive tools to poke at them. It ships with the dev-mode runtime and disappears in production.

What you'll actually use:

- **Extensions** - a card for each one (REST, Hibernate, a database driver...), often with its own views.
- **Configuration** - every property and its current value, editable live.
- **Beans / CDI** - the full list of beans and how they're scoped. When [CDI in Phase 4](04-cdi-with-arc.md) makes you wonder "did my bean get registered?", look here.
- **Endpoints** - every route, often invokable straight from the browser. No Postman needed.

*What just happened:* the same build-time metadata that makes Quarkus fast - the catalog of beans, routes, and config - gets handed to a UI you can browse. When something behaves oddly, "open `/q/dev` and look" is often faster than reading code.

## Continuous testing: red/green without leaving the terminal

Look at the terminal where `quarkus dev` is running - there's an interactive prompt at the bottom. Press `r`:

```console
Tests paused
Press [r] to resume, [h] for more options>
r
--
Running 1/1. Running: ProductResourceTest#listReturnsProducts()
All 1 tests are passing (0 failed), 1 tests were run in 412ms.
Press [r] to re-run, [o] Toggle test output, [h] for more options>
```

*What just happened:* you turned on **continuous testing**. From now on, every code change re-runs the affected tests automatically - only the tests touched by the change, not the whole suite - and you get a green or red result right in the terminal, seconds after you save.

`r` re-runs, `o` toggles test output, `h` shows the full menu.

> 💡 Change the `Product` resource, glance at the terminal, watch its test go red or green - you're getting TDD-style feedback whether or not you set out to "do TDD." (Deeper in [Phase 8](08-testing.md).)

## Dev Services: zero-config infrastructure

Suppose your `Product` service needs a real database. Add the Postgres extension and *don't* configure a connection at all. You'd expect it to fail on startup:

```console
INFO  Dev Services for default datasource (postgresql) started
INFO  Container postgres:16 is starting...
INFO  Profile dev activated. Live Coding activated.
INFO  Installed features: [cdi, hibernate-orm, jdbc-postgresql, rest, smallrye-context-propagation]
```

*What just happened:* Quarkus noticed the Postgres extension but **no datasource configured**, and instead of erroring, spun up a throwaway PostgreSQL **in a container** and wired your app to it automatically - **Dev Services**. Zero connection strings, no database installed, yet a real Postgres. Stop dev mode and the container goes away.

Same for MySQL, MariaDB, Kafka, Redis, MongoDB, and more: add the extension, leave it unconfigured in dev.

> 📝 The rule: **if** an extension needs a service **and** you haven't configured one for this profile, **then** start a throwaway one. The moment you *do* set a connection, Dev Services backs off. Your config always wins.

⚠️ This needs a container runtime - Dev Services starts containers via Testcontainers, so you need **Docker or Podman running**. No runtime, no auto-database. This is a **dev and test** convenience only - production needs real configured connections. See [Docker Without the Magic](/guides/docker-without-the-magic) if containers are new.

> 💡 What just got deleted from your life: the "set up your local environment" wiki page. Clone, `quarkus dev`, and a clean database is waiting.

## Why developer experience actually matters

> 💡 **Fast feedback is a force multiplier.** The length of your inner loop silently sets the ceiling on how fast you can think. A thirty-second rebuild costs your concentration, not just thirty seconds - you context-switch while you wait. Shrink the loop to near-zero and you stay *in* the problem, try more ideas, and catch mistakes while the change is still fresh.

Phase 1's pitch was about *production*: fast startup, low memory, cheap at scale. This phase's pitch is about *you*, every day at your desk - a language famous for slow rebuilds, made to feel instant.

Next we write real REST endpoints for the `Product` service - and feel live reload as we go.

## Recap

- **`quarkus dev` starts dev mode**, which watches your source and keeps the app running so you never manually restart during development.
- **Live reload recompiles on the *next request*, not on save** - the first request after an edit pays a tiny recompile cost; the result is "edit, refresh, see it" with no restart.
- **The Dev UI (`/q/dev`)** is a dev-only web console showing your extensions, config, beans, and endpoints, with tools to poke at them - the app's internal model made visible.
- **Continuous testing** (press `r` in the dev console) re-runs the affected tests automatically on every change, giving red/green feedback in the terminal and nudging you toward a TDD-style flow.
- **Dev Services** auto-starts throwaway containers (Postgres, Kafka, Redis…) when an extension needs a service you haven't configured - zero-config local infra. It needs a container runtime (Docker/Podman) and is **dev/test only**; your own config always wins.
- **Fast feedback is the real point.** A near-instant inner loop keeps you in flow and is a genuine reason teams choose Quarkus, separate from its runtime speed.

## Quick check

Make sure the dev-mode mental model stuck before we start building APIs.

```quiz
[
  {
    "q": "When does Quarkus live reload recompile your changed code?",
    "choices": [
      "Immediately every time you save a file, in the background",
      "On the next request to the app after you've changed a file",
      "Only when you manually restart the dev server",
      "On a fixed timer, every few seconds"
    ],
    "answer": 1,
    "explain": "Dev mode waits until the next request (a refresh, curl, or API call), then recompiles and reloads the changed code before serving that request. The first request after an edit pays a small cost; the rest are instant."
  },
  {
    "q": "You added the Postgres extension but configured no datasource in dev. What does Quarkus do?",
    "choices": [
      "Fails to start because there's no database connection",
      "Falls back to an in-memory map and ignores Postgres",
      "Uses Dev Services to auto-start a throwaway Postgres container and wires the app to it",
      "Prompts you to enter a connection string before starting"
    ],
    "answer": 2,
    "explain": "Dev Services follows the rule: if an extension needs a service and you haven't configured one, start a throwaway one (in a container). It needs a running container runtime and is for dev/test only."
  },
  {
    "q": "What does continuous testing do in Quarkus dev mode?",
    "choices": [
      "Runs the entire test suite once when the app starts",
      "Re-runs the affected tests automatically as you change code, showing red/green in the terminal",
      "Replaces your tests with auto-generated ones",
      "Only runs tests when you push to CI"
    ],
    "answer": 1,
    "explain": "Press 'r' in the dev console to enable it. Quarkus then re-runs only the tests touched by each change, giving immediate red/green feedback without leaving the terminal."
  }
]
```


---

# Building REST APIs

Phase 2's live reload is most fun when there's something to *hit* - an endpoint you can curl, tweak, and see change live. This phase gives you one: an HTTP API for the `Product` you've been carrying along (an `id`, a `name`, a `price`).

**Quarkus didn't invent a new way to write REST APIs.** It uses the exact same Jakarta REST (JAX-RS) annotations you'd write on any Java server - `@Path`, `@GET`, `@POST`, `@Produces`. If you've done the [Jakarta EE guide](/guides/jakarta-ee-from-zero), you already know how to write a Quarkus resource. What Quarkus changes is *underneath* - its REST engine (**Quarkus REST**, historically **RESTEasy Reactive**) does request-matching and wiring at **build time** instead of startup - the "supersonic" story from Phase 1.

📝 So this phase isn't "learn REST" - it's "see the REST you know running on Quarkus," plus two genuinely Quarkus-flavored wrinkles: **extensions** (turning on features like JSON) and the **imperative-vs-reactive** return-type choice. If `@Path`, `@PathParam`, or status codes feel fuzzy, the [JAX-RS phase](/guides/jakarta-ee-from-zero) and [REST APIs Explained](/guides/rest-apis-explained) teach them from scratch.

## It's JAX-RS (Quarkus REST / RESTEasy Reactive)

A REST endpoint in Quarkus is a *resource class*: a plain class marked with `@Path`, whose methods are marked with HTTP-verb annotations. Here's one that returns a list of products as JSON:

```java
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.math.BigDecimal;
import java.util.List;

@Path("/products")
public class ProductResource {

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<Product> list() {
        return List.of(
            new Product(1L, "Mechanical Keyboard", new BigDecimal("129.99")),
            new Product(2L, "USB-C Hub", new BigDecimal("49.50"))
        );
    }
}
```

*What just happened:* `@Path("/products")` mapped this class to `/products`, `@GET` handles `GET` requests, and `@Produces(APPLICATION_JSON)` declares the response format, so `List<Product>` serializes automatically. The imports are `jakarta.ws.rs.*` - standard JAX-RS, nothing Quarkus-specific. (The hardcoded list is a placeholder - real data arrives in [Phase 5](05-persistence-with-panache.md).)

```http
GET /products HTTP/1.1
Host: localhost:8080
```

```json
[
  { "id": 1, "name": "Mechanical Keyboard", "price": 129.99 },
  { "id": 2, "name": "USB-C Hub", "price": 49.50 }
]
```

*What just happened:* the `List<Product>` came back as a JSON array. You wrote zero serialization code. (Quarkus defaults to port `8080`, same as a classic server.)

## Path and query params

📝 A **path param** is part of the URL (`/products/2` = "product with id 2"): a placeholder in `@Path`, bound with `@PathParam`. A **query param** is a value after `?` (`/products?maxPrice=50`), bound with `@QueryParam`. Path param *identifies*; query param *filters*.

```java
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import java.math.BigDecimal;
import java.util.List;

@Path("/products")
public class ProductResource {

    @GET
    @Path("/{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public Product getOne(@PathParam("id") Long id) {
        return service.findById(id);
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<Product> list(@QueryParam("maxPrice") BigDecimal maxPrice) {
        return maxPrice == null ? service.findAll() : service.cheaperThan(maxPrice);
    }
}
```

*What just happened:* in `getOne`, `{id}` lines up with `@PathParam("id") Long id` - Quarkus pulls `2` from `/products/2` and converts it. In `list`, `@QueryParam("maxPrice")` is `null` when the client omits it, so we return everything. (`service` moves into a CDI bean in [Phase 4](04-cdi-with-arc.md).)

```bash
curl http://localhost:8080/products/2
curl "http://localhost:8080/products?maxPrice=60"
```

```console
{"id":2,"name":"USB-C Hub","price":49.50}

[{"id":2,"name":"USB-C Hub","price":49.50}]
```

## Request bodies and JSON

To *create* a product the client sends JSON in the body - and here's the first genuinely Quarkus-flavored step: **JSON binding isn't on by default. You add it with an extension.**

⚠️ Write a `@POST` taking a `Product` body before adding a JSON extension and it won't deserialize - Quarkus doesn't ship Jackson in the core. Add **`quarkus-rest-jackson`**:

```bash
quarkus extension add quarkus-rest-jackson
```

*What just happened:* that edited your build file to include the extension; dev mode picked it up on the next request. From here, JSON binds both in and out - the earlier `@GET`s also rely on this extension being present.

Now the create endpoint. To report **201 Created** instead of the default 200, return a `RestResponse<Product>`:

```java
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import org.jboss.resteasy.reactive.RestResponse;

@Path("/products")
public class ProductResource {

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public RestResponse<Product> create(Product product) {
        Product saved = service.create(product);
        return RestResponse.status(RestResponse.Status.CREATED, saved); // 201 + body
    }
}
```

*What just happened:* the unannotated `Product product` parameter *is* the request body - `@Consumes(APPLICATION_JSON)` deserializes it before your code runs. `RestResponse<Product>` is Quarkus REST's type-safe wrapper over the classic JAX-RS `Response` (either works); returning a bare `Product` would always yield 200.

```json
{
  "name": "Laptop Stand",
  "price": 39.95
}
```

*What just happened:* the client posts a product with no `id` - the server assigns it. The extension binds `name`/`price` onto a `Product` before `create` runs, then serializes the saved product back out.

## Extensions - the Quarkus way to add features

You just used an extension to add JSON - worth pausing on what one actually is, since it's how you'll add every capability from here on.

📝 An **extension** is a Quarkus-aware module - `quarkus-rest-jackson` for JSON, `quarkus-hibernate-orm-panache` for persistence, `quarkus-jdbc-postgresql` for a driver, dozens more:

```bash
quarkus extension add quarkus-hibernate-orm-panache quarkus-jdbc-postgresql
```

*What just happened:* both capabilities added in one go, each wiring itself into the build with sensible defaults.

💡 Why an *extension* and not a plain dependency? An extension hooks into Quarkus's **build-time processing** (Phase 1's idea) - it contributes a build step that does scanning, reflection registration, and wiring *while compiling*, so startup stays instant and the feature still works in a native image ([Phase 9](09-native-compilation.md)), where runtime reflection isn't available. Each feature pays its setup cost once, at build time, instead of on every boot.

## Imperative vs reactive (a preview)

Every handler above returned a value *directly* - the **imperative** style: the method blocks until it has the answer and returns it. Quarkus REST also lets a handler return a **reactive** type - a `Uni<Product>` (a promise) or `Multi<Product>` (a stream) - which hands the request's thread back while waiting on I/O, then resumes when data is ready. Both styles run on the same stack; you mix them per endpoint. [Phase 7](07-reactive-with-mutiny.md) covers `Uni`/`Multi` properly - for now, just know the door exists.

💡 Whichever style you pick: **keep the resource thin.** A handler reads the request, hands the real work to a CDI bean ([Phase 4](04-cdi-with-arc.md)) backed by Panache ([Phase 5](05-persistence-with-panache.md)), and shapes the response. That separation is what lets you swap imperative for reactive without touching the doorway.

## Recap

1. **It's standard JAX-RS, build-time optimized.** Quarkus REST (RESTEasy Reactive) runs the same
   `@Path`/`@GET`/`@POST`/`@Produces` annotations you'd write on any Jakarta server - but does the wiring
   at compile time, which is what makes startup nearly instant.
2. **Path params identify, query params filter.** `@PathParam` binds `{id}` from the path (one specific
   product); `@QueryParam` binds `?maxPrice=...` and is `null` when the client omits it.
3. **JSON comes from the `quarkus-rest-jackson` extension.** Add it, and the engine deserializes the
   unannotated `@POST` body into a Java object and serializes return values back to JSON.
4. **`RestResponse` controls status and headers.** Return a bare object for the default 200, or a
   `RestResponse<Product>` for 201 Created and other correct status codes (the classic `Response` works too).
5. **Extensions are how you add features.** An extension is a build-time-aware module added with
   `quarkus extension add`; it hooks Quarkus's build-time processing, which is why features stay fast and
   work in native images.
6. **Imperative or reactive, same stack.** A handler can return a plain `Product` or a `Uni<Product>` - 
   both work; reactive comes in [Phase 7](07-reactive-with-mutiny.md). Keep the resource thin and push
   logic into a CDI bean.

## Quick check

Make sure the Quarkus-flavored bits stuck:

```quiz
[
  {
    "q": "Your @POST endpoint takes a Product body, but the incoming JSON isn't being deserialized into the object. What's the most likely cause?",
    "choices": [
      "The quarkus-rest-jackson extension isn't added - Quarkus doesn't ship JSON binding in the core, you turn it on with an extension",
      "JAX-RS annotations don't work in Quarkus; you need Quarkus-specific ones",
      "@POST methods can't accept a request body",
      "You must annotate the body parameter with @QueryParam"
    ],
    "answer": 0,
    "explain": "JSON binding is a capability you add via an extension. Without quarkus-rest-jackson, the engine has no JSON mapper, so the body won't deserialize. The annotations are standard JAX-RS, and the unannotated parameter IS the body - adding the extension is what's missing."
  },
  {
    "q": "Why does Quarkus use 'extensions' instead of plain Maven/Gradle dependencies for features like JSON and persistence?",
    "choices": [
      "An extension hooks into Quarkus's build-time processing, doing scanning and wiring at compile time so startup stays fast and the feature works in native images",
      "Extensions are just a rebrand of dependencies with no technical difference",
      "Extensions run only at runtime and skip the build entirely",
      "Plain dependencies aren't allowed in a Quarkus project"
    ],
    "answer": 0,
    "explain": "An extension contributes a build step that moves scanning, reflection registration, and wiring to compile time - the core Quarkus idea. That keeps boot nearly instant and makes the feature survive native compilation, where runtime reflection isn't available."
  },
  {
    "q": "Your create endpoint returns a plain Product and clients always get HTTP 200, even though a resource was created. How do you report 201 Created?",
    "choices": [
      "Return a RestResponse<Product>, e.g. RestResponse.status(RestResponse.Status.CREATED, saved), which carries the status alongside the body",
      "Add @POST(status = 201) to the method",
      "Throw an exception after saving so the server picks a different code",
      "Nothing can change it - Quarkus REST methods only return 200"
    ],
    "answer": 0,
    "explain": "Returning a bare object gives the default 200. To set the status (and headers), return a RestResponse - RestResponse.status(Status.CREATED, saved) reports 201 Created with the body. The classic JAX-RS Response works the same way."
  }
]
```


---

# CDI in Quarkus (ArC)

Phase 3's JAX-RS resource quietly relied on something glossed over: when you wrote `@Inject ProductService`, *somebody* created the service and handed it to the resource. This phase is that somebody - Quarkus's dependency-injection container - and the one thing it does differently from every container you've used before.

**It's the same CDI you already know, but the wiring is figured out while your code compiles, not when it starts.** A traditional container wakes up at startup, scans your classes, reads annotations via reflection, builds the "who needs what" graph, and *then* serves requests. Quarkus does almost all of that at **build time** - the graph is baked in before the app ever runs. Same annotations, same programming model, different timing - the build-time-over-runtime idea from Phase 1, applied to DI.

📝 If you've done [CDI in Jakarta EE](/guides/jakarta-ee-from-zero), you already know the programming model - `@Inject`, `@ApplicationScoped`, qualifiers, producers. This phase shows Quarkus's twist on it rather than re-teaching CDI from scratch.

## It's CDI, wired at build time

📝 Quarkus's DI container is **ArC** - a build-time implementation of **CDI**. No new API: the exact same `jakarta.inject` and `jakarta.enterprise.context` annotations. The difference is *when* the wiring happens.

```java
import jakarta.enterprise.context.ApplicationScoped;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

@ApplicationScoped
public class ProductService {
    private final Map<Long, Product> store = new ConcurrentHashMap<>();

    public List<Product> all()        { return List.copyOf(store.values()); }
    public Product find(long id)      { return store.get(id); }
    public void save(Product p)       { store.put(p.id(), p); }
}
```
*What just happened:* `@ApplicationScoped` is the standard CDI bean-defining annotation - one instance for the whole app. Identical to Jakarta EE; ArC just notices it at compile time instead of startup.

```java
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import java.util.List;

@Path("/products")
public class ProductResource {
    @Inject
    ProductService service;

    @GET
    public List<Product> list() {
        return service.all();
    }
}
```
*What just happened:* `@Inject` says "container, fill this in." ArC finds the `@ApplicationScoped` bean and records the connection. The field isn't `private` - ArC's generated injection code needs to write to it directly, avoiding reflection.

💡 Field injection is compact, which is why you see it everywhere in examples. Constructor injection is still the better habit - more below.

## Why build-time DI is a big deal

💡 A runtime container's startup cost is dominated by **scanning the classpath** and **reflection** to construct beans - both happen every boot. ArC does that once, at build time, and emits plain generated code that wires everything directly. At runtime there's almost nothing left to do - a big slice of why Quarkus boots in tens of milliseconds.

The deeper win is **native images**. GraalVM's closed-world model (Phase 9) is hostile to reflection - anything discovered dynamically at runtime is a problem. Build-time DI sidesteps that entirely, because the wiring is settled before the native compiler runs. It's a big part of how Quarkus offers full CDI *and* compiles to native.

⚠️ The flip side is mostly good news: errors a runtime container throws at *startup* (a missing dependency, an ambiguous bean), ArC often catches at *compile time*.

```java
@Path("/products")
public class ProductResource {
    @Inject
    PricingEngine pricing;   // no bean of this type exists anywhere
}
```

```console
[ERROR] Build step ...ArcProcessor#validate threw an exception:
jakarta.enterprise.inject.UnsatisfiedResolutionException:
Unsatisfied dependency for type com.example.PricingEngine and qualifiers [@Default]
  - java member: com.example.ProductResource#pricing
  - declared on CLASS bean [class=com.example.ProductResource]
```
*What just happened:* ArC validated the whole graph at build time and refused to build. Classic Jakarta EE throws the same standard exception, just at deploy/startup instead. (An ambiguous dependency fails the same way; the fix is the standard `@Qualifier`.) Shifting failures left means you find them on your machine, not in prod.

## Beans & scopes

Same CDI scopes as Jakarta EE - a quick recap, not a re-teach. A scope answers: *how long does a bean live*, and *who shares the same instance*.

| Scope | Annotation | One instance per… | Reach for it when |
|-------|------------|-------------------|-------------------|
| Application | `@ApplicationScoped` | the whole application | stateless services & repositories (the common default in Quarkus) |
| Request | `@RequestScoped` | a single HTTP request | per-request state tied to one call |
| Singleton | `@Singleton` | the whole application | like app-scoped, but eager and proxy-free (a micro-optimization; usually prefer `@ApplicationScoped`) |

📝 `@ApplicationScoped` is the workhorse. Full detail on scopes and the proxy machinery lives in the [Jakarta EE CDI phase](/guides/jakarta-ee-from-zero) - it applies here unchanged.

💡 A bonus from build-time analysis: Quarkus **removes unused beans** - since ArC sees the whole graph, it can leave out beans nothing injects, for a smaller, faster-loading app. (Mark an apparently-unused bean `@Unremovable` if you need to keep it - say it's only used reflectively.)

## Constructor injection & the basics

Constructor injection is the better habit, for the same reasons as in Spring and Jakarta EE:

```java
@Path("/products")
public class ProductResource {
    private final ProductService service;

    public ProductResource(ProductService service) {   // ArC passes one in
        this.service = service;
    }

    @GET
    @Path("/{id}")
    public Product byId(@PathParam("id") long id) {
        return service.find(id);
    }
}
```
*What just happened:* a single injectable constructor doesn't even need `@Inject` - ArC treats the sole constructor as the injection point. `service` can be `final`, the constructor is a clear list of dependencies, and you can unit-test with a plain `new ProductResource(fakeService)` - no container needed.

📝 **Qualifiers** (a custom `@Qualifier` to pick between beans of the same type) and **producers** (`@Produces` methods for non-bean objects) work exactly as in standard CDI - see the [Jakarta EE CDI phase](/guides/jakarta-ee-from-zero); ArC resolves it all at build time.

## The Spring bridge (brief)

💡 Coming from Spring? The `quarkus-spring-di` extension lets Spring's DI annotations work directly - `@Autowired`, `@Component`, `@Service`, `@Value` - so you can lift familiar code over:

```java
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;

@Component                       // Spring's annotation, understood by Quarkus via quarkus-spring-di
public class ProductService {
    @Autowired
    ProductRepository repository;
}
```
*What just happened:* with the extension present, ArC understands `@Component`/`@Autowired` and wires this bean as if it were CDI - still at build time. It's a **compatibility bridge** for migration, not the idiomatic path.

📝 Idiomatic Quarkus uses **standard CDI** but resolves wiring at build time for speed and native-image friendliness. Learn CDI once and you've learned the wiring model for Quarkus, Jakarta EE, and (via the bridge) much of Spring too - the only thing genuinely different is *when* the magic happens.

## Recap

1. **ArC is build-time CDI.** Quarkus's DI container, ArC, implements the standard CDI spec - same `@Inject`,
   `@ApplicationScoped`, qualifiers, producers - but computes the wiring graph at compile time instead of at
   startup. No new API, just different timing.
2. **Why it matters:** doing scanning and reflection once at build time (not on every boot) is a big reason
   Quarkus starts in milliseconds, and doing DI at build time sidesteps reflection - which is what lets full
   CDI coexist with GraalVM native images.
3. **Errors shift left.** A missing or ambiguous bean that a runtime container would throw at startup, ArC
   often catches at build time - the build fails on your machine instead of the server failing in prod.
4. **Scopes are standard CDI.** `@ApplicationScoped` is the common Quarkus default; `@RequestScoped` for
   per-request state; `@Singleton` as an eager, proxy-free variant. Full detail lives in the Jakarta EE CDI
   phase. Bonus: ArC removes unused beans for a smaller app.
5. **Prefer constructor injection** (testable, `final`, no reflection needed); qualifiers and producers work
   as in standard CDI. For Spring devs, `quarkus-spring-di` bridges `@Autowired`/`@Component` - but idiomatic
   Quarkus uses CDI.

With wiring understood, the next piece slots right in: a real persistence layer. Next we give `Product` a
database with Hibernate and Panache.

## Quick check

Test yourself on the ideas that have to stick from this phase:

```quiz
[
  {
    "q": "What is the key difference between Quarkus's ArC container and a traditional CDI container?",
    "choices": [
      "ArC computes the dependency-injection wiring graph at build time, not at application startup",
      "ArC uses a completely different set of annotations from standard CDI",
      "ArC only supports field injection and forbids constructor injection",
      "ArC runs the application as interpreted bytecode instead of compiled code"
    ],
    "answer": 0,
    "explain": "ArC is a build-time implementation of the standard CDI spec. The annotations (@Inject, @ApplicationScoped, etc.) are identical to Jakarta EE; what changes is that the wiring is resolved at compile time rather than at startup - which is what enables fast boot and native images."
  },
  {
    "q": "You @Inject a type that no bean satisfies. In Quarkus, when do you find out?",
    "choices": [
      "At build time - ArC validates the dependency graph and the build fails",
      "Never - Quarkus silently injects null",
      "Only in production, on the first request that uses the bean",
      "At unit-test time only, never during the build"
    ],
    "answer": 0,
    "explain": "Because ArC analyzes the whole graph at build time, an unsatisfied (or ambiguous) dependency fails the build with the standard exception - earlier than a runtime container would throw it at startup. The failure shifts left to your machine."
  },
  {
    "q": "Why does doing dependency injection at build time help Quarkus compile to a native image?",
    "choices": [
      "It avoids runtime reflection and classpath scanning, which the native closed-world model is hostile to",
      "It makes the application use less disk space at build time",
      "It converts all beans into static methods that need no instances",
      "It disables CDI entirely so there is nothing for the native compiler to analyze"
    ],
    "answer": 0,
    "explain": "GraalVM's closed-world native compilation struggles with reflection and dynamic discovery. By resolving DI at build time and generating plain wiring code, ArC removes the runtime scanning/reflection that would otherwise break native compilation - letting full CDI and native images coexist."
  }
]
```


---

# Persistence: Hibernate with Panache

Phase 4 wired up beans with ArC, holding `Product` data in memory - fine until the JVM restarts and everything evaporates. This phase gives `Product` a real database. Good news: you already know most of how this works.

## The mental model: Panache is Hibernate wearing comfortable shoes

📝 **Panache is not a new ORM.** Underneath, it *is* Hibernate ORM - the same engine, entities, persistence context, transactions, and dirty checking from the [Hibernate & JPA guide](/guides/hibernate-and-jpa-from-zero). Panache is a thin layer that deletes the repetitive parts: hand-written getters/setters, boilerplate DAO methods, `EntityManager` plumbing.

- **Liberating:** every bit of JPA knowledge still applies - entities are still transient/managed/detached/removed, and Hibernate still syncs on commit.
- **Sobering:** every JPA *trap* still applies too. The N+1 problem doesn't disappear because the code got shorter.

> 💡 Read Panache as "Hibernate with less typing," never "Hibernate but the rules changed." When something surprises you, check the JPA guide, not the Panache docs.

## Active Record: the entity does the work

📝 The **active-record pattern**: your entity extends `PanacheEntity`, and data-access methods live *on the entity itself* as static methods - `Product.listAll()`, `Product.findById(id)`.

Two things surprise people coming from classic JPA:

1. **Public fields.** Declared `public`, no getters/setters - Panache rewrites the bytecode at build time to generate real accessors.
2. **A free id.** `PanacheEntity` provides an auto-generated `Long id`; you don't declare `@Id`.

```java
package org.acme.catalog;

import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.Entity;
import java.math.BigDecimal;

@Entity
public class Product extends PanacheEntity {
    public String name;          // public field - Panache generates the accessor
    public BigDecimal price;     // 'id' comes free from PanacheEntity
}
```
*What just happened:* `@Entity` is the same JPA annotation as always. Extending `PanacheEntity` adds the generated `id` plus static finders (`listAll`, `findById`, `find`, `count`, `deleteAll`) and instance methods (`persist`, `delete`). The `public` fields are pure ceremony removal, not a different data model.

```java
// CREATE - must run inside a transaction (more below)
Product p = new Product();
p.name = "Mechanical Keyboard";
p.price = new BigDecimal("89.90");
p.persist();                              // INSERT scheduled on the persistence context

// READ
Product found = Product.findById(1L);     // SELECT by primary key
List<Product> all = Product.listAll();    // SELECT * from product

// UPDATE - no save() call needed
found.price = new BigDecimal("79.90");    // dirty checking writes this at commit

// DELETE
found.delete();                           // DELETE scheduled
```
*What just happened:* `p.persist()` makes the transient object managed and schedules the `INSERT`. The update has **no save call** - because `found` is managed, Hibernate's dirty checking notices the changed `price` and emits `UPDATE` on commit, exactly as in plain Hibernate.

## Repository: the same power, a separate class

📝 If you can't extend a base class, or prefer keeping persistence out of the domain object, use the **repository pattern**: keep the entity plain (private fields, `@Id`) and put a separate `ProductRepository` implementing `PanacheRepository<Product>` next to it, injected as a CDI bean.

```java
package org.acme.catalog;

import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class ProductRepository implements PanacheRepository<Product> {
    // Empty body - listAll(), findById(), persist(), delete()... are all inherited.
    // Add custom finders here as you need them.
}
```
*What just happened:* `PanacheRepository<Product>` gives the repository the same method set the active-record entity got, but as instance methods.

```java
@ApplicationScoped
public class CatalogService {
    @Inject
    ProductRepository products;            // ArC injects the repository bean

    public Product priceCheck(Long id) {
        return products.findById(id);      // call methods on the repo, not the entity
    }
}
```
*What just happened:* functionally identical SQL to the active-record version - the difference is purely *where the methods live*.

> 💡 Active-record reads cleaner and is faster for straightforward CRUD. Repository is easier to mock in unit tests and separates concerns more strictly. Pick one per project and stay consistent.

## Queries and transactions

📝 Panache gives a **simplified query syntax** - write only the fragment after `where`:

```java
// Panache shorthand - "name = ?1"
List<Product> hits = Product.list("name", "Mechanical Keyboard");

// Sorted, with positional params
List<Product> cheap = Product.list("price < ?1 order by price", new BigDecimal("50"));

// Paging
List<Product> page = Product.find("order by name")
                            .page(Page.of(0, 20))   // first page, 20 per page
                            .list();
```
```sql
select p.id, p.name, p.price from product p where p.name = 'Mechanical Keyboard'
```
*What just happened:* `Product.list("name", value)` expands to the full JPQL `from Product where name = ?1`. It's just JPQL with the boilerplate omitted - the generated SQL is identical to plain Hibernate.

Writes need a transaction:

```java
@ApplicationScoped
public class CatalogService {

    @Transactional                          // opens a tx; commits on clean return, rolls back on exception
    public Product create(String name, BigDecimal price) {
        Product p = new Product();
        p.name = name;
        p.price = price;
        p.persist();
        return p;                            // INSERT flushed at method exit, when the tx commits
    }
}
```
*What just happened:* `@Transactional` wraps the method in a database transaction. `persist()` schedules the `INSERT`, but the SQL flushes when the transaction commits - if the method threw, it would roll back with nothing written.

> ⚠️ The N+1 trap is alive and well. `Product.listAll()` then looping over a lazy `reviews` collection fires one `SELECT` for the list plus one *per product*. Panache's tidy syntax hides nothing here - the fix is the same `join fetch` from the Hibernate guide. **Watch the generated SQL.** See [Why is my query slow?](/guides/why-is-my-query-slow).

## Dev Services: a database that appears out of nowhere

💡 Add the JDBC driver and Panache extensions:

```console
quarkus extension add jdbc-postgresql hibernate-orm-panache
```

Run `quarkus dev` with **no datasource configured**, and Quarkus notices you have a Postgres driver but no connection URL, so it spins up a throwaway PostgreSQL container and tears it down when you stop. This is **Dev Services** - a fresh project can talk to a real database before you've written a line of config.

For production, a few lines in `application.properties`:

```properties
# Production datasource - Dev Services backs off when these are set
quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=catalog
quarkus.datasource.password=${DB_PASSWORD}
quarkus.datasource.jdbc.url=jdbc:postgresql://db.internal:5432/catalog
```
*What just happened:* once a real `jdbc.url` is present, Dev Services stays out of the way. `${DB_PASSWORD}` pulls from an environment variable (Phase 6). Same code, frictionless local loop *and* normal production connection.

> ⚠️ One thing must change between dev and prod: schema generation. Dev often uses `quarkus.hibernate-orm.database.generation=drop-and-create`, letting Hibernate build tables from your entities. That's a **development convenience only** - in production, never let Hibernate own your schema. Use real migrations (Flyway has a Quarkus extension).

## Recap

1. 📝 **Panache is Hibernate ORM with less boilerplate** - same engine, same persistence context, same
   entity states. Your JPA knowledge (and JPA's traps) carry over unchanged.
2. **Active-record pattern:** `Product extends PanacheEntity`, public fields (accessors generated at build
   time), a free `id`, and static methods on the entity - `Product.findById(id)`, `product.persist()`.
3. **Repository pattern:** keep the entity plain and put a `ProductRepository implements
   PanacheRepository<Product>` next to it, injected as a CDI bean. Same methods, separate class - better
   for testability and separation. Pick one pattern per project.
4. **Queries** use a shorthand (`Product.list("name", name)`, paging, sorting) that's plain JPQL
   underneath; **writes** need `@Transactional`, and the SQL flushes at commit, not at `persist`.
5. ⚠️ **N+1 still bites** - Panache hides boilerplate, not the database. Watch the generated SQL and use
   `join fetch` when looping over lazy relationships.
6. 💡 **Dev Services** auto-starts a throwaway Postgres in dev (zero config); production uses a real
   datasource in `application.properties`. Schema auto-generation is dev-only - use Flyway migrations in
   prod.

## Quick check

The three ideas worth keeping:

```quiz
[
  {
    "q": "You write `Product.findById(1L)` and `product.persist()`, with public fields and no `@Id` on the entity. Which Panache pattern is this, and where does the `id` come from?",
    "choices": [
      "Active-record - the entity extends PanacheEntity, which provides the generated Long id and the static/instance data methods",
      "Repository - findById only exists on a PanacheRepository",
      "Plain JPA - Panache isn't involved when you call findById",
      "It won't compile, because an @Entity must declare its own @Id"
    ],
    "answer": 0,
    "explain": "Calling static finders on the entity and using public fields with a free id is the active-record pattern: Product extends PanacheEntity, which supplies the auto-generated Long id and the finder/persist methods. The repository pattern would put findById on an injected PanacheRepository instead."
  },
  {
    "q": "Inside a `@Transactional` method you load a managed Product and set `product.price` to a new value, but never call any save/update method. What happens at commit?",
    "choices": [
      "Hibernate's dirty checking detects the changed field and emits an UPDATE - Panache uses the same persistence context as plain JPA",
      "Nothing - without an explicit update() call the change is lost",
      "It throws, because you must call persist() again to save changes",
      "The change is saved immediately when you set the field, before commit"
    ],
    "answer": 0,
    "explain": "Panache is Hibernate underneath. A managed entity is tracked by the persistence context, so dirty checking notices the changed price and flushes an UPDATE when the transaction commits - no save call required."
  },
  {
    "q": "You `Product.listAll()` and then loop over each product touching a lazy `reviews` collection, and the endpoint is slow. What's the most likely cause?",
    "choices": [
      "The N+1 problem - one SELECT for the list plus one per product for its reviews; Panache doesn't prevent it, so use join fetch and watch the SQL",
      "Dev Services is using a slow throwaway container; it goes away in production",
      "Panache is missing an index, which it should have generated automatically",
      "listAll() is deprecated and you should use find() with paging to fix performance"
    ],
    "answer": 0,
    "explain": "This is the classic N+1: the list query plus one lazy-load query per product. Panache's short syntax hides the boilerplate but not the database behavior, so the same JPA fix applies - fetch the relationship eagerly with join fetch (or an entity graph) and verify by counting the generated queries."
  }
]
```


---

# Configuration

Phase 5's last snippet snuck in a line you didn't fully unpack: `quarkus.datasource.password=${DB_PASSWORD}`. That `${...}` is config injection, explained properly here.

The mental model is the same as Spring's ([/guides/spring-boot-from-zero](/guides/spring-boot-from-zero)): **configuration is how one build of your app adapts to many environments without recompiling.** You ship a single artifact and feed it different values on your laptop, in test, or in production. Quarkus has one extra wrinkle Spring doesn't - we'll get there at the end.

## `application.properties` and MicroProfile Config

📝 Quarkus config is built on **MicroProfile Config**, a standard with a defined API (`@ConfigProperty`, `Config`) and ordering rule, implemented via an engine called SmallRye. One config file drives *both* Quarkus's own machinery and your application's settings.

```properties
# Quarkus's own settings - the quarkus.* namespace
quarkus.http.port=8081
quarkus.log.level=INFO

# Your application's settings - any namespace you like
catalog.currency=USD
catalog.max-page-size=50
```

*What just happened:* `quarkus.*` keys configure the framework; `catalog.*` keys are yours - Quarkus just makes them available to your code. One flat list of dotted keys; the namespace prefix is the only distinction.

💡 Quarkus also accepts YAML with the `quarkus-config-yaml` extension and `application.yaml` - reads better with dozens of keys, works identically otherwise.

## Injecting a single value with `@ConfigProperty`

`@ConfigProperty` injects one value by key into a CDI bean:

```java
package org.acme.catalog;

import org.eclipse.microprofile.config.inject.ConfigProperty;
import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class PricingService {

    @ConfigProperty(name = "catalog.currency", defaultValue = "USD")
    String currency;

    public String label(Product p) {
        return p.price + " " + currency;
    }
}
```

*What just happened:* `defaultValue = "USD"` is the safety net - if the key is missing everywhere, you get `"USD"` instead of a startup failure. Leave the default off and a missing required key fails loudly at boot, which is often what you want.

⚠️ The injected *type* is checked too. Declare `int max;` for `catalog.max-page-size` and Quarkus converts `"50"` to an `int` - a non-numeric value fails at startup with a clear error, not three layers into a request.

## Type-safe config groups: `@ConfigMapping`

For a *group* of related settings, scattering `@ConfigProperty` fields gets messy. 📝 **`@ConfigMapping`** binds a whole prefixed block into one typed interface - Quarkus's counterpart to Spring's `@ConfigurationProperties`.

```java
package org.acme.catalog;

import io.smallrye.config.ConfigMapping;
import io.smallrye.config.WithDefault;

@ConfigMapping(prefix = "catalog")
public interface CatalogConfig {

    String currency();                 // binds catalog.currency

    @WithDefault("50")
    int maxPageSize();                 // binds catalog.max-page-size

    boolean featuredEnabled();         // binds catalog.featured-enabled
}
```

```properties
catalog.currency=USD
catalog.max-page-size=50
catalog.featured-enabled=true
```

*What just happened:* `maxPageSize()` binds to `catalog.max-page-size` - Quarkus translates camelCase to kebab-case automatically. You don't write an implementation; Quarkus generates one at build time. Inject `CatalogConfig` like any bean and call `config.maxPageSize()` with autocomplete and compile-time method names.

💡 **Prefer `@ConfigMapping` for anything beyond a single value.** Reach for `@ConfigProperty` only for the genuine one-off.

## Profiles and precedence

📝 Quarkus has built-in profiles - `dev`, `test`, `prod`. Unlike Spring's separate per-profile files, Quarkus keeps profile-specific values *in the same file* using a `%profile.` prefix:

```properties
# Applies to every profile (the base)
quarkus.log.level=INFO
catalog.currency=USD

# Only when the dev profile is active
%dev.quarkus.log.level=DEBUG
%dev.catalog.currency=USD

# Only when the prod profile is active
%prod.quarkus.log.level=WARN
%prod.quarkus.datasource.jdbc.url=jdbc:postgresql://db.internal:5432/catalog
```

*What just happened:* unprefixed lines apply everywhere; `%dev.`/`%prod.` override for that profile. `quarkus dev` runs `dev` automatically, `@QuarkusTest` runs `test`, a packaged build runs `prod` - no manual activation, no copy-paste drift.

📝 Config sources layer, higher overriding lower:

1. `@WithDefault` / `defaultValue` baked into your code
2. `application.properties` (packaged in the artifact)
3. OS **environment variables**
4. **System properties** (`-Dkey=value`)

```bash
# The file says port 8081, but this env var wins
QUARKUS_HTTP_PORT=9000 java -jar target/quarkus-app/quarkus-run.jar
```

*What just happened:* the file's `quarkus.http.port=8081` sits at layer 2, the env var at layer 3, so the app boots on 9000 - the everyday way config reaches a containerized app.

⚠️ **Watch the env-var naming.** Dots can't appear in env var names - uppercase the key and replace dots/dashes with underscores. `quarkus.datasource.password` becomes `QUARKUS_DATASOURCE_PASSWORD`. Get it wrong and your override silently does nothing. See [/guides/env-vars-and-config](/guides/env-vars-and-config).

## Build-time vs runtime config: the Quarkus gotcha

⚠️ **Some Quarkus config is fixed at BUILD time and cannot be changed at runtime.** Most config is runtime, but a subset is locked the moment you build the artifact.

Why? Quarkus does aggressive **build-time processing** (Phase 1) - it has to read certain config at build time to pre-compute wiring. A value baked into the build can't be swapped when the process later starts.

```properties
# BUILD-TIME - fixed when you build. Changing the env var at runtime does nothing.
quarkus.datasource.db-kind=postgresql

# RUNTIME - read at startup. Override freely per environment.
quarkus.datasource.jdbc.url=jdbc:postgresql://db.internal:5432/catalog
quarkus.datasource.username=catalog
quarkus.datasource.password=${DB_PASSWORD}
```

*What just happened:* `db-kind` is build-time because Quarkus uses it during the build to decide *which JDBC driver and Hibernate dialect to include* - a packaging decision. Setting `QUARKUS_DATASOURCE_DB_KIND` at startup is ignored. `jdbc.url`, `username`, `password` are runtime, overridable as expected. The Quarkus config reference marks build-time keys with a lock icon, and Quarkus logs a warning if you try to override one at runtime.

💡 The `password=${DB_PASSWORD}` keeps the real secret out of the file, resolving from the environment at startup - nothing sensitive lands in git. Never commit passwords or API keys. See [/guides/secrets-management](/guides/secrets-management).

## Recap

1. 📝 Quarkus config is built on the **MicroProfile Config** standard. One `application.properties` drives both Quarkus's own `quarkus.*` settings and your app's keys, with type-checked conversion built in.
2. **`@ConfigProperty(name=..., defaultValue=...)`** injects a single value into a bean; a missing required key fails at startup, which is usually what you want.
3. **`@ConfigMapping`** binds a whole prefixed group into a typed interface - type-safe, autocomplete-friendly, self-documenting. Prefer it for anything beyond one value (it's Quarkus's `@ConfigurationProperties`).
4. Built-in **profiles** (`%dev.`, `%test.`, `%prod.`) live in the *same* file; `quarkus dev`/`@QuarkusTest`/packaged builds pick them automatically. Sources **layer** (defaults < file < env vars < system props), so env vars override for deployment - mind the `QUARKUS_DATASOURCE_PASSWORD` ↔ `quarkus.datasource.password` naming.
5. ⚠️ The Quarkus-specific trap: **some config is fixed at BUILD time** (because of build-time optimization) and can't be changed at runtime - like `quarkus.datasource.db-kind`. Most app config is runtime; the docs mark build-time keys, and Quarkus warns when you try to override one.
6. 💡 **Never commit secrets.** Use a `${PLACEHOLDER}` and supply the value from an environment variable or secrets manager. One artifact, many environments - driven by inputs, not recompiles.

## Quick check

The three ideas worth keeping before you go reactive in the next phase:

```quiz
[
  {
    "q": "Your application.properties has quarkus.http.port=8081, but you launch with the environment variable QUARKUS_HTTP_PORT=9000. What port does the app use, and why?",
    "choices": [
      "9000 - environment variables sit higher in the source precedence order than application.properties, so they override it",
      "8081 - the packaged file is always authoritative once the app is built",
      "It fails to start because two sources set the same key",
      "Whichever was set first wins, so 8081"
    ],
    "answer": 0,
    "explain": "MicroProfile layers config sources with higher ones overriding lower: defaults < application.properties < env vars < system properties. The env var sits above the file, so the app boots on 9000 - which is exactly how one artifact runs in many environments."
  },
  {
    "q": "Why is @ConfigMapping usually preferred over @ConfigProperty for a group of related settings?",
    "choices": [
      "It binds a whole prefixed block into one typed interface - type-safe, autocomplete-friendly, and a single documented place for those settings",
      "It is the only way to read config at all in Quarkus",
      "It makes the application start faster",
      "It encrypts the values automatically"
    ],
    "answer": 0,
    "explain": "@ConfigProperty injects single values one at a time. @ConfigMapping maps a whole prefix onto a typed interface, giving you type checking, autocomplete on the methods, and one interface that documents what's configurable - Quarkus's equivalent of Spring's @ConfigurationProperties."
  },
  {
    "q": "You set QUARKUS_DATASOURCE_DB_KIND at runtime to switch databases, but Quarkus ignores it. What's going on?",
    "choices": [
      "db-kind is build-time config - Quarkus reads it during the build to bake in the right driver, so it can't be changed when the process starts",
      "The environment variable name is wrong; it should have dots, not underscores",
      "Datasource config can never be set from environment variables",
      "Quarkus only reads db-kind from a YAML file, never properties"
    ],
    "answer": 0,
    "explain": "Because of Quarkus's build-time optimization, a subset of config (like quarkus.datasource.db-kind) is fixed when you build the artifact and cannot be changed at runtime. The driver and dialect were chosen during the build, so a runtime override is ignored - Quarkus even logs a warning. Runtime keys like the jdbc.url override fine."
  }
]
```


---

# Reactive Quarkus with Mutiny

Reactive programming has a scary reputation, but the core idea is something you already understand from waiting tables, standing in lines, or (if you've done JavaScript) the event loop. We'll build the mental model, meet Quarkus's reactive library (Mutiny), write a reactive endpoint end-to-end, and then cover the plain-spoken part most tutorials skip: **when you should not bother.**

You've carried a `Product` since [Phase 3](03-rest-apis.md), where a handler could return `Uni<Product>` instead of a plain `Product`. This is where that promise gets paid off.

## Why reactive exists

📝 Start with **one thread per request**: a request comes in, the server assigns a thread from a pool. When your handler asks the database for a product, it **blocks** - the thread sits frozen waiting. When the reply lands, the thread finishes and returns to the pool.

Most of a request's life is *waiting* - on the database, another service, the network. During that waiting, the thread is pinned and **idle**, and each thread costs real memory. If you have 200 threads and 200 slow requests, request 201 *queues* - not because the CPU is busy, but because every thread is waiting on I/O.

💡 **The reactive idea in one sentence:** when a request has to wait on I/O, hand the thread back so it can advance *other* requests, and resume this one later when data is ready. A handful of threads can then keep thousands of waiting requests in flight.

```mermaid
flowchart TD
  subgraph Blocking["Thread-per-request (blocking)"]
    R1["request 1"] --> T1["thread 1<br/>FROZEN waiting on DB"]
    R2["request 2"] --> T2["thread 2<br/>FROZEN waiting on DB"]
    R3["request 3"] --> Wait["no thread free<br/>→ queued"]
  end
  subgraph Reactive["Event loop (non-blocking)"]
    A["request 1"] --> EL["a few event-loop<br/>threads"]
    B["request 2"] --> EL
    C["request 3"] --> EL
    EL -->|hands thread back<br/>while I/O waits| EL
  end
```

*What just happened:* on the left, each request owns a thread for its entire life. On the right, a small number of **event-loop** threads service all three - a thread isn't stuck on one request's database call, it moves on and comes back when the reply arrives.

This "hand the thread back while you wait" rhythm is *exactly* the JavaScript event loop - see [Async/Await & the Event Loop](/guides/async-await-and-the-event-loop). Reactive Quarkus is that same machine, brought to Java. "Reactive" just means "don't block the thread; describe what to do when the value arrives."

## Mutiny: `Uni` and `Multi`

If your handler can't *block* waiting for the product, you need a value representing *a result that isn't here yet*. **Mutiny** is the reactive library Quarkus uses, with two core types:

- 📝 **`Uni<T>`** - a promise of **one** value that arrives later (or a failure). Same idea as a JavaScript `Promise` or `CompletableFuture`.
- 📝 **`Multi<T>`** - a **stream** of many values over time: query rows, file lines, server-sent events.

```java
import io.smallrye.mutiny.Uni;

Uni<Product> productLater = productService.findById(1L);
// productLater is a promise. No product has been fetched yet.
// Nothing has actually run.
```

*What just happened:* `findById` returned a `Uni<Product>` **immediately**, without touching the database. The variable holds a description of work, not a result - a method returning `Uni<Product>` hands you a *plan*, not an *answer*. It executes only when something **subscribes**.

## Transforming reactively

You can't write `Product p = findById(id); return p.getName();` - there's no product yet. Instead you **attach** the transformation, describing what happens *when* the value shows up:

```java
import io.smallrye.mutiny.Uni;

Uni<String> nameLater = productService.findById(1L)
    .onItem().transform(product -> product.getName().toUpperCase())
    .onItem().transform(name -> "Product: " + name)
    .onFailure().recoverWithItem("Product: UNKNOWN");
```

*What just happened:* `.onItem().transform(...)` says "when the product arrives, map it." `.onFailure().recoverWithItem(...)` substitutes a fallback on failure. None of these lambdas has run yet - the chain reads sequentially but executes asynchronously, later, when subscribed.

💡 Imperative code *does things*. Reactive code *describes things to do*. You compose the "what happens when," and never block waiting for "when."

## Reactive endpoints & data access

A JAX-RS handler returning `Uni<Product>` gets subscribed to by Quarkus REST, which serializes the response when the value arrives - no thread blocks meanwhile. Pair with **reactive Panache**, where `findById` itself returns a `Uni`, and the whole path from HTTP to database is non-blocking:

```java
import io.smallrye.mutiny.Uni;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import org.jboss.resteasy.reactive.RestResponse;

@Path("/products")
public class ProductResource {

    @GET
    @Path("/{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public Uni<RestResponse<Product>> getOne(@PathParam("id") Long id) {
        return Product.<Product>findById(id)                       // returns Uni<Product>
            .onItem().ifNotNull().transform(RestResponse::ok)       // found → 200 + body
            .onItem().ifNull().continueWith(RestResponse.notFound()); // missing → 404
    }
}
```

*What just happened:* `Product.findById(id)` on a **reactive Panache** entity returns a `Uni<Product>` describing the query without blocking. `ifNotNull().transform(...)` wraps a found product in `200 OK`; `ifNull().continueWith(...)` produces `404`. The handler returns instantly; Quarkus subscribes, the database works off-thread, and the response writes when the value lands.

⚠️ Reactive Panache is a *separate* extension from the classic blocking one (`quarkus-hibernate-reactive-panache` vs `quarkus-hibernate-orm-panache` from [Phase 5](05-persistence-with-panache.md)), talking over a reactive driver. Don't mix a blocking call into a reactive chain - it stalls every other request that thread was juggling.

## When to use it (the plain-spoken part)

⚠️ **Reactive is not free.** A Mutiny chain is harder to read, debug, and reason about than straight-line code. Stack traces get worse - a throw three `.onItem()` steps deep points at Mutiny's machinery, not your line number. Mistakes are quiet: block the event loop once and throughput quietly collapses.

💡 Pay this tax only when it buys you something. Reactive shines under **high concurrency with lots of I/O waiting** - thousands of connections mostly idle, waiting on databases or downstream services. That's precisely where thread-per-request hits its wall.

💡 For ordinary CRUD, **imperative Quarkus is perfectly fast.** Quarkus runs imperative handlers on a **worker thread**, off the event loop, so a blocking database call there is completely fine - it blocks a worker, not the event-loop threads. You get straight-line code with throughput that's more than enough for most applications.

📝 **Don't go reactive by default - go reactive when the load profile demands it.** Write imperative first; it's simpler and what you'll be glad to maintain at 2 a.m. Reach for `Uni`/`Multi` when you have a concrete, measured problem - extreme concurrency, heavy I/O fan-out.

## Recap

1. **Blocking pins a thread per request.** One thread per request spends most of its life *idle*, frozen
   on I/O - which caps concurrency and wastes memory when many requests wait at once.
2. **Reactive hands the thread back during waits.** A few non-blocking event-loop threads keep thousands
   of waiting requests in flight - the same idea as JavaScript's event loop, brought to Java.
3. **Mutiny gives you `Uni` and `Multi`.** `Uni<T>` is a promise of one future value; `Multi<T>` is a
   stream of many over time. Both describe work to run later, when subscribed - they don't hold a value.
4. **You compose, not block.** `.onItem().transform(...)` and `.onFailure().recoverWith...()` describe
   "what to do when the value (or error) arrives." The chain reads sequentially but runs asynchronously.
5. **End-to-end non-blocking.** A handler returning `Uni<Product>` plus reactive Panache (`findById`
   returning a `Uni`) keeps the whole path off the blocking model - but never sneak a blocking call into a
   reactive chain.
6. **Use it only when the load demands it.** Reactive wins under high concurrency with heavy I/O waiting;
   for ordinary CRUD, imperative Quarkus runs on a worker thread, is plenty fast, and is far simpler. Don't
   default to reactive.

## Quick check

Make sure the reactive model - and the clear caveat - landed:

```quiz
[
  {
    "q": "In the thread-per-request (blocking) model, why does high concurrency run out of threads even when the CPU is mostly idle?",
    "choices": [
      "Each request pins a thread for its whole life, and that thread sits frozen and idle while waiting on I/O - so threads run out while the machine is mostly waiting, not computing",
      "Threads are deleted after every request, so the pool empties",
      "The CPU can only run one thread at a time, so extra threads are useless",
      "Blocking code uses more CPU per request than reactive code"
    ],
    "answer": 0,
    "explain": "A blocked thread is pinned to one request and idle while it waits on the database or network. With most of each request spent waiting, the pool drains and new requests queue - even though the CPU has little to do. Reactive avoids this by handing the thread back during waits."
  },
  {
    "q": "What does a method returning Uni<Product> actually give you the instant it returns?",
    "choices": [
      "A promise describing how to get a product and what to do when it arrives - no product has been fetched and nothing runs until something subscribes",
      "The fully loaded Product object, fetched synchronously",
      "Null, until the database call finishes in the background",
      "A blocking call that freezes the thread until the product is ready"
    ],
    "answer": 0,
    "explain": "A Uni<Product> is a plan, not an answer. It returns immediately without blocking or even touching the database; the work runs only when subscribed (Quarkus subscribes for you when you return it from a handler)."
  },
  {
    "q": "You're building an ordinary CRUD service with moderate traffic. What's the straight recommendation?",
    "choices": [
      "Use imperative Quarkus - it runs handlers on a worker thread, is plenty fast for CRUD, and is far simpler to read and debug; go reactive only when high concurrency with heavy I/O demands it",
      "Always use reactive - imperative Quarkus is slow and outdated",
      "Mix blocking JDBC calls into reactive chains to get the best of both",
      "Reactive is required for any database access in Quarkus"
    ],
    "answer": 0,
    "explain": "Imperative isn't slow: Quarkus runs imperative handlers on worker threads, so blocking there is fine and throughput is ample for typical CRUD. Reactive adds real complexity and worse stack traces, so reserve it for the high-concurrency, I/O-heavy load profile where it genuinely pays off."
  }
]
```


---

# Testing Quarkus Apps

In the Spring world, testing forces a constant economic choice (see [Testing Spring Boot Apps](/guides/spring-boot-from-zero)): boot the whole app for confidence but pay in seconds, or boot a thin slice to stay fast. That tension exists because a classic JVM app is *expensive to start*. Here's the mental model that reframes everything in this phase: **Quarkus moved that startup cost to build time ([Phase 1](01-what-quarkus-is.md)), so booting a real app in a test is cheap - which means you can lean on real, end-to-end tests far more than you're used to.**

The whole reason slice tests exist is to dodge a slow boot. When the boot is fast, a lot of that ceremony melts away. You'll still write plain unit tests for pure logic - but the default in Quarkus is to boot the genuine application and exercise the real wiring, because doing so costs you almost nothing.

We'll walk four things: `@QuarkusTest` (boot the real app), REST Assured (test the HTTP contract), Dev Services in tests (a real database, free), and then native testing for CI.

## `@QuarkusTest` - boot the real app, cheaply

📝 `@QuarkusTest` starts a **real Quarkus application** for your test class - the same app, wired the same way, beans and all. Because the app boots in milliseconds, this isn't the heavyweight "integration test" tax you'd brace for elsewhere; it's the *normal* way to test in Quarkus. You inject the real beans with `@Inject` and exercise the real wiring. (For the broader where-does-this-test-sit picture, [Unit, Integration & E2E](/guides/unit-integration-e2e) is the companion read.)

Here's a `@QuarkusTest` exercising the `ProductService`:

```java
package org.acme;

import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.*;

@QuarkusTest
class ProductServiceTest {

    @Inject
    ProductService products;          // the REAL bean, wired by Quarkus

    @Test
    void rejectsDuplicateSku() {
        products.create(new Product("Widget", "SKU-1", 9_99));

        assertThatThrownBy(() ->
            products.create(new Product("Widget Clone", "SKU-1", 9_99)))
            .isInstanceOf(DuplicateSkuException.class);
    }
}
```

*What just happened:* `@QuarkusTest` booted the actual application context, and `@Inject ProductService` handed you the genuine, fully-wired bean - not a hand-constructed object, not a mock. You then exercised the real duplicate-SKU rule against the real service. In a classic JVM framework you'd think twice before booting the whole app for one rule like this; in Quarkus the boot is fast enough that this *is* the comfortable default. The same machinery that makes Quarkus cheap to run in production makes it cheap to test.

## Testing endpoints with REST Assured

The service test never touched HTTP. But the endpoint has its own contract - status codes, JSON shape - and Quarkus ships the perfect tool for asserting it.

📝 **REST Assured** is a fluent HTTP-testing library bundled into the Quarkus test setup. It reads almost like a sentence: `given()` (set up the request), `when()` (fire it), `then()` (assert the response). Combined with `@QuarkusTest`, it sends *real* HTTP requests at your running app and lets you assert on status and the JSON body.

```java
package org.acme;

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

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

@QuarkusTest
class ProductResourceTest {

    @Test
    void listReturnsProducts() {
        given()
          .when().get("/products")
          .then()
             .statusCode(200)
             .body("$", not(empty()));        // the JSON array isn't empty
    }

    @Test
    void getByIdReturnsTheProduct() {
        given()
          .when().get("/products/1")
          .then()
             .statusCode(200)
             .body("sku", is("SKU-1"))
             .body("name", is("Widget"));
    }
}
```

*What just happened:* with the app booted by `@QuarkusTest`, REST Assured fired genuine `GET` requests at `/products` and `/products/1`. The first asserts a `200` and that the returned JSON array has contents; the second digs into the JSON body (`body("sku", ...)`) to confirm the serialized fields match. This is the endpoint's contract, tested over real HTTP - no mocked controller, no fake request pipeline. What the client sees is exactly what the test checks.

💡 REST Assured's `body(path, matcher)` uses a GPath/JSONPath-style expression, so you can reach deep into nested JSON without deserializing it yourself. It's the fastest way to pin down "the API returns *this* shape."

## Dev Services in tests: a real database, zero config

Both tests above quietly assumed the app could start - including its database. So where did the database come from? You didn't configure one. The answer is the same magic you met in dev mode.

💡 **Dev Services runs in tests too.** Recall from [Phase 2](02-dev-mode-and-dx.md) that when an extension needs a service you haven't configured, Quarkus auto-starts a throwaway container for it. That behavior covers the `%test` profile as well: a test that hits Postgres gets a *real* Postgres, spun up in a container, wired in automatically, and torn down when the suite finishes. You get production-engine confidence with none of the manual setup - no `@Container` field, no datasource URLs, no "install Postgres first" wiki page.

```console
INFO  Dev Services for default datasource (postgresql) started
INFO  Container postgres:16 is starting...
INFO  Profile test activated.
INFO  ProductResourceTest > listReturnsProducts() PASSED
```

*What just happened:* before the test class ran, Quarkus saw the Postgres extension with no test datasource configured and started a throwaway `postgres:16` container, exactly as it does in dev. Your `@QuarkusTest` then ran against that real database and the container vanished afterward. Think about what this collapses: in the Spring world the equivalent confidence needs Testcontainers wired up by hand with `@Container` and `@DynamicPropertySource`; here it's the default, and you wrote zero lines for it. ⚠️ Like in dev, this needs a container runtime (Docker or Podman) on the machine running the tests - including your CI runner.

## Continuous testing, profiles, and mocking a bean

A few smaller tools round out the picture. Keep these in your back pocket.

**Continuous testing** - you already met it in [Phase 2](02-dev-mode-and-dx.md): with `quarkus dev` running, press `r` and Quarkus re-runs only the tests affected by each change, live in the terminal. The tests you're writing here are exactly what that loop runs. You get red/green feedback seconds after a save, without leaving the editor.

**Test profiles** - when a test needs different config than dev or prod, two tools handle it. Anything under the `%test` profile in `application.properties` applies only when tests run, and `@TestProfile` lets a specific class override config or swap beans for its scope.

```properties
# application.properties - only active while tests run
%test.product.featured-limit=3
%test.quarkus.log.level=WARN
```

*What just happened:* the `%test.` prefix scopes these properties to the test profile, so tests see a `featured-limit` of `3` and quieter logging while dev and prod keep their own values. It's the Quarkus equivalent of a test-only config file - no separate file needed, just a prefix.

💡 When you genuinely need to isolate one bean - say, a service that calls a paid external API you don't want hit in tests - use `@InjectMock` to replace just that bean with a Mockito mock while the rest of the app stays real:

```java
@QuarkusTest
class ProductResourceMockTest {

    @InjectMock
    PricingClient pricing;            // this ONE bean becomes a mock

    @Test
    void usesQuotedPrice() {
        when(pricing.quote("SKU-1")).thenReturn(1_299);
        // ... the rest of the app is real; only PricingClient is faked ...
    }
}
```

*What just happened:* `@InjectMock` told Quarkus to put a Mockito mock of `PricingClient` into the running application context in place of the real one. Everything else - the resource, the service, the database via Dev Services - stays genuine; you've faked exactly the seam you needed to control. That's surgical isolation without dropping the real-app boot.

## Native testing and CI

There's one class of bug a JVM test can never catch, and it's worth naming.

📝 In [Phase 9](09-native-compilation.md) you'll compile the app to a **native executable** with GraalVM. Native compilation does aggressive ahead-of-time analysis, and it can break things that work fine on the JVM - most commonly **reflection**: code that inspects classes at runtime may find them stripped from the native image. `@QuarkusIntegrationTest` exists to catch exactly this. Point it at a test class and it runs that suite against the *actual built artifact* - the native executable (or the runnable JAR) - instead of an in-process JVM app.

```java
package org.acme;

import io.quarkus.test.junit.QuarkusIntegrationTest;

@QuarkusIntegrationTest          // runs ProductResourceTest against the NATIVE binary
class ProductResourceIT extends ProductResourceTest {
}
```

*What just happened:* `ProductResourceIT` inherits every test from the JVM `ProductResourceTest`, but `@QuarkusIntegrationTest` changes *what they run against*. Instead of booting an in-process app, it launches the packaged artifact - the native executable when you build with `-Dnative` - and fires the same REST Assured requests at it over real HTTP. If a serialization path relied on reflection that the native build stripped away, this test goes red where the JVM test stayed green. (The `IT` suffix is the Maven Failsafe convention for integration tests; they run in a separate `verify` phase, not with the regular unit tests.)

⚠️ **Native tests are slow** - building the native image alone can take minutes. Don't run them on every dev-loop save; that's what fast `@QuarkusTest` is for. Run native tests in **CI** instead, as a gate before release (see [Testing in CI](/guides/testing-in-ci)). The everyday inner loop stays JVM-fast; the native check runs where slowness doesn't hurt your flow.

💡 Step back and see the through-line: Quarkus's fast boot makes integration testing cheap enough to *prefer*. Where other stacks push you toward mocks and slices to dodge a slow startup, here the real app, a real database, and real HTTP are the comfortable default - and you reserve the genuinely expensive test, the native one, for CI. Fast boot isn't only a production story; it quietly reshapes how you test.

## Recap

1. **`@QuarkusTest` boots the real app - and that's cheap.** Build-time work means a real, fully-wired app starts in milliseconds, so booting it for a test is the normal default, not a heavyweight last resort. Inject real beans with `@Inject`.
2. **REST Assured tests the HTTP contract.** The bundled `given().when().get(...).then().statusCode(...).body(...)` library fires real requests and asserts status plus JSON shape - the endpoint's contract, over real HTTP.
3. **Dev Services runs in tests too.** A test that needs Postgres gets a real, throwaway Postgres container with zero config - production-engine confidence for free. ⚠️ Needs Docker/Podman, including on CI.
4. **Profiles and `@InjectMock` for the edge cases.** `%test` config and `@TestProfile` scope settings to tests; `@InjectMock` swaps one bean for a Mockito mock while the rest of the app stays real. Continuous testing (Phase 2, press `r`) reruns affected tests live.
5. **`@QuarkusIntegrationTest` runs the suite against the native binary.** It catches native-only bugs (reflection!) that JVM tests can't. ⚠️ Slow - run it in CI, not every dev loop. 💡 Fast boot makes leaning on real integration tests cheap enough to be the default.

## Quick check

Make sure the "real app is cheap to test" model - and when to reach for each tool - actually stuck:

```quiz
[
  {
    "q": "Why is booting the real app with @QuarkusTest considered cheap in Quarkus, unlike classic JVM integration tests?",
    "choices": [
      "Quarkus moved startup work to build time, so the real app boots in milliseconds",
      "@QuarkusTest secretly mocks every bean so nothing really starts",
      "Quarkus only runs one test per JVM to amortize the cost",
      "It skips wiring the beans, so there's nothing to boot"
    ],
    "answer": 0,
    "explain": "Quarkus does heavy lifting at build time (Phase 1), so a real, fully-wired application starts in milliseconds. That cheap boot is exactly why booting the genuine app for a test is the comfortable default in Quarkus rather than an expensive last resort."
  },
  {
    "q": "Your @QuarkusTest hits an endpoint that reads from Postgres, but you never configured a datasource for tests. What happens?",
    "choices": [
      "The test fails because there's no database connection",
      "Dev Services starts a throwaway Postgres container for the test profile and wires the app to it",
      "Quarkus falls back to an in-memory map and ignores Postgres",
      "You must add a @Container field and a datasource URL by hand first"
    ],
    "answer": 1,
    "explain": "Dev Services applies to the %test profile too: when an extension needs a service you haven't configured, Quarkus auto-starts a throwaway container (real Postgres) and wires it in, then tears it down after the suite. It needs a container runtime like Docker or Podman, including on CI."
  },
  {
    "q": "What does @QuarkusIntegrationTest give you that a regular @QuarkusTest cannot, and when should you run it?",
    "choices": [
      "It runs the suite against the actual built artifact (e.g. the native executable) to catch native-only bugs like reflection failures; run it in CI because it's slow",
      "It runs the tests faster than @QuarkusTest, so use it for the everyday dev loop",
      "It mocks the database automatically so no container is needed",
      "It is identical to @QuarkusTest but with a different annotation name"
    ],
    "answer": 0,
    "explain": "@QuarkusIntegrationTest runs the suite against the packaged artifact - the native executable when built with -Dnative - instead of an in-process JVM app, so it catches native-only bugs (commonly reflection issues) that JVM tests miss. Because building the native image is slow, you run it in CI as a release gate, not on every dev-loop save."
  }
]
```


---

# Native Compilation & Containers

This is the phase the whole guide has been building toward. Back in [Phase 1](01-what-quarkus-is.md) we planted one idea - Quarkus moves framework work from startup to build time - and promised that the same idea unlocks something dramatic at the end. This is the end. You're going to take your `Product` service, the same code you've tested in [Phase 8](08-testing.md), and compile it into a single executable file that boots in tens of milliseconds and sips memory. No JVM. No warmup. Just a binary that *is* your application.

The payoff is real, but native compilation has a reputation for being scary and finicky. It isn't, once you understand *why* it works and *where* it bites. So we'll build the mental model first, the way we always do, and only then run the build.

## JVM mode vs native mode

You already know how Java normally runs, from [/guides/java-from-zero](/guides/java-from-zero). Let's recap it because native mode is defined by contrast.

📝 **JVM mode (the normal way)** - `javac` turns your source into portable **bytecode** (`.class` files). At runtime, the **JVM** loads that bytecode, interprets it at first, and uses the **JIT compiler** to recompile your hot methods into native machine code *while the program runs*. That's why a JVM app has a **warmup** period - it gets faster the longer it runs - and why it carries the JVM itself plus a heap in memory.

That model is excellent for long-running services (it's why steady-state Java can match C). But it has two costs that hurt in the cloud: the JVM has to be present and started, and the warmup tax is paid on every fresh boot.

📝 **Native mode** - instead of shipping bytecode for a JVM to run, you use **GraalVM** to **ahead-of-time (AOT) compile** your entire application - your code, the framework, and the parts of the JDK you actually use - into a single standalone OS executable. There is no separate JVM and no bytecode at runtime. The code is already native machine code from the first instruction.

The difference, side by side:

```mermaid
flowchart TB
  subgraph JVM["JVM mode"]
    J1[Build: compile to bytecode] --> J2[Startup: load JVM + warm up JIT] --> J3[Serve requests]
  end
  subgraph Native["Native mode"]
    N1[Build: AOT-compile to one binary] --> N2[Startup: run native code directly] --> N3[Serve requests]
  end
```

*What just happened:* notice where the work lives. In JVM mode, "load the JVM and warm up" sits at **startup**, on every boot. In native mode, all the compilation happened once at **build time**, so startup collapses to "start running." The trade is that the native build itself does a lot more work - we'll feel that shortly. The runtime result is a process that starts in **tens of milliseconds** and uses a **fraction of the memory** of the same app on a JVM, because there's no JVM and no JIT machinery to carry.

💡 **Insight.** Native mode doesn't make your *steady-state* request handling dramatically faster - a warmed-up JVM is already very fast. What it eliminates is the *cost of starting and the cost of existing*: boot time and idle memory. That's exactly the bill you pay over and over in containers and serverless, which is why this matters.

## How it's possible: the closed-world assumption

Here's the natural question: if AOT-compiling Java were easy, every framework would do it. Why is it hard, and why can Quarkus do it cleanly?

📝 **The closed-world assumption** - to compile your app to a fixed native binary, GraalVM must know, *at build time*, every class and method the program could ever execute. It performs a reachability analysis from your entry points, compiles everything it can reach, and discards the rest. There's no "load a class we discover later" at runtime, because the binary is sealed - whatever was not included at build time is not there at all.

This is precisely where a traditional Java framework struggles. Classic frameworks *discover* their wiring at startup: they scan the classpath, read annotations via reflection, and construct objects dynamically. From GraalVM's point of view that's a moving target - it can't be sure which classes will actually be loaded, so it can't safely throw anything away.

💡 **Insight - this is why "build-time over runtime" was the foundational idea.** Look back at [Phase 1](01-what-quarkus-is.md) and [Phase 4](04-cdi-with-arc.md): Quarkus *already* resolved all that scanning, reflection, and dependency-injection wiring at build time. So by the time GraalVM shows up, Quarkus can hand it a precise, finished list of exactly what your `Product` service uses. The closed-world assumption isn't a constraint Quarkus fights - it's the thing Quarkus was designed around from the start. Build-time wiring and native compilation are two sides of the same coin.

## Building native

Enough theory - let's compile the `Product` service. The native build is driven by one command:

```bash
quarkus build --native
```

*What just happened:* this tells Quarkus to run a full build and then invoke GraalVM's native-image tool on the result, producing a standalone executable in `target/` (something like `product-service-1.0.0-SNAPSHOT-runner`, with no `.jar` and no JVM needed to launch it). This step requires GraalVM to be installed locally.

Most people don't want to install and manage GraalVM, so Quarkus offers a container-based build instead:

```bash
quarkus build --native -Dquarkus.native.container-build=true
```

*What just happened:* `quarkus.native.container-build=true` tells Quarkus to perform the native compilation *inside a container image* that already has GraalVM set up. You get a native binary built for Linux (perfect for shipping in a container) without installing GraalVM on your own machine - you only need a working container runtime like Docker or Podman.

When you run the resulting binary, the startup line tells the story:

```console
__  ____  __  _____   ___  __ ____  ______
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
INFO  product-service 1.0.0-SNAPSHOT native (powered by Quarkus) started in 0.018s.
INFO  Profile prod activated.
INFO  Installed features: [cdi, hibernate-orm-panache, rest, jdbc-postgresql]
```

*What just happened:* read the startup time - `0.018s`, eighteen *milliseconds*. Compare that to the sub-second JVM start from [Phase 1](01-what-quarkus-is.md); native shaves another order of magnitude off. And critically, notice it says **native**, not "on JVM" - there is no JVM in that process at all. The binary is the application.

⚠️ **Gotcha - native builds are slow and memory-hungry, on purpose.** That reachability analysis and AOT compilation is a *lot* of computation - expect **minutes**, not seconds, and a build that can demand several gigabytes of RAM. This is why you do **not** native-build in your day-to-day loop. Your inner loop stays in JVM mode (`quarkus dev` with live reload, from [Phase 2](02-dev-mode-and-dx.md)) and your tests run on the JVM too. Native compilation belongs in **CI and release pipelines**, where a slow build once per release is a fine trade for a tiny, fast artifact.

## The reflection gotcha

Now the single most important pitfall to understand, because it's the one that surprises people: **native works on the JVM but fails as native**.

⚠️ **Gotcha - code GraalVM can't *see* at build time gets dropped.** Remember the closed-world assumption: only reachable code makes it into the binary. The classic way to defeat GraalVM's static analysis is **runtime reflection** or **dynamic class loading** - code that decides *at runtime* to instantiate a class by name, or read its fields reflectively. The analysis can't follow a class name that's computed at runtime, so it concludes that class is unreachable and discards it. Your app then runs perfectly in JVM mode (where everything is loaded on demand) and throws a `ClassNotFoundException` or a missing-method error as a native binary.

The good news: for your `Product` service you'll rarely hit this yourself, because **Quarkus extensions already register their reflection needs at build time**. Hibernate, JAX-RS, JSON serialization - the framework code knows what it reflects on and tells GraalVM. The cases that bite are *your own* code or *third-party libraries* that reflect on classes the framework can't know about.

When that happens, you tell GraalVM explicitly. The simplest tool is an annotation:

```java
import io.quarkus.runtime.annotations.RegisterForReflection;

@RegisterForReflection
public class ProductImportRow {
    public String sku;
    public String name;
    public double price;
}
```

*What just happened:* `@RegisterForReflection` is a build-time instruction to GraalVM: "keep this class and its members, and keep them reflectively accessible, even if the static analysis thinks nothing reaches them." You'd reach for this when, say, a CSV or JSON library populates `ProductImportRow` instances by reflection and the analyzer can't see that path. (There's also a config-file form for classes you can't annotate, like ones inside a third-party JAR.)

💡 **Insight - this is why you native-test in CI.** Because the reflection gotcha is invisible in JVM mode, you cannot catch it by running your normal JVM tests. Quarkus lets you run your integration tests *against the native binary* (the `@QuarkusIntegrationTest` style from [Phase 8](08-testing.md)). Wire that into CI and a dropped class becomes a failed test in the pipeline, not a 2 a.m. production page. The rule: if you ship native, you test native.

## When native is worth it

Native is the top of the ladder, not the price of admission. Choose it deliberately.

💡 **Insight - match the mode to the deployment profile.** Native shines in two situations. First, **serverless and scale-to-zero** workloads, where a function spins up on demand and a slow boot becomes a user-facing **cold start** - eighteen milliseconds vs. several seconds is the difference between snappy and sluggish. Second, **high-density** deployments: many small instances where the per-replica memory bill dominates, and native's tiny footprint lets you pack far more onto the same hardware (cheaper, and faster to autoscale).

For an **always-on** service that boots once and runs for weeks, the calculus flips. A JVM-mode Quarkus app *already* starts fast and uses modest memory (the build-time wiring helps either way), and JVM mode keeps your build fast and skips the reflection gotcha entirely. Plenty of teams develop and even deploy their `Product` service in JVM mode and reach for native only where cold-start latency or memory density truly pays for itself. Neither choice is "the advanced one" - they're different tools for different bills.

## Container-first & Kubernetes

Quarkus was built for containers, so packaging is a first-class concern, not an afterthought.

📝 When you generate a Quarkus project it already includes Dockerfiles under `src/main/docker` - separate ones for JVM mode and for native mode. The native Dockerfile produces a tiny image: just a minimal base plus your single executable, no JDK layer to drag along.

A minimal native Dockerfile looks like this:

```dockerfile
FROM quay.io/quarkus/quarkus-micro-image:2.0
WORKDIR /work/
COPY target/*-runner /work/application
EXPOSE 8080
CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]
```

*What just happened:* the base image is a stripped-down runtime (no JVM - the native binary doesn't need one). We copy in the single `*-runner` executable produced by the native build, expose the HTTP port, and run the binary directly. The result is an image measured in **tens of megabytes** instead of the hundreds a JVM-based image carries. If the container model itself is new to you, [/guides/docker-without-the-magic](/guides/docker-without-the-magic) walks through `FROM`, layers, and `CMD` from the ground up.

Build it:

```bash
quarkus build --native -Dquarkus.native.container-build=true
docker build -f src/main/docker/Dockerfile.native -t product-service:native .
```

*What just happened:* the first command produces the Linux-native binary (in a container, so no local GraalVM needed); the second packages that binary into the small image described above. You now have a deployable artifact - a container that starts in milliseconds.

For the orchestration layer, Quarkus generates Kubernetes manifests for you. Add the `quarkus-kubernetes` extension and the next build emits a ready-to-apply `kubernetes.yml`:

```bash
quarkus extension add kubernetes
quarkus build --native -Dquarkus.native.container-build=true
```

*What just happened:* with the extension present, the build drops Kubernetes resource definitions (a `Deployment`, a `Service`) into `target/kubernetes/` - derived from your app's config, so you don't hand-write boilerplate YAML. You can `kubectl apply` them directly or feed them into your pipeline. If Kubernetes concepts like Deployments and Services are unfamiliar, [/guides/kubernetes-without-the-hype](/guides/kubernetes-without-the-hype) covers what each one does.

💡 **Insight - this is the whole point of Quarkus.** Walk the chain back: build-time wiring → closed-world → native binary → a tiny image → a pod that starts in milliseconds and uses little memory. Small, fast containers are cheaper to run and faster to scale - exactly the costs that hurt in the cloud world [Phase 1](01-what-quarkus-is.md) opened with. Native compilation isn't a party trick bolted on at the end; it's the destination the entire design was pointed at.

## Recap

- **JVM mode vs native mode:** JVM mode ships bytecode that a JVM loads, warms up (JIT), and runs - fast at steady state but with boot + warmup + JVM-memory costs. Native mode uses **GraalVM** to **AOT-compile** the whole app into one standalone executable: ~tens-of-ms startup, a fraction of the memory, no JVM, no warmup.
- **Why it's possible - the closed-world assumption:** GraalVM must know every reachable class/method at build time. Classic frameworks discover wiring at startup and can't promise that; Quarkus already resolved its wiring at **build time** ([Phase 1](01-what-quarkus-is.md), [Phase 4](04-cdi-with-arc.md)), so it hands GraalVM a precise list. Build-time wiring and native are two sides of one coin.
- **Building native:** `quarkus build --native` (needs GraalVM) or add `-Dquarkus.native.container-build=true` to build inside a container with no local GraalVM. ⚠️ Native builds take **minutes** and lots of RAM - do them in **CI/release**, keep dev and test in JVM mode.
- **The reflection gotcha:** the #1 pitfall - runtime reflection / dynamic class loading the analysis can't see gets dropped, so code works on the JVM but fails as native. Extensions handle their own; for your/3rd-party reflective code use `@RegisterForReflection` (or config). This is why you **native-test in CI**.
- **When native is worth it:** great for serverless cold-starts and high-density/low-memory deployments; for always-on services, fast JVM-mode Quarkus is often simpler. Choose by deployment profile.
- **Container-first & Kubernetes:** Quarkus ships Dockerfiles (`src/main/docker`) and produces tiny native images; the `quarkus-kubernetes` extension generates manifests. Small, fast containers = cheaper, faster-scaling deployments - the whole point of Quarkus.

## Quick check

Lock in the trade-offs that decide whether and how you go native:

```quiz
[
  {
    "q": "Why can Quarkus AOT-compile to a GraalVM native image when classic frameworks struggle to?",
    "choices": [
      "Because native images don't use any of the framework's features at runtime",
      "Because Quarkus resolves its wiring at build time, so it can give GraalVM the closed-world list of exactly which classes and methods are reachable",
      "Because GraalVM disables reflection entirely, which classic frameworks rely on",
      "Because Quarkus apps have fewer classes than other frameworks"
    ],
    "answer": 1,
    "explain": "Native compilation needs the closed-world assumption - knowing every reachable class/method at build time. Classic frameworks discover their wiring at startup; Quarkus already resolved it at build time, so it can hand GraalVM a precise, finished list."
  },
  {
    "q": "Your Product service passes all its tests on the JVM, but as a native binary it throws a missing-class error when importing a CSV. What's the most likely cause and fix?",
    "choices": [
      "The native build ran out of memory; rerun it with more RAM",
      "A class is loaded via runtime reflection GraalVM couldn't see at build time, so it was dropped - register it with @RegisterForReflection (or config)",
      "Native mode doesn't support CSV files; switch to JSON",
      "The JVM tests were wrong; native is always correct"
    ],
    "answer": 1,
    "explain": "Under the closed-world assumption, only reachable code is kept. Reflective/dynamic class loading the analyzer can't follow gets discarded, so it works on the JVM but fails native. @RegisterForReflection tells GraalVM to keep the class - and native integration tests in CI catch it."
  },
  {
    "q": "For which deployment profile is native compilation most clearly worth its slow build and reflection caveats?",
    "choices": [
      "A single always-on service that boots once and runs for weeks",
      "Local development where you want the fastest possible inner loop",
      "Serverless or high-density deployments where cold-start latency and per-replica memory directly drive cost",
      "Any app, since native is strictly better than JVM mode in every situation"
    ],
    "answer": 2,
    "explain": "Native eliminates boot time and idle memory - exactly the recurring costs in serverless (cold starts) and high-density deployments. For an always-on service, fast JVM-mode Quarkus is often simpler, and dev should stay in JVM mode for the quick loop."
  }
]
```


---

# Production & Where to Go Next

Stop and look at what you can do now. You can stand up a Quarkus service with JAX-RS endpoints and JSON, wire its pieces together with build-time CDI instead of `new` everywhere, persist data through Hibernate with Panache, read configuration from `application.properties` with profiles, and - when you want it - write reactive pipelines with Mutiny and compile the whole thing down to a native executable that boots in tens of milliseconds. That is not a toy. That is the shape of a real cloud-native Java service.

And here's the part that matters most: you understand *why* it's fast. It wasn't a trick. Quarkus moves scanning, reflection, and wiring from runtime to **build time**, so startup is nearly free - and that same build-time work is what lets it compile to native machine code at all. You can now read what's underneath the speed instead of trusting it.

So this last phase isn't more annotations. It's the production essentials you'll want before you ship, the map of where Quarkus goes from here, a clear word on how it sits next to Spring Boot and Jakarta EE, and the one thing that turns all of this from *read* into *yours*: building something and finishing it.

## Production essentials

A service that runs on your laptop isn't quite a service that runs in production. Two small additions cover most of the gap, and in Quarkus both are extensions - you add a capability by adding a dependency.

📝 **Health checks.** Add the SmallRye Health extension and Quarkus exposes `/q/health` for free, built on the MicroProfile Health standard. It splits into `/q/health/live` (**liveness** - "is the process alive, or should it be restarted?") and `/q/health/ready` (**readiness** - "is it ready to take traffic yet?"). These map exactly onto Kubernetes liveness and readiness **probes**, which is the whole point - k8s polls those endpoints to decide when to restart a pod or route requests to it.

**Metrics.** Add the Micrometer extension and you get `/q/metrics` in Prometheus format - request counts, latencies, JVM stats - ready for Prometheus to scrape and Grafana to chart. For **tracing**, the OpenTelemetry extension instruments your service so a request can be followed across service boundaries.

💡 Health, metrics, and traces are the three pillars of knowing what your service is doing in production. The framework specifics are the easy part; the mental model is the valuable part. The [Observability: Logs, Metrics & Traces](/guides/observability-logs-metrics-traces) guide is where that model lives - read it once and these extensions click into place.

**Kubernetes-native deploy.** Quarkus was built for containers, and it shows. The `quarkus-kubernetes` extension generates Kubernetes manifests from your config, and the container-image extensions can build and push an image as part of your normal build. Combined with the native executable from Phase 9, you get a tiny image that starts almost instantly - which is exactly what autoscaling and serverless reward.

## The extension ecosystem - where to go from here

Almost everything beyond the core is an **extension**. Need messaging? Add an extension. Need scheduled jobs, gRPC, or login with Keycloak? Add an extension. Each one brings build-time integration, sensible defaults, and Dev UI support along for the ride.

```mermaid
flowchart TD
  You[You: REST + CDI + Panache + config] --> MSG[Messaging: Kafka / AMQP]
  You --> GRPC[gRPC]
  You --> SCH[Scheduling]
  You --> SEC[Security / OIDC: Keycloak]
  You --> GQL[GraphQL]
  You --> CACHE[Caching]
```

*What this shows:* six common directions out from where you stand. **Reactive Messaging** (Kafka or AMQP) is how you build event-driven systems where a service drops an event and moves on. **gRPC** gives you fast, typed service-to-service calls. **Scheduling** runs cron-style jobs. **OIDC** wires up authentication against an identity provider like Keycloak. **GraphQL** offers a different API shape, and **caching** is a single annotation away.

💡 The sensible move here is not to memorize the catalog - it's to know the catalog exists. When you hit a need, browse the extensions, add the one that fits, and let Quarkus do the build-time wiring. That's the whole workflow.

## Quarkus vs the field, plainly

Quarkus is not the only good answer, and pretending otherwise would do you a disservice. **Quarkus**, **Spring Boot**, and the **Jakarta EE / MicroProfile** servers (Helidon, Open Liberty) all overlap heavily - they all do REST, dependency injection, and persistence, and they all run real production workloads at serious companies.

What's Quarkus's actual edge? Startup time, memory footprint, native compilation, and developer experience - that loved dev mode from Phase 2. Where it's *not* clearly ahead: ecosystem breadth and sheer gravity, where Spring Boot still leads.

Here's the reassuring part. Because Quarkus runs the **shared standards** - CDI, JAX-RS, JPA, MicroProfile - the knowledge moves with you. The CDI you learned here is the CDI in [Jakarta EE](/guides/jakarta-ee-from-zero); the layered, injected, tested service is the same shape you'd build in [Spring Boot](/guides/spring-boot-from-zero). All three are employable. Knowing the standards means you can step between frameworks without starting over - which is exactly why this guide assumed them.

## What to actually build

Reading got you here. *Building* is what makes it stick - and the trick is something small enough to finish but real enough to teach you the messy parts. A concrete path that uses what you've got:

- **Take the `Product` service and make it production-shaped.** Add the health and metrics extensions, confirm `/q/health` and `/q/metrics` respond, then compile it native and run it inside a container. You've now felt the full Quarkus loop - code, observe, build native, ship - on something you already understand.
- **Add a Kafka consumer.** Wire in reactive messaging so the service reacts to events instead of only answering HTTP. That's your first taste of event-driven design without a big new framework to learn.
- **Or build a serverless function and feel the cold-start win.** Deploy a native Quarkus function where startup time is billed and visible. This is where "supersonic subatomic" stops being a slogan and becomes a number you can see.

Whatever you pick, the real instruction is one word: **finish**. One rough project carried all the way to "it runs and I can show it" teaches you more than three polished half-builds abandoned at 80%. Choose the one that excites you and take it end to end.

## A last word, and what to read

One bookmark is worth more than the rest: **quarkus.io/guides**. They're short, task-shaped walkthroughs maintained by the people who build Quarkus - "I want to do X" maps directly to a guide. Pair that with the **extension catalog** on the same site, which is how you'll answer "is there an extension for this?" (there usually is).

And remember the through-line of this whole guide. The speed was never magic. It was build-time work - scanning, reflecting, and wiring done at *compile* time instead of *startup* time - work you now understand well enough to reason about when something behaves strangely. You came in wary of "supersonic subatomic Java"; you're leaving able to explain it, build on it, and ship it. Go build the small thing, finish it, and watch it boot.

## Recap

1. **You can build and reason about a fast Quarkus service** - REST, build-time CDI, Panache, config, optional reactive and native - and you know the speed comes from moving work to build time.
2. **Production essentials are extensions:** SmallRye Health (`/q/health` liveness/readiness for k8s probes), Micrometer (`/q/metrics` for Prometheus), OpenTelemetry for tracing, plus `quarkus-kubernetes` and container-image extensions for deploy.
3. **You add a capability by adding an extension** - messaging (Kafka/AMQP), gRPC, scheduling, OIDC/Keycloak, GraphQL, caching. Know the catalog exists and browse it for your need.
4. **Quarkus, Spring Boot, and Jakarta EE overlap heavily** - Quarkus's edge is startup, memory, native, and DX. All are employable, and the shared standards (CDI/JAX-RS/JPA) let you move between them.
5. **Build one real thing and finish it** - make the `Product` service production-shaped with health and metrics, run it native in a container, add a Kafka consumer or a serverless function. Finishing beats polishing.
6. **Next reading:** quarkus.io/guides and the extension catalog. The speed was never magic - it's the build-time work you now understand.

## Quick check

Test yourself on the decisions that matter most as you leave this guide:

```quiz
[
  {
    "q": "In Quarkus, how do you add a production capability like health checks or Kafka messaging?",
    "choices": [
      "Add the matching extension (a dependency), and Quarkus wires it in at build time",
      "Rewrite the service against a separate, heavier framework",
      "Hand-configure it at runtime with reflection-based scanning",
      "It's only possible in the native build, not on the JVM"
    ],
    "answer": 0,
    "explain": "The Quarkus workflow is: hit a need, add the extension for it, and let Quarkus do the build-time integration. Health, metrics, messaging, gRPC, OIDC, and more are all extensions."
  },
  {
    "q": "Why do Quarkus's /q/health/live and /q/health/ready endpoints matter for deployment?",
    "choices": [
      "They map directly onto Kubernetes liveness and readiness probes, telling k8s when to restart a pod and when to send it traffic",
      "They are required for the native executable to compile",
      "They replace the need for metrics and tracing entirely",
      "They make the JVM start faster"
    ],
    "answer": 0,
    "explain": "Liveness answers 'is the process alive, or should it be restarted?' and readiness answers 'is it ready for traffic yet?' - which is exactly what Kubernetes probes poll to manage your pods."
  },
  {
    "q": "What's the real relationship between Quarkus, Spring Boot, and Jakarta EE?",
    "choices": [
      "They overlap heavily on the shared standards; Quarkus's edge is startup, memory, native, and DX, and knowing the standards lets you move between them",
      "Quarkus replaced both, which are now obsolete",
      "They share no concepts, so learning one teaches you nothing about the others",
      "Only Quarkus is employable in modern jobs"
    ],
    "answer": 0,
    "explain": "All three run CDI, JAX-RS, and JPA and power real production workloads. Quarkus's edge is startup/memory/native/DX, and because the standards are shared, your knowledge moves with you between frameworks."
  }
]
```
