An architecture for Dagger that scales well to large codebases. Provided as a series of simple rules that humans and agents can follow (with examples).An architecture for Dagger that scales well to large codebases. Provided as a series of simple rules that humans and agents can follow (with examples).

How to Structure Dagger Components So Your Build System Doesn’t Hate You

This document was adapted from Dagger Directives for a monorepo that uses Bazel Build, and is provided for ease of use in other organizations.

Contents

This document is extensive, and while each directive is simple, the broader architecture they promote may be unclear; therefore, an end-to-end example is provided to aid comprehension, and the underlying architectural rationale is provided to link the individual directives to broader engineering principles. The sections are presented for ease of reference, with directives first; however, readers are encouraged to begin with whichever section they find most helpful.

Terminology

The following definitions apply throughout this document:

  • Component: An interface annotated with @Component
  • Module: A class or interface annotated with @Module
  • Scope: A custom annotation meta-annotated with @Scope

Component Structure and Scoping

Directives for how components are defined, scoped, and related to one another.

Practice: Library-Provided Components

Libraries and generic utilities should provide components that expose their functionality and declare their component dependencies instead of only providing raw classes/interfaces.

Positive Example: A Networking library provides a NetworkingComponent that exposes anOkHttpClient binding and depends on a CoroutinesComponent.

Negative Example: A Networking library that provides various interfaces and classes, but no component, and requires downstream consumers to define modules and components to wire them together.

This approach transforms Dagger components from details of the downstream application into details of upstream libraries. Instead of forcing consumers to understand a library's internal structure(and figure out how to instantiate objects), library authors provide complete, ready-to-use components that can be composed together and used to instantiate objects. This approach is analogous to plugging in a finished appliance instead of assembling a kit of parts: consumers just declare a dependency on the component (e.g. a fridge), supply the upstream components (e.g. electricity), and get the fully configured objects they need without ever seeing the wiring (e.g. cold drinks). This approach scales well, at the cost of more boilerplate.

Practice: Narrow Scoped Components

Components should export a minimal set of bindings, accept only the dependencies they require to operate (i.e. with @BindsInstance), and depend only on the components they require to operate.

Positive Example: A Feature component that depends only on Network and Database components, exposes only its public API (e.g. FeatureUi), and keeps its internal bindings hidden.

Negative Example: A Feature component that depends on a monolithic App component (which itself goes against the practice), exposes various bindings that could exist in isolation (e.g.FeatureUi, Clock, NetworkPorts and RpcBridge, IntegerUtil), and exposes its internal bindings.

This allows consumers to compose functionality with granular precision, reduces unnecessary configuration (i.e. passing instances/dependencies that are not used at runtime), and optimizes build times. This approach is consistent with the core tenets of the Interface Segregation Principle in that it ensures that downstream components can depend on the components they need, without being forced to depend on unnecessary components.

Practice: Naked Component Interfaces

Components should be defined as plain interfaces ("naked interfaces") without Dagger annotations, and then extended by annotated interfaces for production, testing, and other purposes. Downstream components should target the naked interfaces in their component dependencies instead of the annotated interfaces.

Example:

// Definition interface FooComponent { fun foo(): Foo } // Production Implementation @Component(modules = [FooModule::class]) interface ProdFooComponent : FooComponent // Testing Implementation @Component(modules = [FakeFooModule::class]) interface TestFooComponent : FooComponent { fun fakeFoo(): FakeFoo } @Component(dependencies = [FooComponent::class]) interface BarComponent { @Component.Builder interface Builder { fun consuming(fooComponent: FooComponent): Builder fun build(): BarComponent } }

This ensures Dagger code follows general engineering principles (separation of interface and implementation). While Dagger components are interfaces, the presence of a `@Component` annotation implicitly creates an associated implementation (the generated Dagger code); therefore, depending on an annotated component forces a dependency on its implementation (at the build system level), and implicitly forces test code to depend on production code. By separating them, consumers can depend on a pure interface without needing to include the Dagger implementation in their class path, thereby preventing leaky abstractions, optimising build times, and directly separating production and test code into discrete branches.

Standard: Custom Scope Required

Components must be bound to a custom Dagger scope.

Example:

