AngularAtlased. 22.2

Ch. 13Architecture at scale60 / 75

Hexagonal architecture (ports & adapters)

A domain without Angular, ports as abstract classes and interchangeable adapters: HTTP, localStorage or in-memory, without touching the UI.

architecture ~0 min read

Hexagonal architecture (or ports & adapters) starts from a simple idea: the application's rules should not need to know where the data comes from or how it is shown. The domain sits in the center, written in plain TypeScript. Around it are the use cases, which talk to the outside world only through ports, that is, contracts. At the edge are the adapters: HTTP, localStorage, Angular components. You can swap them without touching the center.

In Angular the idea fits DI very well: an abstract class is both the type of the port and the injection token. { provide: TaskRepository, useClass: HttpTaskRepository } is all the "wiring" you need.

Why it matters
When the backend changes (a different API, GraphQL, offline-first), you modify a single adapter. When you test the rules, you don't start HttpClient. And the business logic no longer spreads through components, where it is hard to find and hard to reuse.

The layers and the dependency rule

There are four layers, and imports only point inward. The domain imports nothing from outside itself. Application knows the domain and the ports, but not the adapters. Adapters implement the ports. The only place that binds a port to a concrete adapter is the composition root: app.config.ts, or the providers of a route.

                 ┌──────────────────────── outside: adapters ──────────────────────────┐
                 │  presentation (driving)                 infrastructure (driven)     │
  user ────────▶ │  TasksPage, TaskItem                    HttpTaskRepository, TaskDto │ ──▶ server
                 │        │   ┌──────────── application ─────────────┐   ▲             │
                 │        └──▶│ TaskBoard / AddTask ──▶ TaskRepository│───┘ implements   │
                 │            │        │      ┌──── domain ────┐     │                  │
                 │            │        └─────▶│ Task, createTask│    │                  │
                 │            │               └─────────────────┘     │                  │
                 │            └───────────────────────────────────────┘                  │
                 └──────────────────────────────────────────────────────────────────────┘
  Rule: import arrows point ONLY inward. The domain imports nothing from outside.
  app.config.ts (composition root) is the only place that binds TaskRepository to an adapter.
LayerContainsMay import
domainentities, rules, domain errorsnothing (only TypeScript)
applicationports (abstract classes), use cases, state for the UIdomain, @angular/core (DI, signals)
infrastructureadapters: HTTP, storage, DTOs, mappingapplication (the ports), domain, HttpClient
presentationsmart and dumb componentsapplication, domain (types) — never infrastructure
LiveDependency rule: is this import allowed?

1. Pick the file that does the importing.

presentation · driving adapters
infrastructure · driven adapters
application · ports + use cases
domain · pure TypeScript
framework & browser
composition root
Examples:
Pick where the import comes from and what it imports. The verdict tells you whether the rule is respected and what you should do instead.

Domain: pure TypeScript

The domain holds the entities and the rules: "a title has at least 3 characters", "two open tasks cannot share the same title". It imports neither Angular, RxJS nor browser APIs. Even the clock and id generation come in as parameters, so the rules stay deterministic. The main gain: the domain is tested with plain functions, without TestBed and without mocks.

// tasks/domain/task.ts — pure TypeScript: no import from Angular, RxJS or the browser
export type TaskId = string & { readonly __brand: 'TaskId' };

export interface Task {
  readonly id: TaskId;
  readonly title: string;
  readonly done: boolean;
  readonly createdAt: Date;
}

export class DomainError extends Error {
  override readonly name = 'DomainError';
}

export function normalizeTitle(raw: string): string {
  return raw.trim().replace(/\s+/g, ' ');
}

// `now` and `id` come in as parameters: the domain neither reads the clock nor generates ids on its own
export function createTask(raw: string, existing: readonly Task[], now: Date, id: TaskId): Task {
  const title = normalizeTitle(raw);
  if (title.length < 3) throw new DomainError('The title needs at least 3 characters.');
  if (existing.some((t) => !t.done && t.title.toLowerCase() === title.toLowerCase())) {
    throw new DomainError('There is already an open task with this title.');
  }
  return { id, title, done: false, createdAt: now };
}

export function toggleTask(task: Task): Task {
  return { ...task, done: !task.done };
}

Ports: abstract classes as tokens

A port describes what the application needs ("list, save, delete"), not how. You write it as an abstract class: it exists at runtime, so inject(TaskRepository) works directly. An interface would disappear at compile time, and you would need a separate InjectionToken (see InjectionToken). Errors are part of the contract: adapters translate their technical errors (timeout, 503) into RepositoryUnavailableError, so the UI does not depend on HttpErrorResponse.

