@NgModule({
[...]
providers: [
{ provide: FlightService, useClass: FlightService }
// Alternative: FlightService
]
[...]
})
export class FlightBookingModule {
}
The Motivation and a First Look
To understand the necessity of treeshakable providers, consider a typical setup using the classic approach. Suppose AppModule brings in a FlightBookingModule which, in turn, declares a service like FlightService. Under the traditional pattern, the dependency graph looks like this:

The key insight is that AppModule ends up with an indirect reference to the service solely because it imports the feature module. Even if the service is never called, bundlers such as webpack or rollup see this reference and preserve the service in the final output, bloating the bundle unnecessarily.
The Angular team addressed this by flipping one dependency in the graph:

Now, AppModule only knows about the service when it actually consumes it. This shift in responsibility is implemented via the provideIn option inside the Injectable decorator:
@Injectable({
providedIn: 'root'
})
export class FlightService {
constructor(private http: HttpClient) {}
[...]
}
This property designates the module whose injection scope receives the service. The literal 'root' is shorthand for the application-wide root injector. Keep in mind that this root scope is shared by all eagerly loaded modules—only lazy-loaded routes and components create their own child scopes that inherit from the root. Consequently, 'root' is the most common choice.
One immediate advantage is that you no longer edit the module to register the service. Once the decorator is written, the service is ready for injection wherever needed.
Why Providers Differ from Components
You might ask why components and directives don't suffer from the same tree-shaking problem. They do, in fact. To counter this, the Angular team developed the build optimizer, which the CLI invokes for production builds. Among its tasks, it strips component decorators and their metadata after AOT compilation, since that data is no longer needed and would otherwise block tree shaking.
Providers are special because they carry runtime metadata: a mapping from a token to a service, registered against a specific injector scope. This information cannot be removed after compilation. Hence, a dedicated API for treeshakable providers was necessary.
Configuring Indirections
Dependency injection exists to let you swap implementations behind a stable token. With treeshakable providers, you can reuse familiar configuration keys directly in the Injectable decorator:
@Injectable({
providedIn: 'root',
useClass: AdvancedFlightService,
deps: [HttpClient]
})
export class FlightService {
constructor(private http: HttpClient) {}
[...]
}
Here, any consumer asking for FlightService will receive an AdvancedFlightService instead.
During testing with version 6.0.0, it became apparent that the dependencies of the replacement class must be listed in the deps array. Without it, Angular falls back to the tokens taken from the constructor of the class referenced by useClass. In the example above, both classes expect an HttpClient, so the array is technically redundant. Future releases may remove the need for deps when using useClass.
Beyond useClass, the same decorator supports useValue, useFactory, and useExisting. Multi providers, however, are not supported in this new API—and for good reason: with multiple values, the token shouldn't dictate the list of implementations ahead of time.
For multi providers, you'll still reach for the traditional API. Alternatively, you can roll your own using factories. A sample implementation is available in the demo repository.
Abstract Classes as Tokens
To guarantee that AdvancedFlightService can stand in for FlightService, a supertype like an abstract class—or a mere interface—helps enforce compatible method contracts.
An abstract class serves double duty: it acts as a type and as a DI token. This is a standard pattern; consumers request the abstraction and receive a concrete implementation.
Interfaces, on the other hand, cannot work as tokens. Although common in other languages, TypeScript erases interfaces at compile time since JavaScript lacks that concept. Tokens must exist at runtime, so interfaces are out of the question.
To use an abstract class as a token, you simply move the Injectable decorator with its configuration onto the abstract class:
@Injectable({
providedIn: 'root',
useClass: AdvancedFlightService,
deps: [HttpClient]
})
export abstract class AbstractFlightService {
[...]
}
Concrete services then implement this abstract type:
@Injectable()
export class AdvancedFlightService implements AbstractFlightService {
[...]
}
Consumers request the abstraction to obtain the configured implementation:
@Component({ [...] })
export class FlightSearchComponent implements OnInit {
constructor(private flightService: AbstractFlightService) {
}
[...]
}
This looks straightforward, but a hidden problem lurks: a circular reference.

Fortunately, the example avoids the trap by using implements rather than extends. This lesser-known feature lets TypeScript treat the abstract class like an interface—it only checks the shape. Once the check passes, the reference is dropped from the compiled output, breaking the cycle.
Using extends would keep the cycle alive, leading to a runtime failure from a chicken-and-egg situation. The takeaway: always prefer implements in such scenarios.
Scoping Services to Lazy Modules
Occasionally, you'll want a service bound to the injector of a lazy-loaded module. This creates a separate instance—an "own singleton"—which can override a service from a parent scope.
To achieve this, point provideIn at the module:
@Injectable({
providedIn: FlightBookingModule,
useClass: AdvancedFlightService,
deps: [HttpClient]
})
export abstract class AbstractFlightService {
}
Although concise, this introduces another cycle:

In discussion with Alex Rickabaugh, a solution emerged: gather all services for the feature into a dedicated service module. I named this module FlightApiModule:

Then, update providedIn to target this new module:
@Injectable({
providedIn: FlightApiModule,
useClass: AdvancedFlightService,
deps: [HttpClient]
})
export abstract class AbstractFlightService {
}
The lazy-loaded module must also import the service module:
@NgModule({
imports: [
[...]
FlightApiModule
],
[...]
})
export class FlightBookingModule {
}
InjectionTokens and Factories
Angular also allows you to use InjectionToken instances as tokens, which is handy when no class fits the role. To make these treeshakable, the InjectionToken constructor now accepts a provider configuration:
export const FLIGHT_SERVICE = new InjectionToken<FlightService>('FLIGHT_SERVICE',
{
providedIn: FlightApiModule,
factory: () => new FlightService(inject(HttpClient))
}
);
For technical reasons, this setup requires a factory function. Since there's no way to infer dependencies from a function's signature alone, you must use the inject method with explicit tokens. The resulting services can then be supplied to whatever the factory constructs.
One limitation: inject currently (as of version 6.0.0) does not accept an abstract class as a token, even though DI itself supports them. This likely stems from TypeScript's lack of a clean type to represent an abstract class constructor. A future version might offer a workaround or relax the signature to accept any. In the meantime, casting the abstract class to any works since it is type-compatible with everything.
With that trick, you can define an injection token whose factory uses the abstract class as a dependency:
export const BOOKING_SERVICE = new InjectionToken<BookingService>('BOOKING_SERVICE',
{
providedIn: FlightApiModule,
factory: () => new BookingService(inject(<any>AbstractFlightService))
}
);
When Only the Classic API Works
Despite the elegance and smaller bundle sizes of treeshakable providers, some situations demand the traditional API. Multi providers were already mentioned. Another case is providing configuration services—for instance, the RouterModule's static forRoot and forChild methods, which accept routing configuration.
Those scenarios still rely on static methods returning a ModuleWithProviders instance:
@NgModule({
imports: [ CommonModule ],
declarations: [ DemoComponent ],
providers: [ /* no services */ ],
exports: [ DemoComponent ]
})
export class DemoModule {
static forRoot(config: ConfigService): ModuleWithProviders {
return {
ngModule: DemoModule,
providers: [
{ provide: ConfigService, useValue: config }
]
}
}
}
