This page defines the dependency-injection vocabulary as Vauban materialises it. The model follows the Jakarta CDI 4.1 spec; implementation choices are described in Internals.

Bean

A bean is a class managed by the container: its lifecycle, its dependency resolution and its scope are controlled by Vauban.

Three conditions are enough for a class to become a Vauban bean:

  1. The class is annotated with a scope (@ApplicationScoped, @Singleton, @Dependent, @RequestScoped, or a scope of your own) or a stereotype. NEW The annotation processor recognises any annotation meta-annotated @Stereotype, @NormalScope or @Scope, declared in the same compilation or in a library; it used to index only the classes carrying a built-in scope, @Produces or @Interceptor, so a class marked only with an application stereotype was no bean (vauban#132).

  2. The class is visible from the module-info.java (exports or same module).

  3. The class shows up in the bean index written at compile time by the annotation processor, or by the Maven plugin for archives it did not compile.

At compile time, every bean receives two generated artefacts:

  • a _Factory — implements io.vidocq.vauban.core.BeanFactory<T> (a vauban-core type) and knows how to instantiate the bean;

  • a _ClientProxy — only for normal scopes; intercepts calls and delegates to the active context.

Scope

A scope defines an instance’s lifetime and the context that holds it.

Diagram

Implemented scopes: see the usage table.

Qualifier

A qualifier is a runtime annotation that discriminates beans of the same type. The Default qualifier is implicit. CDI ships @Default, @Any, @Named; Vauban supports all custom qualifiers, including those with @Nonbinding members.

Producer

A @Produces method or field produces a bean whose instantiation is not controlled by the container but by user code. The associated disposer (@Disposes) runs on destruction. No dedicated class is generated per producer: the container builds the producer bean at runtime from the declaring bean (created through its _Factory) and invokes the producer and disposer through the module’s generated _VaubanComponents provider, falling back to reflection only when no provider arm exists.

Observer

An observer is a @Observes or @ObservesAsync method that reacts to an application event. Async observers are dispatched on virtual threads. Observer resolution is static: the EventDispatcher consults a table built at compile time.

Interceptor

An interceptor wraps a method invocation (@AroundInvoke) or constructor (@AroundConstruct). Interceptor chains are resolved at compile time and stored in the bean’s _Factory.

BeanManager

The BeanManager is CDI’s SPI façade. Vauban exposes it as VaubanBeanManager, read-only for application code. It allows:

  • programmatic bean resolution (getBeans(Type, Qualifier…​));

  • observer resolution;

  • access to active contexts.

The historical mutable methods (addBean, addObserverMethod) are unavailable: everything is frozen at build time.

Injection graph

Diagram

The user never touches the _Factory classes. They call VaubanContainer.builder().build(), select a root bean, and the container does the rest.

Build Compatible Extensions

CDI 4.1 introduces Build Compatible Extensions (BCEs): @Discovery, @Enhancement, @Registration, @Synthesis and @Validation hooks. Vauban runs them in the annotation processor at build time and records the result. NEW Each phase runs for every extension before the next one starts, so an extension’s @Registration sees the beans as every @Enhancement left them, including the classes an enhancement made beans (vauban#131). NEW The recorded result keeps the member values of the annotations an @Enhancement adds, so an added @Named("x") or a qualifier member reaches the container (vauban#135). At boot, the container runs them only for archives the build did not process, and replays the enhancements the recorded patch does not cover. The tests under vauban-core/src/test/java/io/vidocq/vauban/core/extensions/ exercise each hook — BceEnhancementTest, BceRegistrationSynthesisTest and BceRuntimeReplayTest among them.

An enhancement that adds @Inject to a field or initializer creates its injection points, including every initializer parameter; constructor and initializer qualifier changes retain their member values and identify the exact overload (vauban#139). This also applies to a class that gains its scope through enhancement. The frozen build-time patch records class and member additions, so boot does not need to instantiate the extension again to restore them. Direct generated field writes and method calls remain the first access path. For a managed class whose package provider grants its module lookup, non-public enhanced fields and private initializers need no opens; a declaring class without that grant still needs a supplied lookup or qualified opens. The frozen patch records annotation additions, not removals.

The beans an extension synthesises in the processor travel in META-INF/vauban-synthetic-metadata.properties, never in META-INF/vauban-beans.list: a synthetic bean has no class of its own for the container to discover. NEW The bean list used to name them too, so an application whose extension synthesised a bean of a JDK type (Ravel’s, typed java.lang.String) failed to boot with a NullPointerException (ravel#21). NEW A bean type given as a language-model type — addBean(…​).type(types.ofClass(info)), for a class the compilation is still producing — is written into that metadata by name, like a Class type, and loaded at run time. Only the Class types used to be written, so the bean was typed Object alone at run time and its injection points were unsatisfied. A parameterized language-model type is still not written: the format has no notation for it (BUG-20261008-01).

NEW Every value withParam accepts reaches the creator or the observer when the extension ran in the processor: primitive, String, Class, Enum and annotation arrays, a ClassInfo (looked up as a Class), an Annotation or AnnotationInfo (looked up as the annotation, every member kept) and an InvokerInfo. Arrays, annotations and invokers used to be dropped without a word, and Class and Enum params were lost at boot, so the creator read null; a synthetic observer got none of its params. A param the build cannot record now fails the build, naming the bean and the param, and a param naming a class the boot cannot load fails the deployment (vauban#130, BUG-20261008-02).

Build time or container start NEW

CDI Lite lets an implementation run an extension at either moment and does not tell the extension which. Most extensions do the same work both times. One that checks the deployment’s environment — a configuration value, a resource the application will open — would find the build machine’s in the processor. io.vidocq.vauban.api.ExtensionPhase.isBuildTime() tells it: true while the annotation processor or the Maven plugin runs it, false when the container does. Such an extension leaves the check to the container start. Ravel does this for the values @ConfigProperty injects (ravel#21).

The answer belongs to the calling thread, for the duration of the call that opened the phase (ExtensionPhase.atBuildTime(…​), a ScopedValue binding). A build tool that runs extensions itself opens it the same way.

Differences from CDI Full

Vauban implements the Lite profile. The following are absent:

  • runtime portable extensions (Extension, BeforeBeanDiscovery, etc.) — replaced by BCEs;

  • @Specializes;

  • @ConversationScoped;

  • passivation (PassivationCapable);

  • EL (Expression Language) wired into managed beans.