Framework Internals • Signals • Dependency Injection • RxJS Traps • Architecture • Leadership • 2026

Angular Interview Questions for 10+ Years Experience (Senior)

Angular interviews for 10+ years of experience skip basics like what a component is and go after what only experience teaches, such as how change detection and signals behave underneath, the injector, router and RxJS corners that cause rare production bugs, how to shape a codebase that many teams share, and how you lead people through hard calls. This page is written for Angular developers with roughly eight to fifteen years behind them, interviewing for senior, lead or architect roles. Each question shows what the interviewer is really checking, a shape for your answer and a sample you can adapt. Read the sample, then say your own version out loud with a story from your own work.

Search all questions by round, difficulty and level, or save the ones you want to practice.

Framework Internals 6 questions

Hard Technical round Senior Practice question

1. What does the Angular compiler actually turn a component's template into, and why does that matter for build speed, bundle size and the errors you see?

What the interviewer is really testing:
Whether you know templates become plain generated code ahead of time, and can connect that to tree-shaking, incremental builds and template type checking.
Answer frame:

Output: each component gets static fields with a factory and a definition, and the template becomes a function of creation and update instructions.

Ahead of time: templates compile at build time, so the browser never downloads or runs a template compiler.

Locality: a component compiles from its own metadata and what it directly imports, which keeps rebuilds fast and lets libraries ship compiled code.

Payoffs: unused framework instructions tree-shake away, and strict template checks catch binding mistakes at build time.

Sample spoken answer:

“The compiler turns each component into plain JavaScript. The class gets static fields: a factory that knows how to create it with its dependencies, and a definition that holds the selector, the inputs and a template function. That function has two parts: a creation block that builds the DOM once, and an update block that checks bindings on each change detection pass. It's a list of small instructions, like create this element or update this property. Because that happens at build time, the browser never downloads a template compiler, and instructions the app never uses can be tree-shaken out. Each component compiles from its own metadata and the things it directly imports, which keeps rebuilds fast and lets libraries ship compiled code. And with strict template type checking, a wrong input name or a typo in a binding fails the build instead of failing in front of a user.”

Red flag to avoid:

Saying the browser still compiles templates at runtime, or having no idea why framework features an app doesn't use stay out of the bundle.

They may ask next:
  • What does strict template type checking catch that checking the component class alone doesn't?
  • Why does compiling each component from its own metadata matter when you publish an Angular library?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

2. A helper that calls inject() works in a constructor but throws when it's called from a click handler or after an await. Why, and how do you design around it?

What the interviewer is really testing:
Whether you know inject() depends on an injection context that only exists while Angular is creating something, and can design reusable helpers around that.
Answer frame:

Rule: inject() works only while Angular is creating something: constructors, field initialisers, provider factories, and guards or resolvers the router runs.

Why it fails: a click handler runs later, and code after an await resumes in a later task, so the context is gone.

Designs: inject at creation and return functions that use what was captured, or keep an Injector and use runInInjectionContext.

Guard rail: assertInInjectionContext at the top of shared helpers for a clear error.

Sample spoken answer:

“inject() doesn't find things by magic. It reads the injector Angular sets while it's creating a class or running a factory, and that's only true during a constructor, a field initialiser, a provider factory, or functional guards and resolvers that the router runs for you. A click handler runs later, and code after an await resumes in a later task, so the context is gone and inject throws. For reusable helpers, I call them at creation time, usually as a field initialiser, and have them return functions that use what they captured. If code really must resolve something later, I inject the Injector up front and wrap that call in runInInjectionContext. In shared helpers I put assertInInjectionContext at the top, so a teammate gets a clear message naming the helper instead of a vague error deep in the stack.”

Code:
export function injectTracker() {
  assertInInjectionContext(injectTracker);
  const http = inject(HttpClient); // resolved now, while the context exists
  return (event: string) => http.post('/api/track', { event }).subscribe();
}

@Component({ selector: 'app-checkout', template: '<button (click)="pay()">Pay</button>' })
export class CheckoutComponent {
  private track = injectTracker(); // field initialiser: inside the context
  pay() {
    this.track('pay-clicked'); // safe later: nothing is injected here
  }
}
Red flag to avoid:

Saying inject() works anywhere in a component class, or fixing it by keeping the injector in a global variable.

They may ask next:
  • Why does inject work inside a functional route guard but not inside a setTimeout in that guard?
  • What do you gain and lose in tests with inject() compared with constructor parameters?
Say it in 60 seconds
Hard Technical round Senior Practice question

3. A tabs component hides inactive tabs with @if around ng-content, yet every tab's content is created at start and fires its API calls. Why, and how do you make tabs truly lazy?

What the interviewer is really testing:
Whether you know projected content belongs to the parent's view and is always created, and that conditional rendering needs templates instead.
Answer frame:

Ownership: projected content is declared in the parent's template, so it's created when the parent renders, whatever the child does.

@if only places it: wrapping ng-content in @if decides where content shows, not whether it's created.

Lazy pattern: each tab takes an ng-template, and the tabs component stamps only the active one with ngTemplateOutlet.

