Senior Angular Interview Questions (2026): Architecture, Signals, RxJS, and the Real Senior Bar
— Written by Cristian Sifuentes • Jan 2, 2026
I’ve been sitting on the other side of the interview table for Senior Angular roles. Most candidates are technically “proficient”: they can call APIs, bind data, and close tickets. But when you peel back the layers, many are stuck in a bubble of routine.
Being a Senior isn’t measured in how many years you’ve spent writing ngOnInit(). It’s measured in whether you understand why Angular behaves the way it does, whether you can recognize architectural smells early, and whether you can defend trade-offs without turning every decision into dogma.
This is a production-minded checklist of interview conversations that separate “I can build screens” from “I can ship systems.”
Table of Contents
- Part 1 — Architecture & Philosophy
- 1) State management: library or custom?
- 2) Composition vs inheritance (and why BaseComponent is a trap)
- Part 2 — Deep Angular Questions
- 3) NgZone and performance (when to step outside)
- 4) DI resolution modifiers:
@Host,@Self,@SkipSelf,@Optional - 5) Why pipes are “safe” but functions are “dangerous” in templates
- 6) Signals vs RxJS for UI state (glitches, batching, and intent)
- 7) Can we ditch lifecycle hooks yet?
- 8) Sharing data between components: 3 senior patterns
- Part 3 — Practical Case Studies (RxJS at scale)
- 9) Multi-source dashboard:
combineLatest - 10) “Button masher” problem:
exhaustMap - 11) Recursive feed / prefetch:
expand
- 9) Multi-source dashboard:
- Senior bar checklist (what interviewers actually look for)
- Closing thoughts
Part 1 — Architecture & Philosophy
1) State management: library or custom?
If you answer “NgRx” or “Signals” and stop there, you lose points.
A senior answer is: there is no right answer—only trade-offs, and your job is to choose the trade-off that matches the product constraints.
Boilerplate vs control (NgRx)
NgRx buys you:
- a rigid, predictable model
- strong devtools
- explicit events → reducers → effects flows
It costs you:
- boilerplate and ceremony
- a steeper onboarding curve
- “architecture gravity” (everything becomes an action)
A senior conversation includes when that cost is justified:
- app-wide state with long-lived flows
- auditability and traceability requirements
- strict modeling and governance across teams
Signals for “local-global” state (provided services)
In 2026, many “global” state needs are not truly global—they’re shared state with simple rules. Signals + services give you a lightweight baseline:
- App-wide service with signals for theme/user/session
- Feature-scoped service provided at the route/component boundary
- Minimal coupling to RxJS when you don’t need streaming semantics
A senior will say something like:
“If the state is mostly synchronous UI state (tabs, selection, preferences), signals in a service are sufficient. If the state is event-sourced, multi-actor, or needs replay tooling, I’ll consider NgRx.”
That’s the bar: articulate the shape of the problem, then select the tool.
2) Architecture: composition vs inheritance
The most common senior smell in Angular codebases is the BaseComponent that slowly becomes a God-class.
The concept
Juniors often use inheritance to share logic:
class PlayersPage extends BaseListPageclass TeamsPage extends BaseListPage
Seniors prefer composition:
- directives, services, small utilities
- “plug-in behaviors” rather than “becoming a base type”
The rule of thumb
Inheritance is an is-a relationship. Composition is a has-a relationship.
“This component has sorting, has pagination” beats “this component is a BasePage.”
Case study: the searchable list
You have three pages: Players, Teams, Games. All need filtering.
Inheritance approach: BaseListComponent with shared filtering logic.
The problem: Games needs a date filter. You add conditionals. Soon it’s a switch + if/else fortress.
Composition approach: build a standalone behavior (directive or service), then plug it in.
// 1) Create a standalone behavior (Directive)
import { Directive, output, signal } from '@angular/core';
@Directive({
standalone: true,
selector: '[appSearchable]',
})
export class SearchableDirective {
// Keep it generic: the directive emits the query,
// the component decides what "filtering" means for its domain.
query = signal('');
filter = output<string>();
onInput(value: string) {
this.query.set(value);
this.filter.emit(value);
}
}
// 2) Plug it in where needed (Component)
import { Component, signal } from '@angular/core';
import { SearchableDirective } from './searchable.directive';
@Component({
standalone: true,
selector: 'app-player-list',
imports: [SearchableDirective],
template: `
<input
class="w-full rounded-xl border px-3 py-2"
appSearchable
(input)="search.onInput(($any($event.target).value))"
(filter)="onFilter($event)"
placeholder="Search players..."
/>
<!-- render list... -->
`,
})
export class PlayerListComponent {
filtered = signal<string[]>([]);
onFilter(q: string) {
// domain logic lives here
}
}
Why this wins interviews
- Decoupled: add/remove behavior without rewriting page logic
- Testable: test directive once; test domain filtering separately
- Modern Angular alignment: directive composition + signals-first APIs scale better than inheritance
Part 2 — Deep Angular Questions
3) NgZone and performance: when do you step outside?
Angular’s change detection is triggered by Zone.js patching browser events. If you attach high-frequency events (scroll, resize, animation frames), you can accidentally ask Angular to re-check the entire component tree 60 times per second.
The senior move is to run background logic outside Angular, then re-enter only when the UI must change.
import { Component, NgZone, inject, signal } from '@angular/core';
@Component({
standalone: true,
selector: 'app-scroll-metrics',
template: `
<div class="text-sm opacity-80">ScrollY: {{ y() }}</div>
`,
})
export class ScrollMetricsComponent {
private zone = inject(NgZone);
y = signal(0);
ngOnInit() {
this.zone.runOutsideAngular(() => {
const onScroll = () => {
const next = window.scrollY;
// Only re-enter the zone if you actually update UI state.
this.zone.run(() => this.y.set(next));
};
window.addEventListener('scroll', onScroll, { passive: true });
});
}
}
What interviewers want to hear:
- awareness of change detection cost
- intentionality about event frequency
- ability to isolate UI updates vs background work
4) DI resolution modifiers: @Host, @Self, @SkipSelf, @Optional
If I ask this in an interview, I’m not quizzing syntax. I’m checking whether you understand DI boundaries and UI composition.
-
@Self()— resolve only on the current injector (often “this element”) -
@SkipSelf()— ignore current injector, look upward -
@Host()— stop resolution at the host boundary (useful with directives/components) -
@Optional()— allownullwhen a provider is not found (avoid DI errors)
Real use case: UI library directive that must only interact with a provider on its own element (like NgControl):
import { Directive, Optional, Self } from '@angular/core';
import { NgControl } from '@angular/forms';
@Directive({
standalone: true,
selector: '[appControlBridge]',
})
export class ControlBridgeDirective {
constructor(@Optional() @Self() private readonly ngControl: NgControl | null) {
// If no NgControl exists on THIS element, the directive disables itself safely.
}
}
Senior-level signal:
- you talk about hierarchies and boundaries
- you tie modifiers to reusable components and library safety
5) Why pipes are “safe” but functions are “dangerous” in templates
Functions in templates execute on every change detection cycle. If the function allocates, maps arrays, or formats values, it silently becomes a performance leak.
Pure pipes are “safer” because Angular can cache based on input reference changes (not deep equality).
What interviewers want:
- you know the difference between pure and impure pipes
- you don’t use templates as a compute engine
- you move heavy work into signals/computed or memoized selectors
A senior replacement pattern:
- precompute view-model with
computed() - format with a pipe only when needed
6) Signals vs RxJS for UI state (glitches, batching, intent)
This question checks whether you understand semantics, not libraries.
In RxJS, streams derived from the same source can “glitch” when combined: downstream may emit multiple times in ways that aren’t aligned with UI intent (especially when you update related sources).
Signals are synchronously readable and batched. Update two signals and the dependent computed recalculates once, not twice.
Senior framing:
- RxJS is still best for async streams (HTTP, websockets, events, cancellation)
- Signals shine for local UI state (selection, toggles, derived view-model)
- The correct architecture composes both: streams feed signals, signals feed the template
7) Can we ditch lifecycle hooks yet?
Not entirely—but we can shrink them dramatically.
In a signals-first component:
-
ngOnChangesbecomeseffect()tied toinput()signals -
ngOnInitis often unnecessary because inputs exist immediately -
ngOnDestroyis often replaced byDestroyRef+takeUntilDestroyed/ cleanup functions
What a senior says:
“Hooks are lifecycle plumbing. If signals/effects express the lifecycle declaratively, I prefer that—less wiring, fewer edge cases.”
8) Sharing data between components: 3 senior patterns
If you answer “services,” you’re only 30% done. The senior answer is: it depends on distance and lifetime.
Pattern 1 — Direct communication (Parent → Child)
- Data flows down via signal inputs
- Events bubble up via outputs
- Use
input.required()to move errors from runtime to compile-time
Pattern 2 — Scoped state (Feature-specific)
Provide a service at the feature root component or route:
- service instance is private to that subtree
- when user navigates away, the service is destroyed
- prevents stale state and memory leaks across navigations
Pattern 3 — Global state (App-wide)
Use providedIn: 'root' + signals for truly app-wide state:
- user profile, theme settings, cart
- synchronously readable state without subscription overhead
What interviewers want:
- you say “scope it”
- you talk about lifecycle and cleanup
- you can justify why something is global vs feature-scoped
Part 3 — Practical Case Studies (RxJS at scale)
9) Case: multi-source dashboard
Scenario: show profile data from three sources (bio, preferences, permissions). UI should render once all three exist, and update if any source updates later.
Answer: combineLatest.
Why: forkJoin emits once and completes. combineLatest stays alive and re-emits whenever any source changes after all have emitted at least once.
import { combineLatest, map } from 'rxjs';
readonly dashboard$ = combineLatest({
bio: this.api.getBio(id),
prefs: this.api.getPrefs(id),
perms: this.socket.getPermissions(id),
}).pipe(
map(({ bio, prefs, perms }) => ({
name: bio.name,
theme: prefs.theme,
canEdit: perms.includes('edit'),
}))
);
Senior extras:
- cancellation strategy (
switchMapwhen id changes) - error boundary strategy (
catchErrorper source vs at the edge) - UI gating (skeleton until first combined emission)
10) Case: the “button masher” problem
Scenario: user clicks “Save” repeatedly while a 2s POST is in-flight. Ignore clicks until request completes.
Answer: exhaustMap.
import { exhaustMap } from 'rxjs/operators';
save$ = this.clicks$.pipe(
exhaustMap(() => this.api.saveData(payload))
).subscribe();
Senior extras:
- disable the button via UI state (signals)
- handle failure without unlocking prematurely
- surface progress feedback (optimistic UI vs confirmed UI)
11) Case: the recursive feed (prefetch next pages)
Scenario: infinite scroll API returns { hasNext: boolean }. You want to auto-fetch the next 3 pages on load.
Answer: expand (recursive operator).
import { EMPTY } from 'rxjs';
import { expand, reduce } from 'rxjs/operators';
this.api.getPage(1).pipe(
expand((res, index) =>
(res.hasNext && index < 3) ? this.api.getPage(index + 2) : EMPTY
),
reduce((acc, res) => [...acc, ...res.items], [] as any[])
).subscribe(all => this.list = all);
Senior extras:
- use scheduler/backpressure strategies if needed
- cancel on route change (
takeUntilDestroyed) - cache pages to avoid duplicate fetches
Senior bar checklist (what we are really looking for)
Framework versions change. Best practices evolve. The “senior bar” is stable:
✅ Architectural reasoning
You don’t just recommend patterns. You justify them.
“I’d use a feature-scoped service here to avoid stale state across navigation, even though it introduces an extra provider boundary.”
✅ Problem-solving attitude
You can say “I haven’t used that operator” and still reason correctly.
You derive behavior from primitives, not memorized recipes.
✅ Mentorship and humility
Technical depth without collaboration is a liability.
Seniors defend decisions respectfully and change their mind when presented better evidence.
✅ UX awareness
You think about the user experience:
- loading states
- error states
- focus and accessibility
- feedback loops (disable vs throttle vs queue)
We aren’t hiring a human documentation manual. We’re hiring a thought partner.
Closing thoughts
The best senior interview answers are not about being “correct.” They’re about being clear:
- what are we optimizing for?
- what do we give up?
- what failure modes exist?
- how do we keep it maintainable?
If you can consistently explain trade-offs, scope decisions intentionally, and connect Angular mechanics to UX and performance, you’re already playing at the senior level.
— Cristian Sifuentes
Full‑stack Engineer • Angular • Reactive Systems

Top comments (0)