DEV Community

Mahendranath Reddy
Mahendranath Reddy

Posted on

Advanced Senior Angular Developer — Interview Preparation Guide

Advanced Senior Angular Developer — Interview Preparation Guide

Elite-level preparation covering Angular internals, compiler pipeline, custom renderers, advanced RxJS patterns, micro-frontends, SSR/SSG, zoneless architecture, monorepo strategies, and engineering leadership. This guide targets Staff/Principal/Lead Angular Engineer roles.


Table of Contents

  1. Angular Compiler & Build Pipeline Internals
  2. Ivy Rendering Engine Deep Dive
  3. Advanced Change Detection & Zoneless Architecture
  4. Advanced Dependency Injection Patterns
  5. Advanced RxJS Patterns & Custom Operators
  6. Angular Signals — Advanced Patterns
  7. Server-Side Rendering (SSR) & Static Site Generation
  8. Micro-Frontend Architecture
  9. Monorepo with Nx
  10. Advanced Performance Engineering
  11. Custom Angular Libraries & ng-packagr
  12. Advanced Testing Strategies
  13. Custom Renderers & Platform Abstraction
  14. Angular CDK & Advanced Component Patterns
  15. Advanced Security Architecture
  16. Web Workers & Shared Workers in Angular
  17. Design System Architecture
  18. Engineering Leadership & Architecture Decisions
  19. Advanced Coding Challenges
  20. Staff/Principal Engineer Interview Framework

1. Angular Compiler & Build Pipeline Internals

The Angular Compilation Pipeline

Understanding the compiler at a deep level separates senior engineers from principal-level engineers.

TypeScript Source (.ts)
        ↓
Angular Compiler (ngtsc)
  ├── Template Parser → AST (Abstract Syntax Tree)
  ├── Type Checker → Template type checking
  ├── Code Generator → Generated factory code
  └── Definition Emitter → .d.ts files with Angular metadata
        ↓
TypeScript Compiler (tsc)
        ↓
Bundler (esbuild / webpack)
        ↓
Optimizations (tree-shaking, minification, code splitting)
        ↓
Output Bundle
Enter fullscreen mode Exit fullscreen mode

AOT vs JIT Compilation

AOT (Ahead-of-Time) JIT (Just-in-Time)
When Build time Runtime in browser
Bundle size Smaller (no compiler shipped) Larger (+~500KB compiler)
Startup Faster (pre-compiled) Slower (compile on load)
Error detection Build time Runtime
Template type checking Yes (strictTemplates) Partial
Used in Production (default) Development only

Template Type Checking Modes

// tsconfig.json
{
  "angularCompilerOptions": {
    "strictTemplates": true,           // full type checking
    "strictInputTypes": true,          // @Input type checking
    "strictNullInputTypes": true,      // null/undefined input checking
    "strictAttributeTypes": true,      // attribute binding types
    "strictOutputEventTypes": true,    // @Output/EventEmitter types
    "strictDomLocalRefTypes": true,    // template reference variable types
    "strictSafeNavigationTypes": true, // ?. operator types
    "strictDomEventTypes": true        // DOM event types
  }
}
Enter fullscreen mode Exit fullscreen mode

Understanding ngcc and ngtsc

  • ngtsc: Angular's TypeScript compiler plugin — transforms Angular decorators into static fields (ɵfac, ɵcmp, ɵdir, etc.)
  • ngcc: Angular compatibility compiler (legacy) — compiled pre-Ivy libraries to Ivy format. Deprecated in Angular 16+.
// What ngtsc generates from your @Component decorator:
// Your source:
@Component({ selector: 'app-user', template: '<h1>{{name}}</h1>' })
export class UserComponent { name = 'Alice'; }

// Generated (simplified):
export class UserComponent {
  name = 'Alice';
  static ɵfac = () => new UserComponent();
  static ɵcmp = ɵɵdefineComponent({
    type: UserComponent,
    selectors: [['app-user']],
    template: function(rf, ctx) {
      if (rf & 1) { ɵɵelementStart(0, 'h1'); ɵɵtext(1); ɵɵelementEnd(); }
      if (rf & 2) { ɵɵadvance(1); ɵɵtextInterpolate(ctx.name); }
    }
  });
}
Enter fullscreen mode Exit fullscreen mode

esbuild Migration (Angular 17+)

Angular 17 moved from webpack to esbuild as the default builder, offering:

  • 2–10x faster builds in development
  • Faster HMR (Hot Module Replacement)
  • Native ESM support
