Login to manage your account

Please enter a valid email address.
Forgot Password?
Please enter a valid password.
OR

Don't have an account yet? Sign up

Angular interviews lean heavily on architecture, because Angular is chosen for large applications. Expect questions on dependency injection and injector hierarchies, how change detection works and what OnPush changes, the RxJS flattening operators and when each is correct, reactive forms, route guards and lazy loading, and how to avoid subscription memory leaks. Signals and standalone components increasingly appear as well. The questions below cover the framework mechanics and the performance decisions that follow from them.

jobs available in Angular
View jobs

Behavioural Questions

1. Tell me about an Angular application you have worked on. How was it structured and what would you change?

Note: Angular interviews lean heavily on architecture, because Angular is chosen precisely for large applications. Talk about structure, not features.

Cover these:

  • Scale and shape. Roughly how many components, routes, and developers. An application with three developers and one with thirty have completely different problems.
  • How it was organised. Feature modules or standalone components, a shared or core module, lazy-loaded routes, and where state lived — services with RxJS subjects, NgRx, or component state.
  • The hardest problem. Good ones: initial bundle size, change detection performance on a large table, a state management approach that grew unmanageable, or migrating across major versions.
  • What you would change. This is where you show judgement. Common honest answers are that NgRx was too much for the problem, or that shared modules turned into a dumping ground.

2. How do you keep a large Angular codebase maintainable as the team grows?

Answer with conventions and automation, since those are what actually scale rather than individual discipline.

  • A clear module or folder boundary per feature, with a shared area that has an owner and a rule for what may go in it. Shared modules become dumping grounds without that rule.
  • Lazy load every feature route. It keeps the initial bundle small and enforces the boundary — if a feature cannot be lazy loaded, it is too entangled.
  • A presentational and container split. Components that only take inputs and emit outputs are trivially testable and reusable; components that fetch data are not.
  • Automate the rules. ESLint with the Angular plugin, Prettier, strict TypeScript, and a bundle-size budget that fails the build. A convention that is not enforced by CI decays.
  • Generate rather than copy. Angular CLI schematics keep new code consistent by default.

Note: Mention keeping up with Angular's major releases on a schedule. Falling three versions behind is how a codebase becomes unmaintainable, and ng update makes staying current routine.

3. Describe a time you had to improve the performance of an Angular application.

Structure it as measure, diagnose, fix, verify — and be specific about which kind of slowness it was, because they have different causes.

  • Slow initial load usually means bundle size. The tools are source-map-explorer or webpack-bundle-analyzer, and the fixes are lazy-loaded routes, removing a heavy dependency, and setting a budget in angular.json that fails the build when it regresses.
  • Slow interaction usually means change detection. Angular Devtools' profiler shows which components are re-checked and how often. The fixes are ChangeDetectionStrategy.OnPush, the trackBy function on ngFor, moving work out of template expressions and getters, and virtual scrolling for long lists.
  • Memory growth over time usually means unclosed subscriptions.

Note: The single highest-value detail is that a function or getter called in a template runs on every change detection cycle — sometimes hundreds of times a second. Finding one of those and fixing it makes an excellent, concrete story.

4. How do you approach upgrading Angular across major versions?

Show that you treat it as routine maintenance rather than a project, because that is the only approach that works long term.

  • Upgrade one major version at a time, using ng update. Angular ships schematics that rewrite most breaking changes automatically, but only for a single-version step.
  • Read the update guide at update.angular.io for your exact from-and-to versions before starting. It lists what the schematics will not handle.
  • Get the test suite green first. Without tests, an upgrade is a leap of faith.
  • Handle third-party libraries separately. These are usually the real blocker — a library that has not released a compatible version can stop the whole upgrade, so check them before you begin.
  • Keep the upgrade on its own branch with no feature work mixed in, so a rollback is clean.

Note: The strongest thing to say is that you would upgrade on a cadence — every release, or every other one. Teams that fall four versions behind end up doing a rewrite instead of an upgrade, and interviewers have usually lived through that.

5. How do you decide how much state management an Angular application needs?

This is a judgement question, and the answer they are hoping for is that you start small and escalate only on evidence.

The progression:

  • Component state for anything one component owns. Most state is this, and moving it further out is a real cost.
  • A service with a BehaviorSubject or a signal for state shared across a feature. This handles the large majority of applications and needs no library.
  • NgRx or a similar store when you genuinely have the problems it solves: many components reading and writing the same state, a need for time-travel debugging or an audit trail of actions, or complex asynchronous coordination that Effects express better than nested subscriptions.

The cost of a store is real — actions, reducers, selectors, and effects for every piece of state, plus a learning curve for every new joiner. Adopting it for a form is a net loss.

Note: Saying you have used NgRx and would not use it again for that particular project is a strong answer. It shows you evaluate tools rather than collect them.

Technical Questions

1. What is dependency injection in Angular, and how do providers and injection scopes work?

