Mert Özen Further With Every Line
Frontend Angular — #10

Angular Services and Dependency Injection: providedIn, the Injector Tree and Providers

Mert Özen Oct 31, 2026 7 min 2 views
Angular Services and Dependency Injection: providedIn, the Injector Tree and Providers

Why services exist, singletons with providedIn: 'root', constructor injection, component-level providers and hierarchical injectors, useClass/useValue/useFactory, and config values via InjectionToken.

Version note: The code and commands in this post are for Angular 12.

A Component Shouldn't Do Everything Itself

Picture an e-commerce page. The product list sits in the middle, and in the top right corner there is a small badge that says "Sepet: 3 ürün". When the user adds a product from the list to the cart, the number on the badge should go up. Both components live under AppComponent, so they are siblings.

You could solve this with the @Input and @Output we saw earlier: the list emits an event, the parent catches it and passes the number to the badge as an input. For two siblings, that works. Once the checkout page and the product detail page also want to know about the cart, the data turns into a bucket of water being carried up and down the component tree.

What we need is something like an apartment building's central boiler. Each flat doesn't install its own boiler; there is one in the basement, every flat connects a pipe and gets heat. In Angular, that boiler is called a service, and the plumber who connects the pipes is called dependency injection.

Our First Service

Run this command in the project folder:

ng generate service cart

The short form is ng g s cart. The CLI puts cart.service.ts and a test file under src/app. This time app.module.ts is not updated; you will see why in a moment. Let's fill in the generated file:

import { Injectable } from '@angular/core';

export interface CartItem {
  name: string;
  price: number;
}

@Injectable({
  providedIn: 'root'
})
export class CartService {
  private items: CartItem[] = [];

  add(item: CartItem): void {
    this.items.push(item);
  }

  clear(): void {
    this.items = [];
  }

  get count(): number {
    return this.items.length;
  }

  get total(): number {
    return this.items.reduce((sum, item) => sum + item.price, 0);
  }
}

A service is just an ordinary TypeScript class. No template, no selector. It holds the items in the cart, offers methods to add and clear, and computes the count and the total. The items array is private because we don't want anyone outside to touch the cart by going around the service.

What makes the difference is the @Injectable decorator on top. The providedIn: 'root' setting tells Angular "create one instance of this service for the whole app and give it to anyone who asks." Since the service registers itself, you don't need to add it to any module's providers list. That is why the CLI left app.module.ts alone.

Constructor Injection

Now let the product list use the service:

import { Component } from '@angular/core';
import { CartItem, CartService } from '../cart.service';

@Component({
  selector: 'app-product-list',
  templateUrl: './product-list.component.html'
})
export class ProductListComponent {
  products: CartItem[] = [
    { name: 'Mekanik Klavye', price: 1450 },
    { name: 'Kablosuz Mouse', price: 380 }
  ];

  constructor(private cartService: CartService) { }

  addToCart(product: CartItem): void {
    this.cartService.add(product);
  }
}

We never wrote new CartService(). We only told the constructor "I need something of type CartService." When Angular creates the component, it looks at the parameter's type, finds the CartService instance it holds and passes it in. This is called constructor injection.

The private in front of the parameter is a TypeScript shortcut. It declares the parameter and turns it into a property of the class at the same time, so you can use this.cartService anywhere in the class.

It is like a wall socket. You plug in and the power flows. You don't build the power plant, and you don't need to know where the cable comes from. Even if the power comes from a different plant tomorrow, your plug stays the same.

When we talked about lifecycle hooks, we said "use the constructor to receive dependencies." This is exactly what we meant.

Singleton: Two Components, One Cart

Let the badge component ask for the same service:

import { Component } from '@angular/core';
import { CartService } from '../cart.service';

@Component({
  selector: 'app-cart-badge',
  template: `<span>Sepet: {{ cartService.count }} ürün</span>`
})
export class CartBadgeComponent {
  constructor(public cartService: CartService) { }
}

Click "Sepete Ekle" in the list and the number on the badge goes up. The two components know nothing about each other; there is no input or output between them. But since both receive the same CartService instance, they are looking at the same cart.

A service provided with providedIn: 'root' is a singleton: a single instance lives for the lifetime of the app. It is created when the first component asks for it, and everyone after that gets the same object.

This time we wrote public instead of private, because the template reads cartService.count directly. A template can only access public members; had you left it private, Angular 12's strict template checking would give you a compile error.

Hierarchical Injectors: A Separate Copy for Everyone

We don't always want a single copy. Imagine three stopwatches on the screen; starting one should not make the others count too. This time we provide the service at the component level:

import { Injectable, OnDestroy } from '@angular/core';

