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, 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.
;
;
;
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.
;
;
;
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.
[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 - 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:
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; 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:
;
;
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
- 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. - 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.
- 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.
- Scopes are standard CDI.
@ApplicationScopedis the common Quarkus default;@RequestScopedfor per-request state;@Singletonas an eager, proxy-free variant. Full detail lives in the Jakarta EE CDI phase. Bonus: ArC removes unused beans for a smaller app. - Prefer constructor injection (testable,
final, no reflection needed); qualifiers and producers work as in standard CDI. For Spring devs,quarkus-spring-dibridges@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:
[
{
"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."
}
]
Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.
Check your understanding 3 questions
1. What is the key difference between Quarkus's ArC container and a traditional CDI container?
2. You @Inject a type that no bean satisfies. In Quarkus, when do you find out?
3. Why does doing dependency injection at build time help Quarkus compile to a native image?