Rule of thumb: content shown conditionally or more than once should come in as a template.

Sample spoken answer:

“Content you project is declared in the parent's template, so it belongs to the parent's view. Angular creates those components when the parent renders, whether or not the child ever displays them. An @if around ng-content only decides whether the already-created content is placed in the DOM. It doesn't delay creation, so every tab's ngOnInit runs and every API call fires at start. To make tabs lazy, the consumer wraps each tab's body in an ng-template. The tab component picks up that template with a content query, and the tabs component renders only the active one with ngTemplateOutlet. Now a tab's content is created when it's opened and destroyed when it's closed, unless I decide to cache it. My rule of thumb is that if a component shows content conditionally or more than once, it should take a template, not ng-content.”

Code:
@Component({ selector: 'app-tab', template: '' })
export class TabComponent {
  label = input.required<string>();
  body = contentChild.required(TemplateRef); // the lazy body
}

// Tabs template: only the active tab's body is created
// @for (tab of tabs(); track tab.label()) {
//   @if (tab === active()) {
//     <ng-container [ngTemplateOutlet]="tab.body()" />
//   }
// }

// Usage:
// <app-tab label="Orders"><ng-template><app-orders /></ng-template></app-tab>
Red flag to avoid:

Believing an @if around ng-content stops the projected components from being created.

They may ask next:
  • How would you keep a tab's state when the user switches away and back?
  • What would a @defer block inside each tab give you that this doesn't?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

4. After deleting a row from a list of editable row components, the wrong row keeps the half-typed text and the open state. The list tracks by index. Explain the bug.

What the interviewer is really testing:
Whether you see that the track expression decides identity, so it affects correctness and not just speed.
Answer frame:

Identity: track tells Angular which existing DOM node and component instance belongs to which item.

Index tracking: remove one item and everything after it shifts; Angular keeps instances by position and only updates their inputs.

Result: state that lives inside the row component stays with the position, not the data.

Fix: track by a stable id, and keep state that matters in the data or form model.

Sample spoken answer:

“The track expression tells Angular how to match items to the DOM nodes and component instances it already has. With index tracking, position is identity. When I delete the second row, the item that used to be third is now second, so Angular keeps the second component instance, hands it the new item through its input, and destroys the last instance instead. Anything that lives only inside the row component, like a half-typed value in a local field, an expanded flag or which input has focus, stays with the position, so it now sits next to the wrong data. It's slower too, because every row after the deleted one gets new inputs and re-renders. The fix is to track by a stable id from the data. And for state that matters, like unsaved edits, I keep it in the parent's data or the form model, so it survives even if a row component is recreated.”

Red flag to avoid:

Treating track as only a performance hint with no effect on what the user sees.

They may ask next:
  • What happens with tracking by object reference when the list is replaced by a fresh API response?
  • When is tracking by index actually fine?
Say it in 60 seconds
Hard Technical round Senior Practice question

5. You built a toast and dialog system that creates components at runtime. After a long session, memory keeps growing. Where would you look?

What the interviewer is really testing:
Whether you know that runtime-created components are yours to destroy, and can prove a leak with heap snapshots instead of guessing.
Answer frame:

Ownership: a component you create yourself lives until you destroy it, or until its view container's host goes, which for an app-wide toast host is never.

Common leaks: a close path that never calls destroy, and subscriptions, timers or window listeners the toast set up and never removed.

Proof: heap snapshots before and after opening and closing many toasts; follow the retainer chain.

Fix: destroy on every close path, clean up with DestroyRef, or use the CDK dialog and overlay services.

Sample spoken answer:

“When you create components yourself, Angular doesn't know when you're done with them. One placed in a view container is only cleaned up when that container's host is destroyed, and a toast host usually lives as long as the app. One created with createComponent and attached to the application lives until you destroy it. So first I'd check that every close path calls destroy on the ComponentRef, including when the user navigates away with a toast still open. Then I'd look at what each toast sets up: a subscription to a service, a listener on window, an auto-dismiss timer. If those aren't torn down with DestroyRef or in ngOnDestroy, they can keep the instance reachable even after destroy. To prove it, I take a heap snapshot, open and close a hundred toasts, force garbage collection and take another. If toast instances or detached DOM nodes keep growing, I follow the retainer chain to see what's holding them.”

Red flag to avoid:

Assuming Angular garbage collects runtime-created components on its own once they're off screen.

They may ask next:
  • What in the retainer chain tells you the culprit is a subscription and not the DOM?
  • How would you write a test that catches this leak before release?
Say it in 60 seconds
Hard Situational round Senior Practice question

6. Product wants a big list page to keep its scroll position, filters and loaded rows when users come back from a detail page. Would you use a custom RouteReuseStrategy, and what are the traps?

What the interviewer is really testing:
Whether you know what reusing a detached route does to lifecycles and memory, and try simpler options before reaching for it.
Answer frame:

Simpler first: filters in query params, loaded rows in a service cache, and the router's scroll restoration.

How it works: the strategy chooses which routes to detach and store on leave, and hands back the stored view on return.

