---
title: Angular Interview Questions & Answers (2026): RxJS & Signals
description: Angular interview questions that actually get asked in 2026 — dependency injection, RxJS, change detection, standalone components and signals — with real code and answers.
url: https://usegreenroom.app/blog/angular-interview-questions
last_updated: 2026-06-20
---

← Back to blog

Technical

# Angular interview questions and answers

June 20, 2026 · 35 min read

![Angular interview questions and answers — cover from Greenroom, the AI mock interviewer](/assets/blog/angular-interview-questions-hero.webp)

Picture this. A candidate is forty minutes into a video round for a senior frontend role at a mid-size fintech in Bengaluru. The interviewer, a calm, slightly bored-looking engineering manager, asks the question every Angular developer secretly dreads: "Can you walk me through how change detection actually works?"

The candidate, full of confidence from three LeetCode-adjacent flashcard decks and a YouTube binge the night before, says: "Sure — Angular checks every component every time something changes, kind of like polling, and it's slow, that's why we use OnPush to turn it off."

The interviewer's webcam light blinks. There's a pause that lasts exactly one beat too long — the kind of pause where you can *feel* a red flag being typed into a feedback form somewhere. "Turn it off?" the interviewer repeats, gently, the way you'd repeat a toddler's claim that the dog ate their homework. The candidate doubles down: "Yeah, OnPush disables change detection for that component." Which is not what OnPush does, even a little bit, and the rest of the interview is spent watching the candidate try to verbally backpedal out of a hole they dug with their own keyboard.

This is not a hypothetical written for dramatic effect — versions of this exact exchange happen constantly in **Angular interview questions** rounds, because change detection is one of those topics where almost-right sounds suspiciously like memorized-wrong. Angular is not a library you sprinkle into an existing page like jQuery used to be. It's a full, opinionated framework with its own dependency injection system, its own reactivity model (first Zone.js, now increasingly signals), and its own compiler. Interviewers at companies running Angular at scale — and there are a *lot* of them, especially in enterprise software, banking, and the large Indian IT services firms (TCS, Infosys, Capgemini, Cognizant, and the consulting shops that build internal tools for clients who picked Angular in 2018 and never looked back) — are checking whether you understand the architecture, not whether you can recite the lifecycle hook names in alphabetical order.

This guide covers the **Angular interview questions** that actually come up in 2026: dependency injection, RxJS observables (operators, the async pipe, Subjects and BehaviorSubjects), change detection (default vs OnPush, and what Zone.js is really doing under the hood), lifecycle hooks, standalone components, and Angular's newer **signals**-based reactivity model. Real code, not just definitions — because "explain it" and "write it" are different skills, and interviewers increasingly test both.

## Components, modules, and how Angular wires it all together

### Components, modules, services — how do they actually fit together?

A **component** is a TypeScript class decorated with `@Component`, paired with an HTML template and (usually) a CSS file, responsible for one piece of UI. A **service** is a plain class decorated with `@Injectable`, responsible for logic and state that doesn't belong to any one component — fetching data, caching, formatting, talking to a backend. An **NgModule** (in the pre-standalone model) was a container that declared which components belonged together and which services were available to them.

Here's a minimal real example — a component that depends on a service:

```typescript
import { Component, OnInit } from '@angular/core';
import { CandidateService } from './candidate.service';

@Component({
  selector: 'app-candidate-list',
  template: `
    <ul>
      <li *ngFor="let c of candidates">{{ c.name }} — {{ c.role }}</li>
    </ul>
  `
})
export class CandidateListComponent implements OnInit {
  candidates: { name: string; role: string }[] = [];

  constructor(private candidateService: CandidateService) {}

  ngOnInit(): void {
    this.candidateService.getCandidates().subscribe(data => {
      this.candidates = data;
    });
  }
}
```

```typescript
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class CandidateService {
  constructor(private http: HttpClient) {}

  getCandidates(): Observable<{ name: string; role: string }[]> {
    return this.http.get<{ name: string; role: string }[]>('/api/candidates');
  }
}
```

The component never instantiates `CandidateService` itself with `new CandidateService()` — it asks for one in its constructor, and Angular's injector hands it an instance. That single design decision — components *declare* what they need instead of *constructing* it — is the whole reason dependency injection exists, and it's the next question almost every interviewer asks immediately after this one.

### What is dependency injection, and why does Angular use it?

**Dependency injection (DI)** is a design pattern where a class receives its dependencies from an external source rather than creating them itself. In Angular, you declare a dependency as a constructor parameter, and Angular's injector resolves and supplies an instance at runtime, based on how that dependency was registered (its "provider").

Why does this matter beyond being a fancy pattern name? Three concrete reasons interviewers want you to articulate:

