Technology  /  Angular

๐Ÿ…ฐ๏ธ Angular 15 guides ยท updated 2026

Standalone components, signals, RxJS, and dependency injection โ€” building maintainable frontends with the framework enterprises trust.

Angular Services and Dependency Injection: inject(), Providers, and Scopes

Components render; they should not know things. How to call the API, whatโ€™s in the cart, whether the user may delete โ€” that knowledge belongs in services: plain TypeScript classes holding logic and state, handed to whoever needs them by Angularโ€™s dependency injection system.

DI is simultaneously Angularโ€™s most enterprise-flavored feature and its most misunderstood โ€” newcomers treat it as ceremony for importing things. Itโ€™s not. Itโ€™s the machinery that makes a 500-component app testable, swappable, and consistent โ€” the program-to-an-interface discipline with a framework enforcing it. This guide builds it from zero: services, inject(), provider scopes, tokens, and the testing payoff that justifies everything.


Services: Where Non-UI Logic Lives

A service is a class with a decorator:

import { Injectable, inject, signal, computed } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class CartStore {
private items = signal<CartItem[]>([]);
readonly count = computed(() => this.items().length);
readonly totalPaise = computed(() =>
this.items().reduce((s, i) => s + i.paisePerUnit * i.qty, 0));
add(item: CartItem) {
this.items.update((xs) => [...xs, item]);
}
clear() { this.items.set([]); }
}

And consumers ask for it rather than constructing it:

export class HeaderBadge {
cart = inject(CartStore); // โ† DI hands over the shared instance
// template: {{ cart.count() }}
}

Why not just new CartStore() in each component? Three reasons that compound:

  1. Shared instances. providedIn: 'root' means one CartStore for the whole app โ€” every injector of it sees the same items. new would give each component a private, useless copy. Singletons-by-default is exactly right for stores, API clients, and auth state.
  2. Dependency graphs assemble themselves. CartStore might inject HttpClient, which needs interceptors, which need configโ€ฆ DI resolves the whole chain; new would make every component reconstruct it.
  3. Substitutability โ€” the testing/flexibility payoff with its own section below.

The division of labor this creates is modern Angularโ€™s architecture in one sentence: components render state and forward intents; services own state and logic. A component with business rules in it is a refactor waiting to happen; a service is where that logic becomes reusable across pages and trivially unit-testable (no DOM required).


inject(): The Modern Injection Idiom

Two syntaxes request dependencies; youโ€™ll read both, write the first:

export class CheckoutPage {
private cart = inject(CartStore); // modern: inject() in field initializers
private router = inject(Router);
// legacy equivalent:
// constructor(private cart: CartStore, private router: Router) {}
}

inject() won for practical reasons: it works in field initializers (composing with signal/computed setups), in functions (route guards, interceptors โ€” which have no constructors at all), and it survives inheritance without constructor-parameter gymnastics. One rule governs it: inject() only works in an injection context โ€” field initializers, constructors, and provider/guard factories. Calling it later (in a click handler) throws; inject at construction, use forever.

The injection request is by type โ€” inject(CartStore) โ€” and the type is also the default token DI resolves. Which raises the question the next section answers: resolved from where?


Providers and Scopes: Who Gets Which Instance

DI is a hierarchy of injectors: application-level (from app.config.ts and providedIn: 'root'), route-level, and element-level (component providers). A request walks up the tree from the requester until a provider is found โ€” closest wins:

element injector (component providers) โ† checked first
โ†‘
route injector (lazy route providers)
โ†‘
root injector (providedIn: 'root', appConfig)

The scopes in practice:

providedIn: 'root' โ€” the default and the workhorse: one app-wide instance, lazily created on first injection, tree-shaken away entirely if nothing injects it. Stores, API clients, auth: here.

Component providers โ€” a fresh instance per component, shared with its children, destroyed with it:

@Component({
selector: 'app-wizard',
providers: [WizardState], // each wizard gets its own state
...
})