Dependency injection means a class declares what it needs in its constructor and Angular supplies it, rather than the class constructing its own dependencies. That is what makes services swappable in tests.

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

The injector hierarchy is the part interviewers probe. Angular resolves a dependency by walking up from the component's injector to the root injector, and the first provider it finds wins.

  • providedIn: 'root' — one instance for the whole application, and tree-shakable, so it is dropped from the bundle if nothing injects it. This is the default you should use.
  • Providing in a component's providers array — a new instance for each instance of that component and its children. Useful when a service holds per-component state.
  • Providing in a lazy-loaded module creates a separate instance for that module, which surprises people who expected a singleton.

Note: Injection tokens are worth mentioning. Since interfaces do not exist at runtime, you use an InjectionToken to inject a configuration object or a non-class value.

2. How does change detection work in Angular, and what does OnPush do?

Angular patches asynchronous APIs through Zone.js — events, timers, and XHR. When any of them completes, the zone tells Angular that something may have changed, and Angular walks the component tree from the root, re-evaluating every template binding and updating the DOM where a value differs.

The default strategy checks every component on every cycle. That is fine for a small tree and expensive for a large one.

ChangeDetectionStrategy.OnPush tells Angular to skip a component unless one of these happens:

  • An @Input reference changes — a reference, which is why mutating an array in place does not trigger it and creating a new array does.
  • An event fires from within the component or its template.
  • An observable bound with the async pipe emits.
  • Change detection is triggered manually with markForCheck().

The consequence: OnPush pushes you towards immutable data, which is a good thing anyway.

Note: Signals are the direction of travel. They let Angular know exactly which templates depend on which values, enabling fine-grained updates and eventually zoneless applications — worth mentioning as awareness of where the framework is going.

Free workshop by Jobaaj Learnings

3. What is RxJS, and what is the difference between switchMap, mergeMap, concatMap and exhaustMap?

RxJS models values arriving over time as Observables. Angular uses them throughout — HttpClient, router events, and form value changes all return them.

The four flattening operators all take a value, produce an inner observable, and flatten the result. They differ in what happens when a new value arrives while an inner observable is still running:

  • switchMap — cancel the previous inner observable and switch to the new one. Use for a type-ahead search: you only want results for the latest keystroke, and it cancels the in-flight request.
  • mergeMap — run them all concurrently, results arriving in whatever order they finish. Use for independent parallel work where order does not matter.
  • concatMap — queue them and run one at a time in order. Use when order matters, such as a sequence of writes that must be applied in sequence.
  • exhaustMap — ignore new values while one is still running. Use for a submit button: it makes double-clicks harmless.

Note: Using switchMap for a save request is a classic bug — a second click cancels the first save, which may have already reached the server.

4. What is the difference between template-driven and reactive forms in Angular?

Both produce the same result; they differ in where the source of truth lives.

Template-driven forms are built in the template with ngModel. Angular creates the form model implicitly from the directives it finds. They are quick for a login box, but the model is asynchronous to access and hard to test without rendering.

Reactive forms are built in the class:

form = this.fb.group({
  email: ['', [Validators.required, Validators.email]],
  password: ['', Validators.minLength(8)],
});

and bound with [formGroup] and formControlName.

Why reactive is the default recommendation:

  • The model is explicit and synchronously available, so it is unit-testable without a DOM.
  • Validation is composed in code, and custom and asynchronous validators are straightforward.
  • valueChanges is an observable, so debouncing, dependent fields, and conditional validation are natural.
  • Dynamic forms — adding and removing controls at runtime with FormArray — are practical rather than awkward.

Note: Mention typed reactive forms, introduced in Angular 14. They removed the long-standing complaint that form values were typed as any.

5. Explain the Angular component lifecycle hooks and when you would use each.

In the order they run:

  • ngOnChanges — before ngOnInit and again whenever a bound input changes. It receives a SimpleChanges object with previous and current values. Use it to react to input changes.
  • ngOnInit — once, after the first ngOnChanges. This is where initialisation belongs, including data fetching. The constructor should only assign injected dependencies.
  • ngDoCheck — every change detection cycle. Powerful and dangerous; only for custom change detection.
  • ngAfterContentInit / ngAfterContentChecked — after projected content, from ng-content, has been initialised or checked.
  • ngAfterViewInit / ngAfterViewChecked — after the component's own view and child views are ready. This is the earliest point a @ViewChild reference is available.
  • ngOnDestroy — just before the component is removed. Unsubscribe, clear timers, and detach event listeners here.

Note: Two traps come up repeatedly. Modifying a bound value inside ngAfterViewInit throws ExpressionChangedAfterItHasBeenCheckedError in development mode. And ngOnDestroy is the single most important hook for avoiding memory leaks.

6. How do you avoid memory leaks from RxJS subscriptions in Angular?

A subscription that outlives its component keeps the component and everything it references alive. Over a long session with many navigations, that accumulates.

