Angular Interview Questions for Senior Developers — Change Detection, RxJS, Signals & Performance (Deep Answers)
30 senior Angular interview questions with deep answers and real code: change detection, OnPush, RxJS operators, signals, and performance.
- Author
- Randhir Jassal
- Published
- Reading time
- 19 min read
- Views
- 13 views
Junior Angular interviews ask "what is a component." Senior interviews ask why your dashboard re-runs change detection thousands of times a second, why a typeahead fires a request per keystroke and shows stale results, and why that subscription leaks memory until the tab crawls. Angular is a big, opinionated framework, and seniority is knowing the machinery underneath — change detection, RxJS, the new signals model, and where performance goes to die. This guide is 30 questions across components & DI, change detection & performance, RxJS & state, and routing, forms, testing & production.
Each answer is a short answer you could say in one breath, a deep dive into the mechanism (with code), and the senior signal the interviewer is listening for. Examples use modern Angular (v17+: standalone components, signals, the new control flow) and are grounded in Mattrx — a real multi-tenant marketing-analytics platform whose internal admin/ops console is built in Angular (the customer-facing app is React). Real domain, not Foo/Bar.
How to use this guide
Read the deep dive even when the answer seems obvious — seniority is in the mechanism, the failure mode, and the trade-off. The blocks build on each other. If you can explain what Angular does under each snippet, you're ready.
Block A — components, DI & modern Angular
1. NgModules vs standalone components — where is Angular now?
Short answer: Modern Angular (v17+) defaults to standalone components — a component declares its own imports, so you no longer need NgModule boilerplate to wire things up. NgModules still work, but new code is standalone-first; the framework is moving away from modules for most cases.
Deep dive: Standalone components import exactly what they use, which improves tree-shaking and makes dependencies explicit. Mattrx's admin console is fully standalone.
Senior signal: You know standalone is the modern default, can migrate NgModule code to it, and cite explicit imports + better tree-shaking as the win.
2. Walk through the component lifecycle hooks.
Short answer: Key ones in order: ngOnChanges (on input change), ngOnInit (once, after first inputs), ngDoCheck (every CD run), ngAfterContentInit/ngAfterViewInit (projected content / view ready), and ngOnDestroy (cleanup). Most work goes in ngOnInit; cleanup (unsubscribe) goes in ngOnDestroy.
Deep dive: ngOnChanges fires before ngOnInit and on every input change; ngAfterViewInit is where @ViewChild refs are available.
Senior signal: You order the hooks correctly, know ngOnChanges sees input changes while ngDoCheck runs every cycle, and put cleanup in ngOnDestroy.
3. How does Angular's dependency injection work?
Short answer: Angular has a hierarchical injector tree — a request for a token walks up from the component injector to the root. providedIn: 'root' registers a tree-shakable singleton; providing at a component creates a new instance scoped to that component and its children.
Deep dive: Where you provide a service controls its scope and lifetime. Mattrx provides most services providedIn: 'root' and scopes a few (per-feature caches) at the route/component level.
Senior signal: You explain the hierarchical injector + providedIn: 'root' for tree-shakable singletons, and know component-level providers create scoped instances.
4. @Input/@Output vs the new signal inputs?
Short answer: Classic @Input()/@Output() pass data down and events up. Angular v17+ adds signal inputs (input(), input.required()) and output() — inputs become signals you read as myInput(), integrating cleanly with the reactivity model and OnPush.
Deep dive:
@Component({ selector: 'campaign-row', changeDetection: ChangeDetectionStrategy.OnPush,
template: `{{ campaign().name }}` })
export class CampaignRowComponent {
campaign = input.required<Campaign>(); // signal input — read as campaign()
archived = output<string>(); // typed event out
}
Senior signal: You know signal inputs (input()) read like signals and pair naturally with OnPush, and prefer them in new components.
5. ViewChild vs ContentChild — view vs content projection?
Short answer: @ViewChild queries an element/component in this component's own template; @ContentChild queries content projected in via <ng-content>. View children are available in ngAfterViewInit, content children in ngAfterContentInit.
Deep dive: Content projection (<ng-content>) lets a wrapper (a card, a dialog) render caller-supplied content — the basis of composable UI.
Senior signal: You distinguish own-template (ViewChild) from projected content (ContentChild) and know their respective lifecycle timing.
6. Why did Angular move to standalone components?
Short answer: To cut NgModule ceremony, make each component's dependencies explicit, improve tree-shaking, and simplify lazy loading (you can lazy-load a component directly). It lowers the framework's learning curve and boilerplate.
Deep dive: With standalone, a lazy route points at a component via loadComponent, no feature module needed.
Senior signal: You frame standalone as removing indirection (explicit imports, direct lazy loading) rather than just "the new way."
7. Structural vs attribute directives?
Short answer: Structural directives change the DOM layout by adding/removing elements (*ngIf, *ngFor, or the new @if/@for control flow). Attribute directives change appearance/behavior of an existing element (ngClass, ngStyle, custom [appHighlight]). Structural = shape of the DOM; attribute = properties of an element.
Deep dive: Angular v17's built-in @if/@for/@switch control flow replaces the *ngIf/*ngFor directives with faster, ergonomic syntax.
Senior signal: You separate DOM-shaping (structural) from element-decorating (attribute) and know the new @if/@for block syntax supersedes the star directives.
8. Pure vs impure pipes, and the async pipe?
Short answer: A pure pipe (default) recomputes only when its input reference changes — cheap and CD-friendly. An impure pipe runs every change-detection cycle — powerful but a performance risk. The async pipe subscribes to an observable/promise, renders emissions, and auto-unsubscribes on destroy.
Deep dive: The async pipe is the cleanest leak-proof way to render observable data — no manual subscribe/unsubscribe.
Senior signal: You default to pure pipes, treat impure pipes as a perf smell, and use the async pipe to avoid manual subscription management.
Block B — change detection & performance
9. How does Angular's change detection actually work?
Short answer: Angular runs change detection to sync the model to the DOM. Zone.js monkey-patches async APIs (events, timers, XHR) so that after any async task, Angular runs CD — walking the component tree top-down and checking each binding for changes ("dirty checking").
Deep dive: Every async callback triggers a CD pass over the whole tree by default; on a big tree, that's a lot of checks per interaction.
async event (click / http / timer)
-> zone.js notifies Angular
-> CD walks the component tree top-down, checks each binding
-> updates the DOM where a bound value changed
Senior signal: You explain CD as zone.js-triggered top-down dirty checking after async tasks, and know it's the root of most Angular perf issues.
10. Default vs OnPush change detection?
Short answer: Default checks every component on every CD run. OnPush skips a component unless: an @Input reference changed, an event fired inside it, or an observable bound with async emitted. On big trees OnPush slashes the number of checks — it's the single biggest Angular perf lever.
Deep dive:
@Component({
selector: 'campaign-list',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `@for (c of campaigns(); track c.id) { <campaign-row [campaign]="c" /> }`,
})
export class CampaignListComponent {
campaigns = input.required<Campaign[]>(); // new reference -> this subtree is checked
}
Senior signal: You default components to OnPush, know it relies on immutable inputs (new references), and can name the three triggers that still run it.
11. What is Zone.js and what's the zoneless direction?
Short answer: Zone.js patches async APIs so Angular knows when to run CD without you calling it. The downside is overhead and magic. Modern Angular offers zoneless change detection (driven by signals and explicit notifications), removing zone.js — smaller bundles, clearer triggers, and better interop.
Deep dive: In a zoneless app, signals and events tell Angular what changed, so CD runs only where needed — no whole-tree scan after every timer.
Senior signal: You know zone.js is the auto-CD mechanism and that signals enable a zoneless future with more precise, cheaper updates.
12. Why does trackBy / track matter in loops?
Short answer: Without a track expression, when the array reference changes Angular can't tell which items are the same, so it destroys and recreates every DOM node. Tracking by a stable id lets Angular reuse nodes for unchanged items — huge for long lists.
Deep dive:
// track by a stable id -> Angular reuses DOM for unchanged rows instead of rebuilding all.
@for (c of campaigns(); track c.id) { <campaign-row [campaign]="c" /> }
Senior signal: You always provide a stable track/trackBy on non-trivial lists and can explain the DOM-node reuse it enables.
13. What causes ExpressionChangedAfterItHasBeenCheckedError?
Short answer: In dev mode, Angular runs CD twice and errors if a bound value changed between the two passes — usually because a parent's binding was mutated in a child's lifecycle hook (e.g., ngAfterViewInit) after the parent was already checked. It's Angular catching a unidirectional-data-flow violation.
Deep dive: Fix by moving the mutation earlier, using setTimeout/Promise.resolve to defer to the next tick, or restructuring so the value is stable during the pass.
Senior signal: You explain it as the dev-mode double-check catching a value mutated after it was checked, and fix the data-flow rather than suppressing it.
14. How do you make change detection fast?
Short answer: OnPush + immutable data + async pipe everywhere; track on loops; avoid function calls and heavy getters in templates (they run every CD); detach()/markForCheck() for surgical control; and run heavy, non-UI async work outside Angular's zone (runOutsideAngular).
Deep dive: A method call in a template ({{ compute() }}) re-executes on every CD pass — move it to a signal/computed or a pure pipe.
Senior signal: You combine OnPush + immutability, keep templates free of function calls, and know runOutsideAngular for high-frequency non-UI work.
15. How does lazy loading work, and preloading?
Short answer: Lazy-load feature routes with loadComponent/loadChildren so their code ships in a separate chunk downloaded on navigation — smaller initial bundle. Add a preloading strategy (PreloadAllModules or a custom one) to fetch likely-next chunks in the background after the app loads.
Deep dive: Mattrx lazy-loads each admin section and preloads the two most-used ones so navigation feels instant without bloating first load.
Senior signal: You lazy-load by route, know the initial-bundle win, and tune a preloading strategy for perceived speed.
16. What drives Angular bundle size?
Short answer: AOT compilation (default) + tree-shaking remove unused code and templates at build; standalone components + the esbuild-based builder improve both. Watch heavy dependencies (moment, full lodash), non-lazy feature code, and impure pipes. Use source-map-explorer/the bundle budget to catch regressions.
Deep dive: Standalone + @if/@for control flow shrink output vs NgModules + star directives; Mattrx sets bundle budgets in angular.json to fail the build on bloat.
Senior signal: You cite AOT + tree-shaking + lazy loading + standalone, and enforce bundle budgets to catch size regressions in CI.
Block C — RxJS & state
17. Observables vs Promises; cold vs hot?
Short answer: A Promise is a single future value, eager, not cancelable. An Observable is a stream of 0..N values, lazy, cancelable, and composable with operators. Cold observables start work per subscriber (an HTTP call each); hot observables share one execution among subscribers (a Subject, a DOM event stream).
Deep dive: Angular's HttpClient returns cold observables — each subscribe fires the request; share/shareReplay makes them hot to avoid duplicate calls.
Senior signal: You contrast single/eager (Promise) with stream/lazy/cancelable (Observable) and know cold-per-subscriber vs hot-shared, using shareReplay to dedupe.
18. Which RxJS operators do you use most, and why?
Short answer: map/filter/tap for transforms; the flattening operators switchMap/mergeMap/concatMap/exhaustMap for chaining async; debounceTime/distinctUntilChanged for input; combineLatest/withLatestFrom to combine streams; catchError/retry for errors. The flattening operators are where seniors are separated from juniors.
Deep dive: Most bugs come from picking the wrong flattening operator (next question).
Senior signal: You group operators by purpose (transform / flatten / combine / handle errors) and know the flattening choice is the high-stakes one.
19. switchMap vs mergeMap vs concatMap vs exhaustMap?
Short answer: They all map to an inner observable and flatten, differing in how they handle overlap: switchMap cancels the previous inner on a new emission (typeahead); mergeMap runs all in parallel (independent writes); concatMap queues them in order (sequential, order matters); exhaustMap ignores new emissions while one is in flight (prevent double-submit).
Deep dive:
// Typeahead: switchMap cancels the in-flight request when a new term arrives -> no stale results.
this.search.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(term => this.api.search(term)),
).subscribe(r => this.results.set(r));
switchMap: new value -> CANCEL previous, start new (search/typeahead)
concatMap: queue -> run one after another, in order (ordered writes)
mergeMap: run all concurrently (independent parallel)
exhaustMap: ignore new while one runs (dedupe rapid submits)
Senior signal: You choose the flattening operator by the overlap semantics (cancel/queue/parallel/ignore), and reach for switchMap on search, exhaustMap on submit.
20. Subject vs BehaviorSubject vs ReplaySubject?
Short answer: A Subject is a multicast observable with no initial value (late subscribers miss past emissions). BehaviorSubject holds a current value and emits it immediately on subscribe (great for state). ReplaySubject replays the last N emissions to new subscribers.
Deep dive: Mattrx's simple state services expose a BehaviorSubject (or now a signal) so any subscriber immediately gets the current value.
Senior signal: You pick BehaviorSubject for "current state" (immediate value), ReplaySubject for history, and plain Subject for fire-and-forget events.
21. How do you prevent RxJS memory leaks?
Short answer: Unsubscribe from long-lived subscriptions on destroy. Best options: the async pipe (auto-unsubscribes), takeUntilDestroyed() (v16+, ties the stream to the component's lifecycle), or a takeUntil(destroy$) pattern. A subscription that outlives its component is a classic leak.
Deep dive:
// Ties the subscription to the component's lifetime — no manual ngOnDestroy needed.
this.api.pollKpis().pipe(takeUntilDestroyed()).subscribe(k => this.kpis.set(k));
Senior signal: You prefer the async pipe / takeUntilDestroyed over manual ngOnDestroy bookkeeping and know an un-torn-down subscription leaks.
22. What are signals, and how do they relate to RxJS?
Short answer: Signals are Angular's built-in synchronous reactive primitive — a value you read as a function, with computed() for derived values and effect() for side effects. They power fine-grained, zoneless change detection. RxJS remains for async streams/events; signals are for synchronous state. They interop (toSignal/toObservable).
Deep dive:
count = signal(0);
double = computed(() => this.count() * 2); // recomputes only when count changes
constructor() { effect(() => console.log(this.count())); }
this.count.update(n => n + 1); // triggers dependents precisely
Senior signal: You use signals for synchronous state (and derived values), keep RxJS for async streams, and bridge them with toSignal/toObservable.
23. How do you manage state — services vs NgRx?
Short answer: For most apps, a service with signals (or a BehaviorSubject) is enough — simple, no boilerplate. Reach for NgRx (Redux-style store, actions, reducers, effects) when state is large, shared across many features, and you need time-travel/devtools/strict traceability. Don't add NgRx to a small app.
Deep dive: Mattrx's admin console uses signal-based service state; NgRx would be over-engineering for its scope.
Senior signal: You default to signal/service state and justify NgRx only by real scale/traceability needs, not habit.
24. How do you handle errors in RxJS?
Short answer: catchError to intercept and return a fallback/rethrow; retry/retryWhen for transient failures (with backoff); and remember an unhandled error terminates the stream — so on long-lived streams, catch inside the inner observable (e.g., inside switchMap) so the outer stream survives.
Deep dive: A common bug: catchError on the outer stream swallows the error but also kills the subscription; catching inside the inner projection keeps the typeahead alive after a failed request.
Senior signal: You place catchError inside the inner observable to keep long-lived streams alive, and add retry with backoff for transient faults.
Block D — routing, forms, testing & production
25. How do route guards and resolvers work?
Short answer: Guards decide navigation — modern Angular uses functional guards (CanActivateFn) that inject() services and return a boolean/UrlTree. Resolvers pre-fetch data before a route activates so the component loads with data ready. Guards gate; resolvers pre-load.
Deep dive:
export const authGuard: CanActivateFn = () =>
inject(AuthService).isLoggedIn() || inject(Router).createUrlTree(['/login']);
Senior signal: You use functional guards with inject(), return a UrlTree to redirect, and know resolvers pre-load data (used judiciously, as they delay navigation).
26. Reactive vs template-driven forms?
Short answer: Reactive forms define the model in TypeScript (FormGroup/FormControl) — explicit, typed, testable, great for complex/dynamic forms. Template-driven builds the model from template directives (ngModel) — simpler for small forms. Seniors default to reactive for anything non-trivial.
Deep dive:
form = this.fb.group({
name: ['', [Validators.required, Validators.maxLength(120)]],
budget: [0, [Validators.min(0), this.budgetWithinPlan()]], // custom validator
});
Senior signal: You choose reactive forms for complex/dynamic/testable cases, write custom validators as functions, and use typed forms.
27. What are HTTP interceptors used for?
Short answer: Cross-cutting HTTP concerns: attach auth tokens, add correlation IDs, handle errors globally, retry transient failures, show loading state. Modern Angular uses functional interceptors (HttpInterceptorFn) registered with withInterceptors.
Deep dive:
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = inject(AuthService).token();
return next(req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }));
};
Senior signal: You put auth/error/retry/correlation in functional interceptors so every request is handled uniformly, not per-call.
28. How do you test Angular components?
Short answer: TestBed configures a testing module; you render a component, query the DOM via the ComponentFixture, trigger changes, call fixture.detectChanges(), and assert. Mock services via DI providers. Handle async with fakeAsync/tick or waitForAsync, and mock HTTP with HttpTestingController.
Deep dive: The classic pitfall is forgetting detectChanges() (nothing updates) or not flushing async work (tick()), so assertions run before the view settles.
Senior signal: You use TestBed + fixture, mock via DI, and manage async with fakeAsync/tick and HttpTestingController.
29. How does Angular protect against XSS?
Short answer: Angular auto-sanitizes interpolated values and property bindings by context (HTML/URL/style) — untrusted content is escaped by default. You only bypass via DomSanitizer (bypassSecurityTrustHtml) for content you've vetted, and never build templates from user strings. Avoid innerHTML with untrusted data.
Deep dive: The danger is a careless bypassSecurityTrust* on user input — that reopens the XSS hole Angular closed for you.
Senior signal: You rely on Angular's contextual auto-sanitization, treat bypassSecurityTrust* as a red flag needing justification, and never template user input.
30. What Angular anti-patterns do you watch for in production?
Short answer: Not using OnPush, function calls in templates, manual subscriptions without teardown (leaks), missing track on loops, any-typed everything, giant components doing too much, over-using NgRx, and heavy synchronous work in the zone. Most Angular perf/maintainability pain is one of these.
Deep dive: The through-line: fighting the framework's reactivity (default CD, no immutability, leaked subscriptions) instead of working with it (OnPush, signals, async pipe).
Senior signal: You have a concrete checklist and frame most problems as "not working with Angular's change-detection/reactivity model."
Bonus — what makes an answer "senior" in Angular?
Short answer: A junior uses the APIs; a senior understands change detection, the reactivity model, and RxJS semantics — and designs to work with them. "Why is the app janky?" gets a shrug from a junior and "default CD is re-checking a 2,000-node tree on every mousemove — OnPush + signals cut it to the subtree that changed" from a senior.
Deep dive: The recurring senior lens across all 30: Angular is fast and maintainable when you cooperate with change detection (OnPush, immutability, signals), pick the right RxJS operator, and tear down subscriptions. Fight it and you get jank and leaks.
Senior signal: You reason from change detection + reactivity, choose RxJS operators by their overlap semantics, and prevent leaks by construction.
The model to carry forward
Angular rewards developers who understand the engine: change detection is zone-triggered top-down dirty checking (so use OnPush + immutability + signals to check less), RxJS flattening operators differ by how they handle overlap (switch/concat/merge/exhaust), subscriptions must be torn down (async pipe / takeUntilDestroyed), and signals bring precise, zoneless reactivity. Answer each question with the mechanism and the failure it prevents, and you'll sound like someone who has shipped and profiled a real Angular app — which is the signal.
Further reading
- React Interview Questions for Senior Developers — Hooks, Reconciliation, Fiber & Performance (Deep Answers)
- JavaScript Interview Questions for Senior Developers — Closures, the Event Loop, Prototypes & Async (Deep Answers)
- ASP.NET Core Interview Questions for Senior Developers — Middleware, DI, Kestrel & Auth (Deep Answers)
Prepping for a senior Angular interview and want these drilled live, or a mock focused on change detection and RxJS? I'm always happy to help — reach me at randhir.jassal@gmail.com.
Get the next issue
A short, curated email with the newest posts and questions.