Lifecycle trap: a stored component isn't destroyed, so ngOnDestroy and ngOnInit don't run; subscriptions keep going and data goes stale.

Memory trap: stored views pile up unless you store only chosen routes, cap the cache and clear it on logout.

Sample spoken answer:

“I'd try the simpler route first. Filters belong in query params, the loaded rows can sit in a service cache, and the router can restore scroll position, which often gives product what they want with no lifecycle surprises. If the page is heavy enough that rebuilding it is the real problem, a custom RouteReuseStrategy works. It decides which routes to detach and store when the user leaves, and returns the stored view instead of creating a new one when they come back. The traps come from the component not being destroyed. ngOnDestroy doesn't run on leave, so its subscriptions, polling and listeners keep running in the background. ngOnInit doesn't run on return, so data can be stale, and I'd refresh it from router events instead. Stored views also pile up in memory, so I'd store only chosen routes, cap the cache and clear it on logout, or the next user could see the previous user's list.”

Red flag to avoid:

Turning on route reuse for every route without noticing that components are no longer destroyed.

They may ask next:
  • How would you refresh stale data on a reattached page without reloading everything?
  • What changes if the list scrolls inside its own container instead of the window?
Say it in 60 seconds

Change Detection 1 question

Hard Technical round Senior Practice question

7. When would you call markForCheck, detectChanges or detach on ChangeDetectorRef? What does each one actually do, and what bug can each one cause?

What the interviewer is really testing:
Whether you know the difference between marking a view for the next pass, checking it right now and taking it out of the tree, and the failure each one brings.
Answer frame:

markForCheck: marks this view and its ancestors dirty so the next pass checks them; it doesn't run a check itself.

detectChanges: checks this view and its children right now, synchronously, but not its parents.

detach and reattach: takes the view out of normal passes until you reattach it or check it by hand.

Bugs: waiting for a pass nothing starts, stale parent bindings or an error on a destroyed view, and a quietly frozen screen.

Sample spoken answer:

“markForCheck doesn't run anything. It marks this component and every ancestor up to the root as dirty, so the next change detection pass walks down to it, even through OnPush parents. It's the right call when an OnPush component gets data from a callback. The bug is calling it from code running outside the zone and expecting the screen to update, when nothing is going to start that pass. detectChanges checks this view and its children right now, synchronously. It's handy for a component updated outside the normal flow, but it doesn't touch the parents, so a parent binding stays stale, and calling it after the component is destroyed throws. detach takes the view out of the tree, so normal passes skip it until I reattach it or call detectChanges myself. I've used that for a live ticker refreshed a few times a second. The classic bug is forgetting to reattach, so part of the screen quietly freezes.”

Red flag to avoid:

Treating the three as interchangeable ways to refresh the view, or sprinkling detectChanges around until the screen updates.

They may ask next:
  • Why does the async pipe call markForCheck rather than detectChanges?
  • What changes about markForCheck in a zoneless app?
Say it in 60 seconds

Dependency Injection 1 question

Hard Technical round Senior Practice question

8. A directive inside projected content gets a service from an outer component, not from the component it's projected into, which does provide it. How does Angular resolve it?

What the interviewer is really testing:
Whether you know the element injector tree follows where content is declared, the difference between providers and viewProviders, and the resolution options.
Answer frame:

Order: element injectors from the requesting element upward, then environment injectors from a lazy route's injector up to root, then an error.

Projection: projected content sits under the host element in the outer template, so it sees the host's providers but not its viewProviders.

Options: self looks only at this element, skipSelf starts at the parent, host stops at the host element, optional gives null instead of an error.

Fix: move the service to providers if content should share it, or keep it private on purpose.

Sample spoken answer:

“Angular first searches element injectors, starting at the element that asks and walking up through its ancestors. If none of them provides it, it moves to the environment injectors, from a lazy route's injector up to root, and throws if nothing has it. The catch here is projection. Projected content is declared in the outer template, so in the injector tree it sits under the host element of the component it's projected into, not inside that component's own view. It can see what the component lists in providers, but not what it lists in viewProviders, which are only for its own template. So this component almost certainly uses viewProviders, the search skips it, and an outer component that provides the same service wins. I'd move it to providers if content should share it. For tighter control, inject takes self, skipSelf, host and optional, to limit where the search looks or return null instead of throwing.”

Red flag to avoid:

Saying projected content lives inside the receiving component's view, or not knowing element injectors are searched before the route and root injectors.

They may ask next:
  • When would you choose viewProviders on purpose?
  • Where does the host option stop the search when it's used in a directive rather than a component?
Say it in 60 seconds

Signals 2 questions

Hard Technical round Senior Practice question

9. How do signals avoid glitches and wasted work underneath? When does a computed actually recompute, and when does an effect run?

What the interviewer is really testing:
Whether you understand the dependency graph, lazy recomputation and effect scheduling, not just the three function names.
Answer frame:

Graph: reading a signal inside a computed, an effect or a template records a dependency.

Push then pull: a write only marks dependents stale; a computed recomputes lazily, when it's read.