The options, best first:

  • The async pipe. It subscribes and unsubscribes for you, and works with OnPush. If you can bind the observable straight into the template, do that and the problem disappears.
  • takeUntilDestroyed() — since Angular 16, the cleanest programmatic answer:
    this.service.data$
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe(d => this.data = d);
  • A destroy$ subject with takeUntil(this.destroy$), completed in ngOnDestroy. The older standard pattern.
  • A Subscription object collecting subscriptions with .add() and unsubscribed once in ngOnDestroy.

What does not need unsubscribing: observables that complete on their own, such as an HttpClient call, which emits once and completes. It is still safer to apply the same pattern uniformly rather than asking developers to remember which is which.

Note: Long-lived sources are the dangerous ones — router events, a BehaviorSubject in a root service, form valueChanges, and anything from an interval.

7. How does routing work in Angular, and what are route guards and lazy loading?

The router maps URL paths to components. You declare routes as an array, and <router-outlet> marks where the matched component is rendered. Routes are matched in order, so specific paths must come before wildcards.

Lazy loading splits a feature into its own bundle, downloaded only when the route is first visited:

{ path: 'admin', loadChildren: () => import('./admin/admin.routes').then(m => m.ADMIN_ROUTES) }

This is the most effective single change you can make to initial load time in a large application.

Guards run before navigation completes and can allow it, block it, or redirect:

  • CanActivate — may this user reach this route? The usual authentication and role check.
  • CanActivateChild — the same for child routes.
  • CanDeactivate — may the user leave? This is how you warn about unsaved changes.
  • CanMatch — decides whether the route matches at all, which means the lazy bundle is never even downloaded if the user is not permitted. Better than CanActivate for role-gated features.
  • Resolve — pre-fetch data before the component renders, avoiding a flash of an empty view.

Note: Guards are modern functional functions now rather than classes, and a guard is a convenience, never a security control — the server must enforce authorisation regardless.

8. What is the difference between an Angular component, directive, pipe and service?

Four building blocks with four distinct jobs.

  • Component — a directive with a template. It owns a piece of the UI, its own styles, and the logic that drives them. Every screen is a tree of these.
  • Directive — behaviour attached to an existing element, with no template of its own. Two kinds: structural directives change the DOM layout and are prefixed with an asterisk, such as *ngIf and *ngFor; attribute directives change appearance or behaviour, such as ngClass, or a custom one that adds a tooltip.
  • Pipe — a value transformer used in templates, such as {{ price | currency:'INR' }}. Pure pipes are memoised and only re-run when the input reference changes, which makes them cheap; impure pipes run on every change detection cycle and should be avoided.
  • Service — a class with no UI at all, holding business logic, HTTP calls, or shared state, and injected wherever needed.

The rule that ties them together: components should stay thin. If a component is making HTTP calls and transforming data, that logic belongs in a service, and the formatting belongs in a pipe.

9. How do you handle HTTP requests in Angular, and what are interceptors used for?

HttpClient returns a cold observable — the request is not sent until something subscribes, and it emits once then completes. Binding it through the async pipe is usually cleaner than subscribing manually.

getOrders(): Observable<Order[]> {
  return this.http.get<Order[]>('/api/orders').pipe(
    retry({ count: 2, delay: 1000 }),
    catchError(this.handleError)
  );
}

Interceptors sit in the middle of every request and response, so cross-cutting concerns live in one place instead of in every service:

  • Attaching an auth token to outgoing requests.
  • Refreshing an expired token on a 401 and retrying the original request.
  • Centralised error handling — turning HTTP errors into a toast or a redirect.
  • A loading indicator driven by a count of in-flight requests.
  • Logging and correlation ids.

Note: Requests are immutable, so an interceptor must clone rather than mutate: req.clone({ setHeaders: { Authorization: token } }). Interceptors also run in the order they are registered, which matters when one adds a header another depends on.

10. What are Angular signals, and how do they differ from RxJS observables?

A signal is a wrapper around a value that knows when it is read and notifies anything depending on it when it changes. It was introduced in Angular 16 and is the framework's direction of travel for reactivity.

count = signal(0);
double = computed(() => this.count() * 2);

increment() { this.count.update(n => n + 1); }

Reading a signal is a function call. computed derives a value and recalculates lazily only when a dependency actually changed, and effect runs side effects when dependencies change.

How they differ from observables:

  • Signals are synchronous and always hold a current value. Observables are streams over time and may not have emitted yet.
  • Signals track dependencies automatically. You never subscribe or unsubscribe, so there is no leak to manage.
  • Observables are far richer for asynchronous work — cancellation, retries, debouncing, and combining streams are what RxJS is for.

The practical division: signals for component state and derived values; RxJS for events and asynchronous streams such as HTTP and user input. toSignal and toObservable bridge the two.

Note: The reason this matters is performance. Signals tell Angular exactly which templates depend on a changed value, enabling fine-grained updates instead of walking the component tree — which is what makes zoneless Angular possible.

Login to manage your account

Please enter a valid email address.
Forgot Password?
Please enter a valid password.
OR

Don't have an account yet? Sign up as