As applications grow, proper architecture becomes critical. A 5-component demo app can get away with messy structure. A 500-component enterprise app cannot — without clear patterns, you end up with components that are tangled together, impossible to test in isolation, and terrifying to refactor. This module covers advanced patterns for state management, component design, and application structure that keep large codebases maintainable.What You’ll Learn:
Simplify complex state management with facades. The Facade pattern provides a single, unified API that hides the complexity of multiple underlying services. Without a facade, your component might need to inject five different services, coordinate their calls, handle errors from each, and manage loading states — all in one file. With a facade, the component just calls facade.loadUsers() and reads facade.users(). The facade absorbs all the coordination complexity.This is especially valuable in large teams: the facade becomes the “contract” between the UI layer and the data layer. Component developers do not need to understand how data is fetched, cached, or updated — they just use the facade’s public API.
Create custom form controls that integrate seamlessly with Angular’s forms system. The ControlValueAccessor interface is the bridge between custom UI components and Angular’s FormControl. Once implemented, your custom component works with formControlName, [(ngModel)], validation, and all other form features — it becomes a first-class citizen of the forms API. This is how component libraries like Angular Material implement their input components.
// rating.component.ts@Component({ selector: 'app-rating', standalone: true, providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RatingComponent), multi: true } ], template: ` <div class="rating" [class.disabled]="disabled"> @for (star of stars; track star) { <button type="button" (click)="rate(star)" (mouseenter)="hover.set(star)" (mouseleave)="hover.set(0)" [class.active]="star <= (hover() || value())" [disabled]="disabled" > ★ </button> } </div> `})export class RatingComponent implements ControlValueAccessor { stars = [1, 2, 3, 4, 5]; value = signal(0); hover = signal(0); disabled = false; private onChange = (value: number) => {}; private onTouched = () => {}; // Called when form sets value writeValue(value: number): void { this.value.set(value || 0); } // Register callback for value changes registerOnChange(fn: (value: number) => void): void { this.onChange = fn; } // Register callback for touch registerOnTouched(fn: () => void): void { this.onTouched = fn; } // Called when form enables/disables setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; } rate(star: number): void { if (this.disabled) return; this.value.set(star); this.onChange(star); this.onTouched(); }}// Usage with reactive forms@Component({ template: ` <form [formGroup]="form"> <app-rating formControlName="rating" /> </form> `})export class ReviewFormComponent { form = new FormGroup({ rating: new FormControl(0, Validators.required), comment: new FormControl('') });}
Q: Explain the Smart/Presentational component pattern. In a real codebase, how strictly do you enforce this separation, and where does it break down?
Strong Answer: Smart components (containers) manage state, inject services, handle business logic, and coordinate child components. Presentational components (dumb) only receive data via inputs, emit events via outputs, and have no knowledge of services or application state. The benefit: presentational components are trivially testable, highly reusable, and easy to reason about because they are pure functions of their inputs.In practice, I enforce this strictly for shared/reusable components — a ButtonComponent, CardComponent, or DataTableComponent should never inject a service. For feature-level components, I am pragmatic. A UserProfileComponent that only appears in one place might inject a service directly rather than wrapping it in a container. The overhead of creating a container just to pass data down one level is not always worth the abstraction.Where it breaks down: deeply nested component trees. If a smart container needs to pass data through five levels of presentational components, you end up with “prop drilling” — every intermediate component has inputs and outputs it does not use, just passes through. The solutions are either a shared service with signals (components subscribe directly), content projection (skip intermediate levels), or a state management library like NgRx/SignalStore.The other breakdown: when a presentational component needs to trigger a complex action (like opening a modal with specific context). You can emit an event, but the parent needs to handle it, which might require injecting a modal service. At some point, the strict separation creates more indirection than clarity.Follow-up: How does this pattern interact with OnPush change detection?
Answer: Beautifully. Presentational components with OnPush only re-render when their input references change. Since smart components pass data down, the presentational layer only updates when the smart component provides new data. This creates a natural performance boundary. The smart component decides when data changes; the presentational component decides how to display it. Combined with signal inputs, this means each presentational component only re-renders when its specific data changes.
Q: Compare composition versus inheritance in Angular. You have a base component with shared logic -- how would you share that logic without class inheritance?
Strong Answer: Inheritance in Angular is tempting but problematic. If BaseListComponent has load, filter, and paginate methods, and UsersListComponent extends it, you get tight coupling. Changing BaseListComponent can break every subclass. Testing requires understanding the full inheritance chain. And Angular has specific issues: decorators are not inherited cleanly, and lifecycle hooks in the base class can have unexpected interactions with the child.Composition alternatives: First, extract shared logic into a service. A ListState<T> service with signals for items, loading, and pagination can be provided at the component level (so each component gets its own instance). The component injects it and delegates. This is testable — you mock the service — and reusable without coupling.Second, use utility functions for pure logic. Filtering, sorting, and pagination logic can be plain functions or pure pipes that components use independently. No class hierarchy, just function calls.Third, use directives to attach shared behavior. A PaginationDirective can add pagination controls and logic to any list component. A SortableDirective can add sorting behavior to table headers. This is the most Angular-idiomatic approach.Fourth, for shared template patterns, use content projection. A DataListComponent that handles loading state, empty state, and error state accepts item templates via ng-content or ng-template. The consuming component only defines how to render each item.Follow-up: When IS inheritance acceptable in Angular?
Answer: Two cases. First, abstract base classes for ControlValueAccessor implementations where the boilerplate is genuinely identical. Second, when extending third-party library components to add minor customization (though I prefer wrapping over extending). In both cases, the inheritance hierarchy should be shallow — one level deep maximum.
Q: How would you design a generic, reusable data table component that supports sorting, pagination, custom cell templates, and row selection -- without becoming an unmaintainable monolith?
Strong Answer: The key architectural decision is separating behavior from rendering. The data table should handle the mechanics (sorting logic, pagination math, selection tracking) but let consumers control how cells are rendered via content projection.I would define a ColumnDef interface with key, header, sortable flag, and optional width. The component accepts data and columns as inputs. For custom cell rendering, I use ng-template with a template outlet pattern — consumers pass named templates that the table renders for specific columns. The default fallback is plain text interpolation of the cell value.For sorting: I maintain sortKey and sortDir signals internally. Clicking a header toggles the sort. The sorted data is a computed signal that derives from the input data and the sort state. For pagination: page and pageSize signals, with a paginatedData computed that slices the sorted data. The total pages is another computed.For row selection: a Set signal tracks selected IDs. The component exposes selectionChange as an output. A “select all” checkbox computes whether all visible rows are selected.The critical design decision is what NOT to include. I would not build in filtering (let consumers pre-filter data before passing it in), server-side sorting (too many assumptions about API shape), or inline editing (that is a different component). Keeping the scope tight makes the component genuinely reusable.Follow-up: How would you handle 50,000 rows in this table?
Answer: I would integrate CDK virtual scrolling. Instead of rendering all rows, cdk-virtual-scroll-viewport only renders the rows visible in the viewport plus a small buffer. The data table still sorts and paginates the full dataset, but only renders a window. This changes the template from @for over paginatedData to cdkVirtualFor. The sorting and pagination computeds still work on the full array, but the DOM only has 20-30 rows at any time. If the data is too large even for client-side sorting (millions of rows), I would switch to server-side sorting and pagination, where the component emits sort/page change events and the parent fetches the appropriate page from the API.