// tasks/application/task-repository.ts — the PORT
import { Task, TaskId } from '../domain/task';

// Abstract class, not an interface: it exists at runtime, so it is a DI token as-is.
// It has no @Injectable and knows of no implementation.
export abstract class TaskRepository {
  abstract list(): Promise<readonly Task[]>;
  abstract save(task: Task): Promise<void>; // upsert
  abstract remove(id: TaskId): Promise<void>;
}

// Part of the contract: adapters translate their technical errors into this.
export class RepositoryUnavailableError extends Error {
  override readonly name = 'RepositoryUnavailableError';
}

Use cases

A use case orchestrates: it reads through a port, applies the domain rules, and saves through a port. In Angular it is an ordinary service that injects the port, not the adapter. For the UI, a "board" that holds the state in signals (tasks, error, open) is convenient. Components only read it, exactly as in state management.

// tasks/application/add-task.ts — one use case = a class with a single public method
import { Injectable, inject } from '@angular/core';
import { Task, TaskId, createTask } from '../domain/task';
import { TaskRepository } from './task-repository';

@Injectable({ providedIn: 'root' })
export class AddTask {
  private readonly repo = inject(TaskRepository); // the port — does not know which adapter answers

  async execute(title: string): Promise<Task> {
    const existing = await this.repo.list();
    const task = createTask(title, existing, new Date(), crypto.randomUUID() as TaskId); // the rule: in the domain
    await this.repo.save(task); // I/O: through the port
    return task;
  }
}

Adapters

An adapter extends TaskRepository and does all the I/O: HTTP, localStorage, memory. The DTO → domain mapping lives here too (is_done: 1 becomes done: true, the ISO string becomes a Date; see Interfaces & models). The server's format does not get past the adapter.

// tasks/infrastructure/http-task-repository.ts — the HTTP ADAPTER
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { Observable, firstValueFrom } from 'rxjs';
import { Task, TaskId } from '../domain/task';
import { RepositoryUnavailableError, TaskRepository } from '../application/task-repository';
import { TaskDto, taskFromDto, taskToDto } from './task-dto';

@Injectable()
export class HttpTaskRepository extends TaskRepository {
  private readonly http = inject(HttpClient);
  private readonly url = '/api/tasks';

  async list(): Promise<readonly Task[]> {
    const dtos = await this.call(this.http.get<TaskDto[]>(this.url));
    return dtos.map(taskFromDto); // DTO → domain, here and nowhere else
  }

  async save(task: Task): Promise<void> {
    await this.call(this.http.put<TaskDto>(`${this.url}/${task.id}`, taskToDto(task)));
  }

  async remove(id: TaskId): Promise<void> {
    await this.call(this.http.delete<void>(`${this.url}/${id}`));
  }

  // HttpErrorResponse does not leave the adapter: the core receives the port's error
  private async call<T>(request: Observable<T>): Promise<T> {
    try {
      return await firstValueFrom(request);
    } catch (e) {
      if (e instanceof HttpErrorResponse) throw new RepositoryUnavailableError(`HTTP ${e.status}`);
      throw e;
    }
  }
}
LiveThe same UI, three adapters

{ provide: TaskRepository, useClass: InMemoryTaskRepository } — A Map in memory — the data disappears when the injector is destroyed.

Identical UI: TasksPanel → TaskBoard → TaskRepository
  • No tasks.
0 open of 0 done
Who answered
  • 11:35.5DI TaskRepository → InMemoryTaskRepository
  • 11:35.5InMemoryTaskRepository list() → 0 (Map)
Switch the adapter and add tasks. The component and the use cases are identical; the log shows which adapter answered each call. On HTTP you can take the server offline.

Composition root: where you choose the adapter

The port → adapter binding sits in a single place. Globally, in app.config.ts. Locally, in the providers of a route, where the adapter lives as long as the route (see Providers & the injector hierarchy). To keep the feature's details private, it exposes a provideTasks() function. The composition root chooses only "http" or "local".

// app.config.ts — COMPOSITION ROOT: the only file that knows which adapter we use
export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(withInterceptors([authInterceptor])),
    { provide: TaskRepository, useClass: HttpTaskRepository },
  ],
};

Testing: this is where the gain shows