Equality: a computed that recomputes to an equal value doesn't disturb what's below it; Object.is by default, or a custom equal function.

Effects: scheduled, not run on each write, so several writes give one run with the final values.

Sample spoken answer:

“When a computed, an effect or a template reads a signal, Angular records that link, so you get a dependency graph. A write doesn't recompute anything straight away. It just marks everything downstream as possibly stale. The real work happens on read: a computed only recomputes if something it depends on actually changed, and only when someone asks for its value. That's why you never see a half-updated state, the kind of glitch you can get when two observable streams fire one after the other. If a computed recomputes to the same value, by Object.is or a custom equal function, the things below it aren't disturbed. Effects are scheduled rather than run on every write, so three writes in a row give one effect run with the final values. The trap I watch for is a computed that returns a fresh object each time, because then it always looks changed.”

Red flag to avoid:

Describing computed as recalculating on every write, or effects as running synchronously the moment a signal is set.

They may ask next:
  • Why does an effect miss a dependency when the signal is read inside a setTimeout callback?
  • When would you use untracked, and what bug can it hide?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

10. A teammate pushes into the array a signal holds and the screen never updates. In another place, a computed stops reacting after its first run. Explain both bugs.

What the interviewer is really testing:
Whether you know signals compare values by reference and track only the signals actually read on the last run.
Answer frame:

Reference check: pushing into the same array and returning it looks unchanged, so nothing downstream is told.

Fix one: return a new array or object from update, and treat signal values as immutable.

Dynamic tracking: a computed depends only on the signals it read on its last run; a plain field in a condition can hide a signal behind it.

Fix two: make the condition a signal too, so it's tracked on every run.

Sample spoken answer:

“Signals decide whether something changed by comparing the old and new value with Object.is. If the update function pushes into the existing array and returns it, it's the same reference, so the signal sees no change and nothing downstream is told. The fix is to return a new array, like spreading the old one with the new item, and to agree as a team that signal values are treated as immutable. The second bug is about how dependencies are tracked. A computed depends only on the signals it actually read the last time it ran. Here the computed checks a plain boolean field first. On the first run it's false, so it returns early without reading the data signal. Now the computed has no signal dependencies at all, and since the field isn't a signal, nothing ever tells it to run again. Making the flag a signal fixes it, because it's read and tracked on every run.”

Code:
readonly items = signal<Item[]>([]);

// Bug: same array back, so the signal sees no change
addBroken(item: Item) {
  this.items.update((list) => { list.push(item); return list; });
}

// Fix: return a new array
add(item: Item) {
  this.items.update((list) => [...list, item]);
}

// Bug: loaded is a plain field, so data() is never read while it's false
loaded = false;
readonly visible = computed(() => (this.loaded ? this.data() : []));
Red flag to avoid:

Blaming OnPush or change detection and calling detectChanges, instead of fixing how the value is updated and read.

They may ask next:
  • Why can't a custom equal function rescue the in-place push?
  • How would you stop in-place mutation of signal values with types or in code review?
Say it in 60 seconds

RxJS 3 questions

Hard Technical round Senior Practice question

11. A service shares a polling stream with shareReplay(1). Later you notice the API is still being polled after every screen that used it has closed. What went wrong?

What the interviewer is really testing:
Whether you know the refCount default of shareReplay and when it turns into a leak.
Answer frame:

Default: shareReplay(1) means refCount false, so the source subscription stays alive after every subscriber leaves.

When it hurts: harmless for an HTTP call that completes, a leak for polling, sockets or other endless sources.

Fix: shareReplay with bufferSize 1 and refCount true, so the source is torn down with the last subscriber.

Find it: a network tab showing calls on an idle page, and finalize logging on the source.

Sample spoken answer:

“shareReplay(1) on its own is the same as setting refCount to false. Once the first subscriber connects, the source subscription stays alive for good, even after every subscriber has unsubscribed. For a plain HTTP call that doesn't matter, because the request completes and the stream is finished. But this stream polls every few seconds and never completes, so the timer and the requests keep running in the background long after every screen has gone. The fix is shareReplay with bufferSize one and refCount true, so the source is torn down when the last subscriber leaves and started again when someone comes back. I'd find it the way I found it last time: the network tab showing calls on a page that shouldn't be calling anything, then a finalize log on the source that never fires when screens close.”

Code:
// Keeps polling forever once anyone has subscribed
readonly pricesLeaky$ = timer(0, 5000).pipe(
  switchMap(() => this.api.prices()),
  shareReplay(1),
);

// Stops with the last subscriber, restarts with the next one
readonly prices$ = timer(0, 5000).pipe(
  switchMap(() => this.api.prices()),
  shareReplay({ bufferSize: 1, refCount: true }),
);
Red flag to avoid:

Saying shareReplay always cleans up its source when subscribers leave.

They may ask next:
  • When is refCount false actually what you want?
  • How would you write a test that proves the source is torn down?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

12. A type-ahead works until one request fails, then it never searches again, even though later requests would succeed. Why, and where does the error handling belong?

