Senior 4 min · March 28, 2026

Angular DI: Lazy Module Singleton Duplicated

Logout fails in lazy module: two AuthService instances from providedIn:'root' + providers.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Components = UI units: template + class + styles. Use OnPush change detection with immutable @Input to cut renders 80%.
  • @ViewChild is undefined until ngAfterViewInit. Accessing it in ngOnInit is the #1 timing bug.
  • Services use hierarchical DI. providedIn:'root' creates a singleton in the root injector. If the same service is ALSO listed in a lazy module's providers array, Angular creates TWO instances — state updates in one won't be seen in the other.
  • HTTP interceptors are middleware. Order matters: auth (adds token), loading (spinner), error (handles 401). Mismatch order breaks functionality.
  • Route guards: canMatch prevents lazy module from loading at all. canActivate runs after module loads. Use canMatch for role-based access to admin modules.
  • Production killer: RouterModule.forRoot() in a lazy-loaded feature module creates a second Router instance. Navigation events fire twice, views don't update, no error thrown — silent failure for hours.
Plain-English First

Think of an Angular app like a large restaurant chain. Each branch (component) handles its own dining room and customer interactions independently. The franchise rulebook (module) groups related branches together and defines what equipment they share. The central kitchen supplier (service) provides ingredients to every branch without each one needing its own farm. The security guard at the door (route guard) checks if you're allowed in. The kitchen manager (interceptor) adds seasoning to every dish before it leaves the kitchen, without the chef knowing. When one branch runs out of a sauce, it calls the supplier — it doesn't try to grow tomatoes itself. That's Angular's architecture in one lunch conversation.

A team I consulted for spent three weeks hunting a memory leak that was crashing their Angular dashboard every four hours in production. Root cause: they were instantiating a new HttpClient inside a component instead of injecting a shared service, spinning up a fresh connection pool on every component mount and never releasing it. Four hours was exactly how long it took to exhaust the browser's connection limit. The fix was twelve characters. The three weeks were pure ignorance of how Angular's dependency injection actually works.

Angular's architecture — components, modules, services, guards, interceptors — isn't ceremony for ceremony's sake. It's a hard answer to a real problem: how do you build a 200-screen enterprise app with a team of 15 developers without it collapsing into a dependency nightmare? Without this structure you get components doing HTTP calls, state management, DOM manipulation, and business logic all in one file. I've seen it. It looks like someone let an intern rewrite jQuery in TypeScript. The separation isn't optional architecture philosophy — it's load-bearing.

After working through this, you'll be able to wire up a component that consumes a singleton service, register it correctly in a feature module or standalone component, know exactly why providedIn: 'root' exists and when NOT to use it, implement HTTP interceptors for auth and error handling, protect routes with guards, and spot the most common architectural mistakes before they hit your code review. You'll also understand why lazy-loaded modules break service singletons if you don't know what you're doing — which is the interview question that separates people who've actually shipped Angular from people who've done the Tour of Heroes.

Components: The Unit of UI — and Why They Must Stay Dumb

A component's only job is to display data and capture user intent. That's it. The moment a component starts making HTTP calls directly, manipulating global state, or containing business logic, you've built a monolith inside a framework that was designed to stop you from doing exactly that.

Before Angular, teams building large SPAs with frameworks like Backbone or early AngularJS (v1) routinely ended up with controllers that were thousands of lines long. Testing was impossible without spinning up the entire app. Reuse was a joke. A component-based architecture forces a hard boundary: the component owns the template and the interaction logic wired to that template, nothing else.

Every Angular component is defined by three things: a TypeScript class (the brain), a template (the face), and styles (the skin). The @Component decorator is what tells Angular's compiler to treat this class as a UI unit. The selector is how you stamp it into other templates. Change detection is how Angular knows when to re-render — and this is where most intermediate developers are still fuzzy. Angular's default change detection strategy (CheckAlways) rerenders the component on every event cycle. Switch to OnPush for any component receiving data via @Input and you cut unnecessary renders dramatically. On a dashboard with 80 components, this is the difference between 60fps and a janky mess.

Lifecycle hooks matter more than most tutorials admit. ngOnInit fires once after the first ngOnChanges — use it for initialization. ngOnChanges fires every time an @Input reference changes — use it to react to parent data updates, but keep it fast. ngAfterViewInit fires after the component's view and all child views are initialized — this is where you can safely access @ViewChild references. ngOnDestroy fires before the component is destroyed — this is your last chance to clean up subscriptions, timers, and event listeners. I've seen teams skip ngOnDestroy entirely and wonder why their app leaks memory. The hook exists for a reason. Use it.

