Skip to content

Design Rationale

Andy Boothe edited this page Jan 17, 2025 · 3 revisions

Rapier’s design includes several decisions that might not make sense at a glance. The following unpacks the thinking behind some of these foundational decisions to help users better understand these design choices and build intuition about how Rapier works.

Why are Default Values Embedded in the Configuration Source Annotations?

Dagger's Model for Dependency Injection

Dagger uses JSR-330: Dependency Injection for Java as the foundation of its design. In Dagger, dependencies (injection sites) and bindings (logical injection providers) are declared by the user. Dagger then generates code to resolve dependencies using the available bindings. This process is based on logical binding keys, which consist of th declared bound type and an optional JSR-330 qualifier. Each unique binding key can have zero or more dependencies, and exactly one binding. If no matching binding exists for a dependency, Dagger fails to generate the code.

How Rapier Enhances This Model

Rapier extends Dagger’s model by introducing dependency injection for configuration data (e.g., @EnvironmentVariable("PORT")). Users declare dependencies on configuration data, and Rapier generates bindings to satisfy those dependencies. To align with Dagger’s JSR-330-based model, Rapier treats its configuration source annotations (e.g., @EnvironmentVariable) as JSR-330 qualifiers. This tight integration ensures predictable behavior and compatibility with Dagger.

Why Include Default Values in the Annotation?

Including the default value directly within configuration source annotations allows users to declare multiple dependencies on the same type for the same configuration source but with different default values. This is particularly useful in Dagger modules. For example:

@Module
public class ExampleModule {
    @Provides
    public Bar provideBarWithFoo123(
            @EnvironmentVariable(value="FOO", defaultValue="123") Foo foo) {
        return Bar.ofFoo(foo);
    }

    @Provides
    public Bar provideQuuxWithFoo234(
            @EnvironmentVariable(value="FOO", defaultValue="234") Foo foo) {
        return Quux.ofFoo(foo);
    }
}

Here, two distinct dependencies are defined:

  1. @EnvironmentVariable(value="FOO", defaultValue="123") Foo
  2. @EnvironmentVariable(value="FOO", defaultValue="234") Foo

Since the default value is included within the annotation, these bindings are considered different from each other by Dagger, enabling code generation to succeed. If the default values were placed elsewhere, for example in a separate annotation, then it would not be possible to generate multiple bindings with different default values for the same type because Dagger would fail due to duplicate bindings.

Embedded Default Values Promote Flexibility

This design enables flexibility.

If users require a single default value across all dependencies, they can define it as a constant:

@Module
public class ExampleModule {
    public static final String FOO_DEFAULT_VALUE = "123";

    @Provides
    public Bar provideBarWithFoo123(
            @EnvironmentVariable(value="FOO", defaultValue=FOO_DEFAULT_VALUE) Foo foo) {
        return Bar.ofFoo(foo);
    }

    @Provides
    public Bar provideQuuxWithFoo234(
            @EnvironmentVariable(value="FOO", defaultValue=FOO_DEFAULT_VALUE) Foo foo) {
        return Quux.ofFoo(foo);
    }
}

If users need different default values for the same type, embedding the default value in the annotation allows this use case, too.

By embedding the default value directly in the annotation, Rapier empowers users to handle a broad range of use cases, from simple configurations to complex scenarios involving multiple default values.