Angular Service & Dependency Injection
Design Angular services with the right provider scope, injection tokens, and hierarchical injector strategy
When to Use
- Creating a shared service (data fetching, state, utilities) and deciding on its scope
- Replacing a module-level provider with
providedIn: 'root'or a standalone provider - Using
InjectionTokento inject non-class values (config objects, feature flags, URLs) - Injecting services into other services without circular dependencies
- Providing mock implementations in tests
Instructions
- Decorate every service with
@Injectable. UseprovidedIn: 'root'as the default — it makes the service a singleton and tree-shakeable without needing to list it in a module. - Use
providedIn: 'root'for app-wide singletons. Use component-levelproviders: [MyService]only when you need a fresh instance per component subtree. - Create
InjectionToken<T>for any non-class dependency: API base URLs, feature flags, environment config, or factory functions. - Use the
inject()function (Angular 14+) instead of constructor injection when writing standalone functions, guards, or effects. It reads more cleanly and is required outside class constructors. - Avoid circular dependencies by extracting shared state into a lower-level service that neither service imports.
- Prefer
DestroyRefinjection over implementingOnDestroyin services — it keeps cleanup co-located with subscription setup.
// environment config token
export const API_BASE_URL = new InjectionToken<string>('API_BASE_URL', {
providedIn: 'root',
factory: () => 'https://api.example.com',
});
@Injectable({ providedIn: 'root' })
export class ProductService {
private readonly http = inject(HttpClient);
private readonly baseUrl = inject(API_BASE_URL);
private readonly destroyRef = inject(DestroyRef);
getProducts(): Observable<Product[]> {
return this.http.get<Product[]>(`${this.baseUrl}/products`);
}
}
// Override in tests or feature modules
providers: [{ provide: API_BASE_URL, useValue: 'https://staging.example.com' }];
// Component-scoped service — new instance per component tree
@Component({
selector: 'app-wizard',
providers: [WizardStateService], // fresh instance, destroyed with component
template: `...`,
})
export class WizardComponent {}
Details
Injector hierarchy: Angular maintains a tree of injectors that mirrors the component tree. When a token is requested, Angular walks up the tree until it finds a provider. Root injector covers the entire app. Module injectors cover lazy-loaded modules. Component injectors cover a component and its descendants. This hierarchy is the mechanism behind scoped singletons.
providedIn: 'root' vs module providers: Module providers were the Angular 2–12 idiom. They work but require importing the module to get the service. providedIn: 'root' eliminates that coupling and enables tree-shaking — unused services are removed from the bundle automatically.
inject() function: Available in any injection context (constructor, field initializer, factory function, guard, resolver). It cannot be called in a method body called outside the injection context. Use it freely in class field initializers and @Component function callbacks.
Multi-providers: Use multi: true with a token to collect multiple implementations under one token. Angular's HTTP_INTERCEPTORS and APP_INITIALIZER tokens use this pattern.
export const VALIDATORS = new InjectionToken<Validator[]>('VALIDATORS');
providers: [
{ provide: VALIDATORS, useClass: EmailValidator, multi: true },
{ provide: VALIDATORS, useClass: PhoneValidator, multi: true },
];
// inject(VALIDATORS) → [EmailValidator, PhoneValidator]
Service isolation for testing: Provide a mock in TestBed.configureTestingModule:
TestBed.configureTestingModule({
providers: [{ provide: ProductService, useValue: mockProductService }],
});
Tree-shakeable tokens: Pair InjectionToken with a factory to make the default value tree-shakeable:
export const FEATURE_FLAGS = new InjectionToken<FeatureFlags>('FEATURE_FLAGS', {
providedIn: 'root',
factory: () => ({ darkMode: false, betaSearch: false }),
});
Source
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.