@FooScope @Component interface ProdFooComponent : FooComponent { fun foo(): Foo }

Unscoped bindings can lead to subtle bugs where expensive objects are recreated or shared state is lost. Explicit lifecycle management ensures objects are retained only as long as needed, thereby preventing these issues.

Standard: Module Inclusion Restrictions

Components must only include modules defined within their own package or its subpackages; however, they must never include modules from a subpackage if another component is defined in an intervening package.

Example:

Given the following package structure:

src ├── a │ ├── AComponent │ ├── AModule │ ├── sub1 │ │ └── Sub1Module │ └── sub2 │ ├── Sub2Component │ └── sub3 │ └── Sub3Module └── b └── BModule

AComponent may include AModule (same package) and Sub1Module (subpackage with no intervening component), but not Sub3Module (intervening Sub2Component in a.sub2) or BModule (not a subpackage of a).

This enforces strict architectural layering and prevents dependency cycles (spaghetti code), thereby ensuring proper component boundaries and maintainability.

Practice: Dependencies Over Subcomponents

Component dependencies should be used instead of subcomponents.

Example: Foo depends on Bar via @Component(dependencies = [Bar::class]) rather than using@Subcomponent.

While subcomponents are a standard feature of Dagger, prohibiting them favors a flat composition-based component graph, thereby reducing cognitive load, allowing components to be tested in isolation, and creating a more scalable architecture.

Practice: Cross-Package Dependencies

Components may depend on components from any package.

Example: Foo in a.b can depend on Bar in x.y.z.

Allowing components to depend on each other regardless of location promotes reuse, thereby fostering high cohesion within packages.

Standard: Component Suffix

Components must include the suffix Component in their name.

Positive example: ConcurrencyComponent

Negative example: Concurrency

This clearly distinguishes the component interface from the functionality it provides and prevents naming collisions.

Standard: Scope Naming Convention

The name of the custom scope associated with a component must inherit the name of the component (minus "Component") with "Scope" appended.

Example: FooComponent is associated with FooScope.

Consistent naming allows contributors to immediately associate a scope with its component, thereby preventing conflicts and reducing split-attention effects.

Standard: Builder Naming

Component builders must be called `Builder`.

Example:

@Component interface FooComponent { @Component.Builder interface Builder { @BindsInstance fun binding(bar: Bar): Builder fun build(): FooComponent } }

Standardizing builder names allows engineers to predict the API surface of any component, thereby reducing the mental overhead when switching between components.

Standard: Binding Function Naming

Component builder functions that bind instances must be called `binding`; however, when bindings use qualifiers, the qualifier must be appended.

Example:

@Component interface ConcurrencyComponent { @Component.Builder interface Builder { // Unqualified @BindsInstance fun binding(bar: Bar): Builder // Qualified @BindsInstance fun bindingIo(@Io scope: CoroutineScope): Builder @BindsInstance fun bindingMain(@Main scope: CoroutineScope): Builder fun build(): ConcurrencyComponent } }

Explicit naming immediately clarifies the mechanism of injection (instance binding vs component dependency), thereby preventing collisions when binding multiple instances of the same type.

Standard: Dependency Function Naming

Component builder functions that set component dependencies must be called `consuming`.

Example:

@Component(dependencies = [Bar::class]) interface FooComponent { @Component.Builder interface Builder { fun consuming(bar: Bar): Builder fun build(): FooComponent } }

Distinct naming clearly separates structural dependencies (consuming) from runtime data (binding), thereby making the component's initialization logic self-documenting.

Standard: Provision Function Naming

Component provision functions must be named after the type they provide (in camelCase). However, when bindings use qualifiers, the qualifier must be appended to the function name.

Example:

@Component interface FooComponent { // Unqualified fun bar(): Bar // Qualified @Io fun genericIo(): Generic @Main fun genericMain(): Generic }

This ensures consistency and predictability in the component's public API.

Factory Functions

Requirements for the factory functions that instantiate components for ease of use.

Standard: Factory Function Required

Components must have an associated factory function that instantiates the component.

Example:

@Component(dependencies = [Quux::class]) interface FooComponent { // ... } fun fooComponent(quux: Quux = DaggerQuux.create(), qux: Qux): FooComponent = DaggerFooComponent.builder() .consuming(quux) .binding(qux) .build()

This integrates cleanly with Kotlin, thereby significantly reducing the amount of manual typing required to instantiate components.

Exception: Components that are file private may exclude the factory function (e.g. components defined in tests for consumption in the test only).

Standard: Default Component Dependencies

Factory functions must supply default arguments for parameters that represent component dependencies.

Example: fun fooComponent(quux: Quux = DaggerQuux.create(), ...)

Providing defaults for dependencies allows consumers to focus on the parameters that actually vary, thereby improving developer experience and reducing boilerplate.

Practice: Production Defaults

The default arguments for component dependency parameters in factory functions should be production components, even when the component being assembled is a test component.

Example: fun testFooComponent(quux: Quux = DaggerQuux.create(), ...)

This ensures tests exercise real production components and behaviours as much as possible, thereby reducing the risk of configuration drift between test and production environments.

Practice: Factory Function Location

Factory functions should be defined as top-level functions in the same file as the component.

Example: fooComponent() function in same file as FooComponent interface.

Co-locating the factory with the component improves discoverability.

Practice: Factory Function Naming

Factory function names should match the component, but in lower camel case.

Example: FooComponent component has fun fooComponent(...) factory function.

This ensures factory functions can be matched to components easily.

Practice: Default Non-Component Parameters

Factory functions should supply default arguments for parameters that do not represent component dependencies (where possible).

Example: fun fooComponent(config: Config = Config.DEFAULT, ...)

Sensible defaults allow consumers to only specify non-standard configuration when necessary, thereby reducing cognitive load.

Modules and Build Targets

Directives regarding Dagger modules and their placement in build targets.

Standard: Separate Module Targets

Modules must be defined in separate build targets to the objects they provide/bind.

Example: BarModule in separate build target from Baz implementation.

Separating implementation from interface/binding prevents changing an implementation from invalidating the cache of every consumer of the interface, thereby improving build performance.Additionally, it ensures consumers can depend on individual elements independently (crucial forHilt) and allows granular binding overrides in tests.

Standard: Dependency Interfaces

Modules must depend on interfaces rather than implementations.

Example: BarModule depends on Baz interface, not BazImpl.

This enforces consistency with the dependency inversion principle, thereby decoupling the module and its bindings from concrete implementations.

Testing Patterns

Patterns for defining components used in testing to ensure testability.

Standard: Test Component Extension

Test components must extend production components.

Example: interface TestFooComponent : FooComponent

Tests should operate on the same interface as production code (Liskov Substitution), thereby ensuring that the test environment accurately reflects production behavior.

Practice: Additional Test Bindings

Test components should export additional bindings.

Example: TestFooComponent component extends FooComponent and additionally exposes fun testHelper(): TestHelper.

Exposing test-specific bindings allows tests to inspect internal state or inject test doubles without compromising the public production API, thereby facilitating white-box testing where appropriate.

Rationale

The directives in this document work together to promote an architectural pattern for Dagger that follows foundational engineering best practices and principles, which in turn supports sustainable development and improves the contributor experience. The core principles are:

