Creating software is not a one-time process. Over time, and as technology evolves, business requirements also change. Our main goal should be to prepare the application in such a way that we can easily and at a low cost respond to new demands. In this context, code organization becomes a crucial design element that is often forgotten, leading to numerous problems and extending the time required for modifications.
One solution to these problems is the Facade Pattern.
Case Study - The "User List" Component
Before we start, I'd like to emphasize something critical. Regardless of the framework and tools being used the frontend should be as simple as possible. We don't implement core business logic here, our primary goal is to provide a user friendly tool to display the required data and an interface that allows users to trigger business actions.
To illustrate this approach, let's consider a component that displays a list of users. We base it on a data source from which we fetch our users. Unfortunately, the user of our application might not be familiar with the data format returned by our data source, so we want to present this data in a readable format (f.ex by tables).
On the other hand, we also want to allow the user to interact with this data. User should have access to block, delete, or modify user or to filter users based on certain criteria. For this purpose, we will use our data source to make these actions.
For this article, I have prepared a simple component that displays a list of users fetched from an API. In this component, we can also interact with the data through actions like change the displayed page, preview and delete data.
// 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 look, this component could be used in a production application, as all the required functionalities are implemented, so we can deploy the code and close the task, right?
Well, not quite...
Let's look at the problems with this component. First and foremost, although it may sound strange, this component does too much! Our task is to display data, and the component does that, but it also handles API connections, manages pagination, and handles user actions. This is not a huge component, but further development of it and the implementation of new functionalities will make those problems bigger. We need to implement user preview, and we'll have to decide whether that will be a new page, a popup, or something else. The same about deleting a user—we would need to display a confirmation popup, then, after acceptance, send a request to the API and refresh the list. There's a lot of code ahead of us and many dependencies. The second problem is related to testing - writing unit tests for this kind of component can be a nightmare of mocking every possible element, from API requests to popups, redirects, etc. Changing our data source or its format might require rewriting all logic to accommodate it.
Facade as a solution
Quoting someone's smarter than me, here is an explanation of the Facade pattern from refactoring.guru:
A facade is a class that provides a simple interface to a complex subsystem that contains lots of moving parts. A facade might provide limited functionality in comparison 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 short, a facade is a pattern that isolates certain shared elements in one place and provides an interface to interact with them. And here I'd like to reiterate my words from the beginning of the previous paragraph, the frontend should be as simple as possible. In this case, we'll aim to make our component "dumb." Let's try to make it responsible only for displaying data.
Angular Components stand out among other framework elements (Directive/Pipe/Service) because they have a template, the primary task of a component is to display data, so Component doesn't need to know how to communicate with the backend, where the data comes from or how actions are performed.
Our main task during the implementation of the facade is to split our component into two parts. First one where we'll keep the data related to the logic, and the other where we'll use that data and trigger actions.
In our example, our facade will be responsible for:
- Executing API requests for data;
- Managing state—which page we're currently displaying and how much data we want to display;
- Providing functions that allow for state modification (nextPage, prevPage) and data interactions (view, delete);
- Storing additional dependencies such as a possible popup/router—logic not directly related to displaying elements, so that our view (component) has minimal dependencies;
Our view will be responsible for:
- Displaying data;
- Triggering functions;
Let's modify our example code. The simplest way is to create a new service that we'll inject into our 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. As mentioned earlier, it's a simple Angular Service to which we've moved the "more important logic" from the component. We have applied some changes to it. In point #1, we set the main data as private values - I want to be sure that functions nextPage and prevPage (point #3) are the only ways to update information from outside this service. In point #2, I've added one value, a signal with API data and information about which page is currently displayed - this is the data we will make available to our component. Additionally, it's in this facade that I want to have the ability to initiate viewing and deleting a user, which I plan to implement "soon." As you can see, these functions return type are marked as void
.
Why?
Because, by design, UserListComponent
only initiates an action, and how that action is performed should not concern this component, that's what the facade takes care of. And what about informing about the success of the operation or errors? What if we want to implement a "loader" to inform the user that an action is in progress? All of this information should be in our facade, and it should provide the state, which we use in our view to display the necessary elements based on the facade's state.
After isolating the logic, it's time to connect our component with 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();
}
}
In point #1, we inject the facade into the component, in point #2, we refer to the data from the facade and use it in the view. Additionally, every action (functions in point #3) to update this data is called from facade. Looking at this component, we cannot know where the data comes from, what happens when we trigger actions (like how user deletion works). Should this component know that kind of information? No, and in this case, that is a good thing.
Unit Testing
Unit testing is one of the most important things to focus on when creating software. Automatic verification of whether our code works properly and whether our later changes do not break the current logic is an underrated tool, even in frontend development, where we have to rely on "physical" interaction with our application.
The facade pattern and splitting our code into a view and logic, even though we have to maintain two separate code places, simplifies testing. Why? Because we don't focus on the component as a whole (where we would have to test both the component's state and interactions with it) but on its individual elements - the view and the business logic.
Tests for components using the Facade should focus on:
- Testing the business logic, where performing functions from the facade should update its state, and how this state should be verified in the tests;
- Testing the view, which is how our component view looks depending on how the facade's state is presented;
At first looks, there is no differences from those we would face in components that do not implement the facade, but the more dependencies our component has and the more complex the logic we have to implement, the more we appreciate the simplicity we can leverage.
Here's what tests for our component could 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();
})
At the very beginning (#1), I create an object that imitates the facade, this is the most important object for my view, so I need to have control over it in the tests. Note that I didn't implement the actual methods from the facade here, I use createSpy
method from jasmine." In the component, I don't care what these functions are doing. As the initiator of these actions, my question is: are they triggered from the component level (after a button click in points #5 and #6). In point #2, I override the facade in the component with the one I created for the tests. In point #3, I verify whether my component is created.
Point #4 deserves a separate explanation. Here, we verify whether the number of rows in our view matches the number of data entries in our facade. Initially, it's 3, then (by updating the signal) I add 3 more elements, and finally, I check if no rows are displayed when the data is empty. This is the essence. In a very simple way, by updating the data in my mock, I can check if my component behaves correctly. My production facade requires an API request and a change in the page count signal to update this data. If I hadn't split the component and facade, I would have had to mock the ApiDataService or manually set the data by referring to the instance of the component in the test. And Yes, it's ok, but it doesn't make sense because after all, the end user only changes this data by clicking buttons!
Conclusion
Should I always use this pattern from now on? Not really. In what cases should I implement it? It depends.
As an example of implementation, I used a popular functionality - pagination, making API requests, and displaying elements. This is a task that most of us have likely encountered, and I aimed to show how to prepare a component properly and one of many methods for isolating certain logic into a facade.
Using this pattern in this case could be like using a sledgehammer to crack a nut. One of the key risks when implementing the Facade Pattern is inadvertently creating a "God Object". This happens when a single facade becomes responsible for too many tasks, turning into a large, unwieldy structure that tries to manage all business logic, state, and dependencies.
When creating a facade, you should:
-
Ensure that your facade doesn’t take on too many tasks. The primary purpose is to create a simple model dedicated to a specific functionality that helps in management rather than replacing the implementation of business logic. Facade should focus on a single area, providing a clear and minimal interface. By doing this, you not only improve maintainability but also make your facades easier to test and reuse in other parts of your application.
-
Minimize the facade’s interface. Expose only the methods/values required by external application components. One of the reasons for implementing a facade is to hide technical aspects of the code and dependencies. Methods in the facade should describe what they do rather than how they do it.
Good implementation of this pattern makes our code more flexible! A once-defined facade can be incredibly helpful when migrating solutions used in a project. Let’s assume you're creating a proof of concept of a new feature that fetches and performs certain backend requests, which is still under development. By using a facade, we can implement the functionality using sample data (even hardcoded ones) and continue our work without being blocked by missing backend. Afterward, we only need to replace the right methods in the facade to make API requests instead of returning sample data. The same solution applies if we want to change the technology. For example if our component uses data from a Global Store @ngrx/store
, and for some reason, we decide to drop it or we plan to replace API requests from REST to GraphQL we just need to focus development on the facade and make changes there. We know exactly what data our facade must provide to other components, what methods these components call, and what they are supposed to execute. We can modify the technology as needed while maintaining the expected results.
As always, if you're interested in the complete code for the component, I encourage you to play around with my example on StackBlitz.
