Services
A service is a class that does work for components: holds shared data, talks to an API, calculates prices. Components ask for services; services never touch the UI.
On this page · 6 min · Intermediate
On this page
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 6 min
- Difficulty
- Intermediate
- Updated
- Oct 28, 2026
Related tutorials
Learning objectives#
- Create a service with @Injectable
- Share state between components through a service
- Expose read-only signals and methods
- Decide what belongs in a service
Explanation#
Generate one with ng g service cart. @Injectable({ providedIn: "root" }) makes a single shared instance available everywhere.
Components get it with inject(CartService) (or as a constructor parameter).
Keep state private and expose it read-only: private items = signal<Item[]>([]); readonly items$ = this.items.asReadonly(). Changes go through methods (add, remove, clear), so rules live in one place.
Derived values (count, total) are computed() signals in the service, so every component sees the same numbers.
Good service candidates: API access, cart and session state, formatting rules, logging, feature flags. Bad candidates: anything about layout or DOM.
Syntax#
@Injectable({ providedIn: 'root' })
export class CartService {
private readonly items = signal<CartItem[]>([]);
readonly lines = this.items.asReadonly();
readonly total = computed(() => this.items().reduce((s, i) => s + i.price * i.qty, 0));
add(item: CartItem) { … }
}
Code example: A cart service shared by three components#
import { Component, Injectable, computed, inject, input, signal } from '@angular/core';
import { CurrencyPipe } from '@angular/common';
export type Product = { id: number; name: string; price: number };
type Line = Product & { qty: number };
@Injectable({ providedIn: 'root' })
export class CartService {
private readonly items = signal<Line[]>([]);
readonly lines = this.items.asReadonly();
readonly count = computed(() => this.items().reduce((n, l) => n + l.qty, 0));
readonly total = computed(() => this.items().reduce((s, l) => s + l.qty * l.price, 0));
add(product: Product) {
this.items.update(lines => {
const found = lines.find(l => l.id === product.id);
return found
? lines.map(l => (l.id === product.id ? { ...l, qty: l.qty + 1 } : l))
: [...lines, { ...product, qty: 1 }];
});
}
remove(id: number) {
this.items.update(lines => lines.filter(l => l.id !== id));
}
}
@Component({
selector: 'app-add-button',
template: `<button (click)="cart.add(product())">Add {{ product().name }}</button>`,
})
export class AddButton {
product = input.required<Product>();
protected cart = inject(CartService);
}
@Component({
selector: 'app-cart-summary',
imports: [CurrencyPipe],
template: `
<h3>Cart ({{ cart.count() }})</h3>
@for (line of cart.lines(); track line.id) {
<p>{{ line.name }} × {{ line.qty }} <button (click)="cart.remove(line.id)">✕</button></p>
}
<strong>{{ cart.total() | currency: 'INR' }}</strong>
`,
})
export class CartSummary {
protected cart = inject(CartService);
}
@Component({
selector: 'app-shop',
imports: [AddButton, CartSummary],
template: `
<app-add-button [product]="pen" />
<app-add-button [product]="bag" />
<app-cart-summary />
`,
})
export class Shop {
pen: Product = { id: 1, name: 'Pen', price: 20 };
bag: Product = { id: 2, name: 'Bag', price: 650 };
}
Clicking “Add Pen” twice and “Add Bag” once shows “Cart (3)”, “Pen × 2”, “Bag × 1” and “₹690.00”. Removing a line updates the count and total. The two buttons and the summary never talk to each other directly — they share CartService.
This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- CartService keeps items private and exposes a read-only view plus computed count and total.
- add() contains the rule “increase quantity if already in the cart” — in one place.
- AddButton and CartSummary inject the same root instance.
- The summary reads signals from the service, so it updates automatically.
Real-world use case#
In an e-learning app, ProgressService tracks completed lessons; the sidebar, the lesson page and the dashboard all inject it and always agree on the learner’s progress.
Common mistakes#
If components can call service.items.set(...), rules are bypassed. Expose readonly signals and methods.
providers: [CartService] in a component creates a separate copy for that subtree. Use providedIn: "root" for shared state.
Services should not query or change the DOM. Keep them UI-free and testable.
Best practices#
- One responsibility per service
- Private writable signals, public readonly views
- Derive with computed()
- providedIn: "root" for app-wide singletons
Remember this#
Services = logic + shared state, no UI.
providedIn: "root" → one instance for the whole app.
Expose readonly signals; change through methods.
Practice exercise#
Your task: Write a NotificationService with a messages signal, push(text) and dismiss(index), and a component that lists messages with a dismiss button.
Need a hint?
Show solution
import { Component, Injectable, inject, signal } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class NotificationService {
private readonly list = signal<string[]>([]);
readonly messages = this.list.asReadonly();
push(text: string) { this.list.update(l => [...l, text]); }
dismiss(index: number) { this.list.update(l => l.filter((_, i) => i !== index)); }
}
@Component({
selector: 'app-notifications',
template: `
<button (click)="notes.push('Saved!')">Notify</button>
@for (m of notes.messages(); track $index) {
<p>{{ m }} <button (click)="notes.dismiss($index)">✕</button></p>
}
`,
})
export class Notifications {
protected notes = inject(NotificationService);
}
Mini quiz#
What does @Injectable({ providedIn: "root" }) give you?
Want more? Practise all Angular MCQs · Angular interview questions
My notes#
Saved in this browser.