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
- Angular Compiler & Build Pipeline Internals
- Ivy Rendering Engine Deep Dive
- Advanced Change Detection & Zoneless Architecture
- Advanced Dependency Injection Patterns
- Advanced RxJS Patterns & Custom Operators
- Angular Signals — Advanced Patterns
- Server-Side Rendering (SSR) & Static Site Generation
- Micro-Frontend Architecture
- Monorepo with Nx
- Advanced Performance Engineering
- Custom Angular Libraries & ng-packagr
- Advanced Testing Strategies
- Custom Renderers & Platform Abstraction
- Angular CDK & Advanced Component Patterns
- Advanced Security Architecture
- Web Workers & Shared Workers in Angular
- Design System Architecture
- Engineering Leadership & Architecture Decisions
- Advanced Coding Challenges
- 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
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
}
}
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); }
}
});
}
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" }
}
}
}
}
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:
- Predictable memory allocation — no virtual DOM diffing
- Template instructions are tree-shakeable — unused DOM operations are removed
- 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
}
}
});
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
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(),
],
});
Consequences of going zoneless:
- Change detection ONLY runs when a signal changes,
markForCheck()is called, ordetectChanges()is invoked -
async/await,setTimeout, HTTP requests do NOT auto-trigger CD - 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
}
}
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);
}
}
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 })
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
) {}
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();
}
}
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
});
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,
});
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
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
);
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),
);
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));
}),
);
}
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);
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.
}
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()));
}
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(); }
}
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
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));
});
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);
}
}),
);
}
}
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 />
}
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' });
}
}
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',
},
}),
],
};
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 }),
},
});
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);
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/
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"]
}
]
}
]
}
}
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
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
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; }
}
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
}
}
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`,
}
]
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"
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
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
}
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
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);
});
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);
});
});
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);
});
});
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');
});
});
});
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 () => {};
}
}
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;
}
}
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); }
}
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" />
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 },
]
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}`) });
}
}
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;
}
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();
});
}
}
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);
}
// 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());
});
}
}
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
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"
}
}
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
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 */ });
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);
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),
});
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)
- Performance crisis — "I debugged a production memory leak in Angular..." (cover profiling tools, root cause, solution, prevention)
- Architecture decision — "I led the migration from NgRx to Signals-based stores..." (cover context, decision process, rollout, outcome)
- Cross-team influence — "I championed OnPush across a 15-engineer team..." (cover resistance, education strategy, adoption metrics)
- Mentorship — "I grew a mid-level engineer to lead our state management layer..." (cover pairing approach, growth milestones)
- 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)