You have three levels of tests, each one cheap:

  • the domain, with pure functions;
  • the use cases, with the in-memory adapter in place of HTTP;
  • the adapters, with a contract test: the same suite run on every implementation of the port.

The contract test catches exactly the bugs that would otherwise slip through: an adapter that returns DTOs instead of entities, or one that duplicates instead of updating.

// Use-case test: the in-memory adapter instead of HTTP
import { TestBed } from '@angular/core/testing';

describe('AddTask', () => {
  let addTask: AddTask;
  let repo: InMemoryTaskRepository;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [{ provide: TaskRepository, useClass: InMemoryTaskRepository }],
    });
    addTask = TestBed.inject(AddTask);
    repo = TestBed.inject(TaskRepository) as InMemoryTaskRepository;
  });

  it('saves through the port', async () => {
    await addTask.execute('Write tests');
    expect((await repo.list()).map((t) => t.title)).toEqual(['Write tests']);
  });

  it('saves nothing if a domain rule is broken', async () => {
    await expect(addTask.execute('ab')).rejects.toThrow(DomainError);
    expect(await repo.list()).toEqual([]);
  });
});
LiveThe contract test, run on every adapter
6 tests × 4 adapters
Test (same for all) In-memorylocalStorageHTTPHTTP without mapping
a new repository is empty····
save() then list() returns the task····
save() with the same id updates, does not duplicate····
remove() deletes only the requested task····
done is a boolean, createdAt is a Date····
returns domain entities, not DTOs····
Run the suite: the same tests pass on in-memory, localStorage and HTTP, but fail on the 'HTTP without mapping' adapter, which leaks DTOs into the domain.

How to enforce the rule in a team

An architecture rule kept only in your head does not survive the first deadline. Put it in lint. With ESLint's no-restricted-imports, split by layer, any wrong import fails in CI. In an Nx monorepo you do the same with tags and @nx/enforce-module-boundaries (see Monorepo, libraries & boundaries and Barrels, aliases & boundaries).

// The dependency rule, checked on every commit (ESLint no-restricted-imports with regex)
export default [
  {
    files: ['src/app/features/*/domain/**/*.ts'],
    rules: {
      'no-restricted-imports': ['error', { patterns: [
        { regex: '^(@angular/|rxjs)', message: 'The domain is pure TypeScript: no Angular, no RxJS.' },
        { regex: '/(application|infrastructure|ui)/', message: 'The domain does not depend on the outer layers.' },
      ] }],
    },
  },
  {
    files: ['src/app/features/*/application/**/*.ts'],
    rules: {
      'no-restricted-imports': ['error', { patterns: [
        { regex: '/(infrastructure|ui)/', message: 'Application depends on ports, not on adapters or UI.' },
        { regex: '^@angular/common/http', message: 'HTTP only in infrastructure.' },
      ] }],
    },
  },
  {
    files: ['src/app/features/*/ui/**/*.ts'],
    rules: {
      'no-restricted-imports': ['error', { patterns: [
        { regex: '/infrastructure/', message: 'The UI injects the port or the use case, not the adapter.' },
      ] }],
    },
  },
];
// In an Nx monorepo: domain/application/infrastructure as separate libraries, with tags
// and @nx/enforce-module-boundaries (see the Monorepo page).

When YES, when NO

Ports & adapters are worth it
  • Real business rules (prices, validations, flows) that must be tested and reused.
  • An unstable or changing backend: a new API, a REST → GraphQL migration, offline mode.
  • The same logic over several data sources (server, local cache, a mock for demos).
  • Large, long-lived applications with several teams on the same feature.
It's overkill — keep it simple
  • CRUD screens that only display what the server returns. A service with httpResource is enough.
  • Prototypes and small apps: four layers for two forms slow the team down.
  • When the "port" has a single implementation and you won't test it separately. The abstraction adds nothing.
  • Pure UI logic (hover, expand): it stays in the component.

A pragmatic variant works in most projects: a pure domain for the rules that matter, a port only where you have, or will have, several implementations, and the rest in the usual data-access / ui structure (see Folder structure).

Common pitfalls

The port that imports the adapter
@Injectable({ providedIn: 'root', useClass: HttpTaskRepository }) on the abstract class looks convenient, but the port ends up depending on infrastructure, and HttpClient lands in the bundle even where you don't use it. The choice of adapter belongs to the composition root.
// ✗ "Default implementation" placed on the port
@Injectable({ providedIn: 'root', useClass: HttpTaskRepository }) // the port imports the adapter!
export abstract class TaskRepository { /* ... */ }
// Application → infrastructure: the dependency rule is broken, and HttpClient
// ends up in the bundle even for the app that only uses localStorage.

