DEV Community

Niclas Gleesborg
Niclas Gleesborg Subscriber

Posted on

Navigating CDI Scope in Quarkus: A Concurrency perspective

TL;DR
In Quarkus (a tool for building backend services), there are two ways of managing how objects (called beans) are created and used:

Application Scoped (@ApplicationScoped):
Only one object is created and shared by everyone who uses the service.
Good because it saves memory and is fast.
But you must be careful since many people might use it at the same time, which can cause problems.
Request Scoped (@RequestScoped):
A new object is made for each person or request.
Good because it's safe and easy—each user gets their own separate object.
But it uses more memory since many objects are made and then thrown away quickly.
Choosing which one to use depends on balancing how fast your service needs to run versus keeping things safe and simple.

Introduction
Managing the lifecycle and concurrency of service objects is essential in developing performant, robust microservices. Quarkus, a modern Java framework designed for container-native and cloud-native applications, relies heavily on Contexts and Dependency Injection (CDI) to control object lifecycles through annotations known as scopes. Among these, the @ApplicationScoped and @RequestScoped annotations are commonly employed to manage the lifecycle of service objects, directly influencing performance characteristics, memory usage, and concurrency behavior.

The @ApplicationScoped annotation instructs the CDI container to create a single instance of a bean that is shared throughout the application's entire lifecycle. This reuse offers substantial advantages in terms of reduced memory footprint and minimal object instantiation overhead, thereby enhancing runtime efficiency. However, its shared nature inherently introduces concurrency risks, as the single instance is accessible by multiple threads simultaneously. Consequently, improper handling or misunderstanding of thread-safety principles within these beans can lead to race conditions, inconsistent states, or application-wide errors.

In contrast, @RequestScoped beans provide a fresh instance for every individual HTTP request, naturally isolating each request's state. This isolation mitigates thread-safety concerns significantly, simplifying the development model and reducing concurrency-related risks. However, such per-request instantiation comes at the cost of increased object creation and destruction overhead, potentially affecting performance and memory consumption under high load scenarios.

@ApplicationScoped
Definition
@ApplicationScoped is a normal scope in Quarkus, meaning the CDI container creates a single shared instance of the bean for the entire application. This one instance is reused for all injection points and throughout the app’s lifetime. By default, Quarkus instantiates an @ApplicationScoped bean lazily – a client proxy is injected, and the actual bean instance is created only when you first invoke a method on that proxy​

In other words, merely injecting an @ApplicationScoped bean doesn’t construct it; the first method call triggers its creation. Once created, all injections refer to the same instance, so any state within an @ApplicationScoped bean is global (application-wide) state​

This scope remains active for the entire runtime of the application, making the bean effectively a singleton (in terms of one instance) managed by CDI.

Concurrency
Because there is only one instance of an @ApplicationScoped bean shared across the whole application, concurrency is an important concern. The CDI container does not automatically synchronize access to @ApplicationScoped beans. If multiple threads call methods on the bean at the same time (for example, multiple HTTP requests or background jobs using the same service), those calls happen concurrently on the single instance. By default, nothing in Quarkus makes an @ApplicationScoped bean thread-safe – it’s up to your bean’s implementation to handle concurrent access. If the bean is stateless (no mutable shared fields), it is naturally thread-safe. However, if it maintains any state or mutable data (caches, counters, accumulators, etc.), you must ensure thread-safety. Techniques for ensuring thread-safety is beyond the scope of this article. Treat @ApplicationScoped beans as global singletons that need explicit thread-safety measures when accessed by multiple threads. If a bean’s purpose is purely to provide stateless services (e.g. computations, database access using local variables), you don’t need additional locking. If it does maintain state (in-memory caches, etc.), use thread-safe structures or synchronization. If an @ApplicationScoped bean injects a @RequestScoped bean the @ApplicationScoped bean will be instanced for the specific request using a client proxy thus making it safe to use for the request.