1. **Testability.** If `CandidateListComponent` created its own `CandidateService` internally, you couldn't substitute a fake one in a unit test without monkey-patching. Because it's injected, a test can provide a mock `CandidateService` that returns canned data instantly, with no real HTTP call.
2. **Singleton control.** Registering a service with `providedIn: 'root'` makes Angular create exactly one instance for the whole app — shared state (like an auth session or a websocket connection) lives in one place instead of being recreated per component.
3. **Swappability.** You can register *different* implementations behind the same injection token depending on environment — a `MockPaymentService` in dev, a `RealPaymentService` in prod — without touching the components that consume it.

Here's DI scoping in practice — note the difference between `providedIn: 'root'` and providing a service at the component level:

```typescript
@Injectable({ providedIn: 'root' })
export class AuthService { /* one instance, app-wide */ }

@Component({
  selector: 'app-interview-room',
  providers: [SessionTimerService], // new instance per component instance
  template: `...`
})
export class InterviewRoomComponent {
  constructor(private timer: SessionTimerService) {}
}
```

`AuthService` is a true app-wide singleton — every component that injects it gets the same instance. `SessionTimerService`, declared in the component's own `providers` array, gets a fresh instance *per instance of that component* — useful when each interview room needs its own independent timer that shouldn't leak state into a different room. The interview signal here: can you explain *when* you'd choose component-level providers over root-level, not just that the syntax exists.

### Data binding types — interpolation, property, event, two-way

Angular has four binding flavors, and interviewers like asking you to name all four because candidates reliably forget one (usually two-way):

```html
<!-- Interpolation: component → view, text content -->
<h1>{{ candidateName }}</h1>

<!-- Property binding: component → view, DOM property -->
<button [disabled]="isSubmitting">Submit</button>

<!-- Event binding: view → component -->
<button (click)="startInterview()">Start</button>

<!-- Two-way binding: component ↔ view (sugar over property + event) -->
<input [(ngModel)]="candidateName">
```

`[(ngModel)]` is shorthand for `[ngModel]="candidateName" (ngModelChange)="candidateName = $event"` — knowing that it desugars into property binding plus event binding (not some separate magic mechanism) is the kind of detail that separates "used Angular" from "understands Angular."

### Component lifecycle hooks

Angular calls a sequence of lifecycle methods on every component as it's created, updated, and destroyed. The ones that actually come up in interviews:

```typescript
import { Component, OnInit, OnChanges, OnDestroy, SimpleChanges, Input } from '@angular/core';
import { Subscription } from 'rxjs';

@Component({ selector: 'app-timer', template: `{{ secondsLeft }}` })
export class TimerComponent implements OnInit, OnChanges, OnDestroy {
  @Input() durationSeconds = 60;
  secondsLeft = 0;
  private sub?: Subscription;

  ngOnChanges(changes: SimpleChanges): void {
    // Runs before ngOnInit on first call, and on every @Input change after that
    if (changes['durationSeconds']) {
      this.secondsLeft = this.durationSeconds;
    }
  }

  ngOnInit(): void {
    // Runs once, after the first ngOnChanges, inputs are guaranteed set
    this.sub = interval(1000).subscribe(() => this.secondsLeft--);
  }

  ngOnDestroy(): void {
    // Runs once, right before Angular removes the component — clean up here
    this.sub?.unsubscribe();
  }
}
```

The order matters and interviewers test it: `ngOnChanges` (if there are `@Input`s) fires before `ngOnInit`, every single time inputs change — not just once. `ngOnInit` fires exactly once. `ngOnDestroy` is the one candidates skip in real code and then get burned by later — it's where you unsubscribe from Observables, clear intervals, and detach event listeners, and skipping it is the single most common cause of memory leaks in real Angular apps (more on this in the RxJS section, because the two topics are basically the same bug wearing different hats).

### @Input, @Output, and component communication

```typescript
// child.component.ts
@Component({
  selector: 'app-rating-stars',
  template: `
    <span *ngFor="let s of [1,2,3,4,5]" (click)="select(s)">
      {{ s <= rating ? '★' : '☆' }}
    </span>
  `
})
export class RatingStarsComponent {
  @Input() rating = 0;
  @Output() ratingChange = new EventEmitter<number>();

  select(value: number): void {
    this.rating = value;
    this.ratingChange.emit(value);
  }
}
```

```html
<!-- parent.component.html -->
<app-rating-stars [rating]="interviewScore" (ratingChange)="onScoreChange($event)"></app-rating-stars>
```

Data flows down via `@Input`, events flow up via `@Output` and `EventEmitter`. If you name the output `ratingChange` for an input named `rating`, Angular lets you collapse both into Angular's own two-way binding sugar: `[(rating)]="interviewScore"` — a detail interviewers sometimes ask about specifically because it explains *why* `[(ngModel)]` works the way it does (it's the same `xChange` convention, applied to Angular's own built-in directive).

## RxJS and asynchronous Angular

### What is an Observable, and how is it different from a Promise?