// ✓ The port stays empty; the choice lives in the composition root
export abstract class TaskRepository { /* ... */ }
providers: [{ provide: TaskRepository, useClass: HttpTaskRepository }]
  • DTOs that leak into the domain. If the server's type reaches the components, any API change spreads everywhere. The mapping lives in the adapter.
  • Components that inject the concrete adapter (inject(HttpTaskRepository)). Then you have no port at all. Inject the port or the use case.
  • An "anemic" domain. Entities without rules, with all the logic in services: you have the layers, but not the benefit.
  • Technical errors that reach the UI. An HttpErrorResponse in a component means the UI knows it is HTTP. Translate the errors in the adapter.
  • A clock and ids read inside the domain (new Date(), crypto.randomUUID()). The rules become hard to test. Pass them in as parameters.

Cheat sheet

LAYERS (imports point only inward)
  domain          Task, createTask()         pure TypeScript — no Angular, RxJS, browser
  application     TaskRepository (port)      abstract class = type + DI token
                  AddTask / TaskBoard        inject(TaskRepository), signals for the UI
  infrastructure  HttpTaskRepository         extends TaskRepository, DTO ↔ domain, errors translated
  presentation    TasksPage, TaskItem        injects application, never infrastructure
  composition     app.config / route.providers  { provide: TaskRepository, useClass: Http… }

RECIPES
  swap the adapter         another useClass in the composition root (or provideTasks('local'))
  use-case test            providers: [{ provide: TaskRepository, useClass: InMemoryTaskRepository }]
  adapter test             shared contract + HttpTestingController for the HTTP mapping
  DTO                      only in infrastructure; the port speaks in entities
  errors                   the adapter throws RepositoryUnavailableError, not HttpErrorResponse

WHEN NOT
  CRUD without rules, small app, prototype → httpResource in the component + mapping in parse

Ch. 13 · 0 of 4 complete

Finished this lesson?

Mark it and it shows up on your map: in the contents and on the home page.

Marks itself as done once you have read the whole lesson.

  1. Micro frontends
  2. Web Components with Angular Elements
  3. Monorepo, libraries & boundaries
  4. Hexagonal architecture (ports & adapters)
See your whole map

