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.
What is an Angular service, how does DI provide it (providedIn/providers), and why do teams use services?
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 |
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 |
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 |
|---|---|---|
| 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 | New instance per component instance (and its subtree) | Per-screen/wizard state that must reset when component is destroyed |
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;
}
}
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.
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 |
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.
Use the relevant interview-question hub first, then move into a concrete study plan before targeted company sets.