// angular.json  esbuild builder (default in v17+)
{
  "architect": {
    "build": {
      "builder": "@angular-devkit/build-angular:application",
      "options": {
        "browser": "src/main.ts",
        "server": "src/main.server.ts",
        "prerender": true,
        "ssr": { "entry": "server.ts" }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

2. Ivy Rendering Engine Deep Dive

How Ivy Differs from View Engine

Aspect View Engine (legacy) Ivy
Locality Global compilation Local compilation per component
Tree-shaking Poor Excellent (unused framework code removed)
Debug info Limited Rich runtime debuggability
Compilation Two-pass Single-pass
Generated code NgFactory files Static fields on class
Bundle size Larger baseline Smaller, scales with app size

Ivy's Incremental DOM Approach

Ivy uses incremental DOM — instructions are generated for each template operation, enabling:

  1. Predictable memory allocation — no virtual DOM diffing
  2. Template instructions are tree-shakeable — unused DOM operations are removed
  3. Better debugging — breakpoints in generated instructions map to templates
// Ivy template instructions (what runs in the browser):
// rf & 1 = CREATE phase (first render)
// rf & 2 = UPDATE phase (change detection)

static ɵcmp = ɵɵdefineComponent({
  template: function AppComponent_Template(rf, ctx) {
    if (rf & 1) {
      ɵɵelementStart(0, 'div', 0);       // create <div>
      ɵɵelementStart(1, 'span');          // create <span>
      ɵɵtext(2);                          // create text node
      ɵɵelementEnd();                     // close </span>
      ɵɵelementEnd();                     // close </div>
    }
    if (rf & 2) {
      ɵɵadvance(2);                       // move to node 2
      ɵɵtextInterpolate(ctx.message);     // update text
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

Angular's LView & TView Data Structures

At runtime, Ivy maintains two key data structures per component:

  • TView (Template View): Shared blueprint — template instructions, binding info, queries. One per component type.
  • LView (Logical View): Instance data — actual DOM nodes, current binding values. One per component instance.

This separation enables efficient memory use and change detection.


3. Advanced Change Detection & Zoneless Architecture

Zone.js Deep Dive

Zone.js monkey-patches 100+ browser APIs to detect when async operations complete:

// APIs patched by Zone.js (partial list):
// setTimeout, setInterval, setImmediate
// Promise.then, Promise.catch
// XMLHttpRequest.send
// fetch (partially)
// addEventListener/removeEventListener
// MutationObserver, IntersectionObserver
// requestAnimationFrame
Enter fullscreen mode Exit fullscreen mode

The cost: Every async operation — even unrelated to Angular — triggers a change detection cycle across the entire component tree.

Zoneless Architecture (Angular 18+)

// Enable zoneless (Angular 18+ stable)
bootstrapApplication(AppComponent, {
  providers: [
    provideExperimentalZonelessChangeDetection(), // v17-18
    // or in v18+:
    provideZonelessChangeDetection(),
  ],
});
Enter fullscreen mode Exit fullscreen mode

Consequences of going zoneless:

  1. Change detection ONLY runs when a signal changes, markForCheck() is called, or detectChanges() is invoked
  2. async/await, setTimeout, HTTP requests do NOT auto-trigger CD
  3. All async state MUST flow through signals or markForCheck()
// Zoneless-compatible component — must use signals
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <p>Count: {{ count() }}</p>
    <button (click)="increment()">+</button>
  `
})
export class CounterComponent {
  count = signal(0);
  increment() { this.count.update(c => c + 1); } // triggers CD automatically via signal
}

// Zoneless-incompatible — using setTimeout without signals
@Component({})
export class BrokenComponent {
  title = 'Initial';
  ngOnInit() {
    setTimeout(() => {
      this.title = 'Updated'; // WILL NOT render — no signal, no zone, no markForCheck
    }, 1000);
  }
}

// Fixed version
@Component({})
export class FixedComponent {
  title = signal('Initial');
  ngOnInit() {
    setTimeout(() => this.title.set('Updated')); // signal triggers CD
  }
}
Enter fullscreen mode Exit fullscreen mode

Manual Change Detection API

@Component({})
export class ManualCdComponent {
  private cdr = inject(ChangeDetectorRef);
  private ngZone = inject(NgZone);

  // Run outside Angular zone — NO CD triggered
  startPolling() {
    this.ngZone.runOutsideAngular(() => {
      setInterval(() => {
        const data = this.processData();
        // Only re-enter Angular zone when needed
        if (data.hasChanges) {
          this.ngZone.run(() => {
            this.data = data.result;
            // OnPush components need explicit marking
            this.cdr.markForCheck();
          });
        }
      }, 100);
    });
  }

  // Detach component from CD tree entirely (advanced optimization)
  detachFromCDTree() {
    this.cdr.detach();
    // Manually re-attach and check when needed
    setInterval(() => {
      this.cdr.reattach();
      this.cdr.detectChanges();
      this.cdr.detach();
    }, 5000);
  }
}
Enter fullscreen mode Exit fullscreen mode

Change Detection Profiling

// Enable Angular DevTools profiler
// In app.config.ts:
import { enableDebugTools } from '@angular/platform-browser';

bootstrapApplication(AppComponent).then(appRef => {
  if (!environment.production) {
    enableDebugTools(appRef.components[0]);
  }
});

// In browser console:
// ng.profiler.timeChangeDetection({ record: true })
Enter fullscreen mode Exit fullscreen mode

4. Advanced Dependency Injection Patterns

Multi-Level Injector Tree Manipulation

// Skip parent injector — use only self's providers
constructor(@Self() private service: MyService) {}

// Skip self — look up in parent injectors only
constructor(@SkipSelf() private service: MyService) {}

// Optional injection — null if not found (avoids runtime error)
constructor(@Optional() private logger: LoggerService | null) {}

// Host component injector boundary
constructor(@Host() private controlContainer: ControlContainer) {}

// Combining decorators
constructor(
  @Optional() @SkipSelf() private parentConfig: AppConfig | null
) {}
Enter fullscreen mode Exit fullscreen mode

Environment Injectors & Dynamic Components

// Create a dynamic component with its own injector scope
@Injectable({ providedIn: 'root' })
export class DynamicComponentService {
  private appRef = inject(ApplicationRef);
  private environmentInjector = inject(EnvironmentInjector);

  create<T>(component: Type<T>, inputs?: Partial<T>): ComponentRef<T> {
    const ref = createComponent(component, {
      environmentInjector: this.environmentInjector,
    });

    // Set inputs
    if (inputs) {
      Object.entries(inputs).forEach(([key, value]) => {
        ref.setInput(key, value);
      });
    }

    this.appRef.attachView(ref.hostView);
    document.body.appendChild((ref.hostView as EmbeddedViewRef<any>).rootNodes[0]);

    return ref;
  }

  destroy<T>(ref: ComponentRef<T>): void {
    this.appRef.detachView(ref.hostView);
    ref.destroy();
  }
}
Enter fullscreen mode Exit fullscreen mode

Custom Injection Context

// Running code in an injection context (Angular 16+)
import { runInInjectionContext, EnvironmentInjector } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class PluginLoader {
  private injector = inject(EnvironmentInjector);

  loadPlugin(pluginFactory: () => void): void {
    // Execute factory within Angular's injection context
    runInInjectionContext(this.injector, pluginFactory);
  }
}

// Use case: lazy-load a service with inject() calls
this.pluginLoader.loadPlugin(() => {
  const http = inject(HttpClient); // valid — inside injection context
  const store = inject(Store);
  // setup plugin logic
});
Enter fullscreen mode Exit fullscreen mode

Provider Isolation with createEnvironmentInjector

// Create an isolated DI scope for feature modules
const featureInjector = createEnvironmentInjector(
  [
    { provide: FeatureConfig, useValue: customConfig },
    FeatureSpecificService,
  ],
  parentInjector, // inherit from parent
);

// Use in dynamic module loading
const component = createComponent(FeatureComponent, {
  environmentInjector: featureInjector,
});
Enter fullscreen mode Exit fullscreen mode

5. Advanced RxJS Patterns & Custom Operators

Scheduler-Based Operators

import { asyncScheduler, animationFrameScheduler, queueScheduler } from 'rxjs';
import { observeOn, subscribeOn } from 'rxjs/operators';

// observeOn: controls which scheduler delivers notifications (affects callbacks)
source$.pipe(
  observeOn(animationFrameScheduler), // sync with browser paint cycle
).subscribe(val => this.renderFrame(val));

// subscribeOn: controls which scheduler subscription happens on
heavySource$.pipe(
  subscribeOn(asyncScheduler), // defer subscription to next macrotask
).subscribe();

// Use queueScheduler for synchronous recursive operations
// Use asyncScheduler to break synchronous chains (like setTimeout 0)
// Use animationFrameScheduler for smooth DOM animations
Enter fullscreen mode Exit fullscreen mode

Advanced Multicasting

import { Subject, BehaviorSubject, ReplaySubject, AsyncSubject } from 'rxjs';
import { publish, refCount, share, shareReplay, multicast } from 'rxjs/operators';

// Subject comparison
const subject$ = new Subject<number>();       // no initial value, no replay
const behavior$ = new BehaviorSubject<number>(0);  // holds current value
const replay$ = new ReplaySubject<number>(5); // replays last N values
const async$ = new AsyncSubject<number>();    // only emits last value on complete

// share = publish() + refCount()
// shareReplay(1) = publish() + refCount() + replay last 1
// shareReplay({ bufferSize: 1, refCount: true }) — auto-unsubscribes when no subscribers

// Proper cache with expiry
function cacheWithExpiry<T>(ttl: number) {
  return (source: Observable<T>) => {
    let cache$: Observable<T> | null = null;
    let expiry: number | null = null;
    return defer(() => {
      if (!cache$ || (expiry && Date.now() > expiry)) {
        expiry = Date.now() + ttl;
        cache$ = source.pipe(shareReplay(1));
      }
      return cache$;
    });
  };
}

// Usage
const users$ = this.http.get<User[]>('/api/users').pipe(
  cacheWithExpiry(60_000), // cache for 1 minute
);
Enter fullscreen mode Exit fullscreen mode

Building a State Machine with RxJS

type AuthState = 'idle' | 'loading' | 'authenticated' | 'error';

interface AuthMachine {
  state: AuthState;
  user: User | null;
  error: string | null;
}

const authMachine$ = merge(
  loginAction$.pipe(map(() => ({ type: 'LOGIN' as const }))),
  logoutAction$.pipe(map(() => ({ type: 'LOGOUT' as const }))),
  loginSuccess$.pipe(map(user => ({ type: 'SUCCESS' as const, user }))),
  loginError$.pipe(map(error => ({ type: 'ERROR' as const, error }))),
).pipe(
  scan((state: AuthMachine, event): AuthMachine => {
    switch (event.type) {
      case 'LOGIN':    return { ...state, state: 'loading', error: null };
      case 'SUCCESS':  return { state: 'authenticated', user: event.user, error: null };
      case 'ERROR':    return { ...state, state: 'error', error: event.error };
      case 'LOGOUT':   return { state: 'idle', user: null, error: null };
      default:         return state;
    }
  }, { state: 'idle', user: null, error: null }),
  shareReplay(1),
);
Enter fullscreen mode Exit fullscreen mode

Advanced Error Handling Pipeline

function resilientHttp<T>(
  request$: Observable<T>,
  options: { retries: number; retryDelay: number; fallback?: T }
): Observable<T> {
  return request$.pipe(
    retryWhen(errors =>
      errors.pipe(
        scan((count, err) => {
          if (count >= options.retries) throw err;
          return count + 1;
        }, 0),
        delayWhen((count) => timer(options.retryDelay * Math.pow(2, count - 1))),
        tap(count => console.warn(`Retry attempt ${count}`)),
      )
    ),
    timeout(10_000),
    catchError(err => {
      if (options.fallback !== undefined) return of(options.fallback);
      return throwError(() => new AppError(err.message, err.status));
    }),
  );
}
Enter fullscreen mode Exit fullscreen mode

Custom Operator: poll

// Polling operator with exponential backoff on error
function poll<T>(intervalMs: number) {
  return (source: Observable<T>): Observable<T> =>
    timer(0, intervalMs).pipe(
      switchMap(() => source),
    );
}

// Usage
this.orderService.getStatus(orderId).pipe(
  poll(5000),
  distinctUntilKeyChanged('status'),
  takeWhile(order => order.status !== 'delivered', true),
).subscribe(order => this.order = order);
Enter fullscreen mode Exit fullscreen mode

6. Angular Signals — Advanced Patterns

Signal-Based Store (NgRx Signal Store)

import { signalStore, withState, withComputed, withMethods, patchState } from '@ngrx/signals';
import { rxMethod } from '@ngrx/signals/rxjs-interop';

interface UserState {
  users: User[];
  selectedId: string | null;
  loading: boolean;
  error: string | null;
}

export const UserStore = signalStore(
  { providedIn: 'root' },
  withState<UserState>({ users: [], selectedId: null, loading: false, error: null }),

  withComputed(({ users, selectedId }) => ({
    selectedUser: computed(() => users().find(u => u.id === selectedId()) ?? null),
    totalCount: computed(() => users().length),
    activeUsers: computed(() => users().filter(u => u.active)),
  })),

  withMethods((store, userService = inject(UserService)) => ({
    loadUsers: rxMethod<void>(
      pipe(
        tap(() => patchState(store, { loading: true, error: null })),
        switchMap(() =>
          userService.getAll().pipe(
            tapResponse({
              next: users => patchState(store, { users, loading: false }),
              error: (err: Error) => patchState(store, { error: err.message, loading: false }),
            })
          )
        ),
      )
    ),

    selectUser(id: string): void {
      patchState(store, { selectedId: id });
    },
  })),
);

// In component
@Component({})
export class UserListComponent {
  store = inject(UserStore);
  // Direct access to state slices as signals
  // store.users(), store.loading(), store.selectedUser(), etc.
}
Enter fullscreen mode Exit fullscreen mode

Linked Signal (Angular 19)

import { linkedSignal } from '@angular/core';

@Component({})
export class PaginationComponent {
  pageSize = signal(10);

  // linkedSignal resets when dependency changes
  currentPage = linkedSignal({
    source: this.pageSize,
    computation: () => 1, // reset to page 1 whenever pageSize changes
  });

  totalItems = signal(250);
  totalPages = computed(() => Math.ceil(this.totalItems() / this.pageSize()));
}
Enter fullscreen mode Exit fullscreen mode

Resource API (Angular 19)

import { resource } from '@angular/core';

@Component({})
export class UserDetailComponent {
  userId = input.required<string>();

  // Declarative async resource — auto-loads when userId changes
  userResource = resource({
    request: () => ({ id: this.userId() }),
    loader: ({ request, abortSignal }) =>
      fetch(`/api/users/${request.id}`, { signal: abortSignal })
        .then(res => res.json() as Promise<User>),
  });

  // Access as signals
  user = this.userResource.value;       // Signal<User | undefined>
  loading = this.userResource.isLoading; // Signal<boolean>
  error = this.userResource.error;       // Signal<unknown>

  // Manually refresh
  refresh() { this.userResource.reload(); }
}
Enter fullscreen mode Exit fullscreen mode

7. Server-Side Rendering (SSR) & Static Site Generation

Angular Universal / Angular SSR Architecture

Browser Request
      ↓
Node.js Server (Express / Fastify)
      ↓
Angular SSR Engine (renders component tree to HTML string)
      ↓
HTML + Transfer State sent to browser
      ↓
Browser receives HTML (fast first paint)
      ↓
Angular bootstraps and HYDRATES (attaches event listeners to server-rendered DOM)
      ↓
SPA takes over
Enter fullscreen mode Exit fullscreen mode

SSR Setup (Angular 17+ with new application builder)

// server.ts
import { APP_BASE_HREF } from '@angular/common';
import { CommonEngine } from '@angular/ssr';
import express from 'express';
import { fileURLToPath } from 'node:url';
import { dirname, join, resolve } from 'node:path';
import bootstrap from './src/main.server';

const serverDistFolder = dirname(fileURLToPath(import.meta.url));
const browserDistFolder = resolve(serverDistFolder, '../browser');

const app = express();
const commonEngine = new CommonEngine();

app.get('*', (req, res, next) => {
  const { protocol, originalUrl, baseUrl, headers } = req;

  commonEngine
    .render({
      bootstrap,
      documentFilePath: join(browserDistFolder, 'index.html'),
      url: `${protocol}://${headers.host}${originalUrl}`,
      publicPath: browserDistFolder,
      providers: [
        { provide: APP_BASE_HREF, useValue: baseUrl },
        // Server-side only providers
        { provide: REQUEST, useValue: req },
        { provide: RESPONSE, useValue: res },
      ],
    })
    .then(html => res.send(html))
    .catch(err => next(err));
});
Enter fullscreen mode Exit fullscreen mode

Transfer State — Avoiding Duplicate HTTP Requests

// Service that works isomorphically
@Injectable({ providedIn: 'root' })
export class ProductService {
  private transferState = inject(TransferState);
  private http = inject(HttpClient);
  private isPlatformBrowser = isPlatformBrowser(inject(PLATFORM_ID));

  getProducts(): Observable<Product[]> {
    const key = makeStateKey<Product[]>('products');

    // On client: check transfer state first (populated by server)
    if (this.isPlatformBrowser && this.transferState.hasKey(key)) {
      const products = this.transferState.get(key, []);
      this.transferState.remove(key); // consume once
      return of(products);
    }

    return this.http.get<Product[]>('/api/products').pipe(
      tap(products => {
        // On server: store in transfer state so client can reuse
        if (!this.isPlatformBrowser) {
          this.transferState.set(key, products);
        }
      }),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Incremental Hydration (Angular 19)

<!-- Defer hydration until user interacts -->
@defer (hydrate on interaction) {
  <shopping-cart />
} @placeholder {
  <shopping-cart-skeleton />
}

<!-- Hydrate when element enters viewport -->
@defer (hydrate on viewport) {
  <product-recommendations />
}

<!-- Hydrate on idle (after critical content hydrated) -->
@defer (hydrate on idle) {
  <footer-component />
}
Enter fullscreen mode Exit fullscreen mode

SSR Gotchas & Solutions

// Problem: DOM/browser APIs unavailable on server
// Solution: Platform checks

@Injectable()
export class BrowserStorageService {
  private platformId = inject(PLATFORM_ID);

  get(key: string): string | null {
    if (isPlatformBrowser(this.platformId)) {
      return localStorage.getItem(key);
    }
    return null; // graceful degradation
  }
}

// Problem: window/document access
// Solution: Inject tokens
import { DOCUMENT } from '@angular/common';

@Component({})
export class ScrollComponent {
  private document = inject(DOCUMENT);
  private window = this.document.defaultView; // safe reference

  scrollToTop(): void {
    this.window?.scrollTo({ top: 0, behavior: 'smooth' });
  }
}
Enter fullscreen mode Exit fullscreen mode

8. Micro-Frontend Architecture

Module Federation with Angular (Webpack 5)

// Host app webpack.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'shell',
      remotes: {
        'mfe-users': 'mfeUsers@http://localhost:4201/remoteEntry.js',
        'mfe-payments': 'mfePayments@http://localhost:4202/remoteEntry.js',
        'mfe-analytics': 'mfeAnalytics@http://localhost:4203/remoteEntry.js',
      },
      shared: {
        '@angular/core': { singleton: true, strictVersion: true, requiredVersion: '^17.0.0' },
        '@angular/common': { singleton: true, strictVersion: true },
        '@angular/router': { singleton: true, strictVersion: true },
        // Shared state management
        '@ngrx/store': { singleton: true, strictVersion: true },
      },
    }),
  ],
};

// Remote app webpack.config.js
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'mfeUsers',
      filename: 'remoteEntry.js',
      exposes: {
        './Module': './src/app/users/users.module.ts',
        './Routes': './src/app/users/users.routes.ts',
      },
    }),
  ],
};
Enter fullscreen mode Exit fullscreen mode

