Devot Logo
Devot Logo
Arrow leftBack to blogs

Dependency Injection in Angular: A Complete Guide

Hrvoje D.5 min readJul 1, 2025Technology
Hrvoje D.5 min read
Contents:
What is dependency injection?
Dependency injection with standalone components
Functional injection: A modern approach
Advanced DI Concepts
Best practices and common pitfalls
Conclusion

Over the past couple of years, Angular, one of the most used frontend frameworks, has undergone significant changes that have reshaped how development is done.

While improvements like typed reactive forms or hydration for server-side rendering have captured attention, one of the most influential changes came early on: the addition of standalone components and, thus, orientation towards functional programming. This change, in turn, brought along important updates to Angular’s Dependency Injection (DI) system, making it more flexible and easier to use.

What is dependency injection?

Dependency injection is a powerful design pattern that underpins Angular’s architecture. It enables component modularity and the separation of concerns by delivering the needed parts of the application to the consumers (components or services) that request them. At its core, DI consists of several key concepts:

  • Injector

  • Injectable

  • Provider

  • Consumer

Injector

The injector is the backbone of the DI system. It maintains a registry of providers and is responsible for creating and delivering instances of classes when requested. When a component or service declares a dependency, Angular’s injector checks its registry: if an instance already exists, it returns that instance; otherwise, it creates a new one.

Angular uses a hierarchical injector system, meaning that while there’s a root-level injector for the entire application, child injectors can also be created for specific modules or components. This hierarchy not only helps manage dependencies but also allows for more granular control over the scope of services.

Injectable

The @Injectable decorator marks a class as available for DI. When a class is decorated with @Injectable, it tells Angular that it can inject instances of this class wherever needed. In most cases, developers also specify the provider’s scope with the providedIn property:

In this example, MyService is registered in the root injector, meaning a single instance is shared across the application. However, services can also be provided at a more granular level if their scope needs to be limited.

Provider

A provider tells Angular’s injector how to create a dependency. Providers can be defined in several ways:

  • Class Provider: The most common approach, where a developer simply provides the class.

  • Value Provider: When a developer wants to inject a constant value.

  • Factory Provider: When the dependency creation is more complex and requires a custom factory function.

  • Alias Provider: When a dependency token needs to be created to resolve to another.

  • Multi-Provider: When multiple values need to be associated with a single token.

By configuring providers correctly, the developer controls how and when the services are instantiated. For instance, providing a service at the component level will result in a new instance every time that component is created.

Consumer

Consumers are the components or services that use the injected values. They declare their dependencies in their constructors, and Angular’s DI system takes care of providing them. For example:

Here, MyComponent acts as a consumer by requesting an instance of MyService in its constructor. The DI system ensures that the correct instance is provided, whether it’s the singleton from the root or a new one if registered at a lower level.

Dependency injection with standalone components

Angular’s introduction of standalone components further simplifies the DI landscape. Standalone components remove the need for Angular modules (NgModules) in many cases, allowing a developer to declare providers directly within the component metadata.

For example:

This approach streamlines development by keeping component declarations and their dependencies in one place, leading to better organization and easier testing.

Functional injection: A modern approach

Starting with Angular 14, developers can now leverage the inject function to retrieve dependencies, which allows for a more functional and concise style compared to the traditional constructor-based injection.

The new functional approach simplifies the process by allowing to directly inject dependencies into class fields. Here is an example with a service and with a constant:

Benefits of using inject

  • Cleaner syntax: By removing the need for constructor parameters, classes become less cluttered and more focused on their core logic.

  • Immediate availability: The dependency is available as soon as the field is declared, which can be advantageous in certain scenarios, such as lazy-loaded values or initializing class properties.

  • Flexibility: This approach aligns well with standalone components and functional programming patterns, providing a consistent way to manage dependencies across different parts of the application.

Things to keep in mind

  • Context-specific: The inject function is designed to be used in contexts where Angular’s DI system is active (e.g., during component or service initialization). Calling it outside of these contexts may result in errors.

  • Testing considerations: When writing unit tests, a developer needs to ensure that the DI context is correctly set up for classes using inject. This is generally similar to testing components with constructor injection, but it's something to be aware of.

Choosing between approaches

Both methods ultimately achieve the same goal—providing the component or service with the dependencies it needs. The choice between constructor injection and functional injection depends on the project's style, readability preferences, and specific use cases.

If one prefers a more declarative and concise style, especially in standalone components or utility functions, the inject function can be a great addition to one's Angular toolkit.

Advanced DI Concepts

Hierarchical Injectors

Angular’s DI system is hierarchical. This means that providers declared in a component are only available to that component and its children, while providers declared in the root are available globally.

This hierarchy is particularly useful when one needs to override a dependency in a specific part of the application without affecting the entire app.

Multi-Providers

Sometimes, a developer needs multiple values to be associated with a single token. Angular’s multi-provider system allows this by letting an array of values be provided. Multi-providers are often used for things like logging or configuring multiple strategies for a given operation.

Injection Tokens

When injecting values that aren’t classes (like configuration objects or strings), Angular provides the InjectionToken class. This ensures that the DI system can safely reference these values without naming collisions.

Token can be provided in a module or component:

Best practices and common pitfalls

Choose the right scope

Decide whether a service should be provided at the root or component level. Global services reduce boilerplate but may lead to unwanted shared state, while component-level providers offer isolation.

Avoid circular dependencies

Ensure that the services and components do not depend on each other in a way that creates a cycle. Circular dependencies can lead to runtime errors and make debugging difficult.

Use Injection Tokens wisely

For non-class values, always use InjectionToken to prevent naming conflicts and to improve clarity.

Leverage Angular’s hierarchical DI

Understand how Angular’s injector hierarchy works to avoid unexpected behaviors, especially when overriding providers in nested components.

Conclusion

Angular’s dependency injection system is a powerful tool that helps manage complexity, improve testability, and promote a clean separation of concerns within your application. Whether you’re using non-standalone components or the newer standalone components, mastering DI will allow you to build more scalable, maintainable, and modular applications.

By understanding the roles of injectors, injectables, providers, and consumers and by applying best practices, you can leverage Angular’s DI to its fullest potential.

Spread the word:
Keep readingSimilar blogs for further insights
Building a Scalable API Testing Architecture: Tools, Frameworks, and Best Practices
Technology
Leo C.6 min readSep 24, 2025
Building a Scalable API Testing Architecture: Tools, Frameworks, and Best PracticesRobust API testing architecture is key to ensuring performance, reliability, and security across modern software systems. This guide covers layered testing strategies, automation tools, framework design principles, and CI/CD integration best practices.
The Full-Stack AI Developer: Frameworks, Tools, and Deployment Skills You Actually Need
Technology
Iva P.10 min readSep 18, 2025
The Full-Stack AI Developer: Frameworks, Tools, and Deployment Skills You Actually NeedWhat does a full-stack AI developer really do—and why aren’t these roles as common as you'd think in 2025? Discover the skills, salaries, job boards, and step-by-step roadmap for breaking into this future-facing career.
The C4 Model Explained: Clearer Software Architecture Diagrams with Structurizr
Technology
Vladimir Š.12 min readSep 16, 2025
The C4 Model Explained: Clearer Software Architecture Diagrams with StructurizrClarity in software architecture starts with the right diagrams. Explore how the C4 model and Structurizr help developers and architects create consistent, maintainable visuals that scale with your system.