What is an Angular service, how does DI provide it (providedIn/providers), and why do teams use services?

HighIntermediateAngular
Quick Answer

This question is about service responsibilities and boundary design in Angular apps: what belongs in services, why teams split logic out of components, and how provider scope changes behavior in production code. Reference DI only as much as needed to explain lifetime/sharing. Keep DI internals and token/provider mechanics in the dedicated dependency-injection question.

Answer

Core idea

An Angular service is typically a plain TypeScript class that you register with Angular’s dependency injection system. DI creates the instance and injects it where needed. The value is not the class itself — it’s the lifetime + sharing + testability you get from DI and a clean separation from UI code.

Scope guard

This page answers "When and why should logic live in a service?".

If you need DI internals (tokens, injector tree, provider shapes), go to <a href="/angular/trivia/angular-dependency-injection">What is dependency injection in Angular?</a>.
If you need ownership split examples, go to <a href="/angular/trivia/angular-component-vs-service-responsibilities">Angular component vs service responsibilities</a>.

What it is

What it is NOT

A DI-provided class used by components/directives/other services

A special Angular-only construct (it’s still “just a class”)

A place for reusable logic + side effects (HTTP, caching, orchestration)

A place for DOM manipulation (that belongs in components/directives)

A unit that’s easy to mock in tests

A global mutable “god object” used everywhere without boundaries

Interview framing: define service via DI + responsibilities

Why teams use services

To keep components thin and predictable: components render + translate UI events into intent; services own reusable logic and side effects. This improves reuse, testability, and maintainability in large apps.

Typical responsibility

Why it belongs in a service

HTTP/data access (repositories)

Centralizes endpoints, retries, error mapping, DTO→domain mapping

Caching + request de-duplication

Prevents duplicate calls across multiple consumers

Cross-component state (facade/store wrapper)

Makes sharing state explicit; reduces tight coupling between components

Business rules / normalization

One source of truth; easier to unit test than template/component code

App orchestration (multi-step flows)

Keeps complex flows out of UI layer; promotes clean boundaries

What interviewers expect: services for side effects + shared logic

How a service gets an instance: providers and scope

Service lifetime is determined by where it’s provided. Angular DI is hierarchical: child injectors can override parent providers.

Provide it where

Instance lifetime

When to use

@Injectable({ providedIn: 'root' })

Singleton for the app (tree-shakeable provider)

Most services (API clients, facades, shared utilities)

Feature/module providers (module-based apps)

Usually singleton per module injector (depending on module loading)

Legacy module setups; some library patterns

Component providers: [...]

New instance per component instance (and its subtree)

Per-screen/wizard state that must reset when component is destroyed

Scope is an interview hot-spot: root vs component providers
TYPESCRIPT
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, catchError, map, of, shareReplay } from 'rxjs';

type UserDto = { id: string; name: string };
export type User = { id: string; displayName: string };

@Injectable({ providedIn: 'root' })
export class UsersService {
  private readonly apiUrl = '/api/users';
  private users$?: Observable<User[]>;

  constructor(private http: HttpClient) {}

  /**
   * Cache + dedupe: multiple subscribers share one request.
   * If you need refresh, expose an explicit invalidation method.
   */
  getUsers(): Observable<User[]> {
    this.users$ ??= this.http.get<UserDto[]>(this.apiUrl).pipe(
      map(dtos => dtos.map(d => ({ id: d.id, displayName: d.name })) as User),
      // In real apps, prefer a typed error strategy (domain errors) over swallowing.
      catchError(() => of([])),
      shareReplay({ bufferSize: 1, refCount: true })
    );

    return this.users$;
  }

  invalidateUsersCache(): void {
    this.users$ = undefined;
  }
}
                  
TYPESCRIPT
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { AsyncPipe, NgFor } from '@angular/common';
import { UsersService } from './users.service';

@Component({
  selector: 'app-users',
  standalone: true,
  imports: [NgFor, AsyncPipe],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <button type="button" (click)="refresh()">Refresh</button>

    <ul>
      <li *ngFor="let u of (users$ | async)">
        {{ u.displayName }}
      </li>
    </ul>
  `
})
export class UsersComponent {
  private readonly usersService = inject(UsersService);
  readonly users$ = this.usersService.getUsers();

  refresh(): void {
    this.usersService.invalidateUsersCache();
    // reassign stream (simple pattern); in larger apps use a facade/store.
    (this as any).users$ = this.usersService.getUsers();
  }
}
                  

Component-scoped service example (per-instance state)

If you need state that resets when a component is destroyed (wizard/session), provide the service at the component level.

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

@Injectable()
export class WizardStateService {
  step = 1;
  data: Record<string, unknown> = {};

  reset(): void {
    this.step = 1;
    this.data = {};
  }
}

// component.ts
// @Component({
//   ...
//   providers: [WizardStateService]
// })
// export class CheckoutWizardComponent {
//   constructor(public wizard: WizardStateService) {}
// }
                  

Common pitfall

What breaks

Fix

Putting HttpClient calls in components

Duplicated logic, harder tests, messy lifecycles

Move data access + mapping + error policy into a service/facade

Using a root singleton as a dumping ground for random mutable state

Hidden coupling and hard-to-debug state leaks across screens

Keep state ownership explicit (facade/store); use component-scoped services for per-screen state

Services touching DOM directly

SSR/testing issues; breaks separation of concerns

DOM work belongs in components/directives (Renderer2 if needed)

Subjects never completed / manual subscriptions everywhere

Leaks and unpredictable behavior

Prefer async pipe, takeUntilDestroyed(), or centralized streams in facades

Pitfalls seniors mention quickly
Summary

Angular services are where reusable logic and side effects should live so components stay UI-focused. Strong answers show clear boundaries, sensible provider scope (root vs component), and testability benefits. Keep deep DI mechanics and token-level provider patterns in the dedicated DI discussion.

Similar questions
Guides
Preparing for interviews?

Use the relevant interview-question hub first, then move into a concrete study plan before targeted company sets.