All 75 pages · type to filter

  • 01What's new in Angular 21 & 22All the important changes in v21 and v22, briefly, with a link to the page that explains each one.Start
  • 01Your first app, step by stepA reading list built from an empty component to a finished app — signals, @for, inputs and outputs, a service and a form, one step at a time.Start
  • 01Quick decisions“Where do I put this?”, “Signal or Observable?”, “Shared or feature?” — short answers to everyday questions.Start
  • 01Angular interview questions60 real interview questions for junior, mid and senior, with the answer to give, the follow-up and the red flags. Plus a mock interview.Start
  • 02Standalone & bootstrapHow an app starts without NgModule: bootstrapApplication, app.config.ts, imports in the component.Modern fundamentals
  • 02Data binding & eventsInterpolation, property and attribute bindings, events with $event and key filters, and how [( )] is built — what goes into the DOM and what comes back.Modern fundamentals
  • 02Control flow: @if, @for, @switchThe block syntax that replaces *ngIf/*ngFor/ngSwitch, with track, @empty and exhaustive checks.Modern fundamentals
  • 02@let and the new template expressionsLocal variables with @let, arrow functions, spread, regex, instanceof, comments inside tags.Modern fundamentals
  • 02@defer — lazy loading in templatesLoad heavy components only when needed: triggers, prefetch, placeholder, loading, error.Modern fundamentals
  • 02Class & style bindingsWhy [class.x] and [style.x] replaced ngClass and ngStyle (with automatic migration from v21).Modern fundamentals
  • 02Component styles & encapsulation:host, the three encapsulation modes, theming with CSS variables, and why ::ng-deep is a last resort.Modern fundamentals
  • 02Pipes: when and howPure vs impure pipes, custom pipes, and how you choose between a pipe, computed() and a method.Modern fundamentals
  • 03signal() and computed()The reactive primitives: writable state, derived state, equality, untracked and immutability rules.Signals & reactivity
  • 03effect() — when YES, when NOSide effects synchronised with signals: localStorage, logging, imperative APIs. And their traps.Signals & reactivity
  • 03linkedSignal() — editable derived stateA signal that resets when its source changes, but that you can also set manually.Signals & reactivity
  • 03resource, rxResource, httpResourceAsync data as signals: status, value, error, reload, automatic cancellation. Stable since v22.Signals & reactivity
  • 03debounced() — delayed signalsThe “debounce” version of a signal, without RxJS. Perfect for search.Signals & reactivity
  • 03Lists: search, filter, sort, paginateA chain of computed signals that recalculates only what changed, with the filters kept in the URL so a link reopens the same view.Signals & reactivity
  • 03Observables from zeroWhat an Observable is, subscribe and unsubscribe, the operators you use every week, cold vs hot — and when a signal is enough.Signals & reactivity
  • 03Signals ↔ RxJStoSignal, toObservable, takeUntilDestroyed, outputFromObservable — and when RxJS remains the right choice.Signals & reactivity
  • 04input() and output()Inputs as signals (required, transform, alias) and typed outputs. Data down, events up.Components: communication
  • 04model() — two-way bindingBanana-in-a-box [( )] with a single API. When to use it, and when input + output is better.Components: communication
  • 04viewChild & contentChildQueries as signals: viewChild, viewChildren, contentChild, contentChildren, the read option.Components: communication
  • 04Lifecycle & DestroyRefWhich hooks still matter in the signals era, afterNextRender, DestroyRef and typed ngOnChanges.Components: communication
  • 04Content projectionng-content with select, multiple slots, default content and the trap of conditional projection.Components: communication
  • 04ng-template & ngTemplateOutletTemplates as values: render props in Angular, typed context and customising components.Components: communication
  • 05Smart vs DumbContainer vs presentational: who holds the state, who only displays, and how to split them properly.Component design
  • 05Generic components <T>A correctly typed DataTable<T> or Select<T>, with templates for cells.Component design
  • 05Dynamic componentsNgComponentOutlet, createComponent with inputBinding/outputBinding, and a dashboard built from config.Component design
  • 05Directives & host compositionAttribute directives, the host: {} metadata and the Directive Composition API (hostDirectives).Component design
  • 05Custom form controlsFormValueControl (Signal Forms) vs ControlValueAccessor: a star rating, in both variants.Component design
  • 05Change detectionOnPush is the default from v22, zone.js is gone from v21. What triggers rendering and how not to get burned.Component design
  • 06inject() & @Service()The new @Service() decorator from v22, inject() vs the constructor, and the injection context.Dependency Injection
  • 06Providers & the injector hierarchyRoot vs route vs component, useClass/useValue/useFactory/useExisting, and the optional/self/skipSelf modifiers.Dependency Injection
  • 06InjectionToken & provideX()Typed configuration, provideFeature() functions, makeEnvironmentProviders and initializers.Dependency Injection
  • 06injectAsync() — lazy servicesLoad a heavy service only when you need it, with prefetch on idle.Dependency Injection
  • 07Folder structurecore / shared / features, what goes into each, and what a feature looks like inside.Application architecture
  • 07Shared vs featureWhen to move a component into shared and (especially) when NOT to. A concrete checklist.Application architecture
  • 07Interfaces & modelsInline interface or separate file? DTO vs domain model, type vs interface, where to put them.Application architecture
  • 07State management with signalsSimple signal stores, facades, local vs global state, and when you need NgRx.Application architecture
  • 07Style guide & namingThe current official conventions: file names without suffixes, protected, readonly, member order.Application architecture
  • 07Barrels, aliases & boundariesindex.ts, path aliases, a feature’s public API and dependency rules that can be checked automatically.Application architecture
  • 07Errors: @boundary & ErrorHandlerThe new @boundary block from v22.2, the global ErrorHandler, HTTP errors and errors from resource.Application architecture
  • 08Modern routing & lazy loadingloadComponent, child routes, parameters as inputs and parameter inheritance (the new default in v22).Routing
  • 08Guards & resolverscanActivate, canMatch, canDeactivate and resolve as functions, with redirects via UrlTree / RedirectCommand.Routing
  • 09Signal Forms — the basicsForms built on signals: form(), [formField], schema-based validation. Stable since v22.Forms
  • 09Signal Forms — advancedCustom and cross-field validation, async/HTTP with debounce, dynamic lists, disabled/hidden, reusable schemas.Forms
  • 09Typed Reactive FormsTyped FormGroup, nonNullable, FormArray — and how they coexist with Signal Forms.Forms
  • 10HttpClient, interceptors & the API layerHttpClient without setup (v21), Fetch by default (v22), functional interceptors and an API layer with DTO mapping.HTTP & data
  • 11Performance — a checklistWhat really matters: OnPush + signals, track, @defer, lazy routes, images, bundles and hydration.Quality & performance
  • 11Testing with VitestVitest is the default runner from v21. TestBed, components with signals, services, HTTP and harnesses.Quality & performance
  • 11DevTools: look inside your appChrome DevTools and Angular DevTools step by step: what each panel shows, with exercises that work on this site.Quality & performance
  • 11Angular AriaHeadless, accessible components that you style however you like: tabs, accordion, menu, listbox...Quality & performance
  • 12SSR, prerender & render modesWhen to render on the server, when at build time and when only in the browser: RenderMode per route, app.routes.server.ts and the real costs.SSR & hydration
  • 12Hydration: event replay & incrementalHow the browser takes over server HTML without re-rendering it: full hydration, event replay, @defer (hydrate on …) and mismatch errors.SSR & hydration
  • 12Server-compatible codewindow doesn’t exist on the server: PLATFORM_ID, afterNextRender, DOCUMENT, TransferState, the HTTP cache and PendingTasks.SSR & hydration
  • 13Micro frontendsWhen it is worth splitting an app into several deployments and when it is just complexity: Native Federation, Module Federation, iframes, Web Components.Architecture at scale
  • 13Web Components with Angular ElementscreateCustomElement: an Angular component used as an HTML tag anywhere, plus communication between apps through events and BroadcastChannel.Architecture at scale
  • 13Monorepo, libraries & boundariesA workspace with several apps and libraries: ng generate library, Nx, secondary entry points, publishing and dependency rules.Architecture at scale
  • 13Hexagonal architecture (ports & adapters)A domain without Angular, ports as abstract classes and interchangeable adapters: HTTP, localStorage or in-memory, without touching the UI.Architecture at scale
  • 14Signals under the hoodThe reactive graph: producers and consumers, push-pull, why computed is lazy and glitch-free, the diamond problem and when effects run.Advanced reactivity
  • 14Advanced RxJS: higher-order operatorsswitchMap, mergeMap, concatMap and exhaustMap on a live diagram, plus retry with backoff, shareReplay and how to avoid leaks.Advanced reactivity
  • 14NgRx SignalStoreLarge-scale state management on signals: withState, withComputed, withMethods, rxMethod, entities and hooks — and when a simple store is enough.Advanced reactivity
  • 14State machines with signalsNo more impossible states: a typed machine on signals, with transitions, guards and a live state diagram.Advanced reactivity
  • 15WebSockets: real time that survives the networkA typed socket as signals: reconnect with backoff and jitter, heartbeat, an outbox while offline, and SSR safety.Real time & offline
  • 15Optimistic updates & rollbackAn instant UI that stays honest: snapshot and rollback, out-of-order responses, idempotency keys.Real time & offline
  • 15Offline-first: Service Worker & cachingThe Angular Service Worker: caching strategies, update prompts and an outbox for changes made offline.Real time & offline
  • 16Web WorkersHeavy computation without blocking the UI: a real worker in the page, message passing and when it is not worth it.Advanced UI & performance
  • 16Huge lists: virtual scroll100,000 rows without lag: CDK virtual scroll, itemSize, trackBy and the alternatives (pagination, infinite scroll).Advanced UI & performance
  • 16Modern animations & View Transitionsanimate.enter and animate.leave instead of the deprecated @angular/animations, CSS transitions and View Transitions on navigation.Advanced UI & performance
  • 16Memory leaks: finding and fixing themSubscriptions, timers and listeners that outlive their component — how to spot them in DevTools and close them.Advanced UI & performance
  • 17Security: XSS, sanitisation & CSPWhat Angular does automatically against XSS, when bypassSecurityTrust becomes dangerous, plus CSP with nonce, Trusted Types and XSRF.Security & globalisation
  • 17Authentication: tokens & refreshAn interceptor that attaches the token, refreshes once on 401 and replays the queued requests — plus where you keep the token.Security & globalisation
  • 17OAuth 2.1 & OpenID Connect with PKCEThe authorization code flow with PKCE, step by step: verifier and challenge, state and nonce, the ID token, and why a BFF is safer.Security & globalisation
  • 17Internationalisation (i18n)$localize, the i18n attribute, ICU plural/select, a build per language vs runtime translations, and locale-based formatting.Security & globalisation