In this article I'll show how I use the Facade Design Pattern on my angular projects.
Facade Design Pattern
I read about this pattern from this website: facade
This is a structural design pattern that provides a simplified interface to a library, a framework, or any other complex set of classes.
The goal of the Facade is to provide a simple interface to complex subsystem which contain lots parts.
How to implement it
In my angular project’s signals-first architecture, the Facade pattern serves as a critical architectural layer between components and the underlying state management (NgRx Signal Store).
1. Define the State
First, we define the shape of our state and its initial values. Using a _ prefix for internal state properties is a common convention when we plan to rename them in the public API.
// auth.state.ts
export type LocaleModel = 'en' | 'it';
export type UserData = {
email: string;
firstName: string;
lastName: string;
name: string;
local: LocaleModel;
phone?: string;
phonePrefix?: string;
group: 'FREE' | 'PROFESSIONAL' | 'ENTERPRISE';
};
export type AuthState = {
_locale: LocaleModel | null;
_loggedUser: UserData | null;
};
export const initialAuthState: AuthState = {
_locale: null,
_loggedUser: null,
} as const;
2. Create the Signal Store
The store handles the "how" of state management. It manages private state and exposes derived state via computed signals.
import { computed } from '@angular/core';
import { signalStore, withComputed, withMethods, withState } from '@ngrx/signals';
export const AuthStore = signalStore(
{ providedIn: 'root' },
withState(initialAuthState),
withComputed((store) => {
const $isFreeUser = computed(() => store._loggedUser()?.group === 'FREE');
const $isProfessionalUser = computed(() => store._loggedUser()?.group === 'PROFESSIONAL');
const $isEnterpriseUser = computed(() => store._loggedUser()?.group === 'ENTERPRISE');
return {
$isFreeUser,
$isProfessionalUser,
$isEnterpriseUser,
};
}),
withMethods((store) => {
const initialize = () => {};
const changeSelectedLanguage = () => {};
const setLoggedUser = () => {};
const updateUser = () => {};
const logout = () => {};
return {
initialize,
changeSelectedLanguage,
setLoggedUser,
updateUser,
logout,
};
}),
);
export type AuthStore = InstanceType<typeof AuthStore>;
3. Implement the Facade
The Facade provides the "what" to the components. We use the OmitSymbols utility to ensure the Facade strictly implements the functional API of the Store while automatically hiding internal NgRx Signal Store symbols that shouldn't be exposed to components.
// auth.facade.ts
import { inject, Injectable, type Signal } from '@angular/core';
/**
* Drops every symbol-keyed property *plus* any keys in `U`.
*/
export type OmitSymbols<T, U extends PropertyKey = never> = {
[K in keyof T as K extends symbol ? never : K extends U ? never : K]: T[K];
};
@Injectable({ providedIn: 'root' })
export class AuthFacade implements OmitSymbols<AuthStore> {
readonly #authStore = inject(AuthStore);
// Expose signals with $ prefix
readonly $locale = this.#authStore._locale;
readonly $loggedUser = this.#authStore._loggedUser;
readonly $isFreeUser = this.#authStore.$isFreeUser;
readonly $isProfessionalUser = this.#authStore.$isProfessionalUser;
readonly $isEnterpriseUser = this.#authStore.$isEnterpriseUser;
initialize() {
this.#authStore.initialize();
}
changeSelectedLanguage() {
this.#authStore.changeSelectedLanguage();
}
setLoggedUser() {
this.#authStore.setLoggedUser();
}
updateUser() {
this.#authStore.updateUser();
}
logout() {
this.#authStore.logout();
}
}
4. Use inside Angular Components
Components consume a clean, reactive API. They are decoupled from the store's implementation details.
@Component({...})
export class HeaderComponent {
readonly #auth = inject(AuthFacade);
// Direct access to signals from facade
$user = this.#auth.$loggedUser;
$isFreeUser = this.#auth.$isFreeUser;
onLogout() {
this.#auth.logout();
}
}
Why this matters in real projects
Based on the AuthFacade pattern, here is why this is effective:
Abstraction of Complexity
Components shouldn't care how state is managed. The Facade hides the complexity of the AuthStore. If you decide to move a piece of state from a global Store to a specialized Service or even combine it with Route Params, you only update the Facade. Your components remain untouched.
Simplified Public API & Integrity
A Store often contains internal state and complex update logic. The Facade acts as a gatekeeper:
- It exposes only what the component needs.
- It protects the store's integrity by not exposing
patchStateor internal state directly. - It provides a semantic interface:
this.#authFacade.logout()is clearer than interacting with store internals.
Simplified Testing
One of the biggest benefits is simplified unit testing. You can easily mock the AuthFacade in component tests without setting up a full NgRx Signal Store:
{ provide: AuthFacade, useValue: { $loggedUser: signal(mockUser), logout: vi.fn() } }
Enforcement of Naming Conventions
The Facade reinforces the project's reactive patterns. Consistently exposing signals with the $ prefix gives developers an immediate visual cue that they are working with a Signal, promoting a "Signals-First" mindset.
Orchestration & Derived State
Facades are perfect for orchestrating multiple sources, and they can combine data from a Signal Store, a Standard Service, and Route Params into a single computed signal, providing the component with exactly one object to bind to.
Top comments (0)