@Injectable()
export class StopwatchService implements OnDestroy {
  seconds = 0;
  private timerId = 0;

  start(): void {
    this.stop();
    this.timerId = window.setInterval(() => this.seconds++, 1000);
  }

  stop(): void {
    window.clearInterval(this.timerId);
  }

  ngOnDestroy(): void {
    this.stop();
  }
}
@Component({
  selector: 'app-stopwatch',
  template: `
    <p>{{ stopwatch.seconds }} sn</p>
    <button (click)="stopwatch.start()">Başlat</button>
    <button (click)="stopwatch.stop()">Durdur</button>
  `,
  providers: [StopwatchService]
})
export class StopwatchComponent {
  constructor(public stopwatch: StopwatchService) { }
}

The service's @Injectable() is empty this time, with no providedIn. Instead, we added providers: [StopwatchService] to the component's decorator. Now every <app-stopwatch> gets its own copy of the service. When the component is destroyed, its copy goes with it, and the ngOnDestroy we saw earlier runs inside the service too and clears the interval.

What makes this possible is that Angular's injectors form a tree. When a component asks for a dependency, Angular first looks at that component's own injector. If it doesn't find it there, it goes up to the parent, then the parent's parent, and finally to the module and root level. It is like looking for office supplies: you check your own drawer first, then the department cabinet, and if it is not there either, the central storeroom.

You can also provide a service at the module level by listing it in the providers array of an @NgModule. Before providedIn: 'root' came along, this was the standard way, and you will see it a lot in older projects. How this behaves differently with lazy-loaded modules is something we will cover in detail in upcoming posts.

Provider Recipes: useClass, useValue, useFactory

Writing providers: [StopwatchService] is actually shorthand for { provide: StopwatchService, useClass: StopwatchService }. provide means "whoever asks for this," and useClass means "give them an instance of this." The two don't have to be the same:

@Injectable({ providedIn: 'root' })
export class LoggerService {
  log(message: string): void {
    console.log(message);
  }
}

@Injectable()
export class SilentLoggerService extends LoggerService {
  log(message: string): void { }
}
// app.module.ts
providers: [
  { provide: LoggerService, useClass: SilentLoggerService }
]

Every component in the app still asks for LoggerService in its constructor, but now it receives a SilentLoggerService. We changed the behavior without touching a single component. This is where the real power of dependency injection lies.

If you want to decide at runtime which class to hand out, you use useFactory:

import { environment } from '../environments/environment';

providers: [
  {
    provide: LoggerService,
    useFactory: () => environment.production ? new SilentLoggerService() : new LoggerService()
  }
]

A factory is a function, and the object it returns is given to everyone who asks for the dependency. Here we pick the silent logger in a production build and the normal logger during development. If the factory itself needs another service, you list it in a deps array.

useValue hands out a ready-made value: { provide: LoggerService, useValue: { log: () => {} } }. It is most useful in tests, for putting a fake object in place of the real service.

Providing a Config Value With InjectionToken

Sometimes what you want to provide is not a class but a plain settings object: the app name, the number of products per page and so on. TypeScript interfaces disappear after compilation, so Angular cannot use them as keys. The answer is InjectionToken:

import { InjectionToken } from '@angular/core';

export interface AppConfig {
  appName: string;
  pageSize: number;
}

export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');
// app.module.ts
providers: [
  { provide: APP_CONFIG, useValue: { appName: 'Mağaza', pageSize: 20 } }
]

// product-list.component.ts
constructor(@Inject(APP_CONFIG) private config: AppConfig) { }

A token is a unique key that also exists at runtime. The 'app.config' string we passed is just a description that shows up in error messages. When receiving the value, the parameter type is not enough because AppConfig is an interface; so we state which key we want explicitly with @Inject(APP_CONFIG). Inject also comes from @angular/core.

This may look familiar. The { provide: LOCALE_ID, useValue: 'tr' } line from the Turkish locale setup we saw earlier was exactly this: LOCALE_ID is an InjectionToken defined by Angular itself.

A Small Experiment

Create a FavoritesService. Give it an array holding favorite product names, a toggle(name: string) method and a getter that returns the number of favorites. Put a heart button on the product cards, and add a small component at the top of the page that says "Favoriler: 2". Both should receive the same service through the constructor.

Once it works, delete the providedIn: 'root' line and add the service to each product card's providers list. First read the NullInjectorError in the console. Then add the same line to the counter component's providers list as well, and explain to yourself, using the injector tree, why the counter now stays stuck at zero.

The number on the badge updates because Angular re-reads the template on every check round. But what if we want to show a notification the moment the cart changes, or save the cart to the server? The component needs a way to say "let me know when the cart changes." Next up, we step into RxJS, observables, and keeping state in a service with BehaviorSubject.