Angular components should focus on UI/presentation and user interactions, while services should hold reusable logic, business rules, and side effects (HTTP, caching, state). Clean separation keeps components thin, improves testability, and avoids duplicated logic across the app.
What responsibilities belong inside an Angular component vs a service?
Use guided tracks for structured prep, then practice company-specific question sets when you want targeted interview coverage.
Overview
Use components for rendering + wiring UI events to actions. Use services for reusable logic and side effects (HTTP, caching, state, orchestration). A good component reads like a “view model composer”, not a mini back-end.
Area | Component (belongs here) | Service (belongs here) |
|---|---|---|
UI rendering | Template bindings, UI composition, structural directives, view-only formatting | No DOM/template concerns |
User interaction | Click handlers, form submit handlers, mapping UI events → intent/actions | Input validation rules that are reusable across screens |
State | Ephemeral UI state (open/closed, selected tab, local filters, loading flags for UI) | Shared/stateful app logic (cache, session data, cross-component state), facades |
Business rules | Minimal (only what’s needed to connect UI to rules) | Core rules (pricing logic, permission checks, normalization, mapping DTO→domain) |
Data access | Never call low-level APIs directly (avoid HttpClient usage in components) | HTTP calls, retries, error mapping, caching, request dedupe |
Reusability | Rare (components can be reusable, but should avoid app-specific business logic) | High (same service can be used by multiple components) |
Testing | Shallow: assert template bindings + event wiring + observable usage | Deep: unit test rules/side effects without rendering |
Rule of thumb
If the code depends on how something is shown (DOM, template state, view events) → component. If it depends on what something means (rules, data fetching, caching, orchestration) → service.
// user.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, shareReplay } from 'rxjs';
export interface UserDto { id: string; name: string; }
export interface User { id: string; displayName: string; }
@Injectable({ providedIn: 'root' })
export class UserService {
private users$?: Observable<User[]>;
constructor(private http: HttpClient) {}
getUsers(): Observable<User[]> {
// Cache + dedupe requests for this session
this.users$ ??= this.http.get<UserDto[]>('/api/users').pipe(
// Business mapping belongs here (or a dedicated mapper)
// so multiple components get consistent data.
// Keep mapping pure.
// DTO -> domain model:
// (If mapping grows, extract to a pure function.)
shareReplay({ bufferSize: 1, refCount: true })
) as unknown as Observable<User[]>;
return this.users$;
}
}
// user-list.component.ts
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { Observable, map, startWith } from 'rxjs';
import { UserService, User } from './user.service';
@Component({
selector: 'app-user-list',
templateUrl: './user-list.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserListComponent {
// UI state belongs here
readonly search = '';
// Data stream comes from a service
readonly users$: Observable<User[]> = this.userService.getUsers();
// UI-only derived state can be computed in the component
// (keep it cheap; heavy transforms should be in a service/facade)
readonly viewModel$ = this.users$.pipe(
map(users => ({
users,
total: users.length
})),
startWith({ users: [], total: 0 })
);
constructor(private userService: UserService) {}
onRefreshClick(): void {
// UI intent: trigger refresh.
// If refresh needs side-effects (invalidate cache), expose that from the service.
// e.g. this.userService.invalidateUsersCache();
}
}
Common mistake | Why it hurts | Fix |
|---|---|---|
HttpClient calls inside components | Duplicates logic, makes components hard to test and reuse | Move data access to a service/facade |
Business rules in templates (complex expressions) | Hard to read, hard to test, runs every CD cycle | Compute in TS (component for UI-only, service for reusable rules) |
Services manipulating the DOM | Breaks separation, complicates SSR/testing | Keep DOM work in components/directives; services stay UI-agnostic |
“God component” that owns everything | Becomes unmaintainable; PRs become risky | Extract services + presentational components; keep the container thin |
Extra nuance: service scope
Most services are singletons (providedIn: 'root'). If you need per-component instance state (e.g., a wizard session), provide the service at the component level via providers: [WizardStateService] so each component instance gets its own copy.
Keep components focused on UI + event wiring + lightweight view-model shaping. Put reusable logic, side effects, data access, and business rules into services (or facades). This separation improves testability, reuse, and long-term maintainability.