This is the underused gem: two wizards on screen get independent WizardStates, children of each wizard inject their wizardโ€™s instance, and cleanup is automatic (lifecycle-bound). Per-widget state machines, form coordinators, and anything โ€œone per instance of this UIโ€ belongs here โ€” developers who miss this scope end up hand-building instance maps inside root services.

Route providers โ€” scoped to a lazy-loaded route subtree: feature-wide but not global.

The lookup-walks-up rule is also the mechanism behind directives injecting their host component and component families (tabs finding their tab-group) โ€” element DI is proximity-aware by design.


Beyond Classes: InjectionToken and Configurable Providers

Not everything injectable is a class. Configuration objects, primitives, and interface-shaped dependencies use tokens:

app.config.ts
export interface ApiConfig { baseUrl: string; timeoutMs: number; }
export const API_CONFIG = new InjectionToken<ApiConfig>('api.config');
providers: [
{ provide: API_CONFIG, useValue: { baseUrl: 'https://api.example.com', timeoutMs: 8000 } },
]
// consumer
export class OrderApi {
private cfg = inject(API_CONFIG);
}

The provider recipes complete the toolkit: useValue (a ready object), useClass (substitute an implementation โ€” { provide: PaymentGateway, useClass: RazorpayGateway }, the interface-swapping pattern verbatim), useFactory (compute it, injecting other deps), and useExisting (alias). Youโ€™ll configure far fewer providers than you inject โ€” but reading these recipes is required literacy for app.config.ts files and library documentation, where provideRouter(routes) and friends are exactly these recipes behind a function.


The Payoff: Testing and Substitution

Hereโ€™s the section that converts DI skeptics. Because components ask for dependencies by token, tests can answer with fakes:

TestBed.configureTestingModule({
providers: [
{ provide: OrderApi, useValue: fakeOrderApi }, // no HTTP in unit tests
{ provide: AuthStore, useValue: { user: signal(adminUser) } },
],
});

The component under test runs against controlled collaborators โ€” no network, no login flow, deterministic state โ€” without changing a line of component code. Compare the alternative universe where components new their dependencies or import singletons directly: testing requires module-mocking gymnastics, and swapping a payment provider means editing every consumer. DI moves substitution from โ€œinvasive surgeryโ€ to โ€œone provider lineโ€ โ€” the same win constructor injection buys in any language, enforced by the framework.

The design habit that maximizes it: keep services narrow and purposeful (OrderApi, CartStore, PricingRules โ€” not AppService), inject what you use, and let component tests fake at the service seam. Codebases that do this stay testable at scale by default; the seams were drawn from day one.


Layering Services: A Shape That Scales

One service class is easy; the skill is the layering when a feature grows. The structure that recurs in healthy large Angular codebases:

components (render + intents)
โ†“ inject
stores (state + orchestration) CartStore, OrdersStore
โ†“ inject
API services (HTTP boundary) OrderApi, ProductApi
โ†“ inject
HttpClient (+ interceptors)

Each layer knows only the one below: components never touch HttpClient directly (theyโ€™d bypass store state and duplicate loading logic); stores never format for display (thatโ€™s pipes and computeds at the component layer); API services never hold state (theyโ€™re stateless translators between endpoints and typed methods). The payoffs are concrete: swapping a backend touches only API services; testing a store fakes only its API service; testing a component fakes only its store. When you see a component injecting six services, or an API service caching results, the layer boundaries have blurred โ€” the refactor is almost always โ€œmove this knowledge one layer toward where it belongs.โ€

A related judgment developers meet early: utility functions vs services. Pure, stateless helpers (formatting, math, validation functions) should be plain exported functions โ€” modules already share them, no DI needed, trivially importable and testable. A service earns its decorator when it has state, dependencies, or needs substitution in tests. DateUtils as an injectable with no dependencies and no state is DI cosplay; CartStore as a plain module singleton is untestable state. Match the tool to what the code actually is.

Frequently Asked Questions

When is component-scoped state a service vs just signals in the component? Signals in the component until children need the state too โ€” then a component-provided service gives the family shared state without input-drilling. Root services are for state that outlives or spans pages.

