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.
Use this Angular interview question to rehearse a quick answer, common mistake, follow-up, and production pitfall.
What is an Angular service, how does DI provide it (providedIn/providers), and why do teams use services?Frontend interview answer
This Angular interview question tests whether you can explain Angular services in production: DI scope, boundaries, and when to use one, connect it to production trade-offs, and handle common follow-up questions.
- Angular services in production: DI scope, boundaries, and when to use one explanation without falling back to memorized docs wording
- Services and Dependency Injection reasoning, edge cases, and production failure modes
- How you would answer the most likely Angular interview follow-up
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) {}
// }
When root scope is the wrong choice
A root singleton is correct for shared API clients and caches, but it is wrong for draft state that should reset per component instance. Two checkout wizards open at the same time should not silently share one mutable service instance.
@Injectable({ providedIn: 'root' })
export class SearchCacheService {
results = new Map<string, User[]>();
}
@Injectable()
export class WizardStateService {
step = 1;
data: Record<string, unknown> = {};
}
@Component({
selector: 'app-checkout-wizard',
providers: [WizardStateService],
template: `...`
})
export class CheckoutWizardComponent {
constructor(public wizard: WizardStateService, public cache: SearchCacheService) {}
}
// Each CheckoutWizardComponent gets its own WizardStateService,
// but all screens still share the root SearchCacheService.
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 this as one explanation rep, then continue with the Angular interview questions cluster or a guided prep path.