io.thecodeforge.angular.components_modules_services.order_summary.component.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, OnInit, OnChanges, SimpleChanges, OnDestroy } from '@angular/core';
import { OrderService } from '../services/order.service';
import { Order, OrderStatus } from '../models/order.model';

@Component({
  selector: 'app-order-summary',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="order-card" [class.order-card--urgent]="isUrgent">
      <h3>Order #{{ order.id }}</h3>
      <p>{{ order.customerName }} — {{ order.totalAmount | currency }}</p>
      <p class="status" [attr.data-status]="order.status">
        {{ order.status }}
      </p>
      <button
        *ngIf="order.status === OrderStatus.PENDING"
        (click)="confirmOrder()"
      >
        Confirm
      </button>
    </div>
  `
})
export class OrderSummaryComponent implements OnInit, OnChanges, OnDestroy {
  readonly OrderStatus = OrderStatus;

  @Input() order!: Order;
  @Output() orderConfirmed = new EventEmitter<string>();

  isUrgent = false;

  constructor(private readonly orderService: OrderService) {}

  ngOnChanges(changes: SimpleChanges): void {
    if (changes['order']) {
      this.isUrgent = this.orderService.isOrderUrgent(this.order);
    }
  }

  ngOnInit(): void {
    this.isUrgent = this.orderService.isOrderUrgent(this.order);
  }

  ngOnDestroy(): void {
    // Nothing to clean up here, but if this component had
    // subscriptions or timers, this is where they die.
  }

  confirmOrder(): void {
    this.orderConfirmed.emit(this.order.id);
  }
}
Output
Component renders order card with customer name, total, and status.
Clicking 'Confirm' emits orderConfirmed event with the order ID to the parent.
No HTTP call is made inside this component.
With OnPush, Angular skips re-rendering this component unless the `order` reference itself changes.
ngOnChanges fires when parent passes a new order reference, recomputing isUrgent.
ngOnDestroy fires when component is removed from the DOM — cleanup happens here.
OnPush + Mutated @Input = Stale UI
If you mutate a property inside an @Input object (e.g., order.status = 'CONFIRMED') instead of passing a new object reference, OnPush will never detect the change and your UI will silently show stale data. Always treat @Input objects as immutable. Return a new object from the parent: { ...order, status: 'CONFIRMED' }. This is the single most common OnPush bug I've seen in code reviews.
Production Insight
OnPush + immutable @Input is the performance win, but teams accidentally mutate objects.
The symptom: a button click updates the server but the UI doesn't change. No error. No warning.
The fix: change detection only runs when the reference changes, not when a property mutates.
Rule: Never mutate @Input objects. Always create a new reference.
Key Takeaway
Component owns UI only — not data fetching, not business logic, not global state.
OnPush with immutable @Input cuts re-renders by 80% on dashboards.
ngOnDestroy is where subscriptions die — use it, or leak memory.
Choose Change Detection Strategy
IfComponent only receives data via @Input and emits events via @Output
UseUse OnPush. Best performance. Requires immutable @Input.
IfComponent has internal timer, animation, or uses ChangeDetectorRef manually
UseOnPush works with markForCheck(). Use it deliberately.
IfComponent is simple and has no performance constraints
UseDefault (CheckAlways) is acceptable but not optimal for lists or dashboards.
IfComponent is deeply nested with many children relying on global service state
UseOnPush + async pipe. The async pipe triggers change detection on emission.

Services and Dependency Injection: Where Your Architecture Actually Lives

Services are where your business logic lives. Not in components, not in utility files, not in a god-object store — in injectable services with a single, clear responsibility. The reason Angular's DI system exists is that it solves a problem that's invisible when your app is small and catastrophic when it isn't: how do you ensure that the same instance of a stateful object is shared across dozens of components without those components knowing about each other?

providedIn: 'root' is the correct default for most services. It registers the service with the root injector, making it a true singleton for the app's lifetime. Angular's tree-shaking will also remove it from the bundle if nothing actually injects it — something the old providers: [] module pattern doesn't give you. The one time you don't want providedIn: 'root' is when you need component-level or lazy-module-level isolation: a form state service that should reset when a component is destroyed, or a polling service that should stop when a feature module is unloaded.

The DI system is hierarchical. Root injector at the top, module injectors in the middle, component injectors at the bottom. When a component asks for a dependency, Angular walks up the injector tree until it finds a provider. If you provide a service at the component level (via the component's providers array), each instance of that component gets its own service instance. I've used this deliberately for a shopping cart draft service that needed to be isolated per product modal — each modal got its own state, destroyed with the modal.

The inject() function (available since Angular 14) is the modern alternative to constructor injection. It's more composable — you can call it inside functions, computed properties, and conditional blocks. I've fully switched to inject() in standalone components. Constructor injection still works, but inject() is cleaner and plays better with TypeScript's strict mode.

io.thecodeforge.angular.components_modules_services.order_polling.service.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import { Injectable, OnDestroy, inject } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { BehaviorSubject, Observable, Subject, timer } from 'rxjs';
import { switchMap, catchError, takeUntil, tap, retry, distinctUntilChanged } from 'rxjs/operators';
import { Order, OrderStatus } from '../models/order.model';
import { environment } from '../../environments/environment';

export interface OrderPollState {
  orders: Order[];
  lastUpdated: Date | null;
  error: string | null;
  isLoading: boolean;
}

const INITIAL_STATE: OrderPollState = {
  orders: [],
  lastUpdated: null,
  error: null,
  isLoading: false
};

@Injectable({ providedIn: 'root' })
export class OrderPollingService implements OnDestroy {
  private readonly http = inject(HttpClient);

  private readonly stateSubject = new BehaviorSubject<OrderPollState>(INITIAL_STATE);
  readonly state$: Observable<OrderPollState> = this.stateSubject.asObservable();

  private readonly destroy$ = new Subject<void>();
  private readonly POLL_INTERVAL_MS = 15_000;
  private readonly MAX_RETRIES = 3;
  private isPolling = false;

  startPolling(statusFilter: OrderStatus): void {
    if (this.isPolling) return;
    this.isPolling = true;

    timer(0, this.POLL_INTERVAL_MS)
      .pipe(
        switchMap(() => {
          this.patchState({ isLoading: true, error: null });
          return this.http.get<Order[]>(`${environment.apiBase}/orders?status=${statusFilter}`).pipe(
            retry(this.MAX_RETRIES),
            catchError((err: HttpErrorResponse) => {
              this.patchState({
                isLoading: false,
                error: `Order fetch failed: ${err.status} ${err.statusText}`
              });
              return [this.stateSubject.getValue().orders];
            })
          );
        }),
        tap((orders: Order[]) => {
          this.patchState({ orders, isLoading: false, lastUpdated: new Date(), error: null });
        }),
        distinctUntilChanged((prev, curr) => JSON.stringify(prev) === JSON.stringify(curr)),
        takeUntil(this.destroy$)
      )
      .subscribe();
  }

  stopPolling(): void {
    this.isPolling = false;
    this.destroy$.next();
  }

  private patchState(partial: Partial<OrderPollState>): void {
    this.stateSubject.next({ ...this.stateSubject.getValue(), ...partial });
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }
}
Output
OrderPollingService instantiated once at app root.
startPolling('PENDING') begins a 15s interval immediately (timer starts at 0).
First poll: GET /api/orders?status=PENDING — state updates with orders array and lastUpdated timestamp.
On HTTP 503: retries 3 times, then patches state with error message, preserves existing orders in the UI.
Calling stopPolling() cancels the active interval and any in-flight HTTP request.
stopPolling() called: RxJS takeUntil fires, interval destroyed, no memory leak.
Service Provided in Both Root and Lazy Module = Two Instances
If a service has providedIn: 'root' AND is listed in a lazy-loaded module's providers array, Angular creates two instances: one in the root injector, one in the lazy module's injector. Components in that module get the lazy instance; everything else gets the root instance. State changes in one instance are invisible to the other. Symptom: service state updates on one page but not another, seemingly at random. Fix: pick one. Use providedIn: 'root' and remove it from every module's providers array, unless you explicitly want isolated instances.
Production Insight
The singleton service pattern is the most common DI mistake in Angular.
Stateful services (auth, user prefs, polling) must be singletons.
If you provide them in a lazy module, the state is isolated to that module's subtree.
Rule: providedIn:'root' is for app-wide singletons. Lazy-loaded modules must NOT re-provide them.
Key Takeaway
providedIn:'root' creates app-wide singletons. Component-level providers per-instance.
Don't list root-provided services in lazy module providers — that creates a second instance.
Inject() is cleaner than constructor injection. Use it in standalone components.
● Production incidentPOST-MORTEMseverity: high

The Lazy Module That Broke the Singleton

Symptom
User logs out normally. Main app redirects to login page. But the admin dashboard (lazy-loaded) remains accessible. Authentication checks in the admin module pass. Admin data is still visible. Reloading the page fixes it because the lazy module re-initialises.
Assumption
The team assumed providedIn: 'root' guaranteed a single instance everywhere. They didn't know that a lazy-loaded module with its own injector would override the root provider for that module's subtree.
Root cause
AuthService had @Injectable({ providedIn: 'root' }). The lazy-loaded AdminModule also had providers: [AuthService] because the developer saw an error about missing provider and added it. Angular's DI hierarchy: root injector created one instance. The lazy module's injector created a second instance for components inside that module. The main app used the root instance. The admin dashboard used the lazy instance. When the user logged out, the main app called clearSession() on the root instance. The lazy instance's state remained unchanged. The admin dashboard's guards checked the lazy instance and saw an active session. The team never noticed because the app worked fine most of the time — until a logout scenario exposed the divergence.
Fix
1. Removed AuthService from AdminModule's providers array entirely. Only providedIn: 'root' remained. 2. For any service that must be isolated per module (e.g., per-module form state that should reset on module unload), used component-level providers or a factory provider with useFactory. 3. Added an Angular rule in CI: ng lint with custom ESLint rule preventing providers arrays in lazy modules for root-provided services. 4. Documented the rule: 'A service with providedIn:'root' must NEVER be listed in the providers array of any lazy-loaded module.'
Key lesson
  • providedIn:'root' + providers array in lazy module = TWO instances. The lazy instance shadows the root instance for that module's subtree.
  • A service that is stateful (auth, user preferences, polling service) cannot be provided in both root and lazy module — the state will diverge.
  • If you need a service that resets on module unload, provide it at the component level, not in the lazy module's providers array.
  • Write a custom ESLint rule to detect services annotated with providedIn:'root' that appear in any NgModule.providers or Component.providers array.
Production debug guideSymptom → Action mapping for common Angular failures in production5 entries
Symptom · 01
User logs out but still sees protected data in lazy-loaded module
Fix
Check if auth service is provided in both providedIn:'root' AND in the lazy module's providers. That creates two instances. Remove from lazy module providers.
Symptom · 02
Navigation events fire twice, URL updates but view doesn't change
Fix
Check for RouterModule.forRoot() in a lazy-loaded feature module. That creates a second Router instance. Replace with forChild().
Symptom · 03
@ViewChild element is undefined in component code
Fix
@ViewChild is not available until ngAfterViewInit. Move access to ngAfterViewInit. Also check if *ngIf is hiding the element — use { static: false }.
Symptom · 04
HttpInterceptor not adding auth headers to some requests
Fix
Check interceptor order in providers array. Auth interceptor must come BEFORE other interceptors that might modify the request. Also verify request is going through Angular HttpClient, not native fetch.
Symptom · 05
Component renders stale data even though service state changed
Fix
Check if component uses OnPush change detection and @Input object was mutated (same reference). Use immutable updates: pass a new object reference. Also check if async pipe is used correctly.
★ Angular Debug Cheat SheetFast diagnostics for common Angular production issues.
Checked if service has two instances (auth state divergence)
Immediate action
Add constructor log with unique ID
Commands
ng serve --source-map
console.log('Service instance ID:', Math.random());
Fix now
If you see different random numbers in different parts of the app, you have duplicate providers. Remove service from lazy module's providers array.
Check for duplicate Router instances+
Immediate action
Log Router.events subscription count
Commands
this.router.events.subscribe(e => console.log('Router event:', e.type));
Search for 'RouterModule.forRoot' in feature modules: grep -r 'RouterModule.forRoot' src/app/
Fix now
Replace RouterModule.forRoot() with RouterModule.forChild() in any non-root module. Root module should be the only one using forRoot().
Check OnPush change detection not firing+
Immediate action
Verify @Input reference changed, not just properties
Commands
console.log('Input reference changed:', oldValue === newValue);
Check component changeDetection strategy: grep -n 'changeDetection:' component.ts
Fix now
Replace mutation with new object: this.data = { ...this.data, updatedProp: newValue } not this.data.updatedProp = newValue
Check interceptor order+
Immediate action
Verify registration order in providers array
Commands
grep -A 10 'HTTP_INTERCEPTORS' app.config.ts or app.module.ts
console logs in each interceptor to see execution sequence
Fix now
Reorder: AuthInterceptor (adds token) → LoadingInterceptor (counts requests) → ErrorInterceptor (handles 401)
Check for memory leak (subscribers not cleaned)+
Immediate action
Add ngOnDestroy with logging
Commands
grep -n 'subscribe' component.ts | wc -l
grep -n 'takeUntil\|async\|unsubscribe' component.ts
Fix now
Replace manual subscriptions with async pipe in template, or use takeUntilDestroyed().
Component-Level vs Root-Level Service Providers
AspectComponent-Level ProviderRoot-Level Provider (providedIn: 'root')
Instance countOne per component instanceOne for entire app lifetime
LifetimeDestroyed with the componentLives until app teardown
Use caseIsolated form state, per-dialog state, polling service that must stop on navigationHTTP services, auth, user prefs, shared data, cache services
Tree-shakingNo — always included if component is usedYes — removed if nothing injects it
Lazy module isolationScoped to that component subtree only — safeShared across all lazy modules — if service provided in lazy module providers, instance is NOT shared
State leakage riskNone — destroyed with componentHigh if state is not explicitly reset. Also high if service is re-provided in lazy module
Test isolationEasy — each test gets its own instanceRequires TestBed override to isolate
Bundle impactIncluded in the module that declares the componentIncluded in main bundle only if injected

Key takeaways

1
A component that calls HttpClient directly is a design failure, not a shortcut. The component owns the UI; a service owns the data fetching. Mix them and you can't unit test, can't reuse, and can't reason about state.
2
RouterModule.forRoot() in a feature module creates a second Router instance and breaks navigation in ways that don't throw errors
they just cause unpredictable behaviour under load. Always forChild() in feature modules.
3
Reach for providedIn
'root' by default for any stateless or globally-shared service. Switch to component-level providers only when you need isolated, component-scoped state that must be destroyed with its host — like a draft form state or a per-dialog polling stream.
4
The async pipe isn't just a convenience
it's architectural correctness. It guarantees unsubscription on component destroy, triggers OnPush change detection correctly, and removes an entire class of memory leaks.
5
Standalone components are the default in Angular 17+. Use them for all new code. NgModules still make sense for library packaging and large legacy migrations, but standalone removes the 'where do I put this component?' debate entirely.
6
HTTP interceptors are middleware for your API layer. Use them for auth token injection, global error handling, loading state, and request logging. Never put auth logic or error handling inside individual services or components.
7
Route guards centralise access control. Use functional guards (CanActivateFn) for simple checks. Use canMatch over canActivate when you want to prevent a lazy module from loading entirely for unauthorised users.
8
takeUntilDestroyed replaces the destroy$ Subject pattern with zero boilerplate. Combined with toSignal() for Observable-to-signal conversion, modern Angular cleanup is structurally leak-proof.
9
If a service is provided in both root and a lazy module, you get two instances. State updates in one are invisible in the other. This is the single most common DI bug in large Angular apps.
10
OnPush with immutable @Input is the performance win. Mutate objects and the UI doesn't update. No error. Just stale data. Always pass new references.

Common mistakes to avoid

9 patterns
×

Providing a root service in a lazy module's providers array — duplicate instance

Symptom
State updates made in one part of the app are invisible to components in the lazy module. Logout doesn't clear session in lazy module. Two separate service instances exist. Adding console.log with random ID shows different values in different parts of the app.
Fix
Remove the service from the lazy module's providers array entirely. Only providedIn: 'root' should remain. For services that must be isolated per module (e.g., form state that should reset when the module unloads), provide them at the component level, not the module level.
×

Calling RouterModule.forRoot() in a lazy-loaded feature module instead of forChild()

Symptom
Navigation events fire twice (NavigationStart, NavigationEnd appear twice in logs). URL updates in browser address bar but the view doesn't change. No error thrown — just silent failure.
Fix
Replace RouterModule.forRoot(routes) with RouterModule.forChild(routes) in every module except the root AppModule. Only the root module should use forRoot().
×

Accessing @ViewChild in ngOnInit — returns undefined

Symptom
Component property that should reference a DOM element or child component is undefined. No error is thrown — the property just never gets set. The template uses the element, but the component code can't interact with it.
Fix
Move @ViewChild access to ngAfterViewInit. The view (including child components) is guaranteed to be fully initialized there. If the element is conditionally shown (e.g., with *ngIf), use { static: false } and access it after the condition becomes true.
×

Mutating @Input objects with OnPush change detection

Symptom
UI shows stale data even though the underlying data in the service or parent has updated. The server confirms the change, but the screen doesn't reflect it. No errors in console.
Fix
Treat @Input objects as immutable. Instead of this.order.status = 'CONFIRMED', pass a new object: this.order = { ...this.order, status: 'CONFIRMED' }. This creates a new reference, which triggers OnPush change detection.
×

Subscribing to Observables in components without cleanup

Symptom
Memory usage grows over time. After navigating away from a page and back, the component mounts again but the old subscriptions are still active. Network requests continue in the background even after the component is destroyed.
Fix
Use the async pipe in templates whenever possible. For imperative subscriptions, use takeUntilDestroyed() (Angular 16+) or a destroy$ Subject with takeUntil in ngOnDestroy.
×

Incorrect interceptor order — error interceptor before auth interceptor

Symptom
Auth tokens are missing from requests sent after error handling. The error interceptor catches a 401 but doesn't retry with a refreshed token because the auth interceptor hasn't run yet in the retry path.
Fix
Register interceptors in this order: AuthInterceptor (adds token), LoadingInterceptor (spinner), ErrorInterceptor (handles errors). Order is determined by registration order in the providers array.
×

Importing BrowserModule in a feature module

Symptom
Error: 'BrowserModule has already been loaded. If you need access to common directives such as NgIf and NgFor from a lazy loaded module, import CommonModule instead.' Build fails.
Fix
BrowserModule is only for the root AppModule. Use CommonModule in every feature and shared module. CommonModule provides NgIf, NgFor, and other core directives.
×

Using *ngFor on large lists without trackBy

Symptom
DOM nodes unmount and remount on every data refresh. Scrolling position resets, CSS animations restart, input focus is lost inside list items. Perf degrades on large lists.
Fix
Always add trackBy to ngFor on data coming from observables or HTTP responses: ngFor="let item of items; trackBy: trackById". Provide a function that returns a stable, unique identifier.
×

Hardcoding API URLs in services instead of using environment files

Symptom
The app works in development but hits localhost:3000 in staging/production. Deploying to a new environment requires code changes and a redeploy.
Fix
Store API base URLs in environment.ts and environment.prod.ts. Use angular.json file replacements to swap them at build time. Reference environment.apiBase in services.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Angular's dependency injector is hierarchical. If a service is provided ...
Q02SENIOR
When would you choose component-level service providers over providedIn:...
Q03SENIOR
What happens to an active RxJS subscription inside an Angular service th...
Q04SENIOR
What is the difference between canActivate and canMatch route guards, an...
Q05SENIOR
Explain the difference between @ViewChild and @ContentChild. When is eac...
Q06SENIOR
How do HTTP interceptors work in Angular, and why does registration orde...
Q07SENIOR
What is the difference between standalone components and NgModule-based ...
Q08SENIOR
How do Angular Signals differ from RxJS Observables, and when would you ...
Q01 of 08SENIOR

Angular's dependency injector is hierarchical. If a service is provided at both the root injector and inside a lazy-loaded module's providers array, how many instances get created, and how does Angular decide which instance to inject into a component inside that lazy module?

ANSWER
Angular creates two separate instances. The root injector creates one instance at app startup. The lazy-loaded module's injector creates another instance when the module is loaded (on first navigation to a route that triggers lazy loading). Resolution follows the injector tree: Angular walks up from the component's injector to its parent injectors until it finds a provider. For a component inside the lazy-loaded module, the lazy module's injector is closer in the tree than the root injector, so Angular uses the lazy module's instance. For a component outside the lazy module (e.g., in the main app), Angular uses the root instance. This is why state updates in one instance are invisible in the other — they're different objects entirely. The fix: never list a providedIn: 'root' service in a lazy module's providers array.
FAQ · 8 QUESTIONS

Frequently Asked Questions

01
What is the difference between providedIn: 'root' and adding a service to a module's providers array?
02
What is the difference between an Angular module's declarations and exports arrays?
03
How do I stop an Angular service from leaking memory when it uses an RxJS interval or timer?
04
Why does OnPush change detection sometimes show stale data even after state has clearly changed?
05
What is the difference between standalone components and NgModule-based components?
06
When should I use canActivate vs canMatch route guards?
07
How do HTTP interceptors work in Angular?
08
What are Angular Signals and when should I use them instead of RxJS?
🔥

That's Advanced JS. Mark it forged?

4 min read · try the examples if you haven't

Previous
WebMCP — AI Tool Integration for the Web
22 / 27 · Advanced JS
Next
Angular Expansion Panel: Building Accordions with Material