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.