Dependency injection with Dagger
View MarkdownVertique uses Dagger to assemble your application at compile time. You declare the application component, name the modules that are allowed into its graph, and let Dagger validate construction and binding rules before the application starts. Vertique does not scan the classpath at runtime to add services to the graph.
The Dagger model
Section titled “The Dagger model”Four concepts cover most application wiring:
@Componentis the graph entry point. Its methods expose objects that the launcher or your code needs.@Modulegroups bindings. A module can provide an object with@Provides, or describe an interface-to-implementation relationship with@Binds.- Constructor injection marks the normal construction path. Put
@Injecton one constructor and let Dagger resolve that constructor’s parameters recursively. - Multibinding collects independent contributions.
@IntoSetadds one value to a set, while@Multibindsdeclares that an empty set is valid.
The generated REST application from the Quickstart uses the same shape:
@Singleton@Component(modules = { VertxModule.class, RestApplicationModule.class, AppModule.class, GeneratedJaxRsResourcesModule.class})interface AppComponent { VertiqueApplicationComponent applicationComponent();}The component is your composition boundary. The application owns AppModule and the component.
Vertique owns the framework modules and the generated modules described below. A module that is
not reachable from the component is not active, even if its artifact is on the application
classpath.
Choosing modules for the component
Section titled “Choosing modules for the component”Maven artifacts and Dagger modules are related but not identical. A Maven dependency makes a
module type available to the compiler. @Component(modules = { ... }) decides whether that module
participates in the graph. Start with one application starter or the direct core modules, then add
only the capabilities and generated modules that the application uses.
| Composition layer | Typical choices | What it contributes |
|---|---|---|
| Application foundation | CoreApplicationModule, RestApplicationModule, or ServicesApplicationModule |
Vert.x, configuration, lifecycle, deployment, and the selected application surface. The REST and Services starters already include the core foundation. |
| Independent capability | PostgreSQL persistence, Kafka, cache, jobs, workflows, security, resilience, localization, or messaging modules | The runtime and configuration for that capability. Provider-specific modules are selected here as well. |
| Observability provider | MicrometerModule plus the REST, Services, Cache, Resilience, or Prometheus adapter module; the corresponding OpenTelemetry modules |
Metrics, tracing, and their adapters. These modules remain explicit because they own configuration and provider policy. |
| Generated framework wiring | GeneratedJaxRsResourcesModule, GeneratedServicesModule, GeneratedRestClientsModule, GeneratedKafkaConsumersModule, GeneratedDelayedJobsModule, GeneratedWorkflowClientsModule, GeneratedAopModule, or GeneratedEventsModule |
Bindings and runtime descriptors emitted from the application’s annotations. Include the generated module named by the relevant feature. |
| Application-owned wiring | AppModule and any application feature modules |
Deployments, configuration, policy, and bindings that belong to the application. |
The artifact-to-module mapping is versioned in the artifact catalog and the module index. Each module reference says whether the module is an aggregate, a provider, a generated-module façade, or a low-level building block. Do not add every artifact in the catalog. Unused modules make the graph harder to audit and can introduce conflicting bindings.
For a typical REST application, the shape is:
@Component(modules = { RestApplicationModule.class, AppModule.class, GeneratedJaxRsResourcesModule.class, // GeneratedRegistrationsModule.class, // only when this app has registrations // Add capability/provider modules only when the application uses them. // DbPostgresqlModule.class, // MicrometerModule.class, // MicrometerRestModule.class})interface AppComponent extends VertiqueApplicationComponent {}The generated generic module is required here only when the application itself contains
@RegisterAs or @RegisterIntoSet types. A provider’s public aggregate normally includes its own
generated module, so an application includes the provider aggregate and does not reach into the
provider’s generated package.
Constructor injection
Section titled “Constructor injection”Use constructor injection for application classes and framework extension points:
final class BillingService implements BillingOperations { private final BillingRepository repository;
@Inject BillingService(BillingRepository repository) { this.repository = repository; }}If BillingRepository is also constructible by Dagger, Dagger follows the dependency chain and
reports any missing or conflicting binding during compilation. Avoid field injection in application
code. Constructor parameters make dependencies visible, immutable, and straightforward to test.
When a class is registered with a Vertique code-generation annotation, it must be concrete and
have exactly one jakarta.inject.Inject (or legacy javax.inject.Inject) constructor. The
annotation creates the interface binding. It does not supply a construction strategy.
One binding or a set of bindings
Section titled “One binding or a set of bindings”Use a direct binding when one implementation owns an abstraction:
@RegisterAs(Bar.class)final class Foo implements Bar { @Inject Foo() {}}Use a set contribution when zero or more independent implementations may participate:
@RegisterIntoSet(Bar.class)final class FirstBarObserver implements Bar { @Inject FirstBarObserver() {}}Consumers request the matching shape:
final class Coordinator { private final Bar bar; private final Set<Bar> observers;
@Inject Coordinator(Bar bar, Set<Bar> observers) { this.bar = bar; this.observers = observers; }}@RegisterAs means exactly one binding for the target. If two modules provide the same direct
binding, Dagger fails with its normal duplicate-binding diagnostic. @RegisterIntoSet is designed
for multiple contributors, and an empty set is valid when the consuming framework module declares
the corresponding multibinding.
Both annotations are source-only and repeatable, so one class can register under more than one explicit target. The target is always written in the annotation. Vertique does not infer every interface implemented by the class, which keeps the graph reviewable and avoids accidental public bindings.
Vertique’s generated modules
Section titled “Vertique’s generated modules”The vertique-codegen-dagger processor turns the annotations into a
GeneratedRegistrationsModule containing ordinary Dagger bindings:
@Moduleabstract class GeneratedRegistrationsModule { @Binds abstract Bar bindFoo(Foo implementation);
@Binds @IntoSet abstract Bar bindFirstBarObserver(FirstBarObserver implementation);}The generated module is compile-time output. It is not a service-loader file, and putting a provider artifact on the classpath does not activate it by itself. A provider library generates and packages its module during its own build, then includes it from the provider’s public Dagger aggregate. An application that declares its own registrations adds its generated module to its component explicitly.
This mechanism is separate from GeneratedJaxRsResourcesModule and GeneratedServicesModule.
Those modules own JAX-RS resource discovery and service-contract wiring, and
GeneratedRegistrationsModule handles only the two generic registration annotations. The split
is deliberate:
| Concern | Source declaration | Generated module |
|---|---|---|
| HTTP resource registration | JAX-RS @Path resources |
GeneratedJaxRsResourcesModule |
| Typed service-contract registration and clients | @ServiceContract interfaces and implementations |
GeneratedServicesModule |
| Ordinary Dagger type adaptation | @RegisterAs and @RegisterIntoSet |
GeneratedRegistrationsModule |
JAX-RS and Services generation does more than create a simple @Binds. It emits descriptors,
dispatch metadata, clients, or contributors owned by those feature processors. The generic
registration processor must not absorb those responsibilities, and none of these generated
modules is activated by classpath presence alone.
@NoAutoWire is the opt-out. Put it on a type when a manual binding must stay authoritative.
The processor also respects the global vertique.codegen.autoWire=false compiler option.
Framework extensions
Section titled “Framework extensions”Vertique builds on the basic Dagger model with a small set of compile-time extensions:
- Starters expose aggregate modules such as
RestApplicationModule; use them when the framework’s standard composition is appropriate. - Generated JAX-RS and Services modules adapt annotated resources and service contracts into the graph.
- Generic registrations adapt an injectable concrete class to a declared direct or set target.
@BindsOptionalOflets a module consume an optional configuration binding without requiring the module that supplies the value. This is how optional observability configuration stays usable when its main provider is absent.
For example, the Micrometer and OpenTelemetry modules use generic set registrations for their observer, listener, interceptor, and contributor implementations. Their configuration and registry providers are ordinary hand-written Dagger methods because those methods contain policy or construction logic, which goes beyond simple type adaptation.
What the registration annotations can replace
Section titled “What the registration annotations can replace”The annotations can replace a small module method whose only purpose is to adapt an injectable concrete type to an interface or set:
// Before: a hand-written module method.@Provides@IntoSetstatic BarObserver observer(ConcreteBarObserver implementation) { return implementation;}
// After: the class declares the same graph contribution.@RegisterIntoSet(BarObserver.class)final class ConcreteBarObserver implements BarObserver { @Inject ConcreteBarObserver() {}}The annotations do not replace a module that creates configuration, selects a provider, declares an optional seam, contributes lifecycle work, installs a generated framework module, or encodes policy. In particular:
- Keep
GeneratedJaxRsResourcesModuleandGeneratedServicesModuleseparate from generic registrations. - Keep public Micrometer and OpenTelemetry capability modules as the inclusion points. The annotations replace their repetitive observer/interceptor/contributor binding methods, while their configuration, registry, optional-binding, and provider-policy responsibilities stay explicit.
- Keep application-owned deployment and policy modules. A registered class can be injectable, but it does not become a lifecycle deployment or a configuration provider by itself.
- Keep a public façade module even when its implementation is only an include of
GeneratedRegistrationsModule. That façade prevents applications from depending on generated package names and leaves room for future provider wiring.
The annotations mainly reduce binding boilerplate and module internals. They are not a reason to flatten the module graph. Removing or merging public modules would be a separate compatibility decision because applications may name those modules in their components.
Troubleshooting graph errors
Section titled “Troubleshooting graph errors”- Missing binding: add the module that owns the binding to
@Component(modules = { ... }), or add a constructor/provider binding for the missing type. - Duplicate binding: choose one direct owner, remove the competing registration, or use
@NoAutoWirewhen a manual binding is intentional. - Empty set not declared: install the framework module that declares the set with
@Multibinds. - Registration ignored: check that the type is in the current compilation, has one injectable constructor, is assignable to the explicit target, and is not opted out. A dependency JAR is not scanned again by the consuming application’s processor.
- Generated module not found: check the processor dependency and annotation-processing setup, then verify that the generated module is named in the component or included by the provider aggregate.
Learn more
Section titled “Learn more”- Application model: how the component, starters, and generated modules fit into the launcher.
- Code generation: the complete generated-output map and processor setup.
vertique-codegen-coremodule reference: source-only annotation API and compile-scope usage.vertique-codegen-daggermodule reference: processor behavior, generated module contract, and diagnostics.vertique-micrometer-coremodule referencevertique-opentelemetry-coremodule reference
Continue reading
Section titled “Continue reading”- Previous: Application model
- Next: Configuration