What the interviewer is really testing:
Whether you know an error ends the stream it passes through, and can place catchError so one failure doesn't kill a long-lived stream.
Answer frame:

Rule: an error ends the stream it travels through; nothing is emitted after it.

Bug: catchError sits after switchMap on the outer pipe, so the first error kills the input stream.

Fix: catch inside switchMap on the inner request and return a fallback value.

Polish: show an error state, and retry on the inner call only.

Sample spoken answer:

“An error in RxJS ends the stream it travels through. If catchError sits on the outer pipe, after switchMap, the first failed request travels up, catchError swaps in its fallback, and by then the original stream is already dead. It isn't listening to the text box any more, so typing does nothing. The fix is to handle the error inside switchMap, on the inner HTTP observable. Then only that one request fails, it's turned into a fallback value like an empty result with an error flag, and the outer stream keeps listening. I'd also show a small error message instead of silently returning nothing, and if the backend is flaky I'd add a retry with a short delay on the inner call, never on the outer stream. I look for this in every review of search or autocomplete code, because it passes every happy-path test.”

Code:
results$ = this.query.valueChanges.pipe(
  debounceTime(300),
  distinctUntilChanged(),
  switchMap((q) =>
    this.api.search(q).pipe(
      map((items) => ({ items, error: false })),
      catchError(() => of({ items: [], error: true })), // inner: the search stream survives
    ),
  ),
);
Red flag to avoid:

Adding catchError at the end of the pipe and assuming the stream carries on afterwards.

They may ask next:
  • What would a retry on the outer stream do in this case?
  • How would you add a loading flag without creating a second stream?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

13. A component uses takeUntil to clean up, yet a websocket subscription keeps running after it's destroyed. The takeUntil is right there in the pipe. What's wrong?

What the interviewer is really testing:
Whether you know operator order decides what takeUntil actually ends, and that flattening operators wait for their inner streams.
Answer frame:

Order matters: takeUntil completes the stream at the point where it sits.

Bug: placed before switchMap or mergeMap, it completes the outer source, but the flattening operator waits for its active inner stream.

Endless inner: a socket never completes, so it runs for ever.

Fix: takeUntil or takeUntilDestroyed last in the pipe, backed by a lint rule.

Sample spoken answer:

“takeUntil completes the stream at the point where it sits. If it's placed before a switchMap, then on destroy the outer source completes, but switchMap still has an active inner subscription, the websocket, and it waits for that inner stream to finish before completing itself. A socket never finishes, so it runs forever. The same goes for mergeMap and concatMap. So the rule is that takeUntil goes last, after any operator that subscribes to inner streams. The same applies to takeUntilDestroyed, which I prefer now because it removes the hand-made destroy subject. In a big codebase I don't rely on people remembering this. There are lint rules that flag takeUntil placed before flattening operators, and I'd switch them on so it's caught in CI rather than in a memory profile months later.”

Code:
// Leaks: the inner socket stream outlives the component
this.room$
  .pipe(takeUntil(this.destroy$), switchMap((id) => this.socket.join(id)))
  .subscribe();

// Correct: the cleanup operator goes last
this.room$
  .pipe(
    switchMap((id) => this.socket.join(id)),
    takeUntilDestroyed(this.destroyRef),
  )
  .subscribe();
Red flag to avoid:

Saying takeUntil anywhere in the pipe is enough to clean up.

They may ask next:
  • Why does takeUntilDestroyed need a DestroyRef when it's used outside a constructor?
  • Which operators placed after takeUntil can still keep a source alive?
Say it in 60 seconds

Architecture 5 questions

Hard System design round Senior Practice question

14. Design an Angular app for field workers who lose signal for hours. They must keep viewing jobs and saving forms, and nothing may be lost. How would you build it?

What the interviewer is really testing:
Whether you can design for an unreliable network end to end: caching the app, storing data locally, queuing writes safely and handling conflicts honestly.
Answer frame:

App shell: a service worker caches the built files so the app opens offline, and users are told when a new version is ready.

Local data: assigned jobs live in IndexedDB; screens read from there first and refresh when online.

Write queue: every save goes to a local outbox with an id made on the device, syncs in order, and the server ignores repeats of the same id.

Conflicts: version numbers on records, agreed rules for simple fields, and a review screen for real clashes.

Sample spoken answer:

“I'd split it into four parts. First, the app itself: Angular's service worker caches the built files, so the app opens with no signal, and I'd use its update events to tell users a new version is ready rather than swap it under them mid-form. Second, data: the jobs assigned to a worker live in IndexedDB. Screens read from there first and refresh from the server when online, so reads never wait on the network. Third, writes: every save goes into a local outbox with an id generated on the device, and a sync service sends them in order when the connection comes back. The server treats that id as an idempotency key, so a retry after a lost response doesn't create a duplicate. Fourth, conflicts: each record carries a version number. If someone in the office changed the same job, simple fields follow an agreed rule and real clashes go to a review screen instead of being silently overwritten.”

Red flag to avoid:

Relying on the browser cache and a retry loop, with no local queue, no protection against duplicates and no plan for conflicts.

