I am setting up a large monorepo with multiple libraries. I am confused about how to manage providers globally. Should I define them in the root app, or use 'providedIn: root' everywhere? I want to avoid memory leaks and ensure that my singletons are actually singletons across different project boundaries.
Dependency injection in a monorepo should leverage providedIn: root for universal services while using injection tokens to enforce modular boundaries and maintain singleton integrity across project limits.
4 answers
Handling DI in a large monorepo requires a disciplined approach to scoping to avoid configuration sprawl.
- Use providedIn: root for services that are truly universal across your entire build pipeline.
- Create custom injection tokens for domain-specific logic to keep your boundaries clean.
- Avoid manual registration in the root app to keep your testing suites isolated and predictable.
Stick to providedIn: root for almost everything, as it keeps your dependency graph flat and your tree-shaking efficient. Defining providers in the root app module is an anti-pattern in a monorepo because it forces tight coupling between your infrastructure libraries and the final delivery platform.
I remember back in 2019 when we tried to manage global state in a monorepo by centralizing everything in the top-level app module. It became a nightmare of circular dependencies and bloated bundle sizes that took us weeks to untangle because we lost track of what injected where.
We eventually moved to a model where every library manages its own singleton providers via the providedIn token. Once we made that shift, the memory leaks vanished and the build performance improved significantly because our individual packages became truly decoupled units.
The choice between centralized registration and distributed providedIn: root centers on the trade-off between control and encapsulation. Defining providers in the root app module offers a single source of truth, which simplifies debugging for smaller teams, yet this approach frequently creates a bottleneck where the root must import every library in your graph. This configuration is fine for small projects, but it quickly falls apart in complex systems where compile-time dependency graphs should remain acyclic and granular.
Conversely, using providedIn: root everywhere is generally superior for large-scale architectures. By allowing each library to declare its own dependencies, you effectively delegate configuration to the package level, which promotes better isolation. This method is technically safer, provided you are mindful of tree-shaking, as it allows the compiler to prune unused providers automatically. While it may slightly increase the complexity of tracking singleton lifecycles, it prevents the root application from becoming a bloated configuration manifest. When you prioritize modularity, distributed DI is almost always the correct path forward.