Performance
The @ApplicationScoped scope can be very performance-friendly due to its reuse of a single instance. Memory footprint is minimal for the bean itself (only one instance lives in memory, aside from a small proxy). Creation cost is paid only once, when the bean is first needed, rather than per injection or per call. Lazy instantiation means your application startup isn’t burdened by constructing every single @ApplicationScoped bean up front – they initialize on demand. This can improve startup time if some application-scoped beans are never actually used. However, the first invocation on an @ApplicationScoped bean will incur the cost of creation and any initialization (@PostConstruct logic) – so the very first request or usage might be slightly slower for that bean​. After that, all calls are to an already-instantiated object. Just be mindful that if an @ApplicationScoped bean is injected but never used (no method called), it will never be instantiated under Quarkus’s lazy approach – which is efficient (zero cost) but means any initialization code in it won’t run unless triggered. If you require an application bean to initialize at startup (e.g. to pre-load caches), you can force instantiation using mechanisms like the Quarkus @startup annotation or an observer for the startup event, which will call a method and thus initialize the bean eagerly​.

Overall, use @ApplicationScoped when you want one instance for the entire app and either no state or shared state. It’s the most common scope for core services that aren’t tied to a specific user or request.

@RequestScoped
Definition
@RequestScoped is a CDI normal scope in Quarkus that ties a bean’s lifespan to an HTTP request. When a request comes into your application (e.g. a REST call), the CDI container will provide a fresh instance of each @RequestScoped bean needed for that request. The bean is created lazily – similarly to application scope – when a method on the bean is first invoked during the request​.

It then remains available throughout that single request processing pipeline (potentially across multiple components that all inject the bean). Once the request is complete (response sent), the request context is destroyed and the bean instance is disposed. In short, each HTTP request gets its own instance of the bean, and it exists only for the duration of that request​

This scope is automatically active in Quarkus whenever you have the RESTEasy (JAX-RS) or Servlet environment in use. The request context typically covers servlets, filters, JAX-RS resource methods, and any beans used within them.

Concurrency
Under normal conditions, a single HTTP request is handled by one thread at a time, so a @RequestScoped bean instance will not be accessed concurrently by multiple threads (during that request). Each thread/request has its own separate instance, so thread safety is usually not a concern for the bean’s internal state – there’s no shared state across requests by definition. This means you can freely use non-thread-safe structures inside a request bean as long as you don’t leak it out of that request. The isolation provided by the scope prevents concurrency issues between different requests (one request cannot directly access another’s bean instance).

However, there are some caveats. If your application uses asynchronous request handling or reactive processing, you must ensure the request context remains active on threads that continue processing the request. In Quarkus, if you switch threads during a request (for example, using a reactive stream), the request scope might not automatically propagate. Misusing a request-scoped bean outside of its context can lead to unpredictable behavior. Never share a @RequestScoped instance between threads or requests – it should only ever be used within the single request it was created for. Also, if you manually spawn threads inside a request and try to use the request bean there, those threads won’t have the request context unless explicitly carried over. This is a pitfall to avoid (the simpler rule: keep request-scoped usage on the request thread).

Because each request gets a new instance, any state in a @RequestScoped bean is inherently not visible to other requests (no data races across requests). There’s typically no need for synchronization inside such beans unless you perform some concurrent operation within the request itself.

In summary, @RequestScoped beans can be treated as thread-confined to the request. Just be careful not to pass references to them to other threads or cache them beyond the request.

Performance
The overhead of using @RequestScoped beans is generally low and proportional to your request load. For each request, new instances are created as needed and then garbage-collected (or destroyed) afterward. This means there is a bit of churn: if your application handles thousands of requests per second and each request needs a particular request-scoped bean, you will be creating and destroying many instances rapidly. Fortunately, object creation in Java is usually cheap, and Quarkus’s CDI container is optimized for this lifecycle. The container also reuses the context for each request in a streamlined way. One performance benefit of request scope is that it naturally limits scope of data – reducing memory bloat. Also, because Quarkus uses lazy instantiation for normal scopes, a request bean is only allocated if actually used in a given request path. If a particular request doesn’t touch a certain bean, it won’t be created. This avoids unnecessary work.

In summary, @RequestScoped beans have a small per-request cost (construction, injection, destruction) and no cross-request memory usage, which typically leads to a good balance for handling user-specific or request-specific logic. The impact is linear with request volume. As best practice, keep the state in request beans lean (only what’s needed for that request) to minimize overhead.

Top comments (0)