They may ask next:
  • How do you show the worker which saves haven't reached the server yet?
  • What happens to queued saves if a new app version changes the form while they're still waiting?
Say it in 60 seconds
Hard System design round Senior Practice question

15. The same Angular product is sold to many customers, each with its own branding, features and a few custom screens. How would you design it so you still ship one build?

What the interviewer is really testing:
Whether you can keep one codebase and one build while varying look, features and screens at runtime, without a fork per customer.
Answer frame:

Runtime config: load the tenant's settings as the app starts, from the domain or the login, never baked into the build.

Branding: design-system CSS custom properties set from config, so colours and logos are data.

Features: flags read in one service and used by canMatch route guards and the UI, so a hidden area's code never loads.

Custom screens: extension points through injection tokens or lazy routes registered per tenant, and no customer-name checks in shared code.

Sample spoken answer:

“My aim is one build and no forks, because a fork per customer turns every fix into ten fixes. As the app starts, an app initializer fetches the tenant's config, based on the domain or the login, and holds it in a service. Branding comes from the design system's CSS custom properties, so a tenant's colours, logo and fonts are data, not code. Features are flags checked in one place. Routes use a canMatch guard, so a tenant without a module never even loads its code, and menus and buttons read the same flags. Custom screens are the hard part. I'd define extension points, like an injection token for dashboard widgets or a slot in the routes, and register each tenant's components there as lazy chunks. The rule I'd enforce in review is no checks for a customer's name in shared code, because that's how a product slowly turns into a pile of special cases.”

Red flag to avoid:

A separate branch or build per customer, or if-statements on customer names spread through the code.

They may ask next:
  • How do you test that one tenant's change doesn't break another tenant?
  • What would push you to give one large customer their own build after all?
Say it in 60 seconds
Hard System design round Senior Practice question

16. How would you structure an Angular codebase that ten teams work in, so it stays quick to build and hard to tangle?

What the interviewer is really testing:
Whether you can design module boundaries that are enforced by tooling and keep build times and ownership under control as the codebase grows.
Answer frame:

Layers: feature, UI, data-access and utility libraries, with one-way import rules.

Enforcement: lint rules on import boundaries and public entry points, not a wiki page.

Speed: build and test only what a change affects, with caching, and each feature as a lazy route.

Ownership: code owners per library and a small shared core with a high bar.

Sample spoken answer:

“I'd use a monorepo split into libraries by type and by business domain. Feature libraries hold routed pages and the components that talk to services, UI libraries hold presentational components with no services, data-access libraries hold API calls and state, and utility libraries hold plain functions. The rule runs one way: features can use UI and data-access, UI can't import features, and one domain can't reach into another's internals. Each library exposes a public index file and nothing else. I'd enforce that with lint rules on import boundaries, because a rule people read once is forgotten in a month. For speed, CI should build and test only what a change affects, with caching, and each feature is a lazy route so the main bundle doesn't grow with every team. Code owners per library keep reviews with the right people, and the shared core stays small, with a high bar for adding anything.”

Red flag to avoid:

Folders by file type, like components, services and models, across the whole app with nothing enforcing boundaries.

They may ask next:
  • How do you stop the shared library from turning into a dumping ground?
  • How would you split an existing tangled app into libraries without freezing feature work?
Say it in 60 seconds
Hard Technical round Senior Practice question

17. Your interceptor refreshes the access token on a 401. In real use, people get logged out at random, and the logs show several refresh calls at the same moment. What's going on?

What the interviewer is really testing:
Whether you spot a race between parallel requests and between tabs, and know how to make the refresh happen exactly once.
Answer frame:

Race: several requests fail together, and each one starts its own refresh.

Rotation: if each refresh token works only once, the later refreshes send a spent token, get rejected and the app logs out.

One tab: share a single in-flight refresh; failed requests wait for it and retry once with the new token.

Many tabs: coordinate with the Web Locks API or a broadcast channel so one tab refreshes and the others reuse its result.

Sample spoken answer:

“When the token expires, a screen that fires five requests gets five 401s at once, and a naive interceptor starts five refreshes. If the server rotates refresh tokens, each one works only once. The first refresh succeeds, the others send a token that was just used, the server rejects them, and the error path logs the user out. That's why it only shows up in real use and never when you test one request at a time. The fix inside a tab is a single shared refresh in a service: the first 401 starts it, every other failed request waits on the same result, then retries once with the new token. A second 401 after that retry goes to login, not another refresh. Open tabs have the same race, so I'd coordinate across them with the Web Locks API or a broadcast channel, so one tab refreshes and the others use its result.”

Code:
private refreshing$: Observable<string> | null = null;

refreshToken(): Observable<string> {
  // Every caller shares the same in-flight refresh
  this.refreshing$ ??= this.http.post<{ token: string }>('/api/auth/refresh', {}).pipe(
    map((res) => res.token),
    finalize(() => (this.refreshing$ = null)),
    shareReplay(1),
  );
  return this.refreshing$;
}
Red flag to avoid:

Blaming the server for random logouts, or adding retries without making the refresh happen once.

