Skip to content
Angular ng patterns 4 min read

Facade Pattern

The facade pattern wraps a tangle of stores, HTTP services, and business logic behind a single, intention-revealing API. Components depend only on the facade, so they stay thin and declarative while the complexity lives in one well-tested place. In Angular this maps naturally onto an @Injectable service that exposes a handful of signals (or observables) for reading state and a few methods for mutating it. The result is a clean seam that makes components easier to read, test, and refactor.

Why a facade

A “smart” component often grows tentacles: it injects an HTTP client, a state store, a router, a logger, and stitches them together inside the component class. That couples the component to implementation details it should not care about. A facade collapses all of that into one dependency.

Without facadeWith facade
Component injects 4-6 collaboratorsComponent injects 1 facade
Orchestration logic lives in the componentOrchestration lives in the facade
Hard to unit-test (many mocks)Easy to test (mock one facade)
Swapping the data layer touches every componentSwap behind the facade, components unchanged

The component asks what it wants (“load the products”, “select this one”) and the facade decides how.

A products facade

Below, the facade owns a signal store and an HTTP service, then exposes read-only signals plus a couple of commands. Notice that components never see HttpClient or the raw writable signals.

import { Injectable, computed, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';

export interface Product {
  id: string;
  name: string;
  price: number;
}

@Injectable({ providedIn: 'root' })
export class ProductsFacade {
  private readonly http = inject(HttpClient);

  // Private writable state — never exposed directly.
  private readonly _products = signal<Product[]>([]);
  private readonly _selectedId = signal<string | null>(null);
  private readonly _loading = signal(false);

  // Public, read-only projections.
  readonly products = this._products.asReadonly();
  readonly loading = this._loading.asReadonly();
  readonly selected = computed(() =>
    this._products().find((p) => p.id === this._selectedId()) ?? null,
  );
  readonly total = computed(() =>
    this._products().reduce((sum, p) => sum + p.price, 0),
  );

  async load(): Promise<void> {
    this._loading.set(true);
    try {
      const data = await firstValueFrom(
        this.http.get<Product[]>('/api/products'),
      );
      this._products.set(data);
    } finally {
      this._loading.set(false);
    }
  }

  select(id: string): void {
    this._selectedId.set(id);
  }
}

The asReadonly() calls and computed() projections enforce a one-way data flow: components read derived signals and call methods, but cannot mutate state behind the facade’s back.

Keep facades thin. A facade is an orchestrator, not a dumping ground. If a method grows past a few lines, push the real work into the store or a domain service and let the facade delegate.

Consuming it from a component

The component is now almost pure template. It injects one thing, reads signals, and forwards user intent.

import { Component, inject } from '@angular/core';
import { ProductsFacade } from './products.facade';

@Component({
  selector: 'app-product-list',
  standalone: true,
  template: `
    @if (facade.loading()) {
      <p>Loading…</p>
    } @else {
      <ul>
        @for (p of facade.products(); track p.id) {
          <li (click)="facade.select(p.id)">{{ p.name }} — {{ p.price }}</li>
        }
      </ul>
      <p>Total: {{ facade.total() }}</p>
      @if (facade.selected(); as item) {
        <p>Selected: {{ item.name }}</p>
      }
    }
  `,
})
export class ProductListComponent {
  protected readonly facade = inject(ProductsFacade);

  constructor() {
    this.facade.load();
  }
}

Output:

Loading…
Widget — 9.99
Gadget — 19.99
Gizmo — 4.5
Total: 34.48
Selected: Gadget

Testing the facade

Because all orchestration is in one place, tests target the facade directly and components get a trivially mockable dependency.

import { TestBed } from '@angular/core/testing';
import {
  HttpTestingController,
  provideHttpClientTesting,
} from '@angular/common/http/testing';
import { provideHttpClient } from '@angular/common/http';
import { ProductsFacade } from './products.facade';

describe('ProductsFacade', () => {
  let facade: ProductsFacade;
  let http: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [provideHttpClient(), provideHttpClientTesting()],
    });
    facade = TestBed.inject(ProductsFacade);
    http = TestBed.inject(HttpTestingController);
  });

  it('loads products and computes the total', async () => {
    const promise = facade.load();
    http.expectOne('/api/products').flush([
      { id: '1', name: 'Widget', price: 10 },
      { id: '2', name: 'Gadget', price: 5 },
    ]);
    await promise;

    expect(facade.products().length).toBe(2);
    expect(facade.total()).toBe(15);
  });
});

Facades over NgRx or signal stores

A facade pairs especially well with a heavier state library. Instead of leaking Store, action creators, and selector keys into every component, the facade dispatches actions and selects state on their behalf — so you can later replace NgRx with a signal store without touching a single component.

@Injectable({ providedIn: 'root' })
export class CartFacade {
  private readonly store = inject(Store);

  readonly items = this.store.selectSignal(selectCartItems);
  readonly count = computed(() => this.items().length);

  add(productId: string): void {
    this.store.dispatch(cartActions.add({ productId }));
  }
}

Best Practices

  • Expose read-only signals or observables; keep all writable state private inside the facade.
  • Provide facades in 'root' for app-wide singletons, or at a route/component level when state should be scoped and disposed with a feature.
  • Keep methods intention-revealing (select, checkout, archive) rather than CRUD-shaped (setX, getY).
  • Let the facade delegate, not implement — heavy logic belongs in stores and domain services.
  • Mock the facade when testing components, and test the facade itself with HttpTestingController or a stubbed store.
  • Don’t share one giant “AppFacade” across the whole app; create one facade per feature/domain to avoid a god object.
Last updated June 14, 2026
Was this helpful?