Can services inject services? Constantly โ€” CartStore injecting OrderApi injecting HttpClient is the normal shape. The graph is resolved automatically; circular service dependencies throw at runtime and signal a design problem โ€” extract the shared piece, as with module cycles.

Whatโ€™s { optional: true }? inject(Analytics, { optional: true }) returns null instead of throwing when unprovided โ€” right for genuinely optional integrations. Other modifiers (self, skipSelf, host) fine-tune the hierarchy walk; know they exist, reach for them rarely.

Why did my lazy route get a second instance of a โ€œsingletonโ€? It provided the service in its own providers array โ€” creating a route-scoped instance shadowing root. The classic accidental-double-instance bug: provide once, at the intended scope.

How does DI behave during testing beyond provider substitution? TestBed is a configurable injector โ€” which means scope rules apply in tests too: a component-provided service under test gets a fresh instance per component fixture (matching production), while root-provided fakes are shared across the test file unless you reset. Knowing the test injector mirrors the real hierarchy is what makes test behavior predictable rather than mysterious.

Is providedIn: 'root' with an unused service dead weight? No โ€” tree-shakable providers mean unimported, uninjected services vanish from the bundle. This is why providedIn: 'root' beats registering everything in app.config.ts providers arrays.


Reading Legacy DI

Older codebases carry DI idioms worth decoding on sight. Constructor injection (constructor(private cart: CartStore) {}) โ€” the dominant historical form; identical semantics to inject(), mechanical to migrate, no urgency to. @Injectable() without providedIn plus registration in an NgModuleโ€™s providers array โ€” the pre-tree-shaking pattern; such services ship in the bundle whether used or not, and consolidating them to providedIn: 'root' is a safe modernization. @Inject(TOKEN) parameter decorators โ€” the constructor-era syntax for token injection, now inject(TOKEN). forRoot()/forChild() static methods on modules โ€” the module-era convention for โ€œconfigure once at root, reference elsewhere,โ€ whose job standalone-era provideX(config) functions took over; when a libraryโ€™s docs still say SomeModule.forRoot(...), itโ€™s the same pattern in older clothes. And injection hierarchies through NgModules โ€” module-provided services scoped to lazy modules โ€” map directly onto todayโ€™s route-level providers. None of these are wrong in place; the skill is recognizing each as an eraโ€™s answer to the same four questions this guide covered: whatโ€™s injectable, at what scope, configured how, substituted where.

One more habit that compounds: inject interfaces-in-spirit even without TypeScript interfaces. DI tokens are classes in Angular (erasure means interfaces donโ€™t survive to runtime), so the pattern is an abstract class as the token โ€” abstract class PaymentGateway { abstract charge(...): ... } โ€” with implementations provided via useClass. Consumers depend on the abstraction; swaps and fakes stay one provider line. Itโ€™s the program-to-an-interface discipline, adapted to what the platform can express.

Self-Check

  1. Two components inject CartStore (providedIn: 'root'). How many instances exist, and what does each component see?
  2. A StepperState service should be independent per <app-stepper> on the page. Whereโ€™s it provided?
  3. Why does inject(Router) inside a buttonโ€™s click handler throw, and whatโ€™s the fix?
  4. Write the provider line that substitutes FakePaymentGateway for PaymentGateway app-wide.
  5. A component news up its API client: private api = new OrderApi(new HttpService()). Name two concrete costs versus injection.

(Answers: one; both see the same signals and items โ€” thatโ€™s the sharing mechanism. In the stepper componentโ€™s providers array. Click handlers arenโ€™t injection contexts โ€” inject in a field initializer and store the reference. { provide: PaymentGateway, useClass: FakePaymentGateway }. Tests canโ€™t substitute the client (mocking requires module surgery), and the hand-built dependency chain bypasses interceptors/config the DI graph provides โ€” plus every consumer duplicates construction knowledge.)

Next: Routing โ€” URLs as application state: route configuration, params as component inputs, guards, and the navigation lifecycle.