They may ask next:
  • How do you stop the refresh request itself from going through the same 401 handling?
  • Where would you keep the refresh token so a cross-site scripting bug can't read it?
Say it in 60 seconds
Hard Technical round Senior Practice question

18. You inherit a large AngularJS app that has to move to modern Angular without stopping feature work. How do you run the migration?

What the interviewer is really testing:
Whether you know the incremental migration options, their real costs, and how to stop a temporary hybrid state from becoming permanent.
Answer frame:

Two routes: a hybrid app with the upgrade package when screens are tangled, or a route-by-route split when the app divides cleanly by URL.

Rule one: new features only in modern Angular from day one.

Order: shared services and leaf components first, then whole routes.

Costs: two change detection systems, a heavier download and harder debugging, so set an end date.

Sample spoken answer:

“I'd pick between two routes. If screens are tangled together, I'd run a hybrid app with the upgrade package: both frameworks boot on the same page, new Angular components are downgraded so old templates can use them, and old services are upgraded so new code can inject them. If the app splits cleanly by URL, a route-by-route approach is simpler: a new Angular app owns some routes, the old app owns the rest, and I move one route at a time. Either way, the rule from day one is that new features are written only in modern Angular. Then I move shared services and leaf components first, since everything depends on them, and whole routes after. I'd be honest about the costs: in hybrid mode two change detection systems run and trigger each other, the download is heavier and debugging is harder. So I set an end date and track the count of remaining AngularJS files every sprint.”

Red flag to avoid:

Proposing a big-bang rewrite with a feature freeze, or leaving a hybrid app running with no end date.

They may ask next:
  • How would you share login state between the two frameworks during the move?
  • What would you do with a huge AngularJS directive nobody understands?
Say it in 60 seconds

Performance 1 question

Hard Technical round Mid-level, Senior Practice question

19. You wrapped a heavy chart in a @defer block, but the build output shows it still in the main bundle. What decides whether code inside a defer block really gets split out?

What the interviewer is really testing:
Whether you know the conditions for deferred loading and check the build output instead of assuming the block worked.
Answer frame:

Rule: only standalone dependencies that nothing else in the file uses eagerly can move into a lazy chunk.

Common leaks: the component referenced in the class, like a view query, or imported eagerly by another component on the page.

Check: look for a new lazy chunk in the build output or a stats file, not at the template.

Tuning: choose the trigger and prefetch on purpose, and give the placeholder a minimum time so it doesn't flash.

Sample spoken answer:

“A defer block only moves a dependency into a lazy chunk if the compiler can see nothing else needs it eagerly. The component has to be standalone, and it can't be referenced anywhere in that file outside the block. The usual catch is the component class: a view query for the chart, or using the chart class in code, keeps it eager, because the class now needs it at load time. Next I check whether something else imports it eagerly, like another component on the same page, because then it's in the main bundle anyway and the defer block only delays rendering. I don't trust the template to tell me. I look at the build output for a new lazy chunk, or at a stats file to see what pulls the chart in. Once it really splits, I choose the trigger on purpose, like on viewport with prefetch on idle, and set a minimum time on the placeholder so it doesn't flash.”

Code:
// Template: the chart loads when it scrolls into view, fetched early when the browser is idle
// @defer (on viewport; prefetch on idle) {
//   <app-sales-chart [data]="sales()" />
// } @placeholder (minimum 500ms) {
//   <div class="chart-skeleton"></div>
// }

// Class: this query keeps SalesChartComponent in the main bundle
// chart = viewChild(SalesChartComponent);
Red flag to avoid:

Assuming anything inside a defer block is lazy-loaded without checking the build output.

They may ask next:
  • How would you stop the layout jumping when the chart finally appears?
  • How do you choose between on viewport, on interaction and on idle for this chart?
Say it in 60 seconds

Leadership 4 questions

Hard Situational round Senior Practice question

20. A new VP suggests rewriting your large Angular product in another framework because hiring is hard. How do you respond?

What the interviewer is really testing:
Whether you can separate the real problem from the proposed solution, cost a rewrite honestly, and disagree with a senior leader constructively.
Answer frame:

Find the real problem: hiring, delivery speed or old code; check the data.

Cost it honestly: a feature freeze or two systems at once; most effort is rediscovering business rules buried in code.

Cheaper options: upgrade to modern Angular, hire strong web developers and train them, fix the worst areas.

If still right: move route by route with checkpoints, never a big bang.

Sample spoken answer:

“I'd take it seriously rather than defend Angular out of habit. First I'd ask what's actually hurting. If it's hiring, I'd look at the data: how long roles stay open and why candidates say no. Often the real issue is that our Angular is several versions old and the code is hard to work in, not the framework itself. Then I'd lay out the cost honestly. A full rewrite of a large product means either freezing features for a long time or running two systems, and most of the effort isn't writing components, it's rediscovering business rules that only live in the old code. Cheaper options usually get most of the gain: upgrade to modern Angular with standalone components and signals, hire strong web developers and train them, and clean up the worst areas. If a move still makes sense after that, I'd propose doing it route by route, with checkpoints where we can stop, never as a big bang.”

