The Facade pattern decouples NgRx from the rest of the application, acting as an API. It transforms action dispatches into method calls and selectors into Observable properties. Beyond decoupling, the pattern brings several other benefits.
If you prefer a video over an article, there you go ;)
1. Theory
The Facade pattern, also referred to as Repository or API in some contexts, is an architectural construct. It organizes the application into modules grouped across two layers. The first layer splits the codebase into distinct scopes or domains when DDD is in play. Alongside these domains sit the application shell and shared modules.
Within each domain, several module types are common. A typical NgRx application splits into feature, data, ui, and model modules.
The feature module is responsible for container components, while ui holds the presentational ones. The NgRx feature state lives in the data module.
Modules rely on encapsulation and dependency rules for proper isolation. Encapsulation allows certain internal elements to remain hidden from the outside. Dependency rules determine which modules are permitted to interact with one another.
Because TypeScript doesn't offer built-in modularity, additional tooling is necessary. The author suggests either Nx dependency rules or Sheriff, both distributed as ESLint plugins.
These dependency rules apply across both layers. At the top level, domains are forbidden from communicating with each other. Any exceptions have to be added explicitly on a case-by-case basis.
Within a domain, the second layer's rules are tied to module type. Only the feature module is allowed to touch the data module. Presentational components in the ui module receive their input from the container components and have no direct access to the data module.
The model module contains the TypeScript interfaces for the domain models. These interfaces serve as the domain's ubiquitous language, so all other modules within the same domain are granted access to them.
For a deeper dive into architecture and NgRx, the author recommends this article.
The Facade leverages encapsulation to its advantage. Everything NgRx-related — reducers, selectors, and actions — stays tucked away inside the data module. What's visible to the outside world is just the Facade itself plus the provider function responsible for creating the NgRx feature state.
With both Nx and Sheriff, the public surface of a module is declared in its index.ts. For this example, that file might look like this:
import { provideState } from '@ngrx/store';
import { customersFeature } from '@app/customers/data/customers.reducer';
import { provideEffects } from '@ngrx/effects';
import { CustomersEffects } from '@app/customers/data/customers-effects.service';
export { CustomersFacade } from './customers-facade';
export const provideCustomers = [
provideState(customersFeature),
provideEffects(CustomersEffects),
];
2. Implementation
Consider a straightforward CRUD feature state responsible for managing customer data. This state includes actions for adding, modifying, listing, and deleting customers. Additionally, it provides two selectors: one that returns the complete list of customers, and another that retrieves a specific customer based on its identifier.
We begin with the state definition:
export interface State {
customers: Customer[];
}
Here are the NgRx actions:
import { createActionGroup, emptyProps, props } from '@ngrx/store';
import { Customer } from '@app/customers/model';
export const customersActions = createActionGroup({
source: 'Customers',
events: {
load: emptyProps(),
loaded: props<{ customers: Customer[] }>(),
add: props<{ customer: Customer }>(),
added: props<{ customer: Customer }>(),
update: props<{ customer: Customer }>(),
updated: props<{ customer: Customer }>(),
remove: props<{ id: number }>(),
removed: emptyProps(),
},
});
Finally, we have the two selectors. NgRx generates the first automatically through its createFeature function:
const selectAll = customersFeature.selectCustomers;
const selectById = (id: number) => createSelector(
selectAll,
(state: Customer[]) =>
state.find((p) => p.id === id)
);
export const fromCustomers = {selectAll, selectById};
In the absence of the Facade, our container components would communicate directly with the Store by dispatching actions and selecting state.
export class EditCustomerComponent implements OnInit {
#store = inject(Store);
protected customers$: Observable<Customer[]> | undefined;
ngOnInit() {
this.customers$ = this.#store
.select(fromCustomers.selectAll());
}
submit(customer: Customer) {
this.#store.dispatch(customersActions.update({ customer }));
}
}
Now, we introduce the Facade. It exposes methods for the actions we intend to make accessible to the outside world.
@Injectable({ providedIn: 'root' })
export class CustomersFacade {
#store = inject(Store);
update(customer: Customer) {
this.#store.dispatch(customersActions.update({ customer }));
}
}
For the selectors, we provide "read-only properties" implemented as getter functions.
@Injectable({ providedIn: 'root' })
export class CustomersFacade {
#store = inject(Store);
get customers$(): Observable<Customer[]> {
return this.#store.select(fromCustomers.selectAll);
}
update(customer: Customer) {
this.#store.dispatch(customersActions.update({ customer }));
}
}
It's also wise to limit the set of actions revealed to our container components. The updated action serves as a good example. This action is internal since only our NgRx Effect dispatches it. CustomersFacade doesn't include a method for it, making it inaccessible externally.
export class EditCustomerComponent implements OnInit {
#facade = inject(CustomersFacade);
protected customer$: Observable<Customer> | undefined;
ngOnInit() {
this.customer$ = this.#facade.customers$;
}
update(customer: Customer) {
this.#facade.update(customer);
}
}
The component is now unaware that NgRx operates behind the scenes. From the component's viewpoint, the CustomersFacade offers an Observable of Customer[]. When a modification is required, it's just a straightforward method invocation.
This principle extends to all other components and services that depend on NgRx. They need to be refactored to exclusively utilize the Facade.
In the following section, we'll explore the rationale behind this decoupling and the additional benefits it yields.
3. Advantages
3.1. Decoupling
Scattering NgRx throughout the entire codebase is something we want to avoid. This approach facilitates a simpler transition to a different state management library if needed.
It's worth noting that a number of Angular developers I greatly admire are not fond of the Facade pattern.
One common criticism is that swapping NgRx for another state management solution is an unlikely scenario once it's already in use.
Another point raised is that the Facade can invite anti-patterns. For instance, a method might both dispatch an action and query a selector in a single operation.
The topic of why one might migrate away from NgRx will be addressed in the next chapter.
Regarding the potential misuse of the Facade, this ultimately rests on the discipline and code review practices within the development team.
Any pattern can be over-engineered, but that shouldn't preclude its use.
3.1.1. Changed Requirements
Application requirements are prone to change. It's not unusual for some frontend logic to be transferred to the backend or to be phased out entirely. If the only remaining task is to fire a single HTTP request to an API, then it's sensible to streamline the code by removing NgRx for that particular feature.
With a Facade in place, NgRx can be replaced underneath with a BehaviorSubject. The components remain unchanged. As long as the Facade continues to expose the same Observables and the method signatures stay consistent, everything works seamlessly.
Furthermore, the Facade enables mixing straightforward HTTP requests with NgRx. Suppose there's data in our domain that doesn't require complex state management. The Facade could still expose it as a regular property of type Observable. Rather than invoking a selector, it would initiate an HTTP request.
In summary, the Facade grants us the adaptability to respond efficiently to evolving requirements.
3.1.2. Prepared for more
Teams that are new to Angular and dive straight into NgRx can sometimes feel swamped or even daunted by its complexity.
Should those teams choose to postpone NgRx adoption, there's a real risk they'll miss the opportune moment for integration. They could end up maintaining a home-grown state management solution. That becomes a burden — fixing bugs, maintaining, and extending it. It's a situation we'd rather avoid.
The Facade offers a solution here too. It initially operates with a BehaviorSubject internally. When the right moment arrives, swapping that BehaviorSubject for NgRx is all that's needed.
Once more, this modification is confined to the data module. Components and services remain untouched.
In this sense, the Facade acts as a form of insurance. The investment is certainly worthwhile.
3.1.3. Reversing Overengineering
The reverse situation can also occur. Teams with limited NgRx experience might lean towards overuse. The Facade offers assistance here in a manner similar to how it helps in the opposite scenario.
3.1.4. NgRx Component & Signal Store
In my view, once we commit to NgRx, switching to a different state management library is a rare occurrence.
However, NgRx has since introduced its own competitors.
Based on the official download figures, the NgRx Component Store is already at version 2. It can be viewed as a "lightweight Global Store".
This has led many developers to contemplate moving to the Component Store. A third option from NgRx is the Signal Store, designed with Signals at its core.
With these two alternatives within the NgRx ecosystem, the prospect of leaving the Global Store is no longer a distant possibility.
You can probably guess what comes next ;).
For these scenarios, the Facade is the universal solution as well. When you transition to the Signal Store, your properties will yield a Signal instead of an Observable. Since both the Global and Component Store already support signal-based selectors, the migration is relatively straightforward.
3.2. Internal Capabilities
A Facade isn't limited to concealing NgRx internals. It can also incorporate behavior that native NgRx alone simply cannot offer.
In the current setup, container components are responsible for invoking the load method on the CustomersFacade, which in turn dispatches an action to retrieve data from the server.
Should other components also rely on loaded customer data, each one would individually need to trigger load.
Thus, achieving a "lazy loading" behavior directly within NgRx is not feasible. Selectors lack the ability to dispatch actions. However, a Facade can easily handle this.
The Facade needs to remember whether the load action has already been dispatched. Since the properties of our CustomersFacade are implemented as getters, this check can be embedded directly within them:
export class CustomersFacade {
#isLoaded = false;
#store = inject(Store);
get customers$(): Observable<Customer[]> {
this.#assertLoaded();
return this.#store.select(fromCustomers.selectAll);
}
byId(id: number): Observable<Customer | undefined> {
this.#assertLoaded();
return this.#store.select(fromCustomers.selectById(id));
}
#assertLoaded() {
if (!this.#isLoaded) {
this.#store.dispatch(customersActions.load());
this.#isLoaded = true;
}
}
}
With this adjustment, no component needs to invoke load any longer. The Facade could even expose it as a private method.
Another compelling possibility lies in enhancing the functionality of selectors.
NgRx's select method on the Store delivers an Observable that emits its first value synchronously. There is no way to apply pipe operators directly to the selector itself (i.e., what createSelector produces).
Nevertheless, we may want to clone the state to guard against external mutations. We might also want to discard any undefined values from the stream.
Without a Facade, each component is left to handle the presence of undefined and to enforce immutability on its own. This is because the components are the ones calling store.select() and receiving the resulting Observable.
The Facade steps in here as well. Since it provides an Observable, it can seamlessly attach its own pipe operators.
Suppose we want byId to return an Observable<Customer> instead of Observable<Customer | undefined>. We would also like the value to be cloned, as we are working with a template-driven form that performs mutable updates.
The CustomersFacade could be enhanced in this manner:
@Injectable({ providedIn: 'root' })
export class CustomersFacade {
#isLoaded = false;
#store = inject(Store);
byId(id: number): Observable<Customer> {
this.#assertLoaded();
return this.#store
.select(fromCustomers.selectById(id))
.pipe(filterDefined, deepClone);
}
}
The operators filterDefined and deepClone are custom implementations.
3.3. Simplifying Component Tests
Testing a component that relies on NgRx usually involves practical utilities. For instance, provideMockStore sets up a fully functional, albeit stubbed, NgRx feature state.
A typical test for a component rendering all customers might appear as follows:
it('should show customers', () => {
const fixture = TestBed.configureTestingModule({
imports: [CustomersContainerComponent],
providers: [
provideRouter([]),
provideMockStore({
initialState: {
customers: {
customers: [
{
firstname: 'Sabine',
name: 'Miscovics',
country: 'AT',
birthday: '1993-05-09',
},
],
},
currentPage: 1,
pageCount: 1,
},
}),
],
}).createComponent(CustomersContainerComponent);
fixture.detectChanges();
expect(
document
.querySelector('[data-testid=row-customer] p.name')
?.innerHTML.trim()
).toBe('Sabine Miscovics');
});
So what's the limitation of provideMockStore?
To write such a test, you must be aware of the internal structure of the feature state. However, one of the core purposes of selectors is to abstract away that structure from components. With this approach, the details resurface in the test. You also need to know the feature's key, which is why the property customers is repeated twice.
In these types of tests, the goal is solely to validate the component's behavior, not to interact with NgRx mechanics.
Once more, the Facade comes in handy. To mock NgRx behavior, only a straightforward service mock is necessary. This can be further streamlined with any of the popular mocking utilities (testing-library, ng-mocks, [jest|jasmine]-auto-spies, ts-mockito):
it('should show customers', () => {
const facadeMock: Partial<CustomersFacade> = {
get customers$(): Observable<Customer[]> {
return of([
{
id: 1,
firstname: 'Sabine',
name: 'Miscovics',
country: 'AT',
birthdate: '1993-05-09',
},
]);
},
};
const fixture = TestBed.configureTestingModule({
imports: [CustomersContainerComponent],
providers: [
provideRouter([]),
{ provide: CustomersFacade, useValue: facadeMock },
],
}).createComponent(CustomersContainerComponent);
fixture.detectChanges();
expect(
document
.querySelector('[data-testid=row-customer] p.name')
?.innerHTML.trim()
).toBe('Sabine Miscovic');
});
Here, facadeMock serves as a stand-in for the CustomersFacade. It simply supplies an Observable<Customer>, which is all that the test requires.
There's no longer any need to understand the feature state's shape, the feature key, or even the fact that NgRx is being used.
4. Wrap-Up
The Facade is a straightforward pattern to adopt, yet its impact is substantial. It simplifies interactions by turning action dispatches into method calls and selectors into observable properties.
The primary benefit is the decoupling of NgRx from the rest of the codebase. This separation greatly minimizes the workload if migration away from NgRx becomes necessary.
Furthermore, the Facade allows for the incorporation of additional logic or features that plain NgRx does not support.
This includes attaching pipe operators to selectors or setting up an on-demand data loading mechanism for entities.
Finally, testing becomes more straightforward. There's no need to master NgRx-specific testing utilities for mocking feature states. Mocking the Facade is just as simple as mocking any other service.
While the Facade might initially seem like an over-engineered solution, it's not. It delivers significant advantages for very little overhead. It's certainly worth adopting!
An accompanying GitHub repository showcases all variants in Angular 16.
rainerhahnekamp
/
ngrx-facades
A repository illustrating the benefits of the facade pattern in NgRx
For those keen on delving deeper into NgRx, consider joining one of our upcoming Professional NgRx workshops:
🇺🇸 English Workshops:
🇩🇪 German Workshops:
What are your thoughts? Do you see the Facade bringing value to your projects? Maybe you're already using it and have found additional use cases?
Share your insights in the comments!



