An Angular service packages reusable application behavior or shared state behind an injectable boundary. Design its API around one capability, choose provider scope deliberately, and keep HTTP caching, errors, and tests observable to callers.
A service is an injectable class that owns one application capability, such as product retrieval, session state, pricing rules, or telemetry. Components call that public API and remain responsible for presentation and user interaction.
A service is not automatically global. Its provider location determines who shares the instance and when state is discarded. Start with providedIn: "root" for application-wide stateless behavior, then narrow the provider when state belongs to one route or component subtree.
Mark a service with Injectable when Angular must create it and resolve its dependencies. A providedIn value registers the provider without a separate providers array and allows unused services to be removed from production bundles.
Treat the public methods, readonly signals, and Observables as the service contract. Keep writable signals, Subjects, HTTP response shapes, and implementation-specific collaborators private so callers cannot bypass invariants.
Services may compose lower-level collaborators such as HttpClient, configuration tokens, loggers, and domain rules. Keep dependency direction clear: feature services may depend on infrastructure services, while infrastructure code should not import feature components.
Capture dependencies once with inject or constructor injection when the service is created. Circular dependencies usually mean two responsibilities need a smaller shared abstraction or a better ownership boundary.
Good service candidates are behavior or state used by several components, data-access policy, and business rules that should be testable without rendering. A one-line formatting helper or state owned by only one component can remain local.
Choose signals for synchronous state read by templates and Observables for asynchronous event or request pipelines. Keep writable state private, expose readonly views, and mutate through methods that enforce the service rules.
Operational behavior belongs in the service only when the service owns it. Data services can map responses and define cache policy; state services can expose commands and derived state; orchestration services can coordinate several capabilities without leaking those details to components.
Avoid subscriptions whose ownership is unclear. Return request pipelines to the caller, or bind a long-lived subscription to the service lifetime with DestroyRef and takeUntilDestroyed.
A giant service, a public writable Subject, duplicated API URLs, or multiple unexpected instances are design symptoms. Trace who owns the state, where the provider is registered, who subscribes, and whether the public API allows callers to create invalid state.
providedIn: root creates one instance in the application environment injector. A route provider is shared inside that route environment and is released with it. A component provider creates one instance for each component subtree.
A request method should state whether every call is fresh or whether consumers share cached data. Recover only errors the service can meaningfully handle; otherwise preserve the failure so the caller can show retry, offline, or permission UI.
A service test should verify behavior without rendering a component. Mock HTTP, call the method, and assert the request URL, request body, transformation, or emitted state.
Construct pure services directly. Use TestBed when Angular injection or provider overrides matter, and use the HTTP testing provider for HttpClient. For signal state, invoke public commands and read public signals instead of asserting private implementation fields.
import { HttpClient } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs';
export interface Product {
id: number;
name: string;
price: number;
}
@Injectable({ providedIn: 'root' })
export class ProductService {
private readonly http = inject(HttpClient);
listProducts(): Observable<Product[]> {
return this.http.get<Product[]>('/api/products');
}
}
products$ = this.productService.listProducts();
constructor(private productService: ProductService) {}
// template
// <article *ngFor="let product of products$ | async">
// {{ product.name }} - {{ product.price }}
// </article>
getProducts(): Observable<Product[]> {
return this.http.get<Product[]>('/api/products').pipe(
catchError(error => {
return throwError(() =>
new Error('Products could not be loaded', { cause: error })
);
})
);
}
private readonly selectedIdState = signal<number | null>(null);
readonly selectedId = this.selectedIdState.asReadonly();
selectProduct(id: number): void {
this.selectedIdState.set(id);
}
clearSelection(): void {
this.selectedIdState.set(null);
}
HttpClient returns a cold Observable, so each subscription starts a new request. The component may subscribe manually and also use the async pipe, or several consumers may call the same service method independently.
Not always. It converts “the request failed” into “there are no records.” If those states lead to different UI or recovery actions, returning [] hides useful information.
Route scope fits state that belongs to one feature visit: a checkout draft, wizard progress, selected admin filters, or an editor session that should reset when the user leaves the area. A root service survives for the application lifetime and is better for authentication, global settings, or deliberately shared caches.
Explore 500+ free tutorials across 20+ languages and frameworks.