  • Interface Segregation Principle (ISP): Downstream consumers should be able to depend on the minimal API required for their task without being forced to consume/configure irrelevant objects. This reduces cognitive overhead for both maintainers and consumers, and lowers computational costs at build time and runtime. It's supported by directives such as the "Narrow Scoped Components" practice, which calls for small granular components instead of large God Objects, and the "Dependencies Over Subcomponents" practice, which encourages composition over inheritance.
  • Dependency Inversion Principle: High-level elements should not depend on low-level elements; instead, both should depend on abstractions. This reduces the complexity and scope of changes by allowing components to evolve independently and preventing unnecessary recompilation (in a complex build system such as Bazel). It's supported by the "Naked Component Interfaces" directive, which requires the use of interfaces rather than implementations, and the "Module Inclusion Restrictions" standard, which enforces strict architectural layering.
  • Abstraction and Encapsulation: Complex subsystems should expose simple, predictable interfaces that hide their internal complexity and configuration requirements. This allows maintainers and consumers to use and integrate components without deep understanding of the implementation. It's supported by the "Factory Function Required" standard, which encourages simple entry points, and "Default Component Dependencies", which provides sensible defaults to eliminate "Builder Hell".
  • Liskov Substitution Principle (LSP): Objects of a superclass must be replaceable with objects of its subclasses without breaking the application. This ensures test doubles can be seamlessly swapped in during tests, thereby improving testability without requiring changes to production code, and ensuring as much production code is tested as possible. It's supported by the "Test Component Extension" standard, which mandates that test components inherit from production component interfaces.
  • Compile-Time Safety (Poka-Yoke): The system is designed to prevent misconfiguration errors (i.e. "error-proofing"). By explicitly declaring component dependencies in the component interface, Dagger enforces their presence at compile time, and fails if they are missing. This gives consumers a clear, immediate signal of exactly what is missing or misconfigured, rather than failing obscurely at runtime. It's supported by the "Library-Provided Components" practice, which mandates fully declared dependencies, and the "Factory Function Required" standard, which mechanically ensures all requirements are satisfied effectively.

Overall, this architecture encourages and supports granular, maintainable components that can be evolved independently and composed together into complex structures. Components serve as both the public API for utilities, the integration system that ties elements together within utilities, and the composition system that combines utilities together. For upstream utility maintainers, this reduces boilerplate and reduces the risk of errors; for downstream utility consumers, this creates an unambiguous and self-documenting API that can be integrated without knowledge of implementation details; and for everyone, it distributes complexity across the codebase and promotes high cohesion(i.e. components defined nearest to the objects they expose). All together, this fosters sustainable development by reducing cognitive and computational load. \n The disadvantages of this approach and a strategy for mitigation are discussed in the[future work](#future-work) appendix.

End to End Example

The following example demonstrates a complete Dagger setup and usage that adheres to all the directives in this document. It features upstream (User) and downstream (Profile) components, separate modules for production and testing (including fake implementations), and strict separation of interface and implementation via naked component interfaces.

User Feature

Common elements:

/** Custom Scope */ @Scope @Retention(AnnotationRetention.RUNTIME) annotation class UserScope /** Domain Interface */ interface User /** Naked Component */ interface UserComponent { fun user(): User }

Production elements:

/** Real Implementation */ @UserScope class RealUser @Inject constructor() : User /** Production Module */ @Module interface UserModule { @Binds fun bind(impl: RealUser): User companion object { @Provides fun provideTimeout() = 5000L } } /** Production Component */ @UserScope @Component(modules = [UserModule::class]) interface ProdUserComponent : UserComponent { @Component.Builder interface Builder { fun build(): ProdUserComponent } } /** Production Factory Function */ fun userComponent(): UserComponent = DaggerProdUserComponent.builder().build()

Test elements:

/** Fake Implementation */ @UserScope class FakeUser @Inject constructor() : User /** Fake Module */ @Module interface FakeUserModule { @Binds fun bind(impl: FakeUser): User } /** Test Component */ @UserScope @Component(modules = [FakeUserModule::class]) interface TestUserComponent : UserComponent { fun fakeUser(): FakeUser @Component.Builder interface Builder { fun build(): TestUserComponent } } /** Test Factory Function */ fun testUserComponent(): TestUserComponent = DaggerTestUserComponent.builder().build()

Profile Feature

Common elements:

/** Custom Scope */ @Scope @Retention(AnnotationRetention.RUNTIME) annotation class ProfileScope /** Domain Interface */ interface Profile /** Naked Component */ interface ProfileComponent { fun profile(): Profile }

Production elements:

** Real Implementation */ @ProfileScope class RealProfile @Inject constructor( val user: User, private val id: ProfileId ) : Profile { data class ProfileId(val id: String) } /** Production Module */ @Module interface ProfileModule { @Binds fun bind(impl: RealProfile): Profile } /** Production Component */ @ProfileScope @Component(dependencies = [UserComponent::class], modules = [ProfileModule::class]) interface ProdProfileComponent : ProfileComponent { @Component.Builder interface Builder { fun consuming(user: UserComponent): Builder @BindsInstance fun binding(id: ProfileId): Builder fun build(): ProdProfileComponent } } /** Production Factory Function */ fun profileComponent( user: UserComponent = userComponent(), id: ProfileId = ProfileId("prod-id") ): ProfileComponent = DaggerProdProfileComponent.builder().consuming(user).binding(id).build()

Test elements:

/** Test Component */ @ProfileScope @Component(dependencies = [UserComponent::class], modules = [ProfileModule::class]) interface TestProfileComponent : ProfileComponent { @Component.Builder interface Builder { fun consuming(user: UserComponent): Builder @BindsInstance fun binding(id: ProfileId): Builder fun build(): TestProfileComponent } } /** Test Factory Function */ fun testProfileComponent( user: UserComponent = userComponent(), id: ProfileId = ProfileId("test-id") ): TestProfileComponent = DaggerTestProfileComponent.builder().consuming(user).binding(id).build()

Usage

Example of production component used in production application:

class Application { fun main() { // Automatically uses production implementations (RealUser, RealProfile) val profile = profileComponent().profile() // ... } }

Example of production profile component used with test user component in a test:

@Test fun testProfileWithFakeUser() { // 1. Setup: Create the upstream test component (provides FakeUser) val fakeUserComponent = testUserComponent() val fakeUser = fakeUserComponent.fakeUser() // 2. Act: Inject it into the downstream test component val prodProfileComponent = profileComponent(user = fakeUserComponent) val profile = prodProfileComponent.profile() // 3. Assert: Verify integration assertThat(profile.user).isEqualTo(fakeUser) }

Future Work

The main disadvantage of the pattern this document encodes is the need for a final downstreamassembly of components, which can become boilerplate heavy in deep graphs. For example:

fun main() { // Level 1: Base component val core = coreComponent() // Level 2: Depends on Core val auth = authComponent(core = core) val data = dataComponent(core = core) // Level 3: Depends on Auth, Data, AND Core val feature = featureComponent(auth = auth, data = data, core = core) // Level 4: Depends on Feature, Auth, AND Core val app = appComponent(feature = feature, auth = auth, core = core) }

A tool to reduce this boilerplate has been designed, and implementation is tracked by this issue.

Market Opportunity
Threshold Logo
Threshold Price(T)
$0.008654
$0.008654$0.008654
+0.39%
USD
Threshold (T) Live Price Chart
Disclaimer: The articles reposted on this site are sourced from public platforms and are provided for informational purposes only. They do not necessarily reflect the views of MEXC. All rights remain with the original authors. If you believe any content infringes on third-party rights, please contact service@support.mexc.com for removal. MEXC makes no guarantees regarding the accuracy, completeness, or timeliness of the content and is not responsible for any actions taken based on the information provided. The content does not constitute financial, legal, or other professional advice, nor should it be considered a recommendation or endorsement by MEXC.

You May Also Like

Academic Publishing and Fairness: A Game-Theoretic Model of Peer-Review Bias

Academic Publishing and Fairness: A Game-Theoretic Model of Peer-Review Bias

Exploring how biases in the peer-review system impact researchers' choices, showing how principles of fairness relate to the production of scientific knowledge based on topic importance and hardness.
Share
Hackernoon2025/09/17 23:15
Foreigner’s Lou Gramm Revisits The Band’s Classic ‘4’ Album, Now Reissued

Foreigner’s Lou Gramm Revisits The Band’s Classic ‘4’ Album, Now Reissued

The post Foreigner’s Lou Gramm Revisits The Band’s Classic ‘4’ Album, Now Reissued appeared on BitcoinEthereumNews.com. American-based rock band Foreigner performs onstage at the Rosemont Horizon, Rosemont, Illinois, November 8, 1981. Pictured are, from left, Mick Jones, on guitar, and vocalist Lou Gramm. (Photo by Paul Natkin/Getty Images) Getty Images Singer Lou Gramm has a vivid memory of recording the ballad “Waiting for a Girl Like You” at New York City’s Electric Lady Studio for his band Foreigner more than 40 years ago. Gramm was adding his vocals for the track in the control room on the other side of the glass when he noticed a beautiful woman walking through the door. “She sits on the sofa in front of the board,” he says. “She looked at me while I was singing. And every now and then, she had a little smile on her face. I’m not sure what that was, but it was driving me crazy. “And at the end of the song, when I’m singing the ad-libs and stuff like that, she gets up,” he continues. “She gives me a little smile and walks out of the room. And when the song ended, I would look up every now and then to see where Mick [Jones] and Mutt [Lange] were, and they were pushing buttons and turning knobs. They were not aware that she was even in the room. So when the song ended, I said, ‘Guys, who was that woman who walked in? She was beautiful.’ And they looked at each other, and they went, ‘What are you talking about? We didn’t see anything.’ But you know what? I think they put her up to it. Doesn’t that sound more like them?” “Waiting for a Girl Like You” became a massive hit in 1981 for Foreigner off their album 4, which peaked at number one on the Billboard chart for 10 weeks and…
Share
BitcoinEthereumNews2025/09/18 01:26
Vitalik Buterin Reveals Ethereum’s Bold Plan to Stay Quantum-Secure and Simple!

Vitalik Buterin Reveals Ethereum’s Bold Plan to Stay Quantum-Secure and Simple!

Buterin unveils Ethereum’s strategy to tackle quantum security challenges ahead. Ethereum focuses on simplifying architecture while boosting security for users. Ethereum’s market stability grows as Buterin’s roadmap gains investor confidence. Ethereum founder Vitalik Buterin has unveiled his long-term vision for the blockchain, focusing on making Ethereum quantum-secure while maintaining its simplicity for users. Buterin presented his roadmap at the Japanese Developer Conference, and splits the future of Ethereum into three phases: short-term, mid-term, and long-term. Buterin’s most ambitious goal for Ethereum is to safeguard the blockchain against the threats posed by quantum computing.  The danger of such future developments is that the future may call into question the cryptographic security of most blockchain systems, and Ethereum will be able to remain ahead thanks to more sophisticated mathematical techniques to ensure the safety and integrity of its protocols. Buterin is committed to ensuring that Ethereum evolves in a way that not only meets today’s security challenges but also prepares for the unknowns of tomorrow. Also Read: Ethereum Giant The Ether Machine Takes Major Step Toward Going Public! However, in spite of such high ambitions, Buterin insisted that Ethereum also needed to simplify its architecture. An important aspect of this vision is to remove unnecessary complexity and make Ethereum more accessible and maintainable without losing its strong security capabilities. Security and simplicity form the core of Buterin’s strategy, as they guarantee that the users of Ethereum experience both security and smooth processes. Focus on Speed and Efficiency in the Short-Term In the short term, Buterin aims to enhance Ethereum’s transaction efficiency, a crucial step toward improving scalability and reducing transaction costs. These advantages are attributed to the fact that, within the mid-term, Ethereum is planning to enhance the speed of transactions in layer-2 networks. According to Butterin, this is part of Ethereum’s expansion, particularly because there is still more need to use blockchain technology to date. The other important aspect of Ethereum’s development is the layer-2 solutions. Buterin supports an approach in which the layer-2 networks are dependent on layer-1 to perform some essential tasks like data security, proof, and censorship resistance. This will enable the layer-2 systems of Ethereum to be concerned with verifying and sequencing transactions, which will improve the overall speed and efficiency of the network. Ethereum’s Market Stability Reflects Confidence in Long-Term Strategy Ethereum’s market performance has remained solid, with the cryptocurrency holding steady above $4,000. Currently priced at $4,492.15, Ethereum has experienced a slight 0.93% increase over the last 24 hours, while its trading volume surged by 8.72%, reaching $34.14 billion. These figures point to growing investor confidence in Ethereum’s long-term vision. The crypto community remains optimistic about Ethereum’s future, with many predicting the price could rise to $5,500 by mid-October. Buterin’s clear, forward-thinking strategy continues to build trust in Ethereum as one of the most secure and scalable blockchain platforms in the market. Also Read: Whales Dump 200 Million XRP in Just 2 Weeks – Is XRP’s Price on the Verge of Collapse? The post Vitalik Buterin Reveals Ethereum’s Bold Plan to Stay Quantum-Secure and Simple! appeared first on 36Crypto.
Share
Coinstats2025/09/18 01:22