Angular Dependency Injection
Read one Angular interview question at a time, then flip for the complete explanation, example, and code.
Read one Angular interview question at a time, then flip for the complete explanation, example, and code.
Angular Interview Questions · Angular Dependency Injection Companion Article
Angular applications are usually made from many components, services, utilities, configuration objects, and framework features. These parts often depend on one another.
For example, a component may need:
Creating all these dependencies manually inside every component would make the application difficult to maintain and test.
Angular solves this problem using Dependency Injection, commonly called DI.
Dependency Injection allows a class to declare what it needs while Angular takes responsibility for finding or creating those dependencies.
A dependency is simply something that another class needs in order to perform its work.
Consider a product component that needs a service:
export class ProductComponent {
private productService = new ProductService();
}
This works in a very small example, but the component is now responsible for creating the service itself.
This creates several problems:
ProductService.With Angular dependency injection, the component asks Angular for the dependency instead.
import { Component, inject } from '@angular/core';
import { ProductService } from './product.service';
@Component({
selector: 'app-product',
template: `...`
})
export class ProductComponent {
private productService = inject(ProductService);
}
Angular's injector looks for a provider associated with ProductService, obtains the appropriate instance, and supplies it to the component.
The process can be understood as:
Component
↓
Requests ProductService
↓
Angular Injector
↓
Finds Provider
↓
Creates or retrieves ProductService
↓
Returns Service Instance
The component does not need to know how the service was created.
This separation is one of the main benefits of dependency injection.
A provider tells Angular how a dependency should be created or obtained.
You can think of a provider as a rule stored inside Angular's dependency injection system.
Conceptually, it says:
When someone requests this token,
provide this value.
A simple class provider may look like:
providers: [ProductService]
This is shorthand for a more explicit provider configuration:
providers: [
{
provide: ProductService,
useClass: ProductService
}
]
The provide property identifies the dependency.
The provider configuration tells Angular how to obtain its value.
Angular supports several important provider strategies:
Each is useful for a different situation.
Services are one of the most common dependencies in Angular applications.
A service usually contains functionality that should not belong directly inside a component.
Typical responsibilities include:
Example:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class ProductService {
getProducts() {
return ['Laptop', 'Phone', 'Tablet'];
}
}
A component can inject the service:
import { Component, inject } from '@angular/core';
import { ProductService } from './product.service';
@Component({
selector: 'app-products',
template: `...`
})
export class ProductsComponent {
private productService = inject(ProductService);
products = this.productService.getProducts();
}
The component uses the service without manually constructing it.
@Injectable@Injectable() is an Angular decorator used on classes that participate in Angular's dependency injection system.
A common service declaration is:
@Injectable({
providedIn: 'root'
})
export class UserService {
}
The important part is:
providedIn: 'root'
It tells Angular that the service should be available through the application's root environment injector.
For most application-wide services, this is the simplest approach.
Examples include:
AuthService
UserService
ProductService
LoggingService
ConfigurationService
ShoppingCartService
@Injectable() MattersSuppose one service depends on another:
@Injectable({
providedIn: 'root'
})
export class OrderService {
private paymentService = inject(PaymentService);
}
Angular needs dependency metadata to construct and manage these injectable classes correctly.
@Injectable() marks the class for use with Angular's DI system.
inject()Angular provides the inject() function as a direct way to retrieve a dependency from the current injection context.
Example:
import { inject } from '@angular/core';
export class ProductComponent {
private productService = inject(ProductService);
}
Multiple services can be injected:
export class CheckoutComponent {
private cartService = inject(CartService);
private paymentService = inject(PaymentService);
private logger = inject(LoggerService);
}
This approach makes dependencies easy to identify near the fields where they are used.
Angular framework services can also be injected.
Example:
import { Router } from '@angular/router';
import { inject } from '@angular/core';
export class LoginComponent {
private router = inject(Router);
}
Although the syntax looks like Angular is simply retrieving a class, internally the argument acts as an injection token.
inject(ProductService)
Angular searches for the provider registered for ProductService.
Constructor injection is another established way of declaring dependencies.
Example:
import { Component } from '@angular/core';
@Component({
selector: 'app-products',
template: `...`
})
export class ProductsComponent {
constructor(private productService: ProductService) {
}
}
Angular supplies ProductService when it creates the component.
Multiple dependencies can be declared:
constructor(
private authService: AuthService,
private userService: UserService,
private loggerService: LoggerService
) {
}
inject()Both approaches use Angular's dependency injection system.
Constructor style:
constructor(private productService: ProductService) {
}
inject() style:
private productService = inject(ProductService);
inject() can be particularly convenient in:
The important design principle is not simply which syntax is shorter. Dependencies should remain clear, focused, and appropriate to the responsibility of the class.
Angular needs a key that identifies every dependency stored in an injector.
This key is called an injection token or provider token.
A class itself can act as a token.
Example:
providers: [
{
provide: ProductService,
useClass: ProductService
}
]
Here:
ProductService
is the token.
When Angular receives:
inject(ProductService)
it searches for the provider associated with that token.
Classes work well as tokens because classes exist at runtime.
However, not every dependency is represented by a class.
Sometimes you need to inject:
For these situations, Angular provides InjectionToken.
InjectionTokenTypeScript interfaces do not exist as JavaScript values at runtime.
For example:
interface AppConfig {
apiUrl: string;
production: boolean;
}
You cannot reliably use the interface itself as a runtime DI token.
Instead, create an InjectionToken.
import { InjectionToken } from '@angular/core';
export interface AppConfig {
apiUrl: string;
production: boolean;
}
export const APP_CONFIG =
new InjectionToken<AppConfig>('app.config');
Now provide a value:
providers: [
{
provide: APP_CONFIG,
useValue: {
apiUrl: 'https://api.example.com',
production: true
}
}
]
Inject it:
private config = inject(APP_CONFIG);
You can then use:
this.config.apiUrl
This declaration:
InjectionToken<AppConfig>
tells TypeScript what value the token represents.
As a result:
const config = inject(APP_CONFIG);
has the expected AppConfig type.
This improves type safety and development tooling.
InjectionTokenTypical examples include:
API_BASE_URL
APP_CONFIG
FEATURE_FLAGS
DEFAULT_PAGE_SIZE
LOGGER_CONFIG
AUTH_OPTIONS
STORAGE_PROVIDER
Use meaningful names so developers can immediately understand what each token represents.
Angular's dependency injection system is hierarchical.
This means there is not necessarily one single flat container containing every dependency.
Providers can exist at different levels of the application.
A simplified view looks like:
Application-Level Injector
↓
Parent Component
↓
Child Component
↓
Nested Child Component
When Angular needs a dependency, it resolves it according to the applicable injector hierarchy.
The location where a provider is registered affects:
This makes provider placement an architectural decision rather than just a syntax choice.
A service can be provided directly by a component.
Example:
@Component({
selector: 'app-editor',
template: `...`,
providers: [EditorStateService]
})
export class EditorComponent {
private editorState = inject(EditorStateService);
}
Angular creates the service in association with that component's injector scope.
Children in the relevant component subtree can also resolve that provider unless another provider closer to them overrides it.
Component-level providers are useful when each component instance needs isolated state.
Imagine two editors:
<app-editor></app-editor>
<app-editor></app-editor>
If EditorStateService is provided by EditorComponent, each editor can receive its own service instance.
This can be useful for:
Using a root provider in these situations could unintentionally cause unrelated component instances to share data.
Application-wide services are commonly registered at the root level.
The typical declaration is:
@Injectable({
providedIn: 'root'
})
export class AuthService {
}
The service is then available throughout the application wherever that provider can be reached through DI.
Root-level providers are appropriate for services such as:
Example:
@Injectable({
providedIn: 'root'
})
export class AuthService {
isLoggedIn() {
return true;
}
}
Any component can request it:
private authService = inject(AuthService);
Use root scope when state or functionality should be shared widely.
Use component scope when a particular component subtree requires its own independent instance.
Choosing the correct scope prevents many state-management problems.
A value provider supplies an existing value instead of asking Angular to create a class.
It uses:
useValue
Example:
export const API_URL =
new InjectionToken<string>('api.url');
Provider:
providers: [
{
provide: API_URL,
useValue: 'https://api.example.com'
}
]
Usage:
private apiUrl = inject(API_URL);
Value providers are useful for:
Another example:
export const PAGE_SIZE =
new InjectionToken<number>('page.size');
providers: [
{
provide: PAGE_SIZE,
useValue: 20
}
]
The consumer does not need to know where the value came from.
A class provider tells Angular which class should be instantiated for a token.
It uses:
useClass
Example:
providers: [
{
provide: LoggerService,
useClass: ConsoleLoggerService
}
]
Now:
inject(LoggerService)
returns an instance created using ConsoleLoggerService.
This is useful when you want consumers to depend on one abstraction while changing the implementation.
For example:
LoggerService
↓
ConsoleLoggerService
Development could use one implementation:
{
provide: LoggerService,
useClass: ConsoleLoggerService
}
while another configuration could use:
{
provide: LoggerService,
useClass: RemoteLoggerService
}
The consumer does not need to change.
Sometimes creating a dependency requires logic.
For example, a service implementation may depend on:
Angular supports factory providers through:
useFactory
Example:
export function createLogger() {
return new LoggerService();
}
Provider:
providers: [
{
provide: LoggerService,
useFactory: createLogger
}
]
Factories become more useful when dependencies are involved.
export function createApiService(config: AppConfig) {
return new ApiService(config.apiUrl);
}
A factory provider can declare the values it depends on.
Another modern pattern is using inject() inside a provider factory when the factory executes in Angular's injection context.
Example:
providers: [
{
provide: ApiService,
useFactory: () => {
const config = inject(APP_CONFIG);
return new ApiService(config.apiUrl);
}
}
]
Factory providers are useful when simple class construction is not enough.
Typical situations include:
Avoid putting large amounts of business logic inside provider factories. Their main responsibility should be dependency creation and configuration.
An existing provider creates an alias for another registered dependency.
It uses:
useExisting
Example:
providers: [
NewLoggerService,
{
provide: OldLoggerService,
useExisting: NewLoggerService
}
]
Now requests for both tokens can resolve to the existing NewLoggerService instance.
Conceptually:
OldLoggerService
↓
Alias
↓
NewLoggerService Instance
This is different from creating another class instance.
useExisting Is UsefulIt is useful when:
If you want two tokens to point to the same existing dependency, useExisting is often more appropriate than useClass.
The major provider strategies solve different problems.
| Provider | Main Purpose | Typical Example |
|---|---|---|
useValue | Supply an existing value | Configuration |
useClass | Instantiate another class | Alternative implementation |
useFactory | Create value using logic | Runtime configuration |
useExisting | Alias another provider | Backward compatibility |
A useful mental model is:
useValue
"Here is the value."
useClass
"Create this class."
useFactory
"Run this function to create the value."
useExisting
"Use the dependency that already belongs to this other token."
The inject() function cannot simply be called from arbitrary application code.
It requires an Angular injection context.
An injection context exists while Angular is performing certain dependency-injection operations.
Common valid situations include:
InjectionToken factories,Example:
@Injectable({
providedIn: 'root'
})
export class OrderService {
private logger = inject(LoggerService);
}
The field initializer runs while Angular is constructing the injectable class, so inject() can resolve the dependency.
Another example:
export const API_CLIENT = new InjectionToken<ApiClient>(
'api.client',
{
factory: () => {
const config = inject(APP_CONFIG);
return new ApiClient(config.apiUrl);
}
}
);
The token factory executes in an injection context.
Calling inject() later from an ordinary method is not automatically valid.
Do not assume this will work:
loadData() {
const service = inject(ProductService);
}
The method may execute long after Angular has completed dependency construction.
A safer approach is to inject the dependency during class initialization:
private productService = inject(ProductService);
loadData() {
return this.productService.getProducts();
}
Understanding injection context prevents confusing runtime dependency injection errors.
Sometimes a dependency is useful when available but should not be required.
Angular supports optional dependency resolution.
Using inject():
private logger = inject(LoggerService, {
optional: true
});
If Angular cannot find the provider, the result can be null instead of the injection failing because the provider is missing.
The application should therefore handle the missing dependency.
save() {
this.logger?.log('Saving data');
}
Optional injection is useful for:
Do not make a dependency optional simply to hide configuration errors.
If a service is essential to the feature, it should normally remain required.
Consider:
private userService = inject(UserService);
A simplified resolution process is:
1. Angular receives the UserService token.
2. Angular examines the relevant injector.
3. It looks for a provider matching UserService.
4. If necessary, resolution continues through the applicable injector hierarchy.
5. When a provider is found, Angular obtains or creates its value.
6. Angular returns that value to the consumer.
This explains why provider location matters.
Suppose:
Application
|
└── Dashboard
|
└── Report
If Report requests a dependency that is not locally provided, Angular can resolve it from an appropriate ancestor injector.
If Dashboard provides a different version of the same token, consumers within that scope may receive the closer provider rather than the more global one.
This ability to override dependencies is one of the powerful features of hierarchical DI.
Suppose an application needs API configuration.
First define the type:
export interface ApiConfig {
baseUrl: string;
timeout: number;
}
Create a token:
import { InjectionToken } from '@angular/core';
export const API_CONFIG =
new InjectionToken<ApiConfig>('api.config');
Provide the configuration:
providers: [
{
provide: API_CONFIG,
useValue: {
baseUrl: 'https://api.example.com',
timeout: 5000
}
}
]
Use it inside a service:
@Injectable({
providedIn: 'root'
})
export class ProductService {
private config = inject(API_CONFIG);
getApiUrl() {
return `${this.config.baseUrl}/products`;
}
}
This design is better than hardcoding the URL in several services.
The configuration is centralized, strongly typed, and replaceable.
Suppose an application has:
export abstract class StorageService {
abstract save(key: string, value: string): void;
}
One implementation:
@Injectable()
export class BrowserStorageService extends StorageService {
save(key: string, value: string) {
localStorage.setItem(key, value);
}
}
Provider:
providers: [
{
provide: StorageService,
useClass: BrowserStorageService
}
]
Consumer:
export class SettingsComponent {
private storage = inject(StorageService);
saveSettings() {
this.storage.save('theme', 'dark');
}
}
SettingsComponent depends on StorageService, not directly on BrowserStorageService.
Another implementation can therefore replace it later with less impact on the consumer.
This is one of the practical reasons DI improves maintainability.
Consider a reusable shopping filter panel.
@Injectable()
export class FilterStateService {
selectedCategory = '';
}
Provide it directly on the component:
@Component({
selector: 'app-product-filter',
providers: [FilterStateService],
template: `...`
})
export class ProductFilterComponent {
private filterState = inject(FilterStateService);
}
If multiple instances of ProductFilterComponent appear on the page, each component can manage its own local filter service instance.
This is very different from putting the service in the root injector, where consumers would normally share application-level state.
DI also improves testability.
Suppose a component depends on an API service.
Production implementation:
class ProductApiService {
getProducts() {
// Real API request
}
}
During testing, the real API dependency can be replaced with a controlled test implementation.
class FakeProductApiService {
getProducts() {
return ['Test Product'];
}
}
The test can configure:
{
provide: ProductApiService,
useClass: FakeProductApiService
}
The component itself does not need to be rewritten.
This allows tests to isolate the behavior being tested instead of depending on external systems.
Value providers can also be useful in tests:
{
provide: API_CONFIG,
useValue: {
baseUrl: 'test-api',
timeout: 100
}
}
Avoid:
private service = new ProductService();
when the service is designed to participate in Angular DI.
Manual creation bypasses Angular's provider system and makes dependencies harder to replace.
Prefer:
private service = inject(ProductService);
when appropriate.
Consider:
@Component({
providers: [AuthService]
})
If AuthService is supposed to represent one application-wide authentication state, creating a component-scoped version may produce unexpected independent instances.
Before adding a service to providers, decide whether you actually want a new scoped instance.
Developers sometimes register the same service repeatedly because they assume every component needs its own provider declaration.
A service with:
@Injectable({
providedIn: 'root'
})
normally does not need to be added to every component's providers array.
Provider scope should be intentional.
Avoid loosely defined token patterns that are difficult to type-check and maintain.
For configuration and other non-class dependencies, prefer properly typed InjectionToken objects.
Example:
export const APP_CONFIG =
new InjectionToken<AppConfig>('app.config');
inject() From Arbitrary MethodsAvoid treating inject() as a global service locator that can be called anywhere.
Prefer resolving the dependency in a valid injection context and storing it:
private service = inject(ProductService);
Then use:
load() {
return this.service.getProducts();
}
This may hide genuine provider configuration problems:
inject(ImportantService, {
optional: true
});
Use optional dependencies only when the absence of that dependency is an expected and supported application state.
NullInjectorError and Missing ProvidersOne common DI problem occurs when Angular cannot find a provider for a requested token.
Conceptually:
Component
↓
requests Service
↓
Injector searches
↓
No provider found
↓
Injection error
When this happens, check:
providedIn: 'root'?InjectionToken objects being created accidentally?Do not immediately add providers everywhere. First identify the intended lifetime and scope of the dependency.
Provider placement affects application behavior.
Consider these three cases.
Authentication
User session
Global preferences
Application configuration
A root-level provider is usually appropriate.
Checkout workflow
Admin feature state
Feature-specific cache
The provider can be scoped to the appropriate feature or route configuration where the application architecture requires it.
Editor state
Form wizard
Reusable widget state
Temporary selection state
A component-level provider may be appropriate.
Choosing scope deliberately helps prevent unexpected data sharing.
One of the biggest architectural benefits of DI is that components do not need to know how their dependencies are implemented.
Without DI:
Component
↓
Creates API client
↓
Loads configuration
↓
Creates logger
↓
Handles storage
With DI:
Component
↓
Requests required dependencies
↓
Angular DI system supplies them
The component can focus on presentation and feature behavior rather than infrastructure construction.
This usually produces code that is easier to:
Use useValue when you already have the value.
{
provide: API_URL,
useValue: 'https://example.com'
}
Use useClass when Angular should create an implementation class.
{
provide: LoggerService,
useClass: ConsoleLoggerService
}
Use useFactory when creation requires logic.
{
provide: ApiService,
useFactory: () => {
const config = inject(APP_CONFIG);
return new ApiService(config.apiUrl);
}
}
Use useExisting when another registered dependency should be used for the token.
{
provide: OldLogger,
useExisting: NewLogger
}
Use InjectionToken when the dependency cannot conveniently use a class as its runtime token.
const API_URL =
new InjectionToken<string>('api.url');
A component requiring ten or fifteen services may be doing too much.
For example:
private auth = inject(AuthService);
private user = inject(UserService);
private products = inject(ProductService);
private orders = inject(OrderService);
private payment = inject(PaymentService);
private email = inject(EmailService);
private logger = inject(LoggerService);
This can indicate that responsibilities should be reviewed.
Ask:
Should this instance belong to the whole application?
Should it belong only to one feature?
Should every component instance get its own copy?
The answer determines where the provider should live.
Instead of:
const apiUrl = 'https://api.example.com';
inside many services, inject configuration through a suitable token.
This improves portability and maintainability.
Prefer:
new InjectionToken<AppConfig>('app.config')
over loosely typed configuration patterns.
The compiler can then help identify incorrect usage.
DI should make dependencies clear.
Do not create generic helper code whose purpose is simply to retrieve arbitrary services from an injector whenever required.
Explicit dependencies generally make architecture easier to understand.
A provider factory should mainly construct or configure the dependency.
If a factory contains substantial business logic, that logic probably belongs in a dedicated service.
Code using an optional dependency should work correctly when the dependency does not exist.
Example:
private analytics = inject(AnalyticsService, {
optional: true
});
trackPage() {
this.analytics?.track('page-view');
}
The feature continues working even when analytics is not provided.
A useful way to understand Angular Dependency Injection is to separate it into four ideas.
The class that needs something.
ProductComponent
The identifier used to request the dependency.
ProductService
or:
APP_CONFIG
The rule describing how the token should be resolved.
{
provide: APP_CONFIG,
useValue: config
}
The Angular runtime mechanism that stores and resolves providers.
The complete relationship is:
Consumer
↓
Requests Token
↓
Injector searches for Provider
↓
Provider supplies value
↓
Dependency returned to Consumer
Once this model is clear, advanced DI features become much easier to understand.
Consider an e-commerce checkout component.
It may need:
CartService
PaymentService
OrderService
NotificationService
CHECKOUT_CONFIG
The component declares the dependencies:
@Component({
selector: 'app-checkout',
template: `...`
})
export class CheckoutComponent {
private cart = inject(CartService);
private payment = inject(PaymentService);
private orders = inject(OrderService);
private notifications = inject(NotificationService);
private config = inject(CHECKOUT_CONFIG);
checkout() {
// Coordinate checkout operations
}
}
Each dependency has a separate responsibility.
The checkout component coordinates them without needing to know:
That is the practical value of dependency injection.
When adding a new dependency, ask these questions:
Consider an injectable service.
Consider root-level provisioning.
Consider component-level providers.
Consider InjectionToken.
Use useValue.
Use useClass.
Use useFactory.
Use useExisting.
Use optional injection and handle the missing value.
inject() from valid DI-managed code?Confirm that the code runs in an injection context.
Angular Dependency Injection separates using a dependency from creating a dependency.
The most important concepts to remember are:
@Injectable() allows classes to participate correctly in Angular's DI system.inject() retrieves dependencies from the current injection context.InjectionToken is useful for configuration, interfaces, primitives, functions, and other values without a suitable runtime class token.useValue supplies an existing value.useClass tells Angular which class to instantiate.useFactory creates a dependency using a function.useExisting aliases one token to another dependency.inject() must execute in an appropriate injection context.A strong understanding of tokens, providers, injectors, scope, and injection context is more valuable than simply memorizing inject() or @Injectable() syntax. These concepts explain how Angular connects application classes and how developers can control dependency lifetime and implementation as an application grows.