Native Federation (esbuild-compatible, Angular 17+)

// @angular-architects/native-federation setup
// federation.config.js (host)
const { withNativeFederation, shareAll } = require('@angular-architects/native-federation/config');

module.exports = withNativeFederation({
  remotes: {
    'mfe-users': 'http://localhost:4201/remoteEntry.json',
  },
  shared: {
    ...shareAll({ singleton: true, strictVersion: true }),
  },
});
Enter fullscreen mode Exit fullscreen mode

Cross-MFE Communication Patterns

// Pattern 1: Shared EventBus via singleton service
@Injectable({ providedIn: 'root' })
export class EventBusService {
  private eventBus = new Subject<{ type: string; payload: unknown }>();
  readonly events$ = this.eventBus.asObservable();

  emit(type: string, payload: unknown): void {
    this.eventBus.next({ type, payload });
  }

  on<T>(type: string): Observable<T> {
    return this.events$.pipe(
      filter(e => e.type === type),
      map(e => e.payload as T),
    );
  }
}

// Pattern 2: Custom DOM Events (works across framework boundaries)
// Emitter MFE
window.dispatchEvent(new CustomEvent('user:selected', {
  detail: { userId: '123' },
  bubbles: true,
}));

// Receiver MFE
fromEvent<CustomEvent>(window, 'user:selected').pipe(
  map(e => e.detail.userId),
).subscribe(userId => this.loadUser(userId));

