Reactive rendering
Follow a state write through dependency tracking, view eligibility, DOM work, and user-visible cost.
Study 88 original Angular interview questions across 18 connected topic areas. Rehearse the direct answer, inspect the runtime mechanism, compare alternatives, and practice the production judgment expected at senior and Staff level.
88 questions · 18 topic areas · 39 senior or Staff · 12 layered deep dives · updated
Start with components, templates, binding, and dependency injection. Then learn routing, forms, and RxJS before going under the hood with Signals, change detection, rendering, and performance. Senior Angular interviews add state ownership, SSR and hydration trade-offs, production debugging, modernization, and architecture across teams—not obscure syntax trivia.
This hub is reviewed against Angular 22.1. Current defaults are labeled explicitly; NgModules, Zone.js, decorator inputs, webpack builders, Jasmine, and Karma remain where interviewers may encounter long-lived applications.
Check the official release policySeniority is not more API trivia. The signal moves from correct framework use, through runtime reasoning, to operating and evolving systems across teams.
Explain components, templates, binding, services, directives, pipes, and lifecycle timing without relying on memorized slogans.
Study this levelBuild typed forms, lazy routes, cancellable HTTP flows, reusable component APIs, and deliberate loading and error states.
Study this levelTrace Signals, RxJS, dependency injection, OnPush, rendering, hydration, and performance through the runtime.
Study this levelDiagnose production symptoms with evidence and defend state, security, testing, delivery, and modernization trade-offs.
Study this levelShape boundaries across teams, platforms, design systems, deployments, and migrations while preserving autonomy and reversibility.
Study this levelInterviewers rarely stop at one API. Use these paths to anticipate the mechanism and production follow-up that usually comes next.
Start anywhere. Each sequence moves from a visible Angular feature to the underlying ownership, scheduling, or delivery decision.
Follow a state write through dependency tracking, view eligibility, DOM work, and user-visible cost.
Model events over time, navigation, HTTP cancellation, validation, and state ownership together.
Connect component APIs to provider lifetimes and large-application dependency direction.
Choose what renders where, when code arrives, and how behavior is restored safely.
The sequence moves from authoring views to operating systems. Jump to a known gap, or follow the path so that Signals and change detection build on a clear component and state-ownership model.
Learn the component and composition model before adding application-wide state or runtime optimization.
Build navigable, validated, asynchronous workflows with explicit ownership.
Trace notification, change detection, rendering, and browser work as one system.
Choose delivery strategies and build performance, testing, and security evidence.
Design for teams and evolution, then diagnose production symptoms without magic fixes.
Senior scenarios do not have magic fixes. Protect users, separate symptoms from causes, collect evidence across Angular and the browser, and verify the mitigation under the same conditions.
Strong answers preserve behavior, sequence risk, use automated migrations, keep the application releasable, and prove each step before expanding scope.
Do not pitch a rewrite because an API is old. Explain dependency order, compatibility constraints, rollout slices, and the rollback path.
Search questions, direct answers, concepts, tags, or API names. Open the concise answer in place; cornerstone topics continue into a layered engineering explanation.
Showing 88 of 88 questions
Bootstrap, components, templates, binding, services, and the TypeScript/browser model beneath Angular.
Interview focus: Accurate mental models and data flow, not framework vocabulary recitation.
Describe Angular as a compiled application framework with an integrated runtime and platform, not only a component library.
Angular is a TypeScript-first web application framework. It owns component rendering, template compilation, dependency injection, routing, forms, HTTP integration, build tooling, testing integration, and optional server rendering. That integrated platform reduces ecosystem assembly, but it also means architecture and upgrade choices should follow Angular's boundaries rather than treating it like a view-only library.
Trace the entry module through provider creation, root component instantiation, template rendering, and event wiring.
The entry file calls bootstrapApplication with a standalone root component and application providers. Angular creates the environment injector, initializes providers, instantiates the root component, creates its view, evaluates bindings, and attaches the rendered view to the host element. Ahead-of-time compilation has already turned templates into efficient instructions; the runtime connects those instructions to dependency injection, reactivity, events, and change detection.
Explain binding as directional data flow between component state, DOM properties, and events.
Interpolation renders a string representation into text or an attribute context; property binding assigns an expression result to a DOM, directive, or component property; event binding sends an event back to component code. Two-way binding is syntax that combines an input and a corresponding change output. The important model is direction: state flows down through properties, and user intent flows up through events.
Choose inputs/outputs, shared ownership, URL state, or a service based on relationship and lifetime.
Use inputs for parent-owned data and outputs for child events. Lift state to the nearest common owner when siblings coordinate; use a scoped service when the state or workflow outlives one component tree; use the URL for navigable state. Avoid a global event bus as the default because it hides ownership and makes event order and cleanup difficult to reason about.
Connect class and API types to template checking, refactoring, DI tokens, and compiled metadata.
TypeScript gives Angular statically analyzable classes, generics, access modifiers, decorators, and tooling. Angular's compiler extends that value into templates with binding and expression type checks, but TypeScript types disappear at runtime, so interfaces cannot be DI tokens. Runtime behavior still depends on JavaScript values, Angular metadata, providers, and browser APIs.
Signal inputs, outputs, content projection, queries, dynamic views, and reusable component API design.
Interview focus: Ownership, composition, template boundaries, and maintainable public contracts.
Explain how metadata, a class instance, a host element, and a compiled template form one view boundary.
A component is a directive with a template. Its metadata defines the selector, template, styles, imports, change-detection strategy, and host behavior; the class owns instance state and methods; Angular creates a host view and connects bindings to that instance. A good component also has a coherent responsibility and a deliberately small public API—not merely a @Component decorator.
Use signal inputs for reactive reads, outputs for events, and model only for genuine two-way component state.
input() declares a readonly Signal that Angular updates from the parent; output() declares a typed event channel; model() declares a writable signal exposed as an input plus a Change output for two-way binding. Prefer explicit input and output flows. Use model when the child is a true editor of a parent-owned value, not to hide ordinary state changes behind two-way syntax.
Separate consumer-owned content from component-owned layout and understand projection-slot selection.
Content projection lets a component place markup supplied by its consumer into predefined ng-content slots. Angular determines the slot from selectors when it creates the view; the projected nodes remain conceptually owned by the declaring view even though they render inside the child. Use projection for layout and composition, but keep slot contracts small and avoid selectors that couple consumers to internal markup.
Distinguish a component's own view from content projected by its consumer and respect query timing.
A view query reads elements, directives, or components created by the component's own template; a content query reads projected content supplied between the component's tags. Their results become available at different lifecycle boundaries because Angular initializes content before the component's view. Query only what the component must coordinate—exposing data through an input or output is often less coupled.
Choose dynamic views for runtime composition boundaries while preserving injection, lifecycle, and cleanup ownership.
Use TemplateRef plus ViewContainerRef when a reusable template fragment should produce embedded views, and createComponent when the runtime must instantiate a real component type. Suitable cases include overlays, configurable dashboards, plugin surfaces, and schema-driven UI. The owner must also control injector scope, inputs, event subscriptions, placement, destruction, focus, and accessibility; dynamic creation is not a shortcut around normal composition.
Writable and derived state, dependency tracking, effects, linked state, resources, and RxJS interop.
Interview focus: Derive state cleanly and choose reactivity from semantics rather than fashion.
Explain synchronous reads, tracked dependencies, invalidation, lazy derivation, and template notification.
A Signal is a synchronous getter backed by a reactive producer. While a computed value or template executes, Angular records which Signals were read; when one changes, Angular invalidates dependent computed values and marks affected views for a future render. Dependencies are dynamic and follow the branch actually read. Signals notify consumers—they do not deep-observe arbitrary object mutation.
Keep derivation declarative and reserve effects for synchronization with non-reactive systems.
computed expresses a value as a pure function of other Signals, caches it, and preserves one source of truth. An effect runs side-effecting code when dependencies change, so using it to copy one Signal into another creates scheduling, feedback-loop, and synchronization risks. Use effects at imperative boundaries such as logging, storage, a canvas, or a third-party API—and keep them small and cleanup-aware.
Choose Signals for synchronous state graphs and RxJS for asynchronous event streams, then bridge deliberately.
Signals model a current synchronous value and derived state with fine-grained dependency tracking. RxJS models events over time and has strong operators for cancellation, concurrency, buffering, retries, and combining asynchronous sources. Use Signals for local/view state and synchronous derivation; keep RxJS where time and stream semantics are the problem. A healthy Angular application can use both without mirroring every value in both systems.
Connect a Signal read in a template to view dependency tracking and dirty marking.
When an OnPush template reads a Signal, Angular records the view as a consumer. Updating that Signal marks the dependent view so it is eligible for refresh on the next scheduled change-detection pass. OnPush still matters because it lets Angular skip clean subtrees; Signals improve the precision of notifications but do not make expensive templates or giant DOM trees free.
Model writable state that resets with a source and read-oriented async state without misusing effects.
linkedSignal creates writable state whose default value follows reactive source state—for example, a selection that resets when the available options change. resource models async read state with a request, loader, status, value, error, cancellation, and reload semantics. They reduce synchronization effects, but ownership still matters: mutations and complex event workflows often belong in explicit services or RxJS pipelines.
Bridge at ownership boundaries while accounting for initial values, subscription lifetime, and asynchronous emissions.
toSignal subscribes to an Observable and exposes its latest value as a Signal; define an initial value or require a synchronous first emission when necessary, and create it in a stable owner rather than per template evaluation. toObservable exposes Signal changes as an Observable for stream composition. Convert at a boundary, not repeatedly throughout the graph, and keep error and completion semantics visible.
Dirty marking, tree traversal, OnPush, zoneless scheduling, manual APIs, and rendering cost.
Interview focus: Trace the notification, scheduled work, checked views, and resulting DOM update.
Separate what marks a view, what schedules work, which views are traversed, and which DOM bindings change.
Angular renders through a tree of views. Framework notifications—such as a bound event, a new input, a template Signal update, or markForCheck—make relevant views eligible for refresh and schedule work. During change detection Angular traverses the required part of the view tree, reevaluates template bindings, compares results with stored values, and updates only changed DOM bindings. The scheduling source and the traversal strategy are separate concerns.
Explain OnPush as eligibility and subtree-skipping behavior, not immutable-state magic.
Default-strategy views are checked during ordinary application passes. An OnPush subtree can be skipped while it is clean and becomes eligible when it receives a changed input reference, handles a bound event, reads a Signal that changes, is explicitly marked, or is otherwise affected through Angular's view rules. Immutability makes input changes easy to detect, but Angular does not enforce immutable objects.
Replace global async monkey-patching with explicit framework notifications from Signals, inputs, events, and view APIs.
In current Angular, zoneless is the default. Angular schedules rendering from notifications it owns: template-read Signal updates, bound listeners, setInput, markForCheck, view attachment/removal, and related view operations. Zone.js-based applications instead use patched async activity as a broad scheduling signal. Zoneless reduces that global coupling, but application and library code must notify Angular through supported reactive or view APIs.
Legacy context: Zone.js-based applications remain common. Angular 21+ can opt back into zone-based scheduling with provideZoneChangeDetection.
Use manual view control as an explicit optimization or integration boundary, not a first response to stale UI.
markForCheck makes an OnPush view eligible for a future application pass. detectChanges synchronously checks the view and descendants now. detach removes a view from ordinary traversal until it is reattached or checked manually. These APIs are useful for specialized rendering loops and external integrations, but frequent manual calls can hide broken ownership or notifications; prove the need with profiling and test the lifecycle carefully.
Find state that changes after Angular checked it in the same render turn instead of masking the symptom with a timer.
In development mode Angular can verify that a binding did not change after it was already checked in the same render cycle. The error commonly exposes a child changing parent-visible state during view initialization, a getter with side effects, or DOM-derived state written too late. Fix the ownership or timing; setTimeout and unconditional detectChanges often silence the evidence while preserving a fragile feedback loop.
Instrument view checks and trace changing inputs, events, Signal writes, stream emissions, and manual marks before optimizing.
First verify what 'rerender' means: component checks, DOM mutations, or component recreation. Use Angular DevTools and browser performance traces, log identity-changing inputs and route reuse, and inspect bound events, Signal writes, AsyncPipe emissions, effects, and ChangeDetectorRef calls. Form one hypothesis, reproduce it with a minimal interaction, remove the trigger, and confirm both fewer checks and improved user-visible cost.
Provider records, injector hierarchy, tokens, factories, injection context, scope, and lifetime.
Interview focus: Resolution paths, instance ownership, test seams, and architectural consequences.
Trace a token from an injection request through provider lookup, factory creation, caching, and hierarchy.
Angular DI maps a runtime token to a provider record. When code requests the token in an injection context, Angular searches the relevant element and environment injector hierarchy, creates the value from its provider factory if needed, caches it according to that injector's lifetime, and returns it. Provider placement therefore controls availability, instance count, lazy loading, cleanup, and architectural coupling.
Place providers at root, route, or component boundaries based on sharing, isolation, bundle, and cleanup needs.
A root provider is shared across the application environment; a route provider creates feature-scoped configuration and state; a component provider creates an instance for that component subtree. Resolution chooses the nearest matching provider, so lower providers shadow higher ones. Scope is a design decision: it controls sharing and isolation, but also memory, retained state, lazy-boundary behavior, and surprising duplicate instances.
Choose provider construction explicitly and know when aliasing differs from creating another instance.
useClass constructs a class for a token; useValue supplies an existing value; useFactory computes a value with dependencies; useExisting aliases one token to the exact same instance; multi collects contributions into an array. Prefer InjectionToken for non-class contracts. A common trap is using useClass when aliasing was intended, which creates a separate instance and splits state.
Understand why inject() works during framework-managed creation but not in arbitrary callbacks or methods.
inject() needs an active injection context so Angular knows which injector hierarchy to search. That context exists during class field initialization and constructors created by DI, provider factories, route loaders, guards, and functions run with runInInjectionContext. It does not automatically exist later in any method or async callback; capture the dependency during construction or explicitly establish the correct context.
Map provider declarations and injector boundaries, then prove which injector created each instance.
Search every provider declaration, including providedIn, application config, route providers, component providers, imported legacy modules, and test overrides. Identify the injection site and walk its element/environment injector chain. Add temporary instance IDs or creation traces and inspect Angular DevTools. Fix the provider boundary—not the symptom—then verify sharing, cleanup, lazy loading, and tests across navigation.
Angular-focused streams: HTTP cancellation, flattening operators, sharing, cleanup, and Signal interop.
Interview focus: Time, cancellation, concurrency, ownership, error recovery, and duplicate work.
Separate a lazy stream contract from multicast event sources with current-value or replay semantics.
An Observable is a subscription contract and may be cold or hot. A Subject is both observer and Observable and multicasts new events to current subscribers. BehaviorSubject additionally requires and synchronously exposes a current value; ReplaySubject replays a configured history. Use the narrowest semantics needed, expose Observable rather than the writable Subject, and avoid using Subjects as a hidden application-wide event bus.
Choose the operator from cancellation, concurrency, ordering, and duplicate-submission requirements.
switchMap keeps only the newest inner stream and unsubscribes the previous one; mergeMap runs inners concurrently; concatMap queues them in order; exhaustMap ignores new sources while one inner is active. For typeahead use switchMap, independent saves may use bounded mergeMap, ordered writes may use concatMap, and double-submit prevention may use exhaustMap. The business rule chooses the operator.
Compose dependent work so cancellation, errors, loading, and cleanup stay in one observable lifecycle.
Nested subscribe calls create independent lifecycles, making cancellation, error propagation, loading state, ordering, and cleanup harder to coordinate. Use flattening operators for dependent async work and combination operators for independent sources. A subscribe is appropriate at a true side-effect boundary, but the data-flow graph should normally be expressed before that boundary.
Explain automatic subscription switching, cleanup, latest-value rendering, and view notification.
AsyncPipe subscribes to an Observable or Promise, returns the latest emitted value, and marks the view when a new value arrives. If the bound source identity changes it unsubscribes from the old source and subscribes to the new one; it also cleans up when the view is destroyed. Avoid creating a new cold Observable expression in the template on every check, because source replacement can repeat work.
Prefer framework-owned subscription lifetimes and recognize which streams complete naturally.
Prefer AsyncPipe or toSignal for view consumption. For imperative subscriptions tied to an Angular owner, use takeUntilDestroyed with its DestroyRef. One-shot HttpClient streams complete, but router events, Subjects, intervals, DOM events, and long-lived stores do not. Cleanup is necessary but not sufficient: callbacks, effects, caches, and third-party listeners can retain the same component graph.
Define cache lifetime, reset behavior, source completion, and stale-value policy rather than adding sharing blindly.
shareReplay can keep a source subscription or replayed value alive longer than intended, hide stale data behind a service lifetime, or restart work when refCount drops and later rises. The behavior depends on source completion and configuration. State the desired cache owner, invalidation, error reset, and subscriber lifecycle first; sometimes an explicit cache or share with reset options is clearer.
Count subscriptions and reconstruct the source lifecycle before introducing a cache.
HttpClient Observables are cold, so each subscription starts a request. Look for multiple AsyncPipe uses, an imperative subscription plus a template subscription, repeated effect creation, nested subscriptions, retry/repeat, component recreation, and sharing that resets. Instrument subscription and network timing, prove the subscriber graph, then share or cache at the correct owner and verify invalidation—not just request count.
Standalone route configuration, lazy boundaries, guards, resolvers, providers, navigation, and preloading.
Interview focus: Loading strategy, URL state, failure behavior, and the distinction between UX and authorization.
Compose route records and router features through application providers instead of a routing NgModule.
Define typed route records and provide them with provideRouter at application bootstrap. Router features such as preloading, component-input binding, scrolling, and tracing are added as provider features. Routed components render through outlets, while child routes create nested activation trees. NgModule routing remains relevant in legacy applications, but new feature boundaries do not require a routing module.
Place dynamic imports at user-journey boundaries and preload from probability, cost, and network conditions.
Use loadComponent for a standalone routed component and loadChildren for a lazy route tree. Put boundaries around meaningful user journeys so the initial route avoids unrelated code without creating request waterfalls at every nested level. Preload only likely next paths under suitable network and device conditions. Verify output chunks and navigation timing because an eager import elsewhere can pull a lazy feature into the initial graph.
Use guards for navigation UX and resolvers for required route data while keeping authorization on the server.
Guards can redirect, prevent an unsuitable client navigation, or stop a route from matching; they are not a security boundary because users control browser code. Resolvers load data that must exist before activation, but overuse delays navigation and can hide loading UX. Backend authorization must validate every protected operation, and router failures should have explicit redirect or error behavior.
Trace URL recognition, redirects, guards, data resolution, lazy loading, activation, cancellation, and errors.
A navigation is a cancelable transaction: parse and recognize the URL, apply redirects, lazy-load route config, run guards and resolvers, build the target router state, deactivate/activate components, and commit history and scrolling behavior. Observe Router events to locate delay or cancellation, but avoid coupling product logic to a fragile event sequence when route state or a guard can express the requirement directly.
Typed reactive forms, Signal Forms, template-driven forms, validation, custom controls, and large-form design.
Interview focus: State ownership, validation timing, reusable controls, accessibility, and scale.
Choose from workflow complexity, typing, dynamic structure, existing controls, and migration cost.
Template-driven forms suit small forms where the template naturally owns the workflow. Typed reactive forms remain a mature choice for explicit control trees, dynamic forms, and a large ControlValueAccessor ecosystem. Signal Forms, stable in Angular 22, make a Signal model the source of truth with type-safe field trees and schema validation. Choose per form and ecosystem; do not rewrite stable enterprise forms only to adopt a newer API.
Keep control values, nullability, nested groups, and form access consistent at compile time.
Typed forms make the control tree and value shape visible to TypeScript, so setValue, patchValue, getRawValue, validators, and access patterns catch more mismatches. Nullability is deliberate because reset can produce null unless controls are non-nullable. Types do not validate runtime server data; domain parsing and validation still belong at trust boundaries.
Keep validators pure, place relationship rules at the shared owner, and treat server validation as authoritative.
A synchronous validator should be a pure function of current control state. Place a cross-field rule on the smallest group that owns all involved values. Async validators should debounce or avoid unnecessary requests, cancel stale work, and return structured errors; do not duplicate every server rule in the client. Error presentation should account for touched/submitted state and remain accessible.
Bridge an Angular forms control to a custom component without creating competing sources of truth.
ControlValueAccessor adapts a custom UI control to Angular's existing forms APIs. Angular writes model values through writeValue; the control reports user changes and touch through registered callbacks; setDisabledState mirrors disabled status. Keep writeValue free of change emission, preserve accessibility and focus behavior, and test model-to-view and view-to-model directions independently.
Separate domain schema, control construction, rendering, validation, drafts, and submission while containing update cost.
Start from a versioned domain schema rather than component conditionals. Partition the form into owned sections, create only relevant controls, use stable identity for repeated groups, centralize cross-section rules deliberately, and define draft and server-error semantics. Profile validation and rendering before adding manual change detection; virtualize or page very large repeated sections and test migrations between schema versions.
Reusable element behavior, structural view creation, host directives, and pure transformation boundaries.
Interview focus: Use the smallest abstraction and understand its DOM and change-detection cost.
Distinguish behavior on an existing host from reusable creation and removal of embedded views.
An attribute directive attaches behavior, bindings, or host interaction to an existing element or component. A structural directive controls embedded views through TemplateRef and ViewContainerRef. For normal conditionals and loops, use modern @if, @for, and @switch; write a custom structural directive only when reusable view-creation behavior adds a real domain abstraction.
Compose reusable host behavior into components without inheritance while controlling the exposed API.
Host directives let a component or directive apply another directive to its host at compile time and optionally expose selected inputs and outputs. They are useful for focus behavior, menus, selection, accessibility, and shared interaction policies. Keep composition shallow and explicit; a large invisible stack of host behavior can make provider resolution, host bindings, and event ownership hard to trace.
Use reference-based pure transformation by default and understand why deep mutation can leave results stale.
A pure pipe runs when its input value or reference changes, so it is efficient for deterministic display transformation but will not notice deep mutation behind a stable object reference. An impure pipe can run on every relevant check and can become extremely expensive. Prefer immutable inputs, computed state, or explicit async boundaries; use an impure pipe only for a measured case with bounded work.
Input timing, view/content initialization, render callbacks, cleanup, DOM access, and SSR boundaries.
Interview focus: Explain why timing matters and choose the lifecycle boundary that owns the work.
Connect construction, input initialization, content/view checks, rendering, and destruction to ownership.
Angular constructs the class, sets initial inputs, calls ngOnChanges, then ngOnInit, followed by content initialization/check hooks and view initialization/check hooks. Render callbacks run after rendering work, and destruction callbacks release owned resources. The exact list matters less than choosing the boundary whose prerequisites are actually ready and avoiding repeated work in the checked hooks.
Keep language-level construction distinct from framework initialization after inputs are assigned.
The constructor establishes class invariants and receives dependencies; Angular inputs are not ready there. ngOnInit runs after initial inputs are set and is suitable for initialization that depends on them. Prefer field initialization and derived Signals for simple state, and avoid starting work in both places without a clear owner because tests and inheritance become harder to reason about.
Schedule DOM-dependent work after Angular rendering and keep browser-only behavior out of server execution.
afterNextRender schedules work after the next application render; afterEveryRender repeats after renders. They are appropriate for layout measurement or integration with imperative browser libraries when normal bindings cannot express the work. Register them in an injection context, separate write and read phases when possible, clean up owned resources, and remember render callbacks do not run during server rendering.
Tie subscriptions, observers, listeners, timers, and dynamically created views to the owner that created them.
The creator of a long-lived resource should own its teardown. Use framework-managed lifetimes where possible, takeUntilDestroyed for subscriptions, and DestroyRef.onDestroy or ngOnDestroy for listeners, observers, timers, workers, and third-party instances. Also inspect services and caches: destroying a component does not free objects still retained by a root service, detached DOM, or a library callback.
Local, lifted, service, Signal, RxJS, URL, server, cache, and global state with explicit ownership.
Interview focus: Complexity, event flow, derivation, persistence, debugging, and library thresholds.
Place state from who owns it, who needs it, how long it lives, and which system is authoritative.
Keep ephemeral interaction state in the component, lift shared view state to the nearest common owner, use a scoped service for a feature workflow, put navigable state in the URL, and treat backend data as server state with an explicit cache policy. Persist only what must survive reloads. Avoid copying one value into several stores; derived state should be computed from one authoritative source.
Adopt a store for demonstrated coordination and observability needs, not because the application is large.
A global store becomes valuable when many distant features coordinate durable state, transitions need explicit events and effects, reproducible debugging matters, or policy must be standardized across teams. Component Signals and scoped services are simpler for local workflows. Evaluate ownership, event volume, server-cache needs, testability, and team fluency; application size alone is not the threshold.
Encapsulate writable state, expose readonly state and derivations, and make commands own transitions.
Provide the store at the feature lifetime, keep writable Signals private, expose readonly Signals and computed derivations, and offer intention-revealing commands for transitions. Keep async work and error policy explicit rather than hiding them inside arbitrary effects. This is enough for many features; add a formal store when event history, cross-feature coordination, tooling, or standardized effects justify it.
Treat fetched data as a cached remote snapshot with freshness, invalidation, and concurrency semantics.
Server state is owned remotely and the client holds a potentially stale snapshot; it needs request deduplication, freshness, invalidation, error, retry, and mutation reconciliation. Client state is authoritative in the browser, such as an open panel or unsaved draft. Conflating them produces stale caches and duplicated truth. Define keys, ownership, TTL or invalidation events, and optimistic rollback explicitly.
Normalize shared entities when identity and cross-view updates matter, not as a blanket rule for every response.
Normalize when the same entity appears in many collections, updates arrive independently, and one canonical record prevents inconsistent copies. Keep ordered IDs and derive view models with selectors or computed values. For route-local read models and small immutable responses, normalization can add indirection without value. The deciding forces are identity, update patterns, ownership, and debugging cost.
Bundles, lazy boundaries, @defer, lists, change detection, requests, rendering, profiling, and CWV.
Interview focus: Measure the constraint, redesign the expensive path, and verify user-visible improvement.
Start from a user-visible metric, segment the path, and correlate framework, browser, network, and bundle evidence.
Define the failing user journey and metric—LCP, INP, navigation time, frame drops, memory, or bundle cost. Reproduce with representative data and device constraints, then use Angular DevTools, Chrome Performance/Memory/Network, source maps, and bundle analysis to locate time and retention. Change one proven constraint, compare the same workload, and verify the user metric rather than a proxy alone.
Measure DOM, scripting, layout, and update frequency before reducing rendered work and stabilizing identity.
Profile whether the cost is creation, change detection, layout, paint, or repeated updates. Usually the redesign is to avoid rendering 10,000 interactive rows: virtualize, page, aggregate, or progressively disclose. Use stable track identity, OnPush/Signals, cheap templates, and batched updates. Verify frame time, memory, accessibility, keyboard behavior, and the actual user task with production-scale data.
Use the build graph to remove, replace, or move code across real loading boundaries and enforce budgets.
Measure compressed and parsed cost by chunk, inspect source-map bundle output, and identify eager feature imports, heavy libraries, duplicated versions, locale/data payloads, CommonJS limits, and side-effectful packages. Create route and @defer boundaries, replace or narrowly import dependencies, and enforce budgets in CI. Verify cold-load LCP/INP and navigation waterfalls, not bundle bytes alone.
Defer non-critical standalone dependencies at a meaningful visual boundary while designing every loading state.
@defer lets the compiler split eligible standalone dependencies and load them on idle, viewport, interaction, timer, immediate, or a condition. Use it for expensive, non-critical regions with stable placeholders and deliberate prefetching. Do not defer above-the-fold essentials merely to improve a bundle report; account for layout shift, request waterfalls, error states, SSR, and incremental hydration behavior.
Preserve logical row identity so Angular can reuse DOM and component instances through reordering and updates.
The track expression maps each item to stable logical identity. Angular uses it to match old and new collections, reuse views, move DOM, preserve focus and component state, and create or destroy only real additions/removals. Track a unique domain key; index is safe only for truly static order, and returning a new object defeats stable matching.
Treat template expressions as repeatable render work and move expensive derivation to cached, explicit state.
Angular may evaluate template expressions whenever the view is checked, so a method or getter that filters, sorts, allocates, parses, or triggers side effects can repeat frequently. Cheap pure accessors are fine. Move substantial derivation into computed Signals, pure pipes, memoized selectors, or preprocessing at the data boundary, and verify with a profile rather than banning every method syntactically.
CSR, server rendering, prerendering, hybrid routes, transfer cache, hydration, and server/browser boundaries.
Interview focus: Choose per route from freshness, personalization, cacheability, interactivity, and operating cost.
Choose per route from freshness, personalization, crawlability, cacheability, latency, and operating cost.
Use client rendering for authenticated, highly interactive surfaces where server HTML adds little. Use SSR when first-response content must be fresh or request-specific and early HTML improves the journey. Prerender stable, cacheable routes at build time. Modern Angular supports hybrid choices per route; include CDN caching, backend latency, hydration cost, failure modes, and operational capacity in the decision.
Reuse server-rendered DOM and restore client behavior instead of destroying and recreating the page.
Hydration matches the client view structure to server-rendered DOM, reuses those nodes, restores Angular listeners and state, and can replay early events. It improves continuity and avoids a destructive rerender, but requires valid, deterministic HTML and careful browser-only code. Hydration does not remove the cost of downloading and executing the client application.
Render deferred regions on the server while delaying their client code and hydration until an explicit boundary triggers.
Incremental hydration lets the server render the main content of a @defer block while the browser leaves that subtree dehydrated until a hydrate trigger fires. Angular then loads its dependencies, hydrates the boundary, and replays captured events. It can reduce initial JavaScript without above-the-fold layout replacement, but nested boundaries, trigger choice, and interaction readiness must be designed and measured.
Compare server and client DOM inputs, then isolate nondeterminism, invalid markup, or pre-hydration mutation.
Capture the server HTML and the DOM before Angular hydrates, then compare the failing subtree. Check invalid table/nesting markup, direct DOM manipulation, random/time/locale output, request-specific state not transferred, browser-only branches, whitespace configuration, and third-party scripts that mutate early. Fix determinism and ownership; use skip hydration only as a narrow temporary boundary.
Follow the server-only execution path and check platform assumptions, request state, imports, and route rendering configuration.
A refresh executes the server render while client navigation may not. Inspect the server stack and route render mode, then find top-level or lifecycle access to window, document, storage, layout, and DOM libraries. Check request-specific authentication, environment configuration, absolute URLs, ESM compatibility, and transferred data. Add a server-render test for the route and verify both direct and client navigation.
Vitest, TestBed, components, HTTP, Router, Signals, RxJS, integration, and browser-level confidence.
Interview focus: Test behavior and contracts without coupling every assertion to framework internals.
Match test level to risk: pure domain logic, component behavior, boundary integrations, and critical browser journeys.
Unit-test domain transformations and failure rules; component-test public inputs, outputs, accessibility, and rendered behavior; integration-test routing, forms, HTTP adapters, and state boundaries; browser-test a small set of critical journeys. Avoid tests that only restate Angular or assert private method calls and DOM structure with no product meaning. Optimize for fault detection and refactor confidence.
Distinguish the current CLI default from the installed base candidates will still maintain.
New Angular CLI projects use Vitest with a DOM emulation environment by default, and ng test integrates the runner. Karma is still supported and common in mature applications. TestBed remains Angular's component and DI test utility regardless of runner. Use real-browser mode or E2E tests for layout and browser APIs that a simulated DOM cannot validate.
Legacy context: Karma remains supported and is common in existing projects; migrate because it improves the workflow, not merely to follow a default.
Let production-like notifications schedule rendering and test visible outcomes instead of forcing every update manually.
Update state through the same input, event, or Signal API production uses, await fixture.whenStable when rendering is scheduled, and assert visible behavior. A blanket fixture.detectChanges can hide code that fails to notify Angular in zoneless production. Test computed values as pure derivations where useful, and test effects through the external boundary they synchronize—not their implementation details.
Use Angular's boundary harnesses to exercise observable contracts and user behavior while keeping backend and browser scope controlled.
Use the HTTP testing provider and controller to assert request shape and drive response/error timing; use RouterTestingHarness to navigate real route configuration and inspect activated UI; exercise forms through controls and user-facing behavior. Mock the external boundary, not every collaborator. Keep a smaller browser suite for focus, native validation, history, and cross-page behavior.
XSS, sanitization, trusted values, CSP, CSRF, tokens, route guards, and backend trust boundaries.
Interview focus: Know which protections the framework supplies and which only the server can enforce.
Treat template values as untrusted by default, understand security contexts, and avoid bypassing sanitization.
Angular escapes interpolation and sanitizes untrusted values in security-sensitive binding contexts such as HTML and URLs. Templates themselves are trusted executable code and must never be assembled from user input. DomSanitizer bypass methods assert that a value is already safe—they do not sanitize it. Prefer structured rendering, server validation, CSP, and Trusted Types over bypasses.
Choose a session design from XSS, CSRF, refresh, domain, and backend constraints rather than one universal storage rule.
JavaScript-readable storage makes tokens available to any successful XSS. HttpOnly, Secure, SameSite cookies reduce token theft but require a CSRF strategy and careful domain/session design. Short-lived in-memory access tokens plus protected refresh mechanisms are another pattern. The backend must validate every token and authorization decision; Angular interceptors are convenience, not enforcement.
Keep navigation experience in the client and data authorization on trusted infrastructure.
Users can modify browser code, call APIs directly, and bypass a client route entirely. A guard improves navigation UX and can avoid rendering an inappropriate screen, but it cannot protect data or operations. The server must authenticate the caller and authorize every request against current policy, regardless of which Angular route was visible.
Reduce executable trust, constrain script sources, and treat vendor code as privileged code with lifecycle risk.
A strong CSP and Trusted Types policy reduce the paths by which injected text becomes executable, but they require compatible build and runtime behavior. Every third-party script runs with page privileges and can affect performance, privacy, hydration, and token exposure. Inventory, pin, review, sandbox where possible, load only with justified consent, monitor changes, and keep a removal path.
Feature boundaries, libraries, design systems, monorepos, micro-frontends, platforms, and multi-team evolution.
Interview focus: Optimize dependency direction, team autonomy, release safety, observability, and change cost.
Align domain and ownership boundaries, standardize high-leverage platform seams, and measure whether autonomy improves.
Align deployable applications and libraries to business domains with named owners and enforced dependency direction. Standardize the paved road—Angular version, build, testing, observability, security, design-system primitives, and API clients—while letting teams own domain decisions. Use automated boundaries and compatibility tests, not review committees. Measure lead time, upgrade adoption, incidents, bundle budgets, and cross-team dependency wait time.
Invest when repeated cross-team friction and risk justify a product team with adoption, compatibility, and support responsibilities.
Build a platform when many teams repeatedly solve the same build, auth, observability, testing, deployment, accessibility, and upgrade problems and inconsistency creates measurable risk. Treat it as a product: paved-road defaults, escape hatches, versioned contracts, migrations, documentation, telemetry, and support. Do not centralize domain state or every component; platform success is reduced cognitive load and faster safe delivery, not control.
Separate tokens, primitives, and product composites; version contracts and ship migrations with adoption evidence.
Define the supported public API, accessibility behavior, tokens, visual regression contract, and browser/Angular range. Prefer composable primitives over domain-specific mega-components. Deprecate with telemetry and time, ship schematics or codemods, test representative consumers, canary releases, and support parallel versions when necessary. A platform team owns migration ergonomics, not only package publication.
Use independent deployment only when organizational autonomy exceeds the runtime, UX, and governance cost.
Micro-frontends are justified when domains need genuinely independent release and ownership boundaries that a modular monolith cannot provide. Costs include duplicate framework/runtime code, shared dependency negotiation, routing, authentication, cross-app state, design consistency, observability, accessibility, and failure isolation. Start with repository and module boundaries; adopt runtime composition only with measurable autonomy requirements and a platform that owns the seams.
Set journey- and device-based user budgets, connect them to build limits, and provide ownership and exception policy.
Define representative devices, networks, and critical journeys, then set field-oriented LCP, INP, CLS, error, and navigation targets plus build-time JavaScript/CSS/image budgets. Enforce regression checks in CI and monitor real-user percentiles by application and route. Assign owners, publish trend dashboards, and use time-bounded exceptions with remediation—not one universal byte limit detached from product context.
NgModule-to-standalone migration, Signals adoption, build/test evolution, and multi-version upgrades.
Interview focus: Sequence change by risk, automate migrations, preserve behavior, and keep rollbacks available.
Use official migrations and feature-by-feature boundaries while preserving provider scope, routes, tests, and rollback.
Inventory module roles first: declarations, provider scope, routing, initialization, and library compatibility. Run Angular's standalone migrations in small steps, convert leaf components and lazy routes, move providers deliberately, and use importProvidersFrom as a temporary bridge. Keep builds and behavior tests green, compare bundles and DI lifetimes, and remove obsolete modules only after consumers have moved.
Legacy context: NgModules remain valid and interview-relevant in maintained applications; standalone is the recommended direction for new Angular code.
Adopt Signals at synchronous view-state boundaries without rewriting stream workflows or duplicating truth.
Start with local UI state and pure derived view models where Signals remove manual Subjects and subscriptions. Keep RxJS for cancellation, concurrency, WebSockets, and multi-event workflows; bridge once at feature boundaries with toSignal or toObservable. Define ownership and migration conventions, add zoneless compatibility tests, measure bundle/runtime effects, and avoid mirroring every store selector into a second Signal store.
Move one supported major at a time, automate framework migrations, reduce ecosystem blockers, and keep every step releasable.
Map Angular, TypeScript, Node, RxJS, builder, test-runner, and library compatibility first. Upgrade one major at a time with ng update, commit automated migrations separately, replace abandoned blockers, and run unit, integration, SSR, visual, performance, and production smoke tests at each step. Release intermediate states when possible, monitor canaries, and avoid combining framework, state, design-system, and domain rewrites into one change.
Remove unsupported and blocking infrastructure first, then migrate with measured build, test, SSR, and deployment parity.
First reach a supported Angular/Node/TypeScript baseline and eliminate custom webpack assumptions or abandoned test utilities that block upgrades. Move to the stable application builder and its esbuild/Vite-based toolchain, then evaluate Vitest migration for test speed and maintenance. Preserve production output, SSR behavior, source maps, coverage meaning, and browser-level tests; tooling change is successful only if delivery gets faster without losing confidence.
Evidence-first diagnosis of duplicate work, memory growth, stale UI, bundle leaks, and long-running slowness.
Interview focus: Symptom → evidence → hypothesis → mitigation → verification, with user protection first.
Use repeatable navigation and heap evidence to find the retaining owner before changing cleanup code.
Automate a navigation loop, force comparable garbage-collection conditions, and verify the retained heap grows after components should be destroyed. Compare heap snapshots and dominator/retainer paths for component instances, detached DOM, subscriptions, listeners, timers, overlays, caches, and third-party objects. Fix the retaining owner, repeat the same loop, and verify both heap stabilization and correct feature behavior.
Correlate request initiators with component, subscription, effect, retry, and route lifecycles before adding caching.
Protect the backend with rollback, flags, or rate limits if needed, then capture Network initiators and application traces. Count component creation, Observable subscriptions, Signal effect creation, AsyncPipe consumers, retry loops, router reactivation, and polling timers. Build the exact fan-out graph, remove or share at the owning boundary, define invalidation, and verify request cardinality across navigation and error paths.
Trend heap, DOM, listeners, task duration, stream frequency, caches, and network work over time to distinguish accumulation from load.
Record a controlled long-running session and trend heap after GC, DOM node count, listeners, timers, subscriptions, Signal/effect activity, cache sizes, network frequency, long tasks, and frame time. Compare idle and active periods and take heap/CPU profiles early and late. Find the accumulating owner or increasing work rate, mitigate safely, and verify a soak test under production-like data.
Trace the value from network response through transport, caches, state transitions, identity, and template consumption.
Confirm the browser received the fresh payload, then trace HTTP/service-worker/CDN caches, shareReplay or resource lifetime, store selectors, optimistic state, mutation behind stable references, Signal equality, route reuse, and template pipes. Instrument version/timestamp at each boundary to locate where the value diverges. Fix ownership or invalidation and verify reload, navigation, offline, and concurrent-mutation paths.
Use the module graph to find the eager reference or incompatible boundary that defeated dynamic loading.
Verify the chunk with source-map/bundle analysis, then trace every import of its entry component, route tree, shared barrel, provider, constant, stylesheet, and query. A static import anywhere reachable from the initial graph defeats the lazy boundary; @defer dependencies also must meet eligibility rules. Remove the eager edge, rebuild, and test both cold-load bytes and first navigation behavior.
Rehearse the answer you can say aloud, then inspect the runtime, code, version boundary, trade-offs, diagnostic path, and production implications expected in a senior response.
A Signal is a synchronous getter backed by a reactive producer. While a computed value or template executes, Angular records which Signals were read; when one changes, Angular invalidates dependent computed values and marks affected views for a future render. Dependencies are dynamic and follow the branch actually read. Signals notify consumers—they do not deep-observe arbitrary object mutation.
Signals form a graph of producers and consumers. Reading a Signal returns its current value synchronously. If that read happens while Angular is evaluating a computed value, an effect, or a template, the current consumer records a dependency on the producer. The dependency graph follows the code path actually executed, so a branch that stops reading a Signal also stops depending on it.
Writing a new value notifies dependents. A computed value becomes invalid and recomputes lazily when it is next needed; a template consumer is marked so Angular can refresh the affected view in a scheduled render. This is finer-grained notification than treating every asynchronous browser task as evidence that the whole application may have changed.
Signals compare values before notifying. Custom equality can reduce noisy updates, but deep equality may hide mutation and add CPU cost. A Signal does not make the object inside it immutable: mutate an array in place without set or update and Angular has no new value transition to observe. Prefer immutable updates and computed derivation; reserve effects for imperative boundaries.
import {CurrencyPipe} from '@angular/common';
import {ChangeDetectionStrategy, Component, computed, signal} from '@angular/core';
@Component({
selector: 'app-order-summary',
imports: [CurrencyPipe],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<button (click)="quantity.update(q => q + 1)">Add one</button>
<p>{{ quantity() }} × {{ unitPrice() | currency }}</p>
<strong>Total: {{ total() | currency }}</strong>
`,
})
export class OrderSummary {
readonly quantity = signal(1);
readonly unitPrice = signal(24);
readonly total = computed(() => this.quantity() * this.unitPrice());
}The question reveals whether a candidate understands modern Angular reactivity beyond the signal/getter syntax and can connect it to rendering and state design.
A senior design keeps writable state at clear ownership boundaries, exposes readonly views, and lets computed derivations replace synchronization code. Evaluate the whole graph: a precise notification that eventually renders 20,000 nodes is still expensive.
Signals model a current synchronous value and derived state with fine-grained dependency tracking. RxJS models events over time and has strong operators for cancellation, concurrency, buffering, retries, and combining asynchronous sources. Use Signals for local/view state and synchronous derivation; keep RxJS where time and stream semantics are the problem. A healthy Angular application can use both without mirroring every value in both systems.
Signals and Observables overlap at the edges but model different questions. A Signal answers “what is the value now?” synchronously and gives Angular a dependency graph for derivation and rendering. An Observable answers “what events occur over time?” and can be lazy, multicast, finite, infinite, cancellable, buffered, retried, or concurrent.
Local selection, toggles, form-adjacent view state, and computed view models are natural Signal territory. Typeahead, router events, WebSockets, request orchestration, retry policy, and competing async operations are natural RxJS territory. The decision is not local versus global; it is current-value semantics versus temporal event semantics, plus ownership and tooling.
Bridge once at a stable boundary. toSignal subscribes immediately and needs a lifetime and an initial-value policy. toObservable turns Signal invalidations into Observable emissions for operator composition. Repeated conversions or mirrored stores create two sources of truth and obscure error, completion, and cancellation behavior.
import {Component, computed, inject, signal} from '@angular/core';
import {toObservable, toSignal} from '@angular/core/rxjs-interop';
import {debounceTime, distinctUntilChanged, switchMap} from 'rxjs';
@Component({
selector: 'app-user-search',
template: `
<input [value]="query()" (input)="query.set($any($event.target).value)" />
@if (results(); as users) {
<p>{{ resultLabel() }}</p>
}
`,
})
export class UserSearch {
private readonly api = inject(UserApi);
readonly query = signal('');
readonly results = toSignal(
toObservable(this.query).pipe(
debounceTime(250),
distinctUntilChanged(),
switchMap(query => this.api.search(query)),
),
{initialValue: []},
);
readonly resultLabel = computed(() => String(this.results().length) + ' results');
}Modern Angular roles expect candidates to adopt Signals without discarding the async composition strengths that make RxJS valuable.
At scale, publish an architectural rule based on semantics, not a technology quota. Teams should be able to explain the owner, lifetime, error path, and conversion point of every reactive flow.
Angular renders through a tree of views. Framework notifications—such as a bound event, a new input, a template Signal update, or markForCheck—make relevant views eligible for refresh and schedule work. During change detection Angular traverses the required part of the view tree, reevaluates template bindings, compares results with stored values, and updates only changed DOM bindings. The scheduling source and the traversal strategy are separate concerns.
Change detection is a rendering process over Angular views, not a background observer that detects arbitrary JavaScript mutation. First, Angular receives a notification that work may be needed. In a current zoneless application, notifications include a Signal read by a template changing, a bound event, a new component input, markForCheck, and view attachment or removal. Zone-based applications also schedule from patched asynchronous activity.
Next Angular traverses the views that must be considered. Default-strategy views participate in ordinary passes; clean OnPush subtrees can be skipped. A dirty descendant can still require traversal through ancestors to reach it. During a view refresh, generated template instructions reevaluate bindings and compare their results with previously stored values.
Only bindings whose values changed produce corresponding DOM or directive updates. That means component checks, binding evaluations, DOM mutation, layout, and paint are distinct costs. Signals improve notification precision and OnPush improves subtree skipping, but neither removes expensive expressions, unstable list identity, large DOM, layout thrashing, or repeated network emissions.
import {ChangeDetectionStrategy, Component, computed, signal} from '@angular/core';
@Component({
selector: 'app-cart-count',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<button (click)="items.update(xs => [...xs, createItem()])">Add</button>
<span>{{ count() }} items</span>
`,
})
export class CartCount {
readonly items = signal<Item[]>([]);
readonly count = computed(() => this.items().length);
}
// Click listener notifies Angular.
// items.set/update invalidates count and marks this template consumer.
// Angular refreshes the eligible view and updates the text binding if changed.Change detection joins Signals, OnPush, templates, browser rendering, and performance. Strong candidates separate the phases instead of repeating one trigger list.
Performance work should identify the dominant cost in a real interaction. Optimizing checks while layout, a 10,000-row DOM, or duplicate emissions dominate produces a technically true change with no user benefit.
Default-strategy views are checked during ordinary application passes. An OnPush subtree can be skipped while it is clean and becomes eligible when it receives a changed input reference, handles a bound event, reads a Signal that changes, is explicitly marked, or is otherwise affected through Angular's view rules. Immutability makes input changes easy to detect, but Angular does not enforce immutable objects.
OnPush changes when a view subtree is eligible to be refreshed. It does not create a separate rendering engine and it does not freeze state. A clean OnPush subtree can be skipped during an application pass, which is valuable when a large part of the tree did not receive a relevant notification.
Relevant notifications include a new input value compared by Angular, a bound event handled in the subtree, a template Signal dependency changing, AsyncPipe marking the view for an emission, an explicit markForCheck, and view operations that require refresh. Events in a descendant also make Angular traverse the path needed to process that subtree.
Immutable updates make input changes and list identity predictable, which is why they pair well with OnPush. Deeply mutating an input behind the same reference can leave the child without an input-change notification. The fix is a clear ownership and update contract, not sprinkling detectChanges throughout the tree.
import {ChangeDetectionStrategy, Component, input, output} from '@angular/core';
@Component({
selector: 'app-profile-card',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<h2>{{ profile().name }}</h2>
<button (click)="promote.emit(profile().id)">Promote</button>
`,
})
export class ProfileCard {
readonly profile = input.required<Readonly<Profile>>();
readonly promote = output<string>();
}
// Parent replaces the value instead of mutating profile.role in place:
profiles.update(xs => xs.map(p =>
p.id === id ? {...p, role: 'lead'} : p
));The answer exposes whether a candidate understands view eligibility, event propagation, Signals, and state ownership rather than repeating 'OnPush equals immutable inputs.'
Set a default strategy based on team conventions and measure. OnPush is most effective alongside stable component boundaries, explicit state ownership, and profiling that prevents mechanical adoption from becoming ceremony.
Angular DI maps a runtime token to a provider record. When code requests the token in an injection context, Angular searches the relevant element and environment injector hierarchy, creates the value from its provider factory if needed, caches it according to that injector's lifetime, and returns it. Provider placement therefore controls availability, instance count, lazy loading, cleanup, and architectural coupling.
An injection request starts with a runtime token: usually a class or an InjectionToken. The current injection context identifies where resolution begins. Angular checks providers associated with the element and its ancestors, then the environment injector hierarchy created by the application, routes, and other environment boundaries.
A provider record explains how to produce the value: construct a class, return a value, call a factory, alias another token, or aggregate multi providers. The injector normally caches the result, so the injector that owns the provider also owns the instance lifetime. A component provider therefore creates per-subtree state, while a root provider is application-wide.
Resolution details affect bundle and architecture as well as convenience. Route providers can arrive with a lazy feature; component providers can isolate multiple instances; providedIn metadata supports tree-shakable availability. Circular dependencies often reveal confused ownership or bidirectional architecture and should usually be redesigned rather than hidden with late lookup.
import {InjectionToken, inject} from '@angular/core';
import {Routes} from '@angular/router';
export interface AuditSink {
record(event: AuditEvent): void;
}
export const AUDIT_SINK = new InjectionToken<AuditSink>('AUDIT_SINK');
export const routes: Routes = [{
path: 'admin',
providers: [
{provide: AUDIT_SINK, useClass: AdminAuditSink},
],
loadComponent: () => import('./admin-page'),
}];
export class AdminActions {
private readonly audit = inject(AUDIT_SINK);
}DI questions test framework mechanics, service lifetime, lazy loading, testability, and whether a candidate can debug duplicate or missing providers.
Review providers as an ownership map. A shared mutable service at root is a global store whether or not the team calls it one, and a route-scoped service may outlive its components depending on route/injector reuse behavior.
switchMap keeps only the newest inner stream and unsubscribes the previous one; mergeMap runs inners concurrently; concatMap queues them in order; exhaustMap ignores new sources while one inner is active. For typeahead use switchMap, independent saves may use bounded mergeMap, ordered writes may use concatMap, and double-submit prevention may use exhaustMap. The business rule chooses the operator.
Each flattening operator maps a source value to an inner Observable. The difference is the admission and concurrency policy when another source value arrives before an existing inner completes. switchMap cancels the previous subscription, mergeMap admits concurrent work, concatMap queues work, and exhaustMap rejects new work until the active inner completes.
Cancellation means unsubscription, not guaranteed remote rollback. Angular HttpClient can abort an in-flight client request, but a server may already have committed a mutation. That is why switchMap fits discardable reads such as search, while ordered or idempotent mutation policy must reflect backend semantics.
Error placement matters. Catch inside the inner pipeline when one failed request should not kill future source events; catch outside when the entire workflow should end or recover as a unit. Completion also matters for concatMap and exhaustMap because a non-completing inner can block later work indefinitely.
// Latest query wins: cancel stale reads.
query$.pipe(switchMap(q => http.get('/api/search', {params: {q}})));
// Uploads are independent: allow at most three concurrently.
files$.pipe(mergeMap(file => upload(file), 3));
// Writes must preserve order.
changes$.pipe(concatMap(change => save(change)));
// Ignore double-submit while checkout is running.
submit$.pipe(exhaustMap(() => checkout()));The four operators provide a compact test of cancellation, ordering, concurrency, product semantics, and error handling in real Angular HTTP flows.
Operator choice is part of the product's consistency model. Document whether user intent may be dropped, reordered, replayed, or run concurrently, and align it with backend idempotency and capacity.
Angular constructs the class, sets initial inputs, calls ngOnChanges, then ngOnInit, followed by content initialization/check hooks and view initialization/check hooks. Render callbacks run after rendering work, and destruction callbacks release owned resources. The exact list matters less than choosing the boundary whose prerequisites are actually ready and avoiding repeated work in the checked hooks.
Construction is a JavaScript/DI event; it is not an Angular input lifecycle. Angular creates the instance, assigns initial inputs, and reports them through ngOnChanges before ngOnInit. That makes ngOnInit the first lifecycle hook where all initial input values are available, although simple derivation is often clearer as computed state.
Projected content is initialized before the component's own view, which explains the content and view hook ordering and the availability of ContentChild versus ViewChild queries. The checked hooks may run many times and should not contain substantial work or state changes that create feedback into the same render.
Render callbacks such as afterNextRender are application rendering hooks suited to layout measurement and imperative DOM libraries; they do not run during server rendering. Destruction closes the ownership loop. Subscriptions, listeners, observers, timers, dynamic views, and library instances should be tied to DestroyRef or another explicit owner.
import {Component, DestroyRef, afterNextRender, inject, input} from '@angular/core';
@Component({
selector: 'app-chart',
template: '<canvas #canvas></canvas>',
})
export class ChartComponent {
readonly series = input.required<readonly Point[]>();
private readonly destroyRef = inject(DestroyRef);
constructor() {
afterNextRender(() => {
const chart = createChart(/* resolved canvas */, this.series());
this.destroyRef.onDestroy(() => chart.destroy());
});
}
}Lifecycle timing reveals whether a candidate understands input ownership, projected versus view content, DOM readiness, SSR, and cleanup.
Lifecycle bugs are often ownership bugs. Prefer APIs that make dependencies explicit and isolate imperative DOM integrations behind components with server fallbacks and deterministic teardown.
A global store becomes valuable when many distant features coordinate durable state, transitions need explicit events and effects, reproducible debugging matters, or policy must be standardized across teams. Component Signals and scoped services are simpler for local workflows. Evaluate ownership, event volume, server-cache needs, testability, and team fluency; application size alone is not the threshold.
State management starts with ownership, not a library. Component state is cheap to understand because its lifetime and consumers are local. A feature-scoped service can coordinate a workflow and expose readonly Signals or Observables. URL state supports navigation and sharing. A server-state cache solves different problems from a client event store.
A global library earns its cost when many features coordinate durable transitions, effects must be explicit, event history or devtools improve incident debugging, or multiple teams need the same state conventions. It also introduces action/reducer/effect ceremony, indirection, migration responsibility, and the risk that unrelated state accumulates in one global container.
Decide from forces: number and distance of consumers, write paths, event ordering, async effects, offline/optimistic behavior, auditability, persistence, and team skill. Adopt incrementally at a bounded domain. Do not use a global store as a cache for every HTTP response or as a replacement for component inputs.
import {Injectable, computed, signal} from '@angular/core';
@Injectable()
export class CheckoutStore {
private readonly _items = signal<readonly CartItem[]>([]);
readonly items = this._items.asReadonly();
readonly total = computed(() =>
this._items().reduce((sum, item) => sum + item.price * item.quantity, 0)
);
add(item: CartItem) {
this._items.update(items => [...items, item]);
}
}
// Provide at the checkout route so ownership and lifetime are explicit.The question tests architecture judgment: candidates should discuss ownership and complexity rather than arguing for a favorite library.
A Staff decision includes organizational cost: conventions, onboarding, migrations, devtools, incident workflows, and whether the chosen pattern makes domain ownership more or less visible across teams.
Define the failing user journey and metric—LCP, INP, navigation time, frame drops, memory, or bundle cost. Reproduce with representative data and device constraints, then use Angular DevTools, Chrome Performance/Memory/Network, source maps, and bundle analysis to locate time and retention. Change one proven constraint, compare the same workload, and verify the user metric rather than a proxy alone.
Performance begins with a user-visible failure and a repeatable journey. A slow first load, delayed click, janky list, growing heap, and slow route transition require different evidence. Capture the device, network, data size, cache state, route, and percentile so the result is comparable.
Split the path into network, JavaScript loading/parse, Angular work, browser rendering, and retained memory. Use bundle analysis for bytes and dependency graph, Angular DevTools for component checks and timing, Chrome Performance for main-thread tasks/layout/paint, Memory for retainers, and Network for waterfalls and duplicate requests.
Optimize the proven constraint. Move code to a lazy or @defer boundary, reduce rendered DOM, stabilize list identity, remove duplicate subscriptions, cache at an explicit owner, or redesign an expensive interaction. Verify the original metric with the same workload and add a budget or regression test that protects it.
type PerformanceExperiment = {
symptom: 'INP exceeds 300ms when filtering 10k rows';
evidence: readonly [
'180ms scripting in row templates',
'95ms style/layout',
'10,000 live row nodes'
];
hypothesis: 'Rendered DOM and repeated row derivation dominate the interaction';
mitigation: 'Virtualize rows and compute the filtered model once';
verification: 'p75 INP < 200ms; same dataset/device; keyboard behavior preserved';
};Senior candidates should connect Angular mechanisms to browser evidence and user metrics instead of listing optimization techniques.
Build a performance operating model: representative journeys, field percentiles, budgets with owners, regression triage, and architecture reviews for changes that add persistent JavaScript, DOM, or third-party work.
Use client rendering for authenticated, highly interactive surfaces where server HTML adds little. Use SSR when first-response content must be fresh or request-specific and early HTML improves the journey. Prerender stable, cacheable routes at build time. Modern Angular supports hybrid choices per route; include CDN caching, backend latency, hydration cost, failure modes, and operational capacity in the decision.
Client-side rendering sends a lightweight shell and lets the browser produce the route. It keeps infrastructure simple for authenticated applications, but first content waits on JavaScript and data. Server rendering produces HTML per request, which can improve the first response and crawlability for fresh or personalized pages, while adding server capacity, caching, timeout, and request-isolation concerns.
Prerendering produces HTML at build time. It is excellent for stable routes that can be enumerated and cached globally, but content freshness is tied to rebuild and deployment. Modern Angular hybrid rendering allows different route patterns to choose client, server, or prerender behavior rather than forcing one strategy on the whole application.
Hydration is a separate client concern: it reuses server DOM and restores interaction. Incremental hydration can delay code and hydration for @defer boundaries. Rendering strategy should account for backend latency, personalization, CDN cache keys, authentication, client bundle cost, event readiness, and failure behavior—not SEO as a single yes/no switch.
import {RenderMode, ServerRoute} from '@angular/ssr';
export const serverRoutes: ServerRoute[] = [
{path: 'docs/**', renderMode: RenderMode.Prerender},
{path: 'catalog/:id', renderMode: RenderMode.Server},
{path: 'account/**', renderMode: RenderMode.Client},
{path: '**', renderMode: RenderMode.Prerender},
];The question tests whether a candidate can choose a delivery architecture from route and business constraints instead of saying SSR is always better.
Model rendering as a capacity and cache system. Establish request isolation, timeouts, fallbacks, CDN policy, observability, and a client-rendered escape path before using SSR for a large personalized surface.
Align deployable applications and libraries to business domains with named owners and enforced dependency direction. Standardize the paved road—Angular version, build, testing, observability, security, design-system primitives, and API clients—while letting teams own domain decisions. Use automated boundaries and compatibility tests, not review committees. Measure lead time, upgrade adoption, incidents, bundle budgets, and cross-team dependency wait time.
Start from domain and team topology. Each business capability needs an owned boundary with a documented public API and explicit dependency direction. Repository layout is secondary: a monorepo can still have tangled ownership, and multiple repositories can still share a tightly coupled release train.
The platform layer should standardize repeated, high-risk work: supported Angular/TypeScript/Node versions, build and deployment, authentication integration, observability, security headers, testing defaults, accessibility checks, performance telemetry, API client conventions, and design-system primitives. Domain policy and user workflows remain with product teams.
Enforcement should be automated through dependency rules, package boundaries, contract tests, budgets, templates, and migrations. Governance by central review queues reduces autonomy and makes the platform a bottleneck. Measure lead time, change-failure rate, incident detection, upgrade duration, bundle/CWV trends, and cross-team dependency wait time to prove the architecture helps.
// Allowed direction:
// app shell -> domain feature -> domain model -> platform primitives
//
// Forbidden:
// platform -> product domain
// domain A internal -> domain B internal
// shared "utils" -> everything
export type DomainContract<TCommand, TEvent> = {
execute(command: TCommand): Promise<void>;
events(): AsyncIterable<TEvent>;
};
// Publish contracts; keep feature implementation private to its owner.Staff/Principal interviews test organizational architecture: the ability to create leverage and safe autonomy rather than design one application's folder tree.
The architectural unit is the socio-technical system. A technically elegant boundary that forces five teams into one release queue is not modular in the dimension that matters.
Automate a navigation loop, force comparable garbage-collection conditions, and verify the retained heap grows after components should be destroyed. Compare heap snapshots and dominator/retainer paths for component instances, detached DOM, subscriptions, listeners, timers, overlays, caches, and third-party objects. Fix the retaining owner, repeat the same loop, and verify both heap stabilization and correct feature behavior.
First prove retention. A high heap during work may be legitimate allocation that the collector later reclaims. Automate navigation between the two routes, pause at a consistent state, and compare heap after similar collection conditions. If destroyed component instances or their DOM keep accumulating, the retained graph is the evidence.
Use heap snapshot comparison and dominator/retainer paths to find the owning root. Common roots include a long-lived service Subject, an unremoved event listener, timer, ResizeObserver, overlay container, third-party widget registry, cached view, route reuse strategy, or a closure stored in a library. The subscription itself is not always the owner; follow the exact path.
Fix the lifetime at the creator. Use AsyncPipe, takeUntilDestroyed, DestroyRef cleanup, bounded caches, or correct third-party destruction as appropriate. Then rerun the same loop, verify the post-GC floor stabilizes, and check that the intended shared state still survives when it should.
import {Component, DestroyRef, inject} from '@angular/core';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
@Component({selector: 'app-live-chart', template: '<canvas />'})
export class LiveChart {
private readonly destroyRef = inject(DestroyRef);
constructor(feed: PriceFeed) {
feed.prices
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(price => this.render(price));
const onResize = () => this.resize();
window.addEventListener('resize', onResize);
this.destroyRef.onDestroy(() =>
window.removeEventListener('resize', onResize)
);
}
}The scenario distinguishes cleanup folklore from evidence-based memory diagnosis and tests understanding of Angular, browser, RxJS, router, and service lifetimes.
Production readiness includes lightweight counters for component/view churn, cache size, DOM nodes, and long-lived subscriptions, plus a safe profiling playbook and soak tests for screens designed to run all day.
Questions cover durable mechanisms and the decisions engineers make with them. Level labels describe reasoning depth, not claims about any employer's interview frequency. Modern, evergreen, and legacy-relevant labels keep current practice distinct from migration knowledge.
Use the library for interview rehearsal, then follow these existing guides for implementation detail and migration context.
Pair framework depth with JavaScript semantics, system design, architecture judgment, or a personalized preparation plan.