Angular Services
Read one Angular interview question at a time, then flip for the complete explanation, example, and code.
Read one Angular interview question at a time, then flip for the complete explanation, example, and code.
Angular Interview Questions · Angular Services Companion Article
Angular services are classes used to place logic, data access, reusable operations, or shared state outside components. They help keep components focused on the user interface while reusable application logic lives in dedicated classes.
A service can be used by one component, several components, another service, a route, or other parts of an Angular application through Angular's Dependency Injection system.
A typical application may have services for:
Instead of putting everything inside a component, services provide a clear place for logic that should be reusable or independent of the UI.
Consider a component that performs all of these responsibilities:
export class ProductList {
products = [];
loading = false;
loadProducts() {
// Call API
// Transform response
// Handle errors
// Cache products
// Update UI
}
calculateDiscount() {
// Business logic
}
}
The component is now responsible for UI behavior, API communication, business rules, data transformation, and caching.
A cleaner design separates these responsibilities.
Component
|
| uses
v
ProductService
|
| uses
v
Backend API
The component handles presentation while the service handles application logic.
For example:
export class ProductList {
private productService = inject(ProductService);
loadProducts() {
return this.productService.getProducts();
}
}
This separation generally makes applications easier to understand, test, maintain, and extend.
A service is normally a TypeScript class that Angular can create through Dependency Injection.
A common service definition uses @Injectable().
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class ProductService {
getProducts() {
return ['Laptop', 'Mobile', 'Keyboard'];
}
}
Here:
ProductService is the service class.@Injectable() makes dependency-injection configuration available to Angular.providedIn: 'root' makes the service available through the application's root environment injector.Angular's current DI documentation identifies root provisioning as the common approach for application-wide services.
A service can also be generated using Angular CLI.
ng generate service product
The short form is:
ng g s product
A generated service typically gives you a dedicated TypeScript file where service logic can be implemented.
For example:
product.ts
product.spec.ts
Depending on the project and CLI configuration, generated file naming can differ from older Angular projects.
The important part is not how the file was generated. The important part is keeping the service focused on a clear responsibility.
Creating a class is not enough for Dependency Injection. Angular needs to know how a dependency should be created.
This relationship is configured through a provider.
One of the most common approaches is:
@Injectable({
providedIn: 'root'
})
export class UserService {
}
Angular can then create and supply the service wherever it is requested.
There are also situations where a service is registered manually.
For example:
@Component({
selector: 'app-editor',
providers: [EditorService],
template: `...`
})
export class Editor {
}
In this case, the provider belongs to that component's injector rather than automatically being an application-wide root service.
Angular supports automatic provisioning as well as manual provider configuration at component, directive, route, and application levels.
A service used throughout the application is commonly registered with:
@Injectable({
providedIn: 'root'
})
export class AuthService {
}
Typical root-level services include:
Because the root injector manages the service, consumers normally share the same service instance.
For example:
Root Injector
|
+---- AuthService
|
+---- ProductService
|
+---- CartService
Different components requesting CartService from the same root provider receive access to the shared service instance.
A singleton service is a service for which consumers share one instance within a particular injector scope.
For example:
@Injectable({
providedIn: 'root'
})
export class CartService {
items: string[] = [];
addItem(item: string) {
this.items.push(item);
}
}
Suppose both ProductList and CartPage inject this service.
ProductList
\
\
--> CartService instance
/
/
CartPage
If ProductList adds an item, CartPage can observe data maintained by that same root service.
This behavior makes root-provided services useful for shared application state.
However, a service class is not automatically a global singleton in every possible situation. Provider placement matters.
For example:
@Component({
selector: 'app-a',
providers: [CounterService],
template: `...`
})
export class ComponentA {
}
@Component({
selector: 'app-b',
providers: [CounterService],
template: `...`
})
export class ComponentB {
}
Each component can receive a different CounterService instance because each component has its own provider scope.
Understanding provider scope is therefore more accurate than simply memorizing:
> "Angular services are always singleton."
They are not always singleton across every injector.
Once Angular knows how to provide a service, another class can request it.
A modern and concise approach is the inject() function.
import { Component, inject } from '@angular/core';
import { ProductService } from './product.service';
@Component({
selector: 'app-products',
template: `...`
})
export class Products {
private productService = inject(ProductService);
}
Angular resolves ProductService from the active injection context.
Current Angular documentation shows inject() as a standard way to consume a registered service.
After injection, its public methods can be called normally.
export class Products {
private productService = inject(ProductService);
products = this.productService.getProducts();
}
The component does not create the dependency manually.
Avoid this:
export class Products {
productService = new ProductService();
}
Creating the object manually bypasses Angular Dependency Injection.
This can cause problems when:
Let the injector create dependencies whenever the class is designed to participate in Angular DI.
Services are not limited to components.
A service can depend on another service.
For example:
@Injectable({
providedIn: 'root'
})
export class LoggerService {
log(message: string) {
console.log(message);
}
}
Another service can use it:
@Injectable({
providedIn: 'root'
})
export class OrderService {
private logger = inject(LoggerService);
createOrder() {
this.logger.log('Creating order');
// Order creation logic
}
}
This allows larger responsibilities to be divided into smaller services.
For example:
OrderComponent
|
v
OrderService
|
+---- PaymentService
|
+---- InventoryService
|
+---- LoggerService
Each service should still have a meaningful responsibility rather than becoming another location where unrelated logic is collected.
Business logic describes the rules that determine how an application behaves.
Examples include:
This logic often should not live directly inside UI components.
Example:
@Injectable({
providedIn: 'root'
})
export class PricingService {
calculateDiscount(price: number, discountPercent: number): number {
return price - (price * discountPercent / 100);
}
}
The component becomes simpler.
export class ProductDetails {
private pricingService = inject(PricingService);
finalPrice = this.pricingService.calculateDiscount(1000, 10);
}
Now the pricing rule can also be reused by:
ProductDetails
CheckoutPage
OrderPreview
AdminPanel
without copying the calculation into every component.
Suppose this calculation appears in three components:
const total = price - (price * discount / 100);
Later the business changes the rule:
Premium customers receive an additional 5% discount.
If the calculation was copied into several components, each location must be changed.
A service creates one logical location:
calculateFinalPrice(
price: number,
discount: number,
premiumCustomer: boolean
) {
const standardDiscount = price * discount / 100;
const premiumDiscount = premiumCustomer ? price * 0.05 : 0;
return price - standardDiscount - premiumDiscount;
}
This reduces duplicated business rules.
A data service is responsible for obtaining, storing, transforming, or managing data.
A common use is communication with a backend API.
Example structure:
ProductComponent
|
v
ProductService
|
v
HttpClient
|
v
REST API
Example:
import { HttpClient } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class ProductService {
private http = inject(HttpClient);
getProducts() {
return this.http.get<Product[]>('/api/products');
}
}
The component does not need to know HTTP implementation details.
export class ProductList {
private productService = inject(ProductService);
loadProducts() {
return this.productService.getProducts();
}
}
Depending on the architecture, a data service may handle:
For example:
@Injectable({
providedIn: 'root'
})
export class EmployeeService {
private http = inject(HttpClient);
getEmployees() {
return this.http.get<Employee[]>('/api/employees');
}
getEmployee(id: number) {
return this.http.get<Employee>(`/api/employees/${id}`);
}
createEmployee(employee: Employee) {
return this.http.post<Employee>('/api/employees', employee);
}
updateEmployee(id: number, employee: Employee) {
return this.http.put<Employee>(
`/api/employees/${id}`,
employee
);
}
deleteEmployee(id: number) {
return this.http.delete(`/api/employees/${id}`);
}
}
A component consuming this service can remain concerned mainly with the UI workflow.
A service can hold state in memory.
For example:
@Injectable({
providedIn: 'root'
})
export class CounterService {
count = 0;
increment() {
this.count++;
}
}
Because a root-provided service is shared, components using the same instance can access the same state.
However, plain properties do not automatically provide all the benefits of reactive state management.
This is one reason signals are useful for modern Angular service state.
Signals can be used inside services to maintain reactive state.
A useful pattern is:
Private writable signal
|
v
Public read-only signal
|
v
Components
Example:
import { Injectable, signal } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class CounterService {
private readonly _count = signal(0);
readonly count = this._count.asReadonly();
increment() {
this._count.update(value => value + 1);
}
reset() {
this._count.set(0);
}
}
The component can read the state:
export class CounterComponent {
counterService = inject(CounterService);
}
Template:
<p>Count: {{ counterService.count() }}</p>
<button (click)="counterService.increment()">
Increment
</button>
Angular signals track reads and notify interested consumers when their values change. Angular also supports exposing a writable signal as a read-only signal with asReadonly(), which is particularly useful for service-owned state.
Consider this design:
count = signal(0);
Any consumer receiving the service can potentially do:
counterService.count.set(5000);
The component can now modify internal state directly.
A safer design is:
private readonly _count = signal(0);
readonly count = this._count.asReadonly();
Consumers can read:
counterService.count();
But changes happen through service methods:
counterService.increment();
This establishes a clear state-management rule:
Component
|
| calls action
v
Service Method
|
| modifies
v
Private State
|
| exposed as
v
Read-Only State
This pattern prevents arbitrary state changes from being scattered throughout the application.
Signals can also hold objects.
interface User {
id: number;
name: string;
}
@Injectable({
providedIn: 'root'
})
export class UserStateService {
private readonly _user = signal<User | null>(null);
readonly user = this._user.asReadonly();
login(user: User) {
this._user.set(user);
}
logout() {
this._user.set(null);
}
}
A component can react to the current user:
@if (userService.user(); as user) {
<p>Welcome {{ user.name }}</p>
} @else {
<p>Please log in.</p>
}
This is a simple example of service-based reactive state.
Suppose a service stores cart items:
private readonly _items = signal<Product[]>([]);
readonly items = this._items.asReadonly();
A new item can be added using:
addItem(product: Product) {
this._items.update(items => [...items, product]);
}
Removing an item:
removeItem(productId: number) {
this._items.update(items =>
items.filter(product => product.id !== productId)
);
}
Clearing the cart:
clearCart() {
this._items.set([]);
}
The state remains controlled by the service.
Sometimes one value can be calculated from another state value.
For example, a cart may need:
Instead of manually maintaining three independent values, derived values can be calculated.
import {
Injectable,
computed,
signal
} from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class CartService {
private readonly _items = signal<Product[]>([]);
readonly items = this._items.asReadonly();
readonly itemCount = computed(
() => this._items().length
);
readonly total = computed(() =>
this._items().reduce(
(sum, product) => sum + product.price,
0
)
);
}
The source state is:
_items
Derived state is:
_items
|
+---- itemCount
|
+---- total
This reduces the risk of values becoming inconsistent.
Services can act as communication channels between parts of an Angular application.
Imagine:
HeaderComponent
ProductList
CartPage
All three need cart information.
Instead of creating complicated component-reference chains:
Product -> Parent -> Layout -> Header
they can depend on a shared service:
ProductList
|
|
v
CartService <---- HeaderComponent
^
|
|
CartPage
When the cart service state changes, interested consumers can use the current reactive value.
Example:
@Injectable({
providedIn: 'root'
})
export class CartService {
private readonly _items = signal<Product[]>([]);
readonly items = this._items.asReadonly();
readonly count = computed(() => this._items().length);
addItem(product: Product) {
this._items.update(items => [...items, product]);
}
}
The header can display:
Cart ({{ cartService.count() }})
while the product component performs:
this.cartService.addItem(product);
Neither component needs a direct reference to the other.
Consider a wizard component that needs temporary state.
@Component({
selector: 'app-registration-wizard',
providers: [RegistrationStateService],
template: `...`
})
export class RegistrationWizard {
}
A component-scoped provider can create state associated with that component subtree instead of making it global.
This can be useful for:
Using providedIn: 'root' for every stateful service can accidentally make temporary state global.
Provider scope should therefore reflect the required lifetime of the data.
These terms describe responsibilities rather than special Angular service types.
A data service might focus on persistence:
getOrders()
createOrder()
updateOrder()
deleteOrder()
A business service might focus on rules:
calculateOrderTotal()
canCancelOrder()
calculateShippingCharge()
applyDiscount()
For a small application they may exist in one service.
For a large application, separating them can make responsibilities clearer.
Example:
OrderComponent
|
v
OrderService
|
+---- OrderApiService
|
+---- PricingService
|
+---- InventoryService
Avoid splitting services merely to increase the number of files. Separate responsibilities when the separation improves clarity, reuse, or maintainability.
The following example combines several useful service concepts.
import {
Injectable,
computed,
signal
} from '@angular/core';
export interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
@Injectable({
providedIn: 'root'
})
export class CartService {
private readonly _items = signal<CartItem[]>([]);
readonly items = this._items.asReadonly();
readonly totalQuantity = computed(() =>
this._items().reduce(
(total, item) => total + item.quantity,
0
)
);
readonly totalPrice = computed(() =>
this._items().reduce(
(total, item) =>
total + item.price * item.quantity,
0
)
);
addItem(item: CartItem) {
this._items.update(items => {
const existing = items.find(
current => current.id === item.id
);
if (!existing) {
return [...items, item];
}
return items.map(current =>
current.id === item.id
? {
...current,
quantity: current.quantity + item.quantity
}
: current
);
});
}
removeItem(id: number) {
this._items.update(items =>
items.filter(item => item.id !== id)
);
}
clear() {
this._items.set([]);
}
}
A component might use it like this:
@Component({
selector: 'app-cart',
templateUrl: './cart.html'
})
export class Cart {
readonly cart = inject(CartService);
}
Template:
<h2>Shopping Cart</h2>
<p>Total Items: {{ cart.totalQuantity() }}</p>
<p>Total Price: {{ cart.totalPrice() }}</p>
<button (click)="cart.clear()">
Clear Cart
</button>
Notice the responsibility separation:
Cart Component
|
| presentation
v
Cart Service
|
| state + operations
v
Cart Data
The component does not contain the rules for maintaining cart state.
A service can be a practical state-management solution for small and medium-sized features.
Example service state may include:
However, not every value belongs in a global service.
For example, the state of whether one dropdown is open usually belongs to the component itself.
A useful rule is:
Local UI state
-> component
Shared feature state
-> feature/service state
Application-wide reusable state
-> root service when appropriate
State ownership should be intentional.
A common mistake is creating one large service:
@Injectable({
providedIn: 'root'
})
export class AppService {
user: any;
cart: any;
products: any;
orders: any;
notifications: any;
settings: any;
payments: any;
}
This service has too many responsibilities.
Changes in unrelated application features can affect the same class.
Prefer focused responsibilities such as:
AuthService
CartService
ProductService
OrderService
NotificationService
SettingsService
This makes dependencies more understandable.
A component should not become the application's business layer.
Less maintainable:
export class Checkout {
calculateTax() {
// complex tax rules
}
validateCoupon() {
// business rules
}
calculateShipping() {
// shipping rules
}
processPayment() {
// payment logic
}
}
A better separation might be:
Checkout Component
|
+---- PricingService
|
+---- CouponService
|
+---- ShippingService
|
+---- PaymentService
The component coordinates the user workflow while specialized services perform reusable operations.
Services are useful, but creating a service for every small function can make an application unnecessarily complicated.
For example, this may not require its own service:
getFullName(firstName: string, lastName: string) {
return `${firstName} ${lastName}`;
}
Ask:
If the answer to all of these is no, keeping simple local logic near where it is used may be clearer.
Avoid:
const service = new ProductService();
when the service is intended to participate in Angular Dependency Injection.
Prefer:
const service = inject(ProductService);
Not every service needs application-wide lifetime.
Use an appropriate provider scope when state should belong only to a feature, route, or component subtree.
Avoid giant classes such as:
AppService
CommonService
UtilityService
HelperService
that gradually accumulate unrelated methods.
Service names should communicate their responsibility.
Examples:
AuthenticationService
CartService
ProductService
PricingService
NotificationService
Angular's style guidance similarly recommends avoiding overly generic file names when the contents do not share a clear theme.
Instead of:
items = signal<Product[]>([]);
consider:
private readonly _items = signal<Product[]>([]);
readonly items = this._items.asReadonly();
and expose intentional operations:
addItem()
removeItem()
clear()
This gives the service control over how its state changes.
Avoid:
ProductList -> HttpClient
ProductDetails -> HttpClient
ProductSearch -> HttpClient
when all three represent the same product API domain.
A dedicated service gives you:
ProductList ----\
ProductDetails --- ProductService -> API
ProductSearch ---/
API details are then centralized.
A data service generally should not need to decide:
Should this modal be red?
Should this button be hidden?
Which CSS class should this card use?
These decisions normally belong closer to the presentation layer unless they represent real application state or domain rules.
Service state exists in the running application memory.
For example:
private user = signal<User | null>(null);
A page reload creates a new application runtime.
If information must survive reloads or sessions, an appropriate persistence mechanism may be required, such as:
A service can coordinate persistence, but the service instance itself should not be confused with permanent storage.
Choose names that describe the responsibility.
Good examples:
AuthService
EmployeeService
ProductService
CartService
OrderService
PaymentService
NotificationService
Less informative names include:
CommonService
GeneralService
AllService
DataManagerService
HelperService
A developer should be able to understand the service's purpose from its name.
Instead of exposing internal implementation details, expose operations that represent what callers need.
For example:
cartService.addItem(product);
cartService.removeItem(id);
cartService.clear();
is clearer than letting every component directly manipulate:
cartService.items.push(...);
cartService.items.splice(...);
cartService.items.length = 0;
The first design protects the rules of the cart.
Avoid unnecessary use of any.
Less useful:
getProducts(): any {
}
Better:
getProducts(): Observable<Product[]> {
}
For service state:
private readonly _user = signal<User | null>(null);
Strong types help the compiler identify incorrect assumptions before runtime.
Backend models and UI models are not always identical.
An API may return:
{
"first_name": "Amit",
"last_name": "Patil"
}
while the application wants:
interface User {
firstName: string;
lastName: string;
}
A data service or mapping layer can convert the response before components consume it.
This prevents API-specific details from leaking across many UI components.
Suppose a component depends on:
private productService = inject(ProductService);
Because the dependency is supplied through Angular's dependency system, tests can provide a controlled substitute.
Conceptually:
Production
Component -> Real ProductService -> API
Test
Component -> Fake ProductService -> Test Data
This separation makes it easier to test components without requiring every external dependency to behave exactly as it does in production.
Consider creating a service when functionality:
A service should solve a real architectural responsibility rather than simply increase abstraction.
A service may be unnecessary when the logic:
Good architecture is not about maximizing the number of services.
It is about placing responsibilities where they are easiest to understand and maintain.
For a typical e-commerce application, responsibilities might be divided like this:
AuthService
-> login/logout/session behavior
ProductService
-> product API operations
CartService
-> cart state and cart operations
PricingService
-> discounts and totals
OrderService
-> order operations
PaymentService
-> payment workflow
NotificationService
-> application notifications
Components then become consumers of these capabilities rather than containers for all application logic.
Consider adding a product to a cart.
User clicks "Add to Cart"
|
v
Product Component
|
v
CartService.addItem()
|
v
Cart signal changes
|
+----------------+
| |
v v
Header cart count Cart page
updates updates
This demonstrates why a shared reactive service can be useful: one state change can be consumed by multiple parts of the application without tightly coupling those components.
Use these principles as practical guidelines rather than absolute rules.
A service should have a clear responsibility.
ProductService -> products
OrderService -> orders
CartService -> cart
Allow Angular to provide dependencies rather than manually constructing injectable services.
Use root-level provisioning for genuinely application-wide services and narrower scopes when isolation is required.
Reusable domain behavior is easier to maintain when it is separated from UI code.
For signal-based state, consider keeping writable signals private and exposing read-only signals.
If a value can be calculated reliably:
computed(() => ...)
may be safer than manually synchronizing several independent fields.
Split unrelated responsibilities when a service becomes difficult to understand.
The service name should make its role obvious.
Define interfaces and explicit return types where they improve clarity.
Components should not repeatedly contain endpoint URLs and transport-specific details.
Create abstractions when they solve a design problem.
Use a component property when the state belongs only to one component.
Use inputs and outputs when communication follows a clear parent-child relationship.
Use a shared service when several unrelated components require the same functionality or state.
Use a data service when components need backend or persistence operations.
Use a business service when domain rules should be reusable and independent of presentation.
Use a signal-based state service when multiple consumers need reactive shared state.
Use a component- or feature-scoped service when each feature instance needs isolated state.
providedIn: 'root' is commonly used for application-wide services.inject() can be used to retrieve a dependency from an Angular injection context.computed() is useful for state derived from other signal values.Angular services are a core architectural tool for building maintainable applications. Their main value is not simply that they allow code to be reused. They establish clear ownership of responsibilities.
Components can focus on presenting data and handling user interaction, while services can handle operations such as business rules, API communication, shared state, caching, authentication, and cross-component coordination.
For modern Angular applications, a particularly useful pattern is combining Dependency Injection with signal-based state:
Component
|
| inject()
v
Service
|
+---- Private writable signal
|
+---- Public read-only signal
|
+---- Business operations
|
+---- Derived computed state
When services are kept focused, given the correct provider scope, and used only where they add architectural value, they help produce Angular applications that are easier to understand, test, reuse, and maintain.