Skip to content
Angular ng services 4 min read

Introduction to Services

A service in Angular is a plain TypeScript class that encapsulates logic you want to reuse and share — business rules, data access, caching, logging, or shared state — kept deliberately separate from any single component. Components should be thin: they render the view and react to user input, while services do the heavy lifting behind the scenes. This separation keeps your UI code small and testable, and it lets many components depend on the same piece of logic without duplicating it.

Why services exist

Components are tied to a template and a slice of the screen. If you put data-fetching, formatting, or state management directly inside a component, that code becomes hard to reuse and hard to test, and it tends to leak across components through clumsy @Input/@Output chains. A service solves this by being a single, long-lived object that any component (or other service) can ask for.

The core responsibilities of services typically fall into a few buckets:

ResponsibilityExample
Data accessCalling an HTTP API and returning typed results
Business logicValidating an order, calculating a price
Shared stateHolding the current user or a shopping cart
Cross-cutting concernsLogging, analytics, feature flags

Defining a service

A service is just a class. The @Injectable decorator marks it as a candidate for Angular’s dependency injection (DI) system, and providedIn: 'root' registers it as an application-wide singleton — Angular creates exactly one instance and shares it everywhere.

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

export interface Product {
  id: number;
  name: string;
  price: number;
}

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

  add(product: Product): void {
    this.items.push(product);
  }

  remove(id: number): void {
    this.items = this.items.filter((p) => p.id !== id);
  }

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

  list(): readonly Product[] {
    return this.items;
  }
}

The class knows nothing about templates, change detection, or which component uses it. That ignorance is the point — it can be used anywhere and tested in isolation.

providedIn: 'root' makes the service tree-shakable: if nothing injects CartService, the bundler can drop it entirely. Prefer it over registering services in a module or component providers array unless you specifically need a narrower scope.

Consuming a service in a component

Modern Angular components retrieve services with the inject() function. You call it at field-initializer position and store the result; Angular resolves the dependency from the injector hierarchy and hands you the shared instance.

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

@Component({
  selector: 'app-cart',
  standalone: true,
  template: `
    @if (cart.list().length > 0) {
      <ul>
        @for (item of cart.list(); track item.id) {
          <li>{{ item.name }} — {{ item.price | currency }}</li>
        }
      </ul>
      <p>Total: {{ cart.total() | currency }}</p>
    } @else {
      <p>Your cart is empty.</p>
    }

    <button (click)="addSample()">Add sample item</button>
  `,
})
export class CartComponent {
  protected readonly cart = inject(CartService);

  private nextId = 1;

  addSample(): void {
    const product: Product = { id: this.nextId++, name: 'Widget', price: 9.99 };
    this.cart.add(product);
  }
}

Because both CartComponent and any other component inject the same CartService instance, adding an item in one place is visible everywhere that reads the cart.

Services depending on other services

Services compose. A higher-level service can inject() lower-level ones — for example a checkout service that uses the cart and a logger.

import { Injectable, inject } from '@angular/core';
import { CartService } from './cart.service';

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

@Injectable({ providedIn: 'root' })
export class CheckoutService {
  private readonly cart = inject(CartService);
  private readonly logger = inject(LoggerService);

  checkout(): number {
    const total = this.cart.total();
    this.logger.log(`Charging customer ${total.toFixed(2)}`);
    return total;
  }
}

Output:

[checkout] Charging customer 19.98

Angular wires these dependencies for you. You never call new CartService() yourself — the framework constructs the graph and reuses the singleton instances.

Best practices

  • Keep components focused on the view and delegate logic, data access, and state to services.
  • Annotate every service with @Injectable and prefer providedIn: 'root' for app-wide singletons that are tree-shakable.
  • Use inject() over constructor parameter injection in modern code — it reads cleanly and works in field initializers and functions.
  • Expose narrow, intention-revealing methods (add, total) rather than leaking mutable internal state.
  • Never instantiate services with new — let DI provide them so dependencies and lifetimes stay consistent.
  • Keep services free of UI concerns so they remain easy to unit test in isolation.
Last updated June 14, 2026
Was this helpful?