This page covers the CDI 4.1 Lite patterns Vauban supports and how they integrate with the rest of the ecosystem. Every example compiles; the entire injection bytecode is generated at process-classes.

Scopes

CDI 4.1 Lite ships four standard scopes. Vauban supports all of them, plus @RequestScoped whenever a contextual transport is wired in.

Scope Semantics Implemented

@Singleton

One bean per JVM, no proxy.

✅

@ApplicationScoped

One bean per container, normal-scope proxy.

✅

@Dependent

One instance per injection point, no proxy.

✅ (default)

@RequestScoped

One instance per request, normal-scope proxy.

✅ (with Cassini or Foy active)

@SessionScoped

One instance per HTTP session.

// TODO@user: confirm status with Foy / Cassini

Normal-scoped beans (@ApplicationScoped, @RequestScoped) get a _ClientProxy generated at compile time. The proxy intercepts each call and delegates to the active context. @Dependent and @Singleton beans do not.

Proxyable beans and the ProxyLink entry constructor NEW

Because the client proxy subclasses the bean, CDI 4.1 declares some classes unproxyable for normal scopes: final classes, classes with non-private final methods, and classes without a non-private no-arg constructor. Final members are reported as errors (weaving cannot fix them); the missing-constructor case — the common single-@Inject-constructor style — is handled automatically:

In any javac-based build (Maven, Gradle, plain javac, the standard Vidocq setup), nothing to do. The Vauban annotation processor ships an auto-started javac plugin that, when compilation finishes, weaves a synthetic (ProxyLink) entry constructor into the compiled bean class and points the generated proxy’s constructor at it. Your source never declares it. Creating the proxy then runs no bean logic — no duplicated construction side effects, no NullPointerException on dereferenced @Inject parameters. The vauban-maven-plugin applies the same weaving at process-classes as a second layer and for ecosystem jars that do not go through javac.

When the build output was not woven — typically an IDE build: IntelliJ’s build system writes classes to disk after javac finishes, which defeats the javac plugin — the container detects it at boot, before any bean class is loaded, and attaches the vauban-weaver agent to the current JVM: the same transformation is applied when the class is defined. You will see a Vauban warning listing the affected beans plus the JDK’s standard dynamic-agent notice; behaviour is otherwise identical to a woven build. Disable this tier with -Dvauban.weaving.loadtime=disabled, or avoid it entirely in IntelliJ by enabling Delegate IDE build/run actions to Maven. See Internals — client-proxy weaving tiers for the full mechanics.

Without any weaving tier, declare the entry constructor yourself (io.vidocq.vauban.api.ProxyLink, a Vauban extension comparable to Weld’s relaxed construction):

@ApplicationScoped
public class Oidc {
    private final String issuer;

    @Inject
    public Oidc(Config config) {
        this.issuer = config.issuer();
    }

    protected Oidc(ProxyLink link) {   // client-proxy entry point — keep the body empty
        this.issuer = null;            // javac requires blank finals to be assigned
    }
}

Either way the proxy chains to that constructor (always passing null), and the container never selects it for injection or bean instantiation. Field initializers still run in every constructor — keep them side-effect free.

Qualifiers

Discriminate multiple implementations of the same type.

import jakarta.inject.Qualifier;
import java.lang.annotation.*;

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE})
public @interface Audit {}

@ApplicationScoped @Audit
public class AuditLogger implements Logger { /* ... */ }

@ApplicationScoped
public class Service {
    @Inject @Audit Logger logger;
}

@Named, @Default, @Any are supported. Custom qualifiers with @Nonbinding members work too.

Producers and @Disposes

@ApplicationScoped
public class DataSourceProducer {

    @Produces @ApplicationScoped
    public DataSource dataSource(Config config) {
        return DataSource.builder().url(config.url()).build();
    }

    public void close(@Disposes DataSource ds) {
        ds.close();
    }
}

The @Disposes callback runs when the producing bean is destroyed. No dedicated class is generated per producer method: the container invokes producers and disposers through the module’s generated _VaubanComponents provider, with reflection only as a fallback.

Events

public record OrderCreated(String id) {}

@ApplicationScoped
public class OrderService {
    @Inject Event<OrderCreated> events;

    public void create(String id) {
        events.fire(new OrderCreated(id));
    }
}

@ApplicationScoped
public class OrderListener {
    public void onOrder(@Observes OrderCreated event) {
        System.out.println("new order " + event.id());
    }
}

Async observers (@ObservesAsync) are dispatched on the EventDispatcher’s internal `Executors.newVirtualThreadPerTaskExecutor(). See the threading model.

Interceptors

@InterceptorBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Logged {}

@Logged
@Interceptor
@Priority(Interceptor.Priority.APPLICATION)
public class LoggingInterceptor {

    @AroundInvoke
    public Object around(InvocationContext ctx) throws Exception {
        long t0 = System.nanoTime();
        try {
            return ctx.proceed();
        } finally {
            System.out.printf("%s : %d ns%n", ctx.getMethod(), System.nanoTime() - t0);
        }
    }
}

@ApplicationScoped @Logged
public class Service { /* intercepted methods */ }

@AroundConstruct is also supported. Interceptor chains are resolved at compile time and stored in the generated _Factory.

Programmatic Instance<T>

@ApplicationScoped
public class Router {
    @Inject Instance<Handler> handlers;

    public Handler resolve(String name) {
        return handlers.select(NamedLiteral.of(name)).get();
    }
}

Instance<T> enables dynamic resolution from application code while keeping the injection grid static inside the container.

Integration with Cassini (Jakarta REST)

vauban-core is the underlying DI layer of Cassini. @Path resources are CDI beans; @Provider providers (MessageBodyReader/Writer) too. No configuration: Cassini consumes the Vauban BeanManager directly.

@Path("/orders")
@ApplicationScoped
public class OrderResource {
    @Inject OrderService service;

    @POST
    public Response create(OrderRequest req) {
        service.create(req.id());
        return Response.created(URI.create("/orders/" + req.id())).build();
    }
}

Integration with Foy (Jakarta Servlet)

Foy exposes servlets, filters and listeners as CDI beans. @RequestScoped follows the HTTP request/response lifecycle.

JUnit tests with @VaubanTest

@VaubanTest
class GreetingServiceTest {
    @Inject GreetingService greeting;

    @Test
    void hello() {
        assertEquals("Hello, Vauban!", greeting.hello("Vauban"));
    }
}

The extension boots a minimal container per test class. See the JUnit reference.