Software development rarely follows a straight line. As time passes and technology shifts, business needs evolve too. Our job is to design applications that can adapt to new requirements quickly and without excessive cost. Often overlooked, code architecture plays a decisive role in this process, and neglecting it can lead to significant friction and slow down future updates.
AFacade Pattern can help address these challenges.
A Practical Example: The "User List" Component
Before diving in, it's important to state a fundamental principle. No matter the framework or library in use, the frontend should stay as simple as possible. We don't execute core business logic here; our goal is to offer a user-friendly interface for viewing data and triggering business operations.
Let's imagine a component designed to show a list of users fetched from a remote source. The data format from the API might not be intuitive for our users, so we want to present it in a clear, structured way, perhaps using a table.
Additionally, users need to interact with this list. They might want to block, delete, or edit a user, or apply filters to find specific records. These actions will be performed through the same data source that provides the list.
For this discussion, I've created a sample component that loads a list of users from an API. It supports basic interactions, such as navigating between pages, viewing a user's details, and removing a user.
// service to handle api requests
import { HttpClient } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
export type User = {
id: number;
firstName: string;
lastName: string;
gender: 'male' | 'female';
email: string;
phone: string;
};
@Injectable({
providedIn: 'root',
})
export class ApiDataService {
private httpClient = inject(HttpClient);
getUsers(page = 1, perPage = 20) {
return this.httpClient.get<{ users: User[] }>(
`https://dummyjson.com/users?limit=${perPage}&skip=${
perPage * (page - 1)
}`
);
}
}
// user-list.component.ts
import { Component, signal, inject } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { combineLatest, switchMap } from 'rxjs';
import { ApiDataService, User } from '../api-data.service';
@Component({
selector: 'app-user-list',
template: `
<table>
<thead>
<tr>
<th>#ID</th>
<th>Firstname</th>
<th>Lastname</th>
<th>Gender</th>
<th>Email</th>
<th></th>
</tr>
</thead>
<tbody>
@for (user of users()?.users; track user.id) {
<tr>
<td> {{user.id}}</td>
<td> {{user.firstName}}</td>
<td> {{user.lastName}}</td>
<td> {{user.gender == 'male' ? '♂️' : '♀️'}}</td>
<td> {{user.email}}</td>
<td>
<div class="grid-actions">
<button (click)="showUser(user)">🔍</button>
<button (click)="removeUser(user)">❌</button>
</div>
</td>
</tr>
}
</tbody>
</table>
<div class="flex flex-row w-full gap-2">
<button (click)="prevPage()">⏮️</button>
<div class="page-info">{{currentPage()}}</div>
<button (click)="nextPage()">⏭️</button>
</div>
`,
styles: `/** ... **/`,
})
export class UserListComponent {
private apiData = inject(ApiDataService);
currentPage = signal(1);
perPage = signal(10);
users = rxResource({
request: () => ({
page: this.currentPage(),
perPage: this.perPage(),
}),
loader: ({ request }) => {
const { page, perPage } = request;
return this.apiData.getUsers(page, perPage);
},
});
// actions
showUser(user: User) {
// ...
}
removeUser(user: User) {
if (confirm(`This operation will remove user #${user.id}. Are you sure?`)) {
// ...
}
}
nextPage() {
this.currentPage.update((i) => i + 1);
}
prevPage() {
this.currentPage.update((i) => {
return i - 1 < 1 ? 1 : i - 1;
});
}
}
At first glance, this component seems ready for production. It has all the necessary features, so we could deploy it and mark the task as complete, right?
Not exactly...
The issue is that this component takes on too many responsibilities. While its primary job is to display data, it also handles API communication, manages pagination state, and processes user actions. Currently, it's manageable in size, but as we add more features, these responsibilities will become a burden. For example, if we need to add a user preview feature, we must decide if it's a new page, a modal, etc. Similarly, deleting a user would require a confirmation dialog, an API call, and a list refresh. This leads to a lot of code and increased complexity. Another major problem is testing. Writing unit tests for such a component is difficult, requiring mocks for API calls, dialogs, navigation, and more. Also, if we decide to change our data source or its format, we'll likely have to rewrite significant parts of this logic.
How a Facade Helps
To borrow a definition from refactoring.guru:
A facade is a class that provides a simple interface to a complex subsystem which contains lots of moving parts. A facade might provide limited functionality compared to working with the subsystem directly. However, it includes only those features that clients really care about.
Having a facade is handy when you need to integrate your app with a sophisticated library that has dozens of features, but you just need a tiny bit of its functionality.
In essence, a facade isolates shared, complex logic into a single class and offers a simplified interface for interacting with it. This aligns with our goal of keeping the frontend simple. In this case, we want to make our component "dumb," responsible only for rendering data.
Angular Components are distinct from other constructs like Directives or Pipes because they have a template. A component's primary role is to present data, so it shouldn't need to know how to call the backend, where the data originates, or how specific actions are implemented.
When implementing a facade, our main objective is to divide the component into two parts: one that holds the data and related logic, and another that consumes this data by calling actions.
In our example, the facade will be responsible for:
- Handling API calls to fetch data;
- Managing state—tracking the current page number and page size;
- Exposing methods to modify the state (like
nextPageandprevPage) and to perform data actions (likeviewanddelete); - Storing any extra dependencies, such as a router or popup service, so the view component has a lighter dependency list;
Our view component will be responsible for:
- Rendering the data;
- Calling the methods provided by the facade;
Let's refactor our example. The simplest approach is to create a service that we inject into the component.
import { inject, Injectable, signal, computed } from '@angular/core';
import { combineLatest, switchMap } from 'rxjs';
import { rxResource } from '@angular/core/rxjs-interop';
import { ApiDataService, User } from '../api-data.service';
@Injectable()
export class UserListFacade {
// #1
private apiData = inject(ApiDataService);
private currentPage = signal(1);
private perPage = signal(10);
private userQuery = rxResource({
request: () => ({
page: this.currentPage(),
perPage: this.perPage(),
}),
loader: ({ request }) => {
const { page, perPage } = request;
return this.apiData.getUsers(page, perPage);
},
});
// #2
paginationData = computed(() => {
return {
page: this.currentPage(),
data: this.userQuery.value()?.users,
};
});
// #3
nextPage(): void {
this.currentPage.update((i) => i + 1);
}
prevPage(): void {
this.currentPage.update((i) => {
return i - 1 < 1 ? 1 : i - 1;
});
}
displayUserDetails(user: User): void {
// ... redirect OR display modal with user data
}
deleteUser(user: User): void {
if (confirm(`This operation will remove user #${user.id}. Are you sure?`)) {
// ... make request to delete user
}
}
}
This is our facade. It's essentially an Angular Service that now contains the more critical logic from the component. We've made a few key changes here. In point #1, we've made the primary data private, ensuring the nextPage and prevPage methods (point #3) are the only means to update this state externally. In point #2, we've added a single signal that holds the API data along with the current page info. This is the state we'll expose to the component. We've also included methods to initiate viewing and deleting a user, which we plan to implement. As you can see, these functions have a return type of void.
Why is that?
Because, by design, the UserListComponent merely triggers an action. How that action is processed is not the component's concern; that responsibility lies with the facade. What about notifying the user of success or failure, or showing a loading indicator? All of that state should be managed by the facade, which can then expose it, allowing the view to display the appropriate UI elements.
Now, let's connect our component to the facade:
// user-list.facade.ts
@Component({
selector: 'app-user-list',
template: `
<table>
<thead>
<tr>
<th>#ID</th>
<th>Firstname</th>
<th>Lastname</th>
<th>Gender</th>
<th>Email</th>
<th></th>
</tr>
</thead>
<tbody>
@for (user of paginationData().data; track user.id) {
<tr>
<td> {{user.id}}</td>
<td> {{user.firstName}}</td>
<td> {{user.lastName}}</td>
<td> {{user.gender == 'male' ? '♂️' : '♀️'}}</td>
<td> {{user.email}}</td>
<td>
<div class="grid-actions">
<button (click)="showUser(user)">🔍</button>
<button (click)="removeUser(user)">❌</button>
</div>
</td>
</tr>
}
</tbody>
</table>
<div class="flex flex-row w-full gap-2">
<button class="prev-page" (click)="prevPage()">⏮️</button>
<div class="page-info">{{paginationData().page}}</div>
<button class="next-page" (click)="nextPage()">⏭️</button>
</div>
`,
styles: `/** ... **/`,
+ providers: [UserListFacade], // #1
})
export class UserListComponent {
+ private userListFacade = inject(UserListFacade);
- private apiData = inject(ApiDataService);
- private currentPage = signal(1);
- private perPage = signal(10);
-
- users = rxResource({
- request: () => ({
- page: this.currentPage(),
- perPage: this.perPage(),
- }),
- loader: ({ request }) => {
- const { page, perPage } = request;
- return this.apiData.getUsers(page, perPage);
- },
- });
-
- paginationData = computed(() => {
- return {
- page: this.currentPage(),
- data: this.users()?.users,
- };
- });
+ paginationData = this.userListFacade.paginationData; // #2
// #3
// actions
showUser(user: User) {
this.userListFacade.displayUserDetails(user);
}
removeUser(user: User) {
this.userListFacade.deleteUser(user);
}
nextPage() {
- this.currentPage.update((i) => i + 1);
+ this.userListFacade.nextPage();
}
prevPage() {
- this.currentPage.update((i) => {
- return i - 1 < 1 ? 1 : i - 1;
- });
+ this.userListFacade.prevPage();
}
}
Here, in point #1, we inject the facade into our component. In point #2, we access the data from the facade and use it in our template. All actions (the functions in point #3) are called from the facade. Looking at this component now, we have no idea where the data comes from or what happens when we call an action. Is that knowledge necessary for the component? No, and in this case, that's a good thing.
Unit Testing
Unit testing is a crucial part of software development. Automatically verifying that our code works and that changes don't introduce regressions is an often-underestimated practice in frontend development, which frequently relies on manual, visual checks.
The facade pattern, by separating the view from the logic, simplifies testing. Since we now have two distinct pieces, even though we maintain two files, we can test them in isolation. We no longer need to test the component's interaction with the backend and its state management all at once, but rather focus on the view and the logic separately.
Tests for a component using a facade should focus on:
- Testing the business logic—verifying that calling a facade method updates its state as expected;
- Testing the view—checking how the component's template renders based on a given facade state;
At first, these tests may seem similar to those for a non-facade component. However, the benefits become clear as the component grows in complexity and dependencies. With a facade, testing is more straightforward. Here’s an example of what our tests might look like:
// #1
const simpleFacade = {
paginationData: signal({
page: 1,
data: [generateSimpleUser(), generateSimpleUser(), generateSimpleUser()]
}),
nextPage: jasmine.createSpy('nextPageFn'),
prevPage: jasmine.createSpy('prevPageFn'),
displayUserDetails: jasmine.createSpy('displayUserDetailsFn'),
deleteUser: jasmine.createSpy('deleteUserFn'),
}
describe('UserListComponent', () => {
let component: UserListComponent;
let fixture: ComponentFixture<UserListComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UserListComponent]
})
// #2
.overrideComponent(UserListComponent, {
set: {
providers: [
{
provide: UserListFacade,
useValue: simpleFacade
}
],
}
})
.compileComponents();
fixture = TestBed.createComponent(UserListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
// #3
it('should be created', () => {
expect(component).toBeTruthy();
});
// #4
it('should display table with rows depends on `vm().data properties', () => {
let tablesRow = fixture.nativeElement.querySelectorAll('table tbody tr');
expect(tablesRow.length).toEqual(3);
simpleFacade.paginationData.update((paginationData) => {
return {
...paginationData,
data: [
...paginationData.data,
generateSimpleUser(),
generateSimpleUser(),
generateSimpleUser(),
]
}
});
fixture.detectChanges();
tablesRow = fixture.nativeElement.querySelectorAll('table tbody tr');
expect(tablesRow.length).toEqual(6);
simpleFacade.paginationData.update((paginationData) => {
return {
...paginationData,
data: []
}
});
fixture.detectChanges();
tablesRow = fixture.nativeElement.querySelectorAll('table tbody tr');
expect(tablesRow.length).toEqual(0);
});
// #5
it('should call facade `nextPage` when user click on `nextPage` button', () => {
const nextPageButton = fixture.debugElement.query(By.css('button.nextPageBtn'));
nextPageButton.nativeElement.click();
expect(simpleFacade.nextPage).toHaveBeenCalled();
})
// #6
it('should call facade `prevButton` when user click on `prevButton` button', () => {
const prevPageButton = fixture.debugElement.query(By.css('button.prevPageBtn'));
prevPageButton.nativeElement.click();
expect(simpleFacade.prevPage).toHaveBeenCalled();
})
In the beginning (#1), I create a mock object that mimics the facade. This is the most critical object for my view tests, so I need to control it fully. Note that I don't implement the actual methods; I use the createSpy function from Jasmine. In the component tests, I'm not interested in what these methods do, only that they are called when expected (e.g., after a button click in points #5 and #6). In point #2, I provide this mock to the component. In point #3, I check that the component is created successfully.
Point #4 is worth explaining further. Here, I verify that the number of rows rendered matches the number of data items in the mock facade. Initially, there are 3 items. I then update the signal to have 6 items, and finally, I set it to an empty array to confirm no rows are shown. This is where the pattern shines. By simply updating the mock's data, I can easily test my component's reactivity. In the real facade, this would require an API call and updating the page signal. Without this separation, I'd need to mock the API service or manually set the component's properties. It's doable, but it's not ideal because, from the user's perspective, data changes only happen through button clicks!
Conclusion
Should I use this pattern everywhere from now on? Not necessarily. When is it appropriate to implement? It depends.
The example I used—pagination, API calls, and item display—is a common feature that many of us have built. My goal was to show a proper component structure and a method for extracting logic into a facade. It's a very common task, and this approach can be overkill for some simple cases.
However, one of the main risks of the Facade Pattern is accidentally creating a "God Object". This happens when a facet takes on too many responsibilities and becomes a massive, overly complex structure attempting to manage all business logic, state, and dependencies. When defining a facade, you should:
-
Make sure your facade doesn’t take on too many tasks. Its role is to provide a simple interface for a specific feature, not to replace your business logic layer. A facet should focus on one domain area in particular, offering a clear and minimal API. This will improve code maintainability and make the facade reusable in other parts of the application.
-
Keep the facade’s interface minimal. Only expose the methods and values that are needed by the rest of the application. A key goal of a facade is to hide the technical implementation details. The methods should describe what they do, not how they achieve it.
A well-implemented facade makes our code much more flexible! A defined facade is a huge asset when modernizing a codebase. Suppose you are building a proof of concept for a new feature that interacts with a backend which isn't ready yet. By using a facade, you can implement the feature with sample data and keep working without being blocked. Later, you just swap the method bodies to make the actual API calls. This also works when migrating technologies. If your component gets its data from a global store like @ngrx/store and you decide to remove it, or if you are switching from REST to GraphQL, all the changes can be contained within the facade. You know the contract it must provide to other parts of the app, and the expected behavior, so you can safely change the underlying technology and still meet the required outcomes.
If you'd like to explore the full code example, you can check out my project on StackBlitz.