Red flag to avoid:

Dismissing the idea out of loyalty to Angular, or agreeing to a big-bang rewrite without costing it.

They may ask next:
  • What numbers would you bring to that conversation?
  • What do you do if the VP decides on the rewrite anyway?
Say it in 60 seconds
Medium Behavioral round Senior Practice question

21. How have you set Angular standards across several teams, so code from different teams looks and behaves the same without slowing anyone down?

What the interviewer is really testing:
Whether you set a few standards that prevent real bugs and make them stick through tooling, rather than writing a long document nobody reads.
Answer frame:

Pick few: rules drawn from real bugs, like OnPush by default and no nested subscribes.

Automate: lint rules in CI and code generators with the right defaults.

Show, don't tell: one example feature beats a long document.

Keep it alive: a regular slot where anyone can challenge a rule.

Sample spoken answer:

“At my last company five teams shared one Angular platform and each had its own style, so moving people between teams was slow and reviews were full of style arguments. I started by collecting the bugs from the past few months and picking the handful of rules that would have stopped most of them: OnPush by default, no subscribe inside subscribe, typed reactive forms, and no HTTP calls straight from components. Then I automated them. We added lint rules that failed CI, and generators so a new component came out with OnPush and the right folder layout. I built one example feature showing everything together, which people copied far more than they read the document. Every month we had a short session where anyone could propose changing or dropping a rule. After a couple of quarters, style comments had mostly gone from reviews and people discussed logic instead.”

Red flag to avoid:

A long style guide nobody enforces, or rules imposed with no way to challenge them.

They may ask next:
  • How did you handle a team that pushed back hard on one of the rules?
  • Which rule did you later drop, and why?
Say it in 60 seconds
Hard Behavioral round Senior Practice question

22. Tell me about an Angular project you led that missed its date badly or was stopped. What was your part in it, and what do you do differently now?

What the interviewer is really testing:
Whether you own your part in a failure at the project level, read the warning signs honestly and changed how you lead.
Answer frame:

The project: what it was, what was promised and who depended on it.

What went wrong: the real causes, including your own part, not only outside factors.

What you did: how you told people, re-planned and saved what could be saved.

Now: the specific habits you changed.

Sample spoken answer:

“At my last company I led a rebuild of our customer portal in modern Angular, with four developers and a launch date promised to sales for the end of the quarter. Two months in, we'd rebuilt the easy screens nicely and hadn't touched billing, which depended on an old backend nobody fully understood. My part was real: I planned screens by how visible they were, not by risk, and I kept reporting green because each sprint looked fine. When billing blew up the estimate, I went to my manager and the sales lead the same day, said plainly that the date was gone and why, and offered a cut: launch with the new account and orders screens, and keep the old billing pages behind a link. We shipped that about six weeks late. Now I take the riskiest integration to production in the first weeks, and I report risks, not just progress.”

Red flag to avoid:

A story where the failure was all someone else's fault, or where nothing about how you lead changed afterwards.

They may ask next:
  • How did the team react when you reset the date, and how did you handle it?
  • Looking back, which warning signs did you miss?
Say it in 60 seconds
Medium Behavioral round Senior Practice question

23. You're hiring Angular developers for your team. How do you run the technical interview, and what separates a strong mid-level candidate from a senior one?

What the interviewer is really testing:
Whether you hire on judgement and real work rather than trivia, and can describe the difference between levels clearly and fairly.
Answer frame:

Real work: a code review of a flawed component and a short pairing task, not trivia.

Probe why: ask for the reason behind each choice and what changes at scale.

Mid vs senior: mid builds it right; senior ranks risks, prevents the bug class and teaches in review.

Fairness: the same exercise and scoring notes for everyone.

Sample spoken answer:

“I avoid trivia, because knowing hook names says little about how someone works. My main exercise is a code review: a short Angular component with real problems, like a nested subscribe, a missing cleanup, a getter that creates a new array each time and an unsafe innerHTML. I ask them to review it as if a teammate wrote it. Then a short pairing task, like adding a filter to a list, where I ask why at each choice. A strong mid-level developer finds most of the bugs and builds the feature correctly. A senior also ranks what matters most, explains how they'd stop that bug class coming back, asks what happens with ten thousand rows or a slow network, and writes review comments that teach instead of just flag. Everyone gets the same exercise, and I score against the same notes straight after, so I'm comparing people fairly and not on gut feel.”

Red flag to avoid:

Hiring on framework trivia or speed alone, with no look at judgement or how the candidate explains.

They may ask next:
  • How would you adjust the interview for someone strong in another framework but new to Angular?
  • What would make you turn down a candidate who solved the task perfectly?
Say it in 60 seconds
Were you asked something else? Share it A person checks every question before it goes on the site. No name is shown.
For the call itself

You practiced these. On the real call, ClapAssist helps with the rest.

ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your live interview audio and screen are never stored. Your resume and notes are saved to your account so the app fills them in on any computer. It stays out of screen share on every plan, including Free; only you can see it.

Download with 10 free minutes
Mac and Windows · Stays out of screen share · No card