// Pattern 3: Shared state via BroadcastChannel API (cross-tab/MFE)
const channel = new BroadcastChannel('app-state');
channel.postMessage({ type: 'CART_UPDATED', items: cart });
channel.onmessage = (event) => this.handleStateUpdate(event.data);
Enter fullscreen mode Exit fullscreen mode

9. Monorepo with Nx

Nx Workspace Structure

apps/
├── shell/                   # Host Angular app
├── mfe-users/               # Remote MFE
├── mfe-payments/            # Remote MFE
└── api/                     # NestJS backend (optional)
libs/
├── shared/
│   ├── ui/                  # Shared component library
│   ├── data-access/         # Shared NgRx/services
│   ├── utils/               # Pure utility functions
│   └── models/              # Shared TypeScript interfaces
├── users/
│   ├── feature/             # Smart components (routed pages)
│   ├── ui/                  # Dumb/presentational components
│   ├── data-access/         # User-specific services/store
│   └── domain/              # User domain models/validators
└── payments/
    ├── feature/
    ├── ui/
    └── data-access/
Enter fullscreen mode Exit fullscreen mode

Nx Dependency Constraints

// .eslintrc.json  enforce architectural boundaries
{
  "rules": {
    "@nx/enforce-module-boundaries": [
      "error",
      {
        "depConstraints": [
          {
            "sourceTag": "type:feature",
            "onlyDependOnLibsWithTags": ["type:ui", "type:data-access", "type:util", "type:domain"]
          },
          {
            "sourceTag": "type:ui",
            "onlyDependOnLibsWithTags": ["type:ui", "type:util", "type:domain"]
          },
          {
            "sourceTag": "type:data-access",
            "onlyDependOnLibsWithTags": ["type:data-access", "type:util", "type:domain"]
          },
          {
            "sourceTag": "scope:users",
            "onlyDependOnLibsWithTags": ["scope:users", "scope:shared"]
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Nx Affected Commands (CI Optimization)

# Only run tests for affected projects (based on git diff)
nx affected:test --base=main --head=HEAD

# Only build affected apps
nx affected:build --base=main

# Visualize dependency graph
nx graph

# Run tasks in parallel with dependency awareness
nx run-many --target=build --all --parallel=3

# Generate project with Nx generators
nx g @nx/angular:app my-app --routing --style=scss
nx g @nx/angular:lib shared/ui --buildable --publishable
Enter fullscreen mode Exit fullscreen mode

10. Advanced Performance Engineering

Bundle Analysis & Optimization

// 1. Angular budget alerts in angular.json
"budgets": [
  { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" },
  { "type": "anyComponentStyle", "maximumWarning": "4kb", "maximumError": "8kb" },
  { "type": "anyScript", "maximumWarning": "100kb" },
  { "type": "total", "maximumWarning": "2mb" }
]

// 2. Source map explorer
npx source-map-explorer dist/app/browser/main.*.js

// 3. Webpack Bundle Analyzer
ng build --stats-json
npx webpack-bundle-analyzer dist/app/browser/stats.json
Enter fullscreen mode Exit fullscreen mode

Advanced Virtual Scrolling

// Custom virtual scroll strategy for variable height items
@Injectable()
export class VariableHeightScrollStrategy implements VirtualScrollStrategy {
  private viewport!: CdkVirtualScrollViewport;
  private index$ = new BehaviorSubject<number>(0);
  readonly scrolledIndexChange = this.index$.asObservable();
  private itemHeights = new Map<number, number>();

  attach(viewport: CdkVirtualScrollViewport): void {
    this.viewport = viewport;
    this.updateTotalContentSize();
    this.updateRenderedRange();
  }

  detach(): void {}

  onContentScrolled(): void {
    this.updateRenderedRange();
  }

  onDataLengthChanged(): void {
    this.updateTotalContentSize();
    this.updateRenderedRange();
  }

  onContentRendered(): void {
    this.measureRenderedItemHeights();
    this.updateTotalContentSize();
  }

  onRenderedOffsetChanged(): void {}

  scrollToIndex(index: number, behavior: ScrollBehavior): void {
    const offset = this.getOffsetForIndex(index);
    this.viewport.scrollToOffset(offset, behavior);
  }

  private measureRenderedItemHeights(): void {
    // Measure actual DOM heights and cache them
  }
  private updateTotalContentSize(): void { /* ... */ }
  private updateRenderedRange(): void { /* ... */ }
  private getOffsetForIndex(index: number): number { return 0; }
}
Enter fullscreen mode Exit fullscreen mode

Runtime Performance Monitoring

@Injectable({ providedIn: 'root' })
export class PerformanceMonitorService {
  // Track Angular CD cycles in production
  private cdCount = 0;
  private lastCdTime = 0;

  trackChangeDetection(): void {
    // Use PerformanceObserver for long tasks
    if ('PerformanceObserver' in window) {
      const observer = new PerformanceObserver(list => {
        list.getEntries().forEach(entry => {
          if (entry.duration > 50) { // Long task > 50ms
            console.warn(`Long task detected: ${entry.duration.toFixed(2)}ms`, entry);
            this.reportLongTask(entry);
          }
        });
      });
      observer.observe({ entryTypes: ['longtask'] });
    }
  }

  // Web Vitals tracking
  trackCoreWebVitals(): void {
    // LCP
    new PerformanceObserver(list => {
      const entries = list.getEntries();
      const lcp = entries[entries.length - 1];
      this.reportMetric('LCP', lcp.startTime);
    }).observe({ type: 'largest-contentful-paint', buffered: true });

    // CLS
    let clsValue = 0;
    new PerformanceObserver(list => {
      list.getEntries().forEach((entry: any) => {
        if (!entry.hadRecentInput) clsValue += entry.value;
      });
      this.reportMetric('CLS', clsValue);
    }).observe({ type: 'layout-shift', buffered: true });
  }

  private reportMetric(name: string, value: number): void {
    // Send to analytics
  }

  private reportLongTask(entry: PerformanceEntry): void {
    // Send to monitoring service
  }
}
Enter fullscreen mode Exit fullscreen mode

Image Optimization with NgOptimizedImage

import { NgOptimizedImage } from '@angular/common';

@Component({
  imports: [NgOptimizedImage],
  template: `
    <!-- LCP image: add priority -->
    <img ngSrc="/hero.jpg" width="1200" height="600" priority alt="Hero" />

    <!-- Regular image: lazy by default -->
    <img ngSrc="/product.jpg" width="400" height="400" alt="Product" />

    <!-- Responsive with srcset -->
    <img ngSrc="/photo.jpg" width="800" height="600" sizes="(max-width: 768px) 100vw, 50vw" alt="Photo" />
  `
})
export class ImageComponent {}

// Custom loader for CDN
providers: [
  provideImgixLoader('https://my-cdn.imgix.net'),
  // or custom:
  {
    provide: IMAGE_LOADER,
    useValue: (config: ImageLoaderConfig) =>
      `https://cdn.example.com/${config.src}?w=${config.width}&q=80`,
  }
]
Enter fullscreen mode Exit fullscreen mode

11. Custom Angular Libraries & ng-packagr

Building a Publishable Library

# Generate library in Nx or Angular workspace
ng generate library @my-org/ui-components --prefix=my
# or with Nx:
nx g @nx/angular:lib shared/ui --buildable --publishable --importPath="@my-org/ui"
Enter fullscreen mode Exit fullscreen mode

Library Structure & Public API

// libs/ui/src/index.ts — public API barrel
export { ButtonComponent } from './lib/button/button.component';
export { InputComponent } from './lib/input/input.component';
export { ModalService } from './lib/modal/modal.service';
export { TableModule } from './lib/table/table.module';
// NEVER export internals — they're private API
Enter fullscreen mode Exit fullscreen mode

ng-package.json Configuration

{
  "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
  "lib": {
    "entryFile": "src/index.ts",
    "cssUrl": "inline"
  },
  "assets": ["./styles/**/*", "CHANGELOG.md"],
  "deleteDestPath": false
}
Enter fullscreen mode Exit fullscreen mode

Secondary Entry Points

libs/ui/
├── src/index.ts                  (primary: @my-org/ui)
├── ng-package.json
├── testing/
│   ├── src/index.ts              (secondary: @my-org/ui/testing)
│   └── ng-package.json
└── styles/
    └── themes.scss
Enter fullscreen mode Exit fullscreen mode

12. Advanced Testing Strategies

Testing Harnesses (Angular CDK)

// Build a test harness for your component
class ButtonHarness extends ComponentHarness {
  static hostSelector = 'my-button';

  async click(): Promise<void> {
    return (await this.host()).click();
  }

  async getText(): Promise<string> {
    return (await this.host()).text();
  }

  async isDisabled(): Promise<boolean> {
    return (await this.host()).getProperty('disabled');
  }

  async isLoading(): Promise<boolean> {
    return (await this.host()).hasClass('loading');
  }
}

// Usage in tests — resilient to DOM structure changes
it('should disable button when loading', async () => {
  const loader = TestbedHarnessEnvironment.loader(fixture);
  const button = await loader.getHarness(ButtonHarness);

  component.isLoading = true;
  fixture.detectChanges();

  expect(await button.isDisabled()).toBe(true);
});
Enter fullscreen mode Exit fullscreen mode

Testing with Spectator

import { createComponentFactory, Spectator } from '@ngneat/spectator';

describe('UserComponent', () => {
  let spectator: Spectator<UserComponent>;

  const createComponent = createComponentFactory({
    component: UserComponent,
    mocks: [UserService],
    detectChanges: false,
  });

  beforeEach(() => spectator = createComponent());

  it('should load users on init', () => {
    const userService = spectator.inject(UserService);
    userService.getAll.andReturn(of([mockUser]));

    spectator.detectChanges();

    expect(spectator.queryAll('user-card')).toHaveLength(1);
  });
});
Enter fullscreen mode Exit fullscreen mode

Integration Testing with Cypress Component Testing

// users.component.cy.ts
import { UserListComponent } from './user-list.component';
import { mount } from 'cypress/angular';

describe('UserListComponent', () => {
  it('should render users from service', () => {
    mount(UserListComponent, {
      providers: [
        { provide: UserService, useValue: { getAll: () => of(mockUsers) } },
      ],
      imports: [RouterTestingModule],
    });

    cy.get('[data-cy="user-card"]').should('have.length', 3);
    cy.get('[data-cy="user-name"]').first().should('contain', 'Alice');
  });

  it('should filter users by search', () => {
    mount(UserListComponent, { /* ... */ });

    cy.get('[data-cy="search-input"]').type('Alice');
    cy.get('[data-cy="user-card"]').should('have.length', 1);
  });
});
Enter fullscreen mode Exit fullscreen mode

Contract Testing with Pact

// Ensures frontend and backend API contracts are compatible
describe('UserService Pact', () => {
  const provider = new PactV3({
    consumer: 'angular-frontend',
    provider: 'user-api',
  });

  it('should get user by id', async () => {
    await provider.addInteraction({
      states: [{ description: 'user 1 exists' }],
      uponReceiving: 'a request for user 1',
      withRequest: { method: 'GET', path: '/api/users/1' },
      willRespondWith: {
        status: 200,
        body: MatchersV3.like({ id: '1', name: 'Alice', email: 'alice@test.com' }),
      },
    });

    await provider.executeTest(async (mockServer) => {
      const service = new UserService(new HttpClient(/* ... */));
      const user = await firstValueFrom(service.getUser('1'));
      expect(user.name).toBe('Alice');
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

13. Custom Renderers & Platform Abstraction

Building a Custom Renderer

import { Renderer2, RendererFactory2, RendererStyleFlags2 } from '@angular/core';

@Injectable()
export class CanvasRendererFactory implements RendererFactory2 {
  createRenderer(hostElement: Element | null, type: RendererType2 | null): Renderer2 {
    return new CanvasRenderer();
  }
}

class CanvasRenderer implements Renderer2 {
  readonly data: { [key: string]: any } = {};
  private canvas!: HTMLCanvasElement;
  private ctx!: CanvasRenderingContext2D;

  createElement(name: string, namespace?: string): any {
    return { tagName: name, children: [], styles: {}, attributes: {} };
  }

  createText(value: string): any {
    return { type: 'text', value };
  }

  appendChild(parent: any, newChild: any): void {
    parent.children.push(newChild);
    this.renderToCanvas(parent); // re-render affected subtree
  }

  setStyle(el: any, style: string, value: any): void {
    el.styles[style] = value;
    this.renderToCanvas(el);
  }

  private renderToCanvas(el: any): void {
    // Custom canvas rendering logic
  }

  // ... implement all Renderer2 methods
  destroy(): void {}
  destroyNode: null = null;
  createComment(value: string): any { return {}; }
  insertBefore(parent: any, newChild: any, refChild: any): void {}
  removeChild(parent: any, oldChild: any): void {}
  selectRootElement(selectorOrNode: any): any { return {}; }
  parentNode(node: any): any { return null; }
  nextSibling(node: any): any { return null; }
  setAttribute(el: any, name: string, value: string): void {}
  removeAttribute(el: any, name: string): void {}
  addClass(el: any, name: string): void {}
  removeClass(el: any, name: string): void {}
  removeStyle(el: any, style: string): void {}
  setProperty(el: any, name: string, value: any): void {}
  setValue(node: any, value: string): void {}
  listen(target: any, eventName: string, callback: (event: any) => boolean | void): () => void {
    return () => {};
  }
}
Enter fullscreen mode Exit fullscreen mode

14. Angular CDK & Advanced Component Patterns

Overlay Service for Custom Popups

@Injectable({ providedIn: 'root' })
export class TooltipService {
  private overlay = inject(Overlay);
  private injector = inject(Injector);

  show<T>(component: Type<T>, origin: ElementRef, data: Partial<T>): OverlayRef {
    const positionStrategy = this.overlay
      .position()
      .flexibleConnectedTo(origin)
      .withPositions([
        { originX: 'center', originY: 'bottom', overlayX: 'center', overlayY: 'top', offsetY: 8 },
        { originX: 'center', originY: 'top', overlayX: 'center', overlayY: 'bottom', offsetY: -8 },
      ]);

    const overlayRef = this.overlay.create({
      positionStrategy,
      scrollStrategy: this.overlay.scrollStrategies.reposition(),
      hasBackdrop: false,
    });

    const portal = new ComponentPortal(
      component,
      null,
      Injector.create({
        providers: Object.entries(data).map(([key, value]) => ({
          provide: key, useValue: value,
        })),
        parent: this.injector,
      })
    );

    const ref = overlayRef.attach(portal);
    return overlayRef;
  }
}
Enter fullscreen mode Exit fullscreen mode

Compound Component Pattern

// Context token for parent-child communication
const TABS_CONTEXT = new InjectionToken<TabsContext>('TABS_CONTEXT');

interface TabsContext {
  activeTab: Signal<string>;
  registerTab(id: string): void;
  setActive(id: string): void;
}

@Component({
  selector: 'my-tabs',
  template: `
    <div class="tabs-header"><ng-content select="my-tab-header" /></div>
    <div class="tabs-body"><ng-content select="my-tab-panel" /></div>
  `,
  providers: [{ provide: TABS_CONTEXT, useExisting: TabsComponent }],
})
export class TabsComponent implements TabsContext {
  activeTab = signal('');
  private tabs = signal<string[]>([]);

  registerTab(id: string): void {
    this.tabs.update(tabs => [...tabs, id]);
    if (this.tabs().length === 1) this.activeTab.set(id);
  }

  setActive(id: string): void { this.activeTab.set(id); }
}

@Component({
  selector: 'my-tab-panel',
  template: `<div [hidden]="!isActive()"><ng-content /></div>`,
})
export class TabPanelComponent implements OnInit {
  @Input({ required: true }) id!: string;
  private context = inject(TABS_CONTEXT);

  isActive = computed(() => this.context.activeTab() === this.id);

  ngOnInit() { this.context.registerTab(this.id); }
}
Enter fullscreen mode Exit fullscreen mode

Form Control Value Accessor

@Component({
  selector: 'my-rating',
  template: `
    <button *ngFor="let star of stars; let i = index"
            (click)="setValue(i + 1)"
            [class.active]="i < value">★</button>
  `,
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => RatingComponent),
    multi: true,
  }],
})
export class RatingComponent implements ControlValueAccessor {
  stars = [1, 2, 3, 4, 5];
  value = 0;
  isDisabled = false;

  private onChange = (value: number) => {};
  private onTouched = () => {};

  writeValue(value: number): void { this.value = value ?? 0; }
  registerOnChange(fn: (value: number) => void): void { this.onChange = fn; }
  registerOnTouched(fn: () => void): void { this.onTouched = fn; }
  setDisabledState(isDisabled: boolean): void { this.isDisabled = isDisabled; }

  setValue(value: number): void {
    if (this.isDisabled) return;
    this.value = value;
    this.onChange(value);
    this.onTouched();
  }
}

// Usage:
// <my-rating formControlName="productRating" />
Enter fullscreen mode Exit fullscreen mode

15. Advanced Security Architecture

Content Security Policy with Angular

// server.ts — set CSP headers
app.use((req, res, next) => {
  const nonce = crypto.randomBytes(16).toString('base64');
  res.locals.nonce = nonce;

  res.setHeader('Content-Security-Policy', [
    `default-src 'self'`,
    `script-src 'self' 'nonce-${nonce}'`,
    `style-src 'self' 'unsafe-inline'`, // Angular needs this for component styles
    `img-src 'self' data: https://cdn.example.com`,
    `connect-src 'self' https://api.example.com`,
    `frame-src 'none'`,
    `object-src 'none'`,
  ].join('; '));

  next();
});

// Angular Universal: inject nonce into bootstrap script
providers: [
  { provide: CSP_NONCE, useFactory: () => res.locals.nonce },
]
Enter fullscreen mode Exit fullscreen mode

JWT Refresh Token Flow

@Injectable()
export class TokenRefreshInterceptor implements HttpInterceptor {
  private isRefreshing = false;
  private refreshSubject = new BehaviorSubject<string | null>(null);

  constructor(private auth: AuthService, private http: HttpClient) {}

  intercept(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
    return next.handle(req).pipe(
      catchError(err => {
        if (err.status !== 401 || req.url.includes('/auth/refresh')) {
          return throwError(() => err);
        }

        if (this.isRefreshing) {
          // Queue requests while refresh is in-flight
          return this.refreshSubject.pipe(
            filter(token => token !== null),
            take(1),
            switchMap(token => next.handle(this.addToken(req, token!))),
          );
        }

        this.isRefreshing = true;
        this.refreshSubject.next(null);

        return this.auth.refreshToken().pipe(
          switchMap(({ accessToken }) => {
            this.isRefreshing = false;
            this.refreshSubject.next(accessToken);
            return next.handle(this.addToken(req, accessToken));
          }),
          catchError(refreshErr => {
            this.isRefreshing = false;
            this.auth.logout();
            return throwError(() => refreshErr);
          }),
        );
      }),
    );
  }

  private addToken(req: HttpRequest<unknown>, token: string): HttpRequest<unknown> {
    return req.clone({ headers: req.headers.set('Authorization', `Bearer ${token}`) });
  }
}
Enter fullscreen mode Exit fullscreen mode

Preventing Prototype Pollution

// Safe object merging utility
function safeMerge<T extends object>(target: T, source: Partial<T>): T {
  const result = { ...target };
  for (const key of Object.keys(source)) {
    // Block prototype pollution attempts
    if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
      continue;
    }
    if (Object.prototype.hasOwnProperty.call(source, key)) {
      (result as any)[key] = (source as any)[key];
    }
  }
  return result;
}
Enter fullscreen mode Exit fullscreen mode

16. Web Workers & Shared Workers in Angular

Integrating Web Workers

// heavy-computation.worker.ts
/// <reference lib="webworker" />

import { runHeavyAlgorithm } from './algorithms';

self.addEventListener('message', ({ data }: MessageEvent<{ type: string; payload: unknown }>) => {
  switch (data.type) {
    case 'COMPUTE':
      const result = runHeavyAlgorithm(data.payload);
      postMessage({ type: 'RESULT', payload: result });
      break;

    case 'STREAM':
      // Stream progress back to main thread
      for (const chunk of processInChunks(data.payload)) {
        postMessage({ type: 'PROGRESS', payload: chunk });
      }
      postMessage({ type: 'COMPLETE' });
      break;
  }
});

// Worker service (main thread)
@Injectable({ providedIn: 'root' })
export class ComputationService {
  private worker: Worker | null = null;
  private responses$ = new Subject<{ type: string; payload: unknown }>();

  constructor() {
    if (typeof Worker !== 'undefined') {
      this.worker = new Worker(new URL('./heavy-computation.worker', import.meta.url));
      this.worker.onmessage = ({ data }) => this.responses$.next(data);
    }
  }

  compute<T>(payload: unknown): Observable<T> {
    if (!this.worker) {
      // Fallback: run synchronously (SSR or unsupported)
      return of(runHeavyAlgorithm(payload) as T);
    }

    return new Observable(observer => {
      const subscription = this.responses$.pipe(
        filter(r => r.type === 'RESULT'),
        take(1),
      ).subscribe(r => {
        observer.next(r.payload as T);
        observer.complete();
      });

      this.worker!.postMessage({ type: 'COMPUTE', payload });

      return () => subscription.unsubscribe();
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

17. Design System Architecture

Token-Based Design System

// design-tokens.scss
:root {
  // Primitive tokens
  --color-blue-50: #eff6ff;
  --color-blue-500: #3b82f6;
  --color-blue-900: #1e3a8a;

  // Semantic tokens (reference primitives)
  --color-primary: var(--color-blue-500);
  --color-primary-hover: var(--color-blue-600);
  --color-surface: #ffffff;
  --color-on-surface: #0f172a;

  // Component tokens (reference semantic)
  --button-bg: var(--color-primary);
  --button-text: white;
  --button-radius: 0.375rem;
  --button-padding-x: 1rem;
  --button-padding-y: 0.5rem;

  // Spacing scale
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-4: 1rem;
  --space-8: 2rem;
}

// Dark theme override
[data-theme="dark"] {
  --color-surface: #0f172a;
  --color-on-surface: #f8fafc;
  --color-primary: var(--color-blue-400);
}
Enter fullscreen mode Exit fullscreen mode
// Theme service
@Injectable({ providedIn: 'root' })
export class ThemeService {
  private theme = signal<'light' | 'dark'>('light');
  private document = inject(DOCUMENT);

  readonly isDark = computed(() => this.theme() === 'dark');

  toggle(): void {
    this.theme.update(t => t === 'light' ? 'dark' : 'light');
    effect(() => {
      this.document.documentElement.setAttribute('data-theme', this.theme());
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

18. Engineering Leadership & Architecture Decisions

Architecture Decision Records (ADR)

# ADR-001: Adopt Signals as Primary State Primitive

**Date:** 2024-01-15
**Status:** Accepted
**Deciders:** Frontend Tech Lead, Senior Engineers

## Context
Our app uses a mix of BehaviorSubjects, NgRx, and local state. Change detection
performance is degrading as components multiply.

## Decision
Adopt Angular Signals as the primary state primitive for new features. Continue
using NgRx only for complex event-driven flows requiring audit trails or time-travel debugging.

## Consequences
- **Positive:** Fine-grained reactivity, simpler mental model, zoneless-ready
- **Positive:** Reduces RxJS complexity for simple state scenarios
- **Negative:** Team learning curve (~2 sprints)
- **Negative:** Migration cost for existing BehaviorSubject-based stores
- **Mitigation:** Provide migration guide, pair programming sessions, 20% sprint capacity for migration

## Alternatives Considered
1. Continue with NgRx — rejected: too heavy for simple feature state
2. Zustand-style service — rejected: not Angular-idiomatic
Enter fullscreen mode Exit fullscreen mode

Technical Debt Quantification

// Use SonarQube / ESLint complexity rules to measure:
// - Cyclomatic complexity per function (target < 10)
// - Cognitive complexity (target < 15)
// - Component size (target < 300 lines)
// - Duplicate code blocks

// ESLint rules for Angular quality
{
  "rules": {
    "complexity": ["error", { "max": 10 }],
    "@angular-eslint/component-max-inline-declarations": ["error", { "template": 3 }],
    "@angular-eslint/no-lifecycle-call": "error",
    "@angular-eslint/use-lifecycle-interface": "error",
    "@angular-eslint/prefer-on-push-component-change-detection": "warn",
    "@angular-eslint/no-output-on-prefix": "error",
    "@typescript-eslint/no-explicit-any": "error",
    "@typescript-eslint/strict-boolean-expressions": "error"
  }
}
Enter fullscreen mode Exit fullscreen mode

Incident Post-Mortem Template

## Incident: Memory Leak in Dashboard — [Date]

**Impact:** Dashboard degraded after ~2h of use, causing tab crashes for 12% of users.
**Duration:** 4h 23m (detection to resolution)
**Severity:** P1

### Timeline
- 14:00 — Monitoring alerts: heap usage trending upward
- 14:45 — Identified: Observable subscriptions not unsubscribed in DashboardComponent
- 15:10 — Root cause: RxJS interval inside ngOnInit, ngOnDestroy not implemented
- 15:30 — Fix deployed: Added takeUntilDestroyed, heap stabilized
- 18:23 — All-clear confirmed

### Root Cause
Angular lifecycle mismatch — component re-created on route navigation but prior
subscriptions leaked. Missing ngOnDestroy due to oversight in code review.

### Action Items
1. [P0] ESLint rule: warn on setInterval/fromEvent without takeUntil in components
2. [P1] Add leak detection to E2E suite using `Chrome.memory` API
3. [P2] Component template with subscription management patterns for team
4. [P3] Lunch-and-learn on memory management in Angular
Enter fullscreen mode Exit fullscreen mode

19. Advanced Coding Challenges

Implement a Type-Safe Event Bus

type EventMap = {
  'user:login': { userId: string; timestamp: Date };
  'cart:updated': { items: CartItem[]; total: number };
  'notification:show': { message: string; type: 'info' | 'error' | 'success' };
};

@Injectable({ providedIn: 'root' })
export class TypedEventBus {
  private subjects = new Map<keyof EventMap, Subject<any>>();

  private getSubject<K extends keyof EventMap>(event: K): Subject<EventMap[K]> {
    if (!this.subjects.has(event)) {
      this.subjects.set(event, new Subject());
    }
    return this.subjects.get(event)!;
  }

  emit<K extends keyof EventMap>(event: K, payload: EventMap[K]): void {
    this.getSubject(event).next(payload);
  }

  on<K extends keyof EventMap>(event: K): Observable<EventMap[K]> {
    return this.getSubject(event).asObservable();
  }
}

// Usage — fully type-safe:
eventBus.emit('user:login', { userId: '123', timestamp: new Date() });
eventBus.on('cart:updated').subscribe(({ items, total }) => { /* typed */ });
Enter fullscreen mode Exit fullscreen mode

Implement createSignalStore (NgRx Signal Store Clone)

type StateSignals<T> = { [K in keyof T]: Signal<T[K]> };

function createSignalStore<T extends object>(initialState: T) {
  const state = signal(initialState);

  const stateSignals = Object.keys(initialState).reduce((acc, key) => {
    acc[key as keyof T] = computed(() => state()[key as keyof T]) as Signal<T[keyof T]>;
    return acc;
  }, {} as StateSignals<T>);

  function patchState(partial: Partial<T>): void {
    state.update(current => ({ ...current, ...partial }));
  }

  function select<R>(selector: (s: T) => R): Signal<R> {
    return computed(() => selector(state()));
  }

  return { ...stateSignals, patchState, select, $state: state.asReadonly() };
}

// Usage
const store = createSignalStore({ count: 0, name: 'Angular', loading: false });
store.count(); // Signal<number>
store.patchState({ count: 5 });
const doubled = store.select(s => s.count * 2);
Enter fullscreen mode Exit fullscreen mode

Build an Async Pipeline Orchestrator

interface PipelineStep<TIn, TOut> {
  name: string;
  execute: (input: TIn) => Observable<TOut>;
  retries?: number;
  timeout?: number;
}

function createPipeline<T>(steps: PipelineStep<any, any>[]): (input: T) => Observable<any> {
  return (initialInput: T) =>
    steps.reduce(
      (acc$: Observable<any>, step) =>
        acc$.pipe(
          switchMap(input =>
            step.execute(input).pipe(
              timeout(step.timeout ?? 30_000),
              retry({ count: step.retries ?? 0, delay: (err, attempt) => timer(attempt * 1000) }),
              catchError(err => throwError(() => new PipelineError(step.name, err))),
            )
          ),
        ),
      of(initialInput),
    );
}

// Usage:
const checkoutPipeline = createPipeline([
  { name: 'validate-cart',   execute: cartService.validate,    timeout: 5_000 },
  { name: 'apply-discounts', execute: discountService.apply,   retries: 2 },
  { name: 'process-payment', execute: paymentService.charge,   timeout: 30_000 },
  { name: 'create-order',    execute: orderService.create,     retries: 3 },
  { name: 'send-receipt',    execute: emailService.sendReceipt },
]);

checkoutPipeline(cart).subscribe({
  next: order => this.router.navigate(['/order', order.id]),
  error: (err: PipelineError) => this.handlePipelineError(err),
});
Enter fullscreen mode Exit fullscreen mode

20. Staff/Principal Engineer Interview Framework

What Interviewers Are Actually Testing

At this level, the interview shifts from "can you implement X?" to "how would you lead a team to build X reliably at scale?"

Dimension What they probe Strong signal
Technical depth Internals, trade-offs, failure modes "The issue is Zone.js overhead on high-frequency events — here's how we eliminated it..."
Technical breadth Cross-domain thinking "We used a BroadcastChannel to sync state across MFEs without a shared dependency"
System design Scalability, maintainability Identifies non-obvious bottlenecks and proposes measurable solutions
Engineering judgment Prioritization, pragmatism Knows when NOT to use a pattern
Leadership Influence, mentorship, culture "I introduced ADRs because we kept revisiting settled decisions"
Communication Explaining to non-engineers Adjusts vocabulary to audience automatically

STAR Stories to Prepare (Angular-Specific)

  1. Performance crisis — "I debugged a production memory leak in Angular..." (cover profiling tools, root cause, solution, prevention)
  2. Architecture decision — "I led the migration from NgRx to Signals-based stores..." (cover context, decision process, rollout, outcome)
  3. Cross-team influence — "I championed OnPush across a 15-engineer team..." (cover resistance, education strategy, adoption metrics)
  4. Mentorship — "I grew a mid-level engineer to lead our state management layer..." (cover pairing approach, growth milestones)
  5. Angular version migration — "I planned and executed our v14 → v17 upgrade..." (cover phased strategy, risk mitigation, communication)

Questions to Ask Your Interviewers

These signal strategic thinking:

  • "What's the biggest Angular-specific technical challenge your team faces today?"
  • "How do you decide when to upgrade Angular versions, and how do you manage the migration?"
  • "What does the relationship between frontend and design systems engineering look like here?"
  • "How do you balance feature velocity against architectural improvements?"
  • "What does success look like for this role in the first 6 months?"

Quick Reference — Angular Internals Cheat Sheet

Term What it is
ɵcmp Static component definition generated by ngtsc
ɵfac Factory function for creating component instances
LView Runtime data per component instance (DOM nodes, binding values)
TView Shared template definition per component type
RenderFlags Bitmask: 1 = Create, 2 = Update phase
ɵɵdefineComponent Ivy runtime instruction to register a component
ɵɵelementStart/End Ivy instructions for creating DOM elements
ɵɵtextInterpolate Ivy instruction for updating text bindings
ApplicationRef Root of the Angular app — manages view tree
EnvironmentInjector Standalone DI scope without NgModule
InjectionContext Runtime context where inject() can be called

At the Staff/Principal level, technical excellence is the baseline. What differentiates you is the ability to multiply that excellence across an entire engineering organization — through architecture, mentorship, documentation, and the courage to make hard technical calls and own their outcomes.

Top comments (0)