A **Promise** represents a single future value — it resolves once, and that's it. An **Observable** represents a *stream* of values over time — it can emit zero, one, or many values, and it can be cancelled mid-stream. Angular leans on RxJS Observables everywhere — `HttpClient` requests, route parameter changes, form value changes, the `async` pipe — because so much of what a UI does is genuinely a stream, not a one-shot value: a search box's keystrokes, a websocket's messages, a timer's ticks.

```typescript
// Promise: one value, no cancellation
fetch('/api/candidates').then(res => res.json()).then(data => console.log(data));

// Observable: streamable, cancellable
const sub = this.http.get('/api/candidates').subscribe(data => console.log(data));
sub.unsubscribe(); // cancels the in-flight request if it hasn't resolved yet
```

That cancellability is the practical reason Observables, not Promises, back Angular's HTTP layer: if a user navigates away from a page before a request resolves, Angular can cancel it. A Promise, once created, can't be cancelled — you can only ignore its eventual result.

### Common RxJS operators — map, filter, switchMap, mergeMap

Operators transform a stream without mutating the original Observable — each one returns a new Observable. The four interviewers ask about most:

```typescript
import { of } from 'rxjs';
import { map, filter, switchMap, mergeMap } from 'rxjs/operators';

// map: transform each emitted value
this.candidateService.getCandidates().pipe(
  map(candidates => candidates.filter(c => c.role === 'frontend'))
);

// filter: only let through values matching a predicate
searchInput$.pipe(
  filter(query => query.length > 2)
);

// switchMap: map to a new Observable, cancelling the previous inner one
searchInput$.pipe(
  switchMap(query => this.http.get(`/api/search?q=${query}`))
);

// mergeMap: map to a new Observable, running all inner ones concurrently
candidateIds$.pipe(
  mergeMap(id => this.http.get(`/api/candidates/${id}`))
);
```

`switchMap` vs `mergeMap` is the single most-asked RxJS follow-up, because the difference has real consequences: in a search-as-you-type box, you want `switchMap` — if the user types "ang" then "angular," you want the "ang" request *cancelled*, not just ignored, because otherwise a slow "ang" response can land *after* the "angular" response and overwrite it with stale results on screen. `mergeMap` keeps every inner Observable alive concurrently, which is correct when requests are independent and you want all of them to complete — like firing off five candidate-detail lookups in parallel and collecting every result, none of which should cancel another.

Getting this wrong in production looks exactly like the typeahead race-condition bug covered in [frontend system design interviews](/blog/frontend-developer-interview-questions) — same underlying problem, RxJS just gives you a one-operator fix for it (`switchMap`) instead of hand-rolling an `AbortController`.

### The async pipe, and the subscribe/unsubscribe memory leak problem

```html
<!-- Manual subscribe — you own the cleanup -->
<ul><li *ngFor="let c of candidates">{{ c.name }}</li></ul>
```
```typescript
ngOnInit(): void {
  this.sub = this.candidateService.getCandidates().subscribe(data => this.candidates = data);
}
ngOnDestroy(): void {
  this.sub.unsubscribe(); // forget this and you've got a leak
}
```

```html
<!-- async pipe — Angular owns the cleanup -->
<ul><li *ngFor="let c of (candidates$ | async)">{{ c.name }}</li></ul>
```
```typescript
candidates$ = this.candidateService.getCandidates();
// no manual subscribe, no manual unsubscribe — the template handles both
```

The `async` pipe subscribes when the template renders and automatically unsubscribes when the component is destroyed — which eliminates an entire category of bugs at the source. The manual version is fine *if* you remember `ngOnDestroy`, but "if you remember" is exactly the kind of sentence that should make you nervous in a large codebase with dozens of contributors. The classic interview war story here, the one worth having ready: a component subscribes to a `websocket$` stream in `ngOnInit` without storing the subscription, the user navigates the route in and out of that component fifty times in a session, and now there are fifty live subscriptions all still firing callbacks on a component that no longer exists — `ExpressionChangedAfterItHasBeenCheckedError` everywhere, CPU climbing, and a support ticket titled "app gets slower the longer it's open." It's the Angular equivalent of forgetting to call `clearInterval`, and yes, several real production incidents have looked exactly like that.

### Subjects vs BehaviorSubjects

A plain `Observable` (from `http.get`, for instance) is typically "cold" — it doesn't start producing values until something subscribes, and each subscriber can get an independent execution. A **Subject** is both an Observable *and* an Observer — you can `.next()` values into it manually, and it multicasts them to every current subscriber:

```typescript
import { Subject, BehaviorSubject } from 'rxjs';

const interviewEvents$ = new Subject<string>();
interviewEvents$.subscribe(e => console.log('Listener A:', e));
interviewEvents$.next('question-asked'); // Listener A logs it
interviewEvents$.subscribe(e => console.log('Listener B:', e));
interviewEvents$.next('answer-submitted'); // both A and B log it — but B missed "question-asked"
```

A **BehaviorSubject** is a Subject with memory — it requires an initial value and always replays its *most recent* value to any new subscriber, immediately on subscribe:

```typescript
const currentScore$ = new BehaviorSubject<number>(0);
currentScore$.subscribe(score => console.log('Late subscriber sees:', score)); // logs 0 immediately
currentScore$.next(85);
currentScore$.subscribe(score => console.log('Even later subscriber sees:', score)); // logs 85 immediately
```

This is the practical reason most shared app state in Angular services is backed by a `BehaviorSubject`, not a plain `Subject`: a component that subscribes to "current user" or "current interview state" *after* that state was already set needs to see the current value immediately, not wait silently for the next change that might never come.

![Angular interview topics shown on a structured technical interview screen](/assets/blog/pool-structured-screen.webp)

Angular rounds test the framework's structure — DI, RxJS streams, and change detection — not just whether you recognize the API names.

## Change detection: the part everyone half-understands

### How does Angular's change detection actually work?

This is the question from the cold open, and it's worth getting precisely right because "almost right" is where candidates get caught. Angular doesn't poll your data on a timer. Historically (and still, by default, in most apps), Angular uses **Zone.js**, a library that monkey-patches asynchronous browser APIs — `setTimeout`, `Promise.then`, DOM event handlers, `XMLHttpRequest`, and more — so that Angular gets notified whenever any of them fire. When Zone.js detects one of these "could have changed something" events, it triggers a change detection pass: Angular walks the component tree (by default, the *whole* tree, top to bottom) and checks each component's template bindings against their previous values. If a binding's value changed, Angular updates that part of the real DOM.

The "candidate's wrong answer" from the cold open — "OnPush disables change detection" — is wrong in a specific, testable way: OnPush doesn't disable anything. It *narrows the conditions under which a component is checked at all*.

### Default vs OnPush — what actually changes

```typescript
@Component({
  selector: 'app-candidate-card',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `<div>{{ candidate.name }} — {{ score }}</div>`
})
export class CandidateCardComponent {
  @Input() candidate!: { name: string };
  @Input() score = 0;
}
```

With the **default** strategy, every change detection cycle (triggered by Zone.js on basically any async event, anywhere in the app) re-checks `CandidateCardComponent`'s bindings, even if nothing relevant to it changed. With **OnPush**, Angular only checks this component when one of three things happens:

1. An `@Input` reference changes (a *new object/array*, not a mutation of the existing one — this is the part that bites people).
2. An event originates from within the component itself (a click handler firing, for instance).
3. You manually call `ChangeDetectorRef.markForCheck()`, or an Observable bound via the `async` pipe emits.

That first point is the trap that catches almost everyone the first time they try OnPush: if a parent does `this.candidate.name = 'New Name'` (mutating the existing object), OnPush won't see it, because the *reference* to `candidate` never changed — only its insides did. If the parent instead does `this.candidate = { ...this.candidate, name: 'New Name' }` (a new object), OnPush detects the new reference and re-checks. This is exactly why OnPush pairs so naturally with immutable data patterns and with the RxJS/`async`-pipe style of state management covered above — both habits push you toward replacing objects rather than mutating them, which OnPush requires to function correctly anyway.

```typescript
// ❌ OnPush component won't re-render — same object reference
updateCandidateBroken(): void {
  this.candidate.name = 'Updated'; // mutation, reference unchanged
}

// ✅ OnPush component re-renders — new object reference
updateCandidateCorrect(): void {
  this.candidate = { ...this.candidate, name: 'Updated' };
}
```

### Zone.js, and why Angular is moving away from depending on it

Zone.js's trick — monkey-patching every async API in the browser to know when "something might have changed" — works, but it's a blunt instrument: it can't tell *which* component's data actually changed, only that *some* async event fired *somewhere*, so the default strategy's answer is "recheck everything, to be safe." It's also not free: Zone.js patching adds bundle size and a small but real performance tax to every async call in the app, which is why recent Angular versions ship a `provideZonelessChangeDetection()` option and the framework is steadily moving toward **signals** (next section) as a way to track dependencies precisely instead of conservatively re-checking the whole tree on every tick. The honest interview answer here isn't "Zone.js is bad" — it's "Zone.js made an entire generation of Angular apps correct-by-default at the cost of being broader than necessary, and signals are the fix for the 'broader than necessary' part."

### What causes performance issues, and how do you fix them?

The two real culprits interviewers want named:

1. **A huge default-strategy tree with frequent async events.** Every keystroke, every `setTimeout`, every HTTP response retriggers a full-tree check. Fix: push leaf/list components to OnPush, especially ones rendered many times (table rows, cards in a grid).
2. **Expensive work inside a template expression or a pipe without `pure: true`.** A function call directly in a template (`{{ getFilteredList() }}`) re-runs on *every* change detection pass, regardless of whether its inputs changed — if that function filters or sorts a large array, you're redoing that work dozens of times a second for no reason. Fix: compute it once in the component class when the source data actually changes, or use a pure pipe (which Angular only re-evaluates when its input reference changes — the same reference-equality rule as OnPush).

```typescript
// ❌ Recomputed on every change detection cycle, however many that is per second
get expensiveFilteredList() {
  return this.candidates.filter(c => c.score > 70).sort((a, b) => b.score - a.score);
}

// ✅ Computed once, only when candidates actually changes
@Input() set candidates(value: Candidate[]) {
  this._candidates = value;
  this.filteredList = value.filter(c => c.score > 70).sort((a, b) => b.score - a.score);
}
```

### Lazy loading and AOT vs JIT

**Lazy loading** splits the app's route modules into separate bundles loaded only when a user navigates to that route, instead of one giant initial bundle — the same idea as React's `lazy()`/code-splitting, applied at the router level. **AOT (Ahead-of-Time) compilation** compiles Angular templates to JavaScript at *build* time, shipping smaller, faster-starting bundles with errors caught at build time; **JIT (Just-in-Time)** compiles templates in the *browser* at runtime, which is slower to start and was historically the dev-mode default. Modern Angular CLI projects use AOT by default even in development — JIT mostly comes up in interviews as a "know the history" question now, not a real production choice.

### Pipes, pure vs impure, and a worked debugging example

Pipes transform values directly in a template — `{{ price | currency }}`, `{{ date | date:'short' }}`. By default, pipes are **pure**: Angular only re-runs a pure pipe when its input *reference* changes, exactly the same reference-equality rule that governs OnPush. An **impure** pipe (`@Pipe({ name: 'myPipe', pure: false })`) re-runs on *every* change detection cycle regardless of whether its input changed — which sounds convenient until you realize "every change detection cycle" might be dozens of times per second.

Here's a realistic bug, the kind interviewers love putting in front of you as a "spot the problem" exercise:

```typescript
@Pipe({ name: 'topScorers' })
export class TopScorersPipe implements PipeTransform {
  transform(candidates: Candidate[], minScore: number): Candidate[] {
    console.log('topScorers pipe ran'); // add this and watch it fire constantly
    return candidates.filter(c => c.score >= minScore).sort((a, b) => b.score - a.score);
  }
}
```

```html
<li *ngFor="let c of candidates | topScorers:70">{{ c.name }}</li>
```

This looks fine and, because the pipe is pure by default, it actually *is* fine — Angular only re-runs `transform` when `candidates` or `70` change by reference. The bug shows up when someone "fixes" a perceived staleness issue by mutating `candidates` in place (`this.candidates.push(newCandidate)`) instead of reassigning it (`this.candidates = [...this.candidates, newCandidate]`) — the pipe never re-runs, because the reference never changed, and the new candidate silently never appears in the sorted list. The fix is the same one-line habit as the OnPush fix earlier in this guide: stop mutating, start replacing. It's genuinely the same root cause wearing two different costumes, and naming that connection out loud is a strong interview signal — it shows you understand the underlying mechanism (reference equality), not two unrelated facts you memorized separately.

### A worked "design a feature" exercise: a live interview timer with auto-save

Senior Angular interviews increasingly include a short design exercise rather than a pure trivia round — something like "design a component that shows a countdown timer during a live interview, auto-saves the candidate's answer every 10 seconds, and cleans up properly when the user navigates away." Walking through this out loud is a good rehearsal, because it forces you to combine almost everything above into one coherent answer instead of reciting facts in isolation:

1. **State.** A `signal<number>` for `secondsRemaining` (synchronous, local, UI-only — a textbook signal use case) and an `answerText` signal bound to a textarea via `[(ngModel)]` or a reactive form control.
2. **The ticking timer.** `interval(1000)` from RxJS, mapped down to remaining seconds, because a timer is fundamentally a *stream* over time — exactly the asynchronous-stream case signals don't try to replace.
3. **Auto-save.** `answerText$` (exposed via `toObservable(answerTextSignal)`) piped through `debounceTime(2000)` and `switchMap(text => this.api.saveAnswer(text))` — debounce so you're not saving on every keystroke, `switchMap` so an in-flight save from an older draft never lands after, and overwrites, a newer one.
4. **Cleanup.** `ngOnDestroy` unsubscribes from the timer and the auto-save stream — or, more idiomatically in 2026 Angular, both are wired through `takeUntilDestroyed()`, which ties a subscription's lifetime to the component's automatically, removing the "forgot to unsubscribe" bug class entirely rather than relying on a developer remembering `ngOnDestroy` every time.
5. **Change detection.** If this component is marked OnPush (likely, since it's a leaf component rendered once per active interview), the `async`/signal bindings handle triggering re-checks correctly without you needing to call `markForCheck()` manually — which is exactly why OnPush and the signal/Observable-driven template style are designed to be used together.

```typescript
import { Component, signal } from '@angular/core';
import { interval } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { debounceTime, switchMap } from 'rxjs/operators';

@Component({
  selector: 'app-interview-timer',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `<p>{{ secondsRemaining() }}s remaining</p>`
})
export class InterviewTimerComponent {
  secondsRemaining = signal(600);

  constructor(private api: AnswerApiService, private destroyRef: DestroyRef) {
    interval(1000).pipe(
      takeUntilDestroyed(this.destroyRef)
    ).subscribe(() => this.secondsRemaining.update(s => Math.max(0, s - 1)));
  }
}
```

Notice there's no `ngOnDestroy` at all in that snippet — `takeUntilDestroyed()` handles it. An interviewer who sees you reach for that instead of manually wiring `ngOnDestroy` for a simple subscription will generally read it as "this person works in a current Angular codebase," which, fairly or not, carries weight.

## Standalone components — Angular without NgModules

Angular spent its first decade requiring every component to be declared inside an `NgModule`. Since Angular 14–19's standalone APIs matured (and became the CLI default), components can declare their own dependencies directly, no `NgModule` required:

```typescript
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router';

@Component({
  selector: 'app-candidate-list',
  standalone: true,
  imports: [CommonModule, RouterLink],
  template: `
    <ul>
      <li *ngFor="let c of candidates">
        <a [routerLink]="['/candidates', c.id]">{{ c.name }}</a>
      </li>
    </ul>
  `
})
export class CandidateListComponent {
  candidates: { id: number; name: string }[] = [];
}
```

No `@NgModule` declaring this component, no separate module file to keep in sync. Bootstrapping a whole app standalone looks like this:

```typescript
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient } from '@angular/common/http';
import { provideRouter } from '@angular/router';
import { AppComponent } from './app.component';
import { routes } from './app.routes';

bootstrapApplication(AppComponent, {
  providers: [provideRouter(routes), provideHttpClient()]
});
```

Why interviewers ask about this: it's a genuine architectural shift, not a syntax tweak. NgModules existed largely to solve a problem — "which components/directives/pipes can see each other, and which services are available where" — that standalone components solve more directly, by having each component list its own dependencies. The interview-worthy nuance: standalone components didn't *remove* DI or lazy loading, they removed the NgModule *boilerplate* layer that used to be required to configure both. A candidate who says "standalone components got rid of dependency injection" has it backwards — DI is more central than ever, it's just configured via `provideX()` functions at bootstrap instead of `NgModule` `providers` arrays.

## Testing Angular components and services

Testing comes up more often than candidates expect, usually as "how would you test this service" rather than a request to write a full test file from memory. The DI design covered earlier is what makes Angular testing tractable in the first place — because services are injected, not constructed internally, you can swap in a fake for any dependency in a test without touching the class under test:

```typescript
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { CandidateService } from './candidate.service';

describe('CandidateService', () => {
  let service: CandidateService;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [CandidateService]
    });
    service = TestBed.inject(CandidateService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  it('fetches candidates from the API', () => {
    service.getCandidates().subscribe(candidates => {
      expect(candidates.length).toBe(2);
    });

    const req = httpMock.expectOne('/api/candidates');
    req.flush([{ name: 'Asha', role: 'frontend' }, { name: 'Rohan', role: 'backend' }]);
  });
});
```

`HttpClientTestingModule` replaces the real `HttpClient` with one that records requests instead of sending them over the network — `req.flush(...)` lets the test control exactly what the "server" responds with, deterministically, with no real network call and no flakiness. This is the same DI principle from the start of this guide, just applied to tests instead of runtime: because `CandidateService` asks for an `HttpClient` rather than constructing one, the test can supply a fake one.

Component tests follow the same shape via `TestBed.createComponent`, and the interview-relevant nuance for OnPush components specifically: a test that mutates an `@Input` object in place and expects the template to reflect it will fail unless the test explicitly calls `fixture.detectChanges()` again *and* the input was replaced by reference, not mutated — the exact same rule that governs OnPush in production, showing up again in a test file. Once you see this pattern (reference equality) recurring across change detection, pure pipes, and tests, the whole framework starts feeling like variations on one idea rather than dozens of unrelated facts to memorize — which is itself a good thing to say out loud in an interview if a follow-up question gives you the opening.

## Angular signals — the new reactivity model

Signals are Angular's newer, fine-grained reactivity primitive, designed to let Angular know *exactly* which piece of state changed and update *only* what depends on it — no Zone.js, no "recheck everything to be safe."

```typescript
import { Component, signal, computed, effect } from '@angular/core';

@Component({
  selector: 'app-score-tracker',
  standalone: true,
  template: `
    <p>Raw score: {{ score() }}</p>
    <p>Grade: {{ grade() }}</p>
    <button (click)="increment()">+5</button>
  `
})
export class ScoreTrackerComponent {
  score = signal(0);
  grade = computed(() => (this.score() >= 80 ? 'Strong hire' : this.score() >= 50 ? 'Borderline' : 'No hire'));

  constructor() {
    effect(() => console.log(`Score changed to ${this.score()}, grade is now ${this.grade()}`));
  }

  increment(): void {
    this.score.update(current => current + 5);
  }
}
```

Three pieces, and interviewers expect you to distinguish them precisely:

- **`signal()`** creates a reactive, readable-as-a-function value — read it with `score()`, never just `score`. Update it with `.set(value)` (replace) or `.update(fn)` (derive from current value).
- **`computed()`** derives a new signal from others — it automatically tracks which signals it reads (`score()` here) and recalculates *only* when one of them actually changes, without you declaring a dependency array (the RxJS/React-hooks-style dependency-array bug class — "forgot a dependency" — structurally can't happen with `computed`, because the tracking is automatic, not declared).
- **`effect()`** runs a side effect whenever any signal it reads changes — the signals equivalent of `useEffect`, minus the dependency array entirely, for the same reason.

The honest, nuanced interview answer on "signals vs RxJS observables, which do I use": signals are for **synchronous, local component/app state** — scores, form values, UI flags, derived values — replacing what used to be plain class fields plus manual change detection triggers. RxJS Observables remain the right tool for **asynchronous streams over time** — HTTP requests, websocket messages, route parameter changes, keystroke debouncing with `switchMap`. They're complementary, not competing — Angular even ships `toSignal()` and `toObservable()` interop functions specifically because real apps need both at once (an HTTP-backed Observable feeding into a signal-based UI is an extremely common shape in 2026 codebases).

<div class="verdict"><strong>The core truth:</strong> Angular interviews reward understanding the framework's structure — dependency injection, change detection (and exactly what triggers it), RxJS streams, and increasingly signals — over memorized API names. The candidate who can correctly explain why OnPush requires new object references, not mutation, stands out over one who just lists lifecycle hook names in order.</div>

## How Greenroom's prep compares to the usual options

If you're prepping for an Angular round right now, you've probably already tried, or at least considered, a few of the standard options. Worth being honest about where each one actually helps and where it quietly doesn't:

**A GeeksforGeeks-style question dump.** These are genuinely useful for *coverage* — they'll remind you that "AOT vs JIT" and "pure vs impure pipes" exist as topics. What they can't do is simulate the follow-up. Real Angular interviews rarely stop at "what is OnPush" — they continue with "okay, now why didn't this specific component re-render when I mutated this array," and a question dump has no mechanism to ask you that, because it isn't a conversation, it's a page you scroll.

**LeetCode.** Excellent for algorithms, close to irrelevant for Angular's actual interview surface area, which is overwhelmingly architectural ("how would you structure this service," "why is this change detection cycle slow") rather than algorithmic. If your Angular interview has a coding component, it's far more likely to be "find the bug in this OnPush component" than "reverse a linked list."

**A friend's WhatsApp-forwarded PDF of "Top 50 Angular Questions."** We've all received one of these. They're usually a few years stale (still asking about `ngModule`-only patterns with no mention of standalone components or signals), and because they're text you read silently, you never practice the part of the interview that actually trips people up: producing a correct explanation *out loud*, in real time, while someone is listening and may interrupt with "wait, why?"

**Generic ChatGPT prompting** ("give me 20 Angular interview questions"). Genuinely fine for generating a list, weak for testing whether *you* can answer it under the mild social pressure of a live conversation — and it won't push back when your answer is subtly wrong, the way the interviewer from this post's cold open did. Asking an LLM to "quiz me" is closer to right, but most people don't keep that conversation going past three questions before sliding back into silently reading definitions.

**Random Udemy course flashcards.** Useful for first-pass exposure if you're new to Angular entirely. The gap is the same one running through every option above: recognition (reading an answer and nodding) is a different cognitive skill from production (constructing that same answer from scratch, verbally, in front of someone watching you think). Flashcards train recognition. Interviews test production.

[Greenroom](/) is built specifically for the production side: spoken mock interviews that ask Angular questions out loud, follow up on your answers the way a real interviewer would ("you said OnPush skips the component — skips it *when*, exactly?"), and give feedback on how clearly you explained the concept, not just whether you used the right words. It won't replace reading the RxJS docs or Angular's own guides — you should still do that — but it closes the specific gap that question dumps, flashcards, and silent reading all share: none of them rehearse the moment where you have to produce a correct, coherent answer live, under a little pressure, with a human (or AI) waiting and ready to ask "why" again.

To be fair about tradeoffs: a spoken mock interview takes longer per question than skimming a list, and if you genuinely just need a fast refresher on "what's the syntax for an async pipe," a quick doc check is faster than booking a session. Use question dumps and docs for breadth and syntax recall; use verbal practice for the specific skill of explaining structure and reasoning out loud, since that's what the actual interview measures.

## How to prepare for an Angular round, concretely

Angular interviews mix three modes, often in the same 45-minute slot: rapid-fire concept questions ("what's the difference between a Subject and a BehaviorSubject"), "spot the bug" code review (a snippet with a memory leak or a broken OnPush assumption), and open-ended architecture discussion ("how would you structure state for a multi-step form across five components"). Prepare for all three, not just the first:

- **For concept questions:** be able to explain DI, change detection, and RxJS *without* notes, out loud, in under 60 seconds each — if you can't, you don't know it well enough yet, you've just recognized it.
- **For spot-the-bug:** deliberately write a few broken OnPush components and broken subscriptions yourself, then explain why they're broken. Building the bug is the fastest way to recognize it instantly when an interviewer hands you one.
- **For architecture discussion:** practice narrating a real decision from a project you've actually built — why you chose a `BehaviorSubject` over a signal for some piece of state, or where you put a service's `providedIn` scope and why. Real decisions you can defend beat textbook answers every time.

For the surrounding fundamentals — [TypeScript](/blog/typescript-interview-questions) generics and structural typing show up constantly inside Angular code, since the framework leans hard on TypeScript's type system for `@Input` types and DI tokens — and [JavaScript](/blog/javascript-interview-questions) closures and the event loop explain a lot of *why* RxJS and Zone.js behave the way they do underneath Angular's abstractions. If you're comparing frameworks for a broader [frontend developer interview](/blog/frontend-developer-interview-questions), or want React's equivalent concepts side by side, [React interview questions](/blog/react-interview-questions) covers the same underlying ideas (re-renders, memoization, reactivity) through a different lens — useful context even if your target role is Angular-only, since interviewers occasionally ask "how would this be different in React" as a way to test whether you actually understand the *why*, not just the Angular-specific *how*. For the verbal-delivery side of all of this — structuring an answer, not rambling, handling a follow-up gracefully — see [coding-interview communication tips](/blog/coding-interview-communication-tips). For grounding facts straight from the source, Angular's own documentation at angular.dev and the RxJS official documentation are the two references worth bookmarking — both are current, both are free, and both are exactly what a thorough interviewer expects you to have actually read at some point.

## Frequently asked questions

### What are the most common Angular interview questions?

Common Angular questions cover components, modules and services, data binding types, dependency injection, lifecycle hooks, component communication (@Input/@Output), RxJS (Observables vs Promises, operators like switchMap and mergeMap, the async pipe, Subjects vs BehaviorSubjects), change detection (default vs OnPush, and the role of Zone.js), standalone components, Angular signals, plus performance topics like lazy loading and AOT vs JIT.

### What is dependency injection in Angular?

Dependency injection is a design pattern where Angular provides a class's dependencies (like services) from the outside rather than the class creating them itself. You declare dependencies in the constructor and Angular's injector supplies registered instances. This improves testability, reuse and modularity, and lets you control whether a service is a singleton or scoped to a component.

### How does change detection work in Angular?

Angular's change detection checks component data bindings and updates the DOM when data changes, triggered by events, HTTP responses and timers via Zone.js. The default strategy checks the whole component tree, while OnPush only checks a component when its inputs change by reference or an event fires within it, which significantly improves performance in large apps.

### What's the difference between OnPush and the default change detection strategy?

The default strategy re-checks a component's bindings on every change detection cycle, triggered by almost any async event anywhere in the app. OnPush only re-checks a component when one of its `@Input` references changes (not a mutation of the same object), an event originates from inside the component, or you manually call `markForCheck()` or use the async pipe. OnPush requires treating data as immutable — replacing objects rather than mutating them — to work correctly.

### What are Angular signals and how are they different from RxJS observables?

Signals are a fine-grained, synchronous reactivity primitive — `signal()` holds a value, `computed()` derives a value that auto-tracks its dependencies, and `effect()` runs side effects when a signal changes, all without Zone.js or a manual dependency array. RxJS Observables remain the right tool for asynchronous streams over time, like HTTP requests or websocket messages. The two are complementary — Angular ships `toSignal()` and `toObservable()` interop specifically because real apps need both.

### How should I prepare for an Angular interview?

Focus on the framework's structure — dependency injection, change detection, RxJS streams, standalone components and signals — rather than memorizing API names, since understanding why concepts exist is what's tested. Practise explaining things like why OnPush requires new object references out loud with a voice-based mock interview, because Angular rounds mix concept questions with verbal code-review and architecture discussions.

Angular rounds reward understanding the framework's structure, out loud, under real follow-up questions. Greenroom runs spoken technical interviews that follow up on your reasoning. Free to start.
