Back to Blog

Angular Coding Interview Questions for Experienced Developers (6+ Years) 2026

Roundexa Team 1 Sep 2026
Angular Coding Interview Questions for Experienced Developers (6+ Years) 2026

Past the scenario discussion, senior Angular interviews almost always move into a live coding round — writing a custom operator, fixing a change-detection bug, or building a small piece of infrastructure from scratch. Here are the 10 coding exercises that come up most often at the 6+ years level, each with a working solution. Want the discussion-style questions first? See the Scenario-Based Questions guide.

Q1

Write a custom RxJS operator that retries a failed HTTP call with exponential backoff.

Compose it from existing operators inside a pipeable function — no need to hand-roll retry logic.

function retryWithBackoff<T>(maxRetries = 3, delayMs = 500) {
  return (source: Observable<T>) =>
    source.pipe(
      retryWhen(errors =>
        errors.pipe(
          scan((count, err) => {
            if (count >= maxRetries) throw err;
            return count + 1;
          }, 0),
          delayWhen(count => timer(delayMs * Math.pow(2, count)))
        )
      )
    );
}
// usage: this.http.get(url).pipe(retryWithBackoff(3, 500))

Q2

Implement a custom structural directive, e.g. *appHasPermission='admin', that shows/hides an element based on a permission check.

A structural directive manipulates the template via TemplateRef and ViewContainerRef, adding or clearing the view based on the check.

@Directive({ selector: '[appHasPermission]' })
export class HasPermissionDirective {
  @Input() set appHasPermission(role: string) {
    this.permissions.has(role)
      ? this.vcr.createEmbeddedView(this.tpl)
      : this.vcr.clear();
  }
  constructor(
    private tpl: TemplateRef<unknown>,
    private vcr: ViewContainerRef,
    private permissions: PermissionsService
  ) {}
}

Q3

Implement a debounced search box using RxJS fromEvent.

fromEvent turns the input's keyup events into a stream you can debounce and dedupe before hitting the API.

fromEvent(inputEl, 'keyup').pipe(
  map((e: Event) => (e.target as HTMLInputElement).value),
  debounceTime(300),
  distinctUntilChanged(),
  switchMap(term => this.api.search(term))
).subscribe(results => this.results = results);

Q4

Write an async validator for a reactive form that checks username availability via an API call.

Async validators return an Observable/Promise of ValidationErrors, and should debounce internally so they don't fire on every keystroke.

function usernameTakenValidator(api: UserService): AsyncValidatorFn {
  return (control: AbstractControl) =>
    timer(300).pipe(
      switchMap(() => api.checkUsername(control.value)),
      map(isTaken => (isTaken ? { usernameTaken: true } : null))
    );
}
// usage: new FormControl('', [], [usernameTakenValidator(this.userService)])

Q5

Given an OnPush component that doesn't update when an array item is mutated, fix the code.

OnPush skips checks when a reference is mutated in place — the fix is to replace the array reference instead of mutating it.

// Before (bug): view never updates under OnPush
addItem(item: Item) {
  this.items.push(item);
}

// After (fixed): new reference triggers change detection
addItem(item: Item) {
  this.items = [...this.items, item];
}

Q6

Write a pure pipe timeAgo that converts a timestamp into '5 minutes ago', and explain why it should be pure.

A pure pipe only recomputes when its input reference changes, which is cheap and predictable — an impure pipe re-runs on every change-detection cycle, which is wasteful for something this simple.

@Pipe({ name: 'timeAgo', pure: true })
export class TimeAgoPipe implements PipeTransform {
  transform(value: string | Date): string {
    const seconds = Math.floor((Date.now() - new Date(value).getTime()) / 1000);
    if (seconds < 60) return `${seconds}s ago`;
    const minutes = Math.floor(seconds / 60);
    if (minutes < 60) return `${minutes}m ago`;
    const hours = Math.floor(minutes / 60);
    return hours < 24 ? `${hours}h ago` : `${Math.floor(hours / 24)}d ago`;
  }
}

Q7

Set up a multi-provider dependency injection token, similar to how HTTP_INTERCEPTORS works, for pluggable logging services.

multi: true tells Angular to collect every provider registered against the token into an array, instead of the last one winning.

export const LOGGERS = new InjectionToken<Logger[]>('LOGGERS');

@NgModule({
  providers: [
    { provide: LOGGERS, useClass: ConsoleLogger, multi: true },
    { provide: LOGGERS, useClass: RemoteLogger, multi: true },
  ],
})
export class LoggingModule {}

// consumer: constructor(@Inject(LOGGERS) private loggers: Logger[]) {}

Q8

Fix a component with a setInterval and subscription that isn't cleaned up, causing a memory leak.

Angular 16+'s takeUntilDestroyed() ties the stream's lifetime to the component automatically, replacing the manual destroy$ Subject pattern.

export class TickerComponent {
  private destroyRef = inject(DestroyRef);

  ngOnInit() {
    interval(1000)
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe(() => this.tick++);
  }
}

Q9

Implement a large list (10,000+ items) efficiently using Angular CDK's virtual scroll.

cdk-virtual-scroll-viewport renders only the items in (and just around) the visible viewport, keeping DOM node count constant regardless of list length.

<cdk-virtual-scroll-viewport itemSize="48" class="viewport">
  <div *cdkVirtualFor="let item of items; trackBy: trackById">
    {{ item.name }}
  </div>
</cdk-virtual-scroll-viewport>

Q10

Build a minimal state store from scratch using BehaviorSubject, with select() and dispatch()-style methods.

This is the pattern most 'roll your own NgRx' interview questions are testing — immutable updates driven through a single BehaviorSubject.

@Injectable({ providedIn: 'root' })
export class Store<T> {
  private state$: BehaviorSubject<T>;
  constructor(initial: T) { this.state$ = new BehaviorSubject(initial); }

  select<K>(selector: (state: T) => K): Observable<K> {
    return this.state$.pipe(map(selector), distinctUntilChanged());
  }

  setState(patch: Partial<T>) {
    this.state$.next({ ...this.state$.value, ...patch });
  }
}

Final Thoughts

Coding rounds test whether you can turn the concepts you can talk about into working code under time pressure. Type these out by hand rather than reading them — muscle memory matters more than recognition here. Pair this with the scenario-based questions and build both muscles with mock interviews at Roundexa.com.

Ready to Practice?

Take a free AI mock interview on Roundexa and get instant, actionable feedback before the real one.

Practice on Roundexa