Angular Services and DI

Angular services encapsulate reusable logic and shared state, and the framework's built-in dependency injection (DI) system hands an instance to any component, directive, or service that asks for one.

What Is a Service?

A service is just a plain TypeScript class with a focused job: fetching data, talking to a browser API, formatting values, or holding state that multiple components need to share. Services keep components thin — a component should describe what the user sees and does, not how data is fetched or transformed.

Creating an Injectable Service

Decorating a class with @Injectable tells Angular's compiler how to construct it and what to inject into its own constructor. The providedIn: 'root' option registers the service with the application's root injector, so it becomes an app-wide singleton that is also tree-shakeable if nothing ever imports it.

logger.service.ts

import { Component, Injectable } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';

@Injectable({
  providedIn: 'root'
})
export class LoggerService {
  private logs: string[] = [];

  log(message: string): void {
    const entry = `[${new Date().toISOString()}] ${message}`;
    this.logs.push(entry);
    console.log(entry);
  }

  getHistory(): string[] {
    return [...this.logs];
  }
}

@Component({
  selector: 'app-root',
  standalone: true,
  template: `
    <button (click)="logMessage()">Log a message</button>
    <p>History entries: {{ logger.getHistory().length }}</p>
  `
})
export class AppComponent {
  constructor(private logger: LoggerService) {
    this.logger.log('AppComponent created');
  }

  logMessage(): void {
    this.logger.log('Button clicked');
  }
}

bootstrapApplication(AppComponent).catch((err) => console.error(err));

Constructor Injection

The classic way to receive a service is to declare it as a constructor parameter with an access modifier such as private or protected. Angular's injector inspects the constructor's type metadata, finds (or creates) a matching instance, and passes it in automatically — you never call 'new LoggerService()' yourself.

greeting.component.ts

import { Component, Injectable } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';

@Injectable({
  providedIn: 'root'
})
export class LoggerService {
  private logs: string[] = [];

  log(message: string): void {
    const entry = `[${new Date().toISOString()}] ${message}`;
    this.logs.push(entry);
    console.log(entry);
  }

  getHistory(): string[] {
    return [...this.logs];
  }
}

@Component({
  selector: 'app-greeting',
  standalone: true,
  template: `
    <button (click)="greet()">Say Hello</button>
  `
})
export class GreetingComponent {
  constructor(private logger: LoggerService) {}

  greet(): void {
    this.logger.log('Greeting button clicked');
  }
}

bootstrapApplication(GreetingComponent).catch((err) => console.error(err));
  • Testability — a component can be unit-tested with a fake/mock service instead of the real one.
  • Singletons by default — providedIn: 'root' gives every consumer the same instance and the same state.
  • Loose coupling — components depend on an interface-like contract, not on how data is actually fetched.
  • Swappable implementations — you can provide a different class for the same token in tests or per-environment builds.

Provider Scopes

Where you register a service controls its lifetime and how many instances exist. Root-level registration is the most common choice, but component-level providers create a fresh instance per component subtree, which is useful for state that should reset when a feature is closed.

ScopeWhere declaredLifetime
providedIn: 'root'Inside @Injectable()One instance for the whole app
Component providersproviders: [MyService] on @ComponentOne instance per component instance (and its children)
providedIn: 'platform'Inside @Injectable()Shared across multiple Angular apps on one page
Note: The functional inject() call does the same job as constructor injection but can be used anywhere in an injection context — including field initializers and functional guards — not just inside a constructor.

cart.service.ts

import { Injectable, inject, Component } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';

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

  addItem(name: string): void {
    this.items.push(name);
  }

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

@Component({
  selector: 'app-cart-badge',
  standalone: true,
  template: `<span>Items: {{ cart.itemCount }}</span>`
})
export class CartBadgeComponent {
  protected cart = inject(CartService);
}

bootstrapApplication(CartBadgeComponent).catch((err) => console.error(err));
Note: If two services inject each other in their constructors, Angular throws a circular dependency error at startup. Break the cycle by extracting the shared logic into a third service, or by injecting one side lazily with an Injector.

Exercise: Angular Services and DI

Which decorator marks a class as available for dependency injection?