Dependency Injection
Dependency Injection Interview with follow-up questions
1. What is Dependency Injection in Angular?
Dependency Injection is a design pattern where a class receives its dependencies from an external injector instead of creating them itself. It's one of Angular's core systems, and it gives you loose coupling, easy testing (swap real services for mocks), and controllable instance scope.
The mechanics: you register a provider (typically @Injectable({ providedIn: 'root' })), and Angular's injector resolves a dependency by its token (usually the class type) when a class requests it — via the inject() function or constructor parameters.
@Injectable({ providedIn: 'root' })
export class Logger {}
export class AppComponent {
private logger = inject(Logger); // injector supplies the instance
}
The detail that signals depth: Angular's injectors are hierarchical (an EnvironmentInjector at the root/route level and element injectors per component), so where you provide a service determines whether it's an app-wide singleton (providedIn: 'root') or a per-component instance.
Follow-up 1
Can you explain how Dependency Injection works?
In Angular, Dependency Injection works by defining dependencies in the constructor of a class or component. When an instance of the class or component is created, Angular's DI system automatically resolves and injects the required dependencies. This is done by providing a dependency injection container, which is responsible for creating and managing instances of the required dependencies.
Follow-up 2
What are the benefits of using Dependency Injection?
There are several benefits of using Dependency Injection in Angular:
- Modularity and Reusability: Dependency Injection allows for the separation of concerns and promotes modularity, making it easier to reuse and test individual components.
- Flexibility: By injecting dependencies from external sources, it becomes easier to switch implementations or configurations without modifying the dependent class or component.
- Testability: Dependency Injection makes it easier to write unit tests for individual components, as dependencies can be easily mocked or replaced with test doubles.
- Maintainability: With Dependency Injection, the code becomes more maintainable and easier to understand, as the dependencies are clearly defined and managed externally.
Follow-up 3
Can you give an example of Dependency Injection in Angular?
Sure! Here's an example of how Dependency Injection can be used in Angular:
import { Injectable } from '@angular/core';
@Injectable()
class UserService {
constructor(private http: HttpClient) {}
getUsers() {
return this.http.get('/api/users');
}
}
@Component({
selector: 'app-user-list',
template: `<ul>
<li>{{ user.name }}</li>
</ul>`
})
class UserListComponent {
users: any[];
constructor(private userService: UserService) {}
ngOnInit() {
this.userService.getUsers().subscribe((users) => {
this.users = users;
});
}
}
2. What are the different types of providers in Angular?
A provider tells the injector how to create the value for a given token. The four kinds:
useClass— instantiate a class (the default;{ provide: Api, useClass: Api }, or justApi). Useful to swap implementations:{ provide: Api, useClass: MockApi }.useValue— supply a ready-made value or object (config, constants):{ provide: API_URL, useValue: 'https://…' }.useFactory— run a function to compute the instance, optionally using other deps viainject()/deps: good for runtime decisions.useExisting— alias one token to another so both resolve to the same instance.
providers: [
{ provide: Logger, useClass: ConsoleLogger },
{ provide: API_URL, useValue: '/api' },
{ provide: Client, useFactory: () => new Client(inject(API_URL)) },
]
A good closing note: providedIn: 'root' on @Injectable is shorthand for a useClass root provider that's tree-shakable, which is why it's the preferred way to register most services.
Follow-up 1
Can you explain the difference between useClass, useValue, and useFactory?
Sure!
useClass: This provider is used to provide a class as a dependency. It creates a new instance of the specified class for each injection.
useValue: This provider is used to provide a specific value as a dependency. It can be any value, such as a string, number, or object.
useFactory: This provider is used to provide a value created by a factory function as a dependency. The factory function is called each time the dependency is injected, allowing for dynamic creation of the value.
Follow-up 2
How do you choose which provider to use?
The choice of provider depends on the specific use case and requirements of your application.
useClass is typically used when you want to provide a specific implementation of an interface or abstract class.
useValue is used when you want to provide a specific value that does not need to be instantiated.
useFactory is used when you need to dynamically create a value based on certain conditions or parameters.
Follow-up 3
Can you give an example of each type of provider?
Certainly!
- useClass example:
@Injectable()
class MyService {
// ...
}
@NgModule({
providers: [{ provide: MyInterface, useClass: MyService }]
})
class MyModule {}
- useValue example:
const myValue = 'Hello, World!';
@NgModule({
providers: [{ provide: 'MyValue', useValue: myValue }]
})
class MyModule {}
- useFactory example:
function myFactory() {
return Math.random();
}
@NgModule({
providers: [{ provide: 'RandomNumber', useFactory: myFactory }]
})
class MyModule {}
3. What is a token in Angular Dependency Injection?
A token is the key the injector uses to look up a provider. For most dependencies the token is the class itself (the type), which is why constructor/inject() resolution works automatically.
But when there's no class to use as a key — an interface (which doesn't exist at runtime), a primitive, or a config object — you create an InjectionToken:
export const API_URL = new InjectionToken('API_URL');
// provide
{ provide: API_URL, useValue: 'https://api.example.com' }
// consume
const url = inject(API_URL);
The gotcha interviewers look for: you can't inject an interface because TypeScript interfaces are erased at compile time and leave no runtime token — so you use an InjectionToken (or an abstract class) instead. InjectionToken is also generic, giving you type safety on what the token resolves to.
Follow-up 1
How is a token used in Dependency Injection?
A token is used in Dependency Injection to define the dependency that needs to be injected into a component or service. It is used as a key to look up the provider that is responsible for creating and providing the dependency.
Follow-up 2
Can you give an example of a token in Angular?
Sure! Here's an example of a token defined as a string:
const MY_TOKEN = 'myToken';
Follow-up 3
What is the difference between a string token and an InjectionToken?
In Angular, a string token is a simple string value that is used as a token. It is less flexible and doesn't provide any type safety.
On the other hand, an InjectionToken is a class that extends the abstract class InjectionToken. It provides type safety and allows you to define more complex tokens with additional properties.
Here's an example of an InjectionToken:
import { InjectionToken } from '@angular/core';
const MY_TOKEN = new InjectionToken('myToken');
4. What is an injector in Angular?
An injector is the engine of Angular's DI: given a token, it finds the matching provider, creates (or reuses) the instance, resolves that instance's own dependencies, and hands it over. It also caches instances so a singleton is created once.
The part that earns marks is the injector hierarchy:
EnvironmentInjector— application/root level (and route-level for lazy-loaded routes); this is whereprovidedIn: 'root'services live.- Element (node) injectors — created per component/directive; providers listed in a component's
providersarray live here.
When a class requests a dependency, Angular walks up from the element injector toward the root until it finds a provider (resolution modifiers like @Optional, @Self, @SkipHost adjust this). This hierarchy is exactly what lets you scope a service to a component subtree versus the whole app. You can also grab the current injector programmatically via inject(Injector).
Follow-up 1
How does an injector work in Dependency Injection?
In Dependency Injection, an injector works by following a hierarchical tree-like structure. When a component or service requests a dependency, the injector checks if it has already created an instance of that dependency. If it has, it provides the existing instance. If not, it creates a new instance and provides it. The injector also resolves any dependencies that the requested dependency may have, recursively creating and providing instances as needed.
Follow-up 2
Can you explain the hierarchy of injectors in Angular?
In Angular, injectors are organized in a hierarchical tree-like structure. At the root of the hierarchy is the application-level injector, which is created when the Angular application is bootstrapped. Each component in the application has its own injector, which is a child of the parent component's injector. This creates a nesting of injectors that mirrors the nesting of components in the application. When a dependency is requested, the injector first checks its own instance cache, then checks its parent injector, and continues up the hierarchy until it finds a matching dependency or reaches the root injector.
Follow-up 3
What happens if an injector can't find a dependency?
If an injector can't find a dependency, it throws an error. This error indicates that the requested dependency is not provided or not available in the current injector's hierarchy. To resolve this error, you need to ensure that the dependency is properly provided either at the current injector level or at a higher level in the hierarchy.
5. How can you create a singleton service in Angular?
Use providedIn: 'root' on the @Injectable decorator. Angular registers it on the root environment injector, so a single instance is created lazily on first use and shared everywhere — and because it's referenced this way, an unused service is tree-shaken out of the bundle.
@Injectable({ providedIn: 'root' })
export class MyService { /* … */ }
The nuances interviewers probe:
- Don't also list the service in a component's
providersarray — that creates a new instance per component subtree and breaks the singleton. - In the legacy NgModule world, the old pattern was providing it once in a shared module;
providedIn: 'root'replaced that and is now the standard. - A singleton is only "global" within one injector tree — a lazy-loaded route with its own providers can get a separate instance, which is occasionally intentional. Use
providedIn: 'root'when you truly want one app-wide instance.
Follow-up 1
What is the purpose of a singleton service?
The purpose of a singleton service in Angular is to provide a single instance of a service that can be shared across multiple components. This ensures that the state and data of the service is consistent throughout the application.
Singleton services are commonly used for managing shared data, handling API requests, and implementing application-wide functionality.
Follow-up 2
Can you give an example of a singleton service?
Sure! Here's an example of a singleton service in Angular:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class DataService {
private data: string[] = [];
addData(item: string): void {
this.data.push(item);
}
getData(): string[] {
return this.data;
}
}
In this example, the DataService is a singleton service that manages a list of data items. The addData method is used to add items to the list, and the getData method is used to retrieve the list of data items.
Follow-up 3
What happens if you provide a service at the component level?
If you provide a service at the component level in Angular, a new instance of the service will be created for each component that declares it. This means that each component will have its own separate instance of the service, and changes made to the service's state or data in one component will not affect the other components.
Providing a service at the component level can be useful in scenarios where you want each component to have its own isolated instance of the service, such as when the service needs to maintain component-specific state.
Live mock interview
Mock interview: Dependency Injection
- Read your scene and goals
- Talk it out; goals tick off live
- Get a score and stronger lines
Your voice and your AI key never touch our servers; the key stays in this browser and is sent only to Google. Only your round scores are saved to track progress.