Conceptually, this architecture could be applied to any frontend framework. Since I have more knowledge of Angular than the other frontend frameworks, I will write Angular examples when necessary.
If you wish to directly jump to the architecture, click or tap here.
Before rushing into the technical parts, let’s ask ourselves:
While putting in place a backend architecture is common sense, it may be harder to have a clean architecture on the frontend side.
For multiple reasons.
Historically, new Javascript framework were popping every day. Being able to master a framework takes time and effort. You need a strong experience to know every capability of a framework and figure out what to do when facing a programmatic challenge.
Currently, the frontend frameworks seem to be dominated by React, Angular and Vue for some years know. This brings stability. Some pattern and architectural concept are shared among them.
Frontend uses a lot of concepts that you need to know in order to code your product. Concepts that you don’t meet when working on backend.
Some of the challenges and concepts a frontend developer meet every day:
This reason is very influenced by my own perception of IT architecture and what I see in some projects around me.
Less effort is put into the frontend architectures while more effort is put in the backend architectures.
Some would say that business logic and security are sensitive backend topics. Those two things must be rock solid. While the frontend is just there to “display things from the backend”.
Indeed, the frontend is nothing without a strong backend. But the reverse is also valuable. Having a strong backend, you don’t want a weak frontend full of bugs and hard to maintain to communicate with your backend.
Disclaimer: I mainly use IntelliJ IDEA.
Modern IDE has strong refactoring capabilities for Java projects. Renaming filenames and moving classes from package to package is easy.
I cannot tell the same for Angular projects. Imports unchanged, NgModule
not updated, etc. I always have the feeling that some “smart links” are missing between files.
By highlighting the separation of concern with a good architecture, the number of those refactoring will decrease and be easier to process.
You may or may not agree with all of those reasons, but you certainly agree on the fact that the frontend architecture deserves time and effort to be well organized.
The main goal of an architecture is to reduce the coupling between the elements that compose the software. Separation of concern, modularity, abstraction, etc are the main ideas to achieve that.
Unfortunately, the frontend frameworks suffer from two specific issues that need to be tackled to reach this goal:
Given those objectives and issues, the following architecture put its prioritizes on:
We need a catchy name. Let’s call this architecture the SCA architecture: the Simple and Clean Angular architecture.
Let’s dive into the graph:
The architecture is composed of 4 big parts:
Depending on your project and uses cases, you may also use the local storage or the session storage. Those belong to the infrastructure.
The store could also be categorized as an infrastructure layer. But since it deserves special attention, let’s keep it as a separate part.
This is the center of our application. It’s composed of services. It can be one simple service or multiple separated services. The domain layer should represent the use-cases, so you need to organize them as logically as possible.
The domain exposes methods that are actions available for the components. Those actions execute business logic, use the infrastructure, and/or modify the state of the store.
The domain also exposes selectors that are Observables being selections of the store. RxJs
operators can be applied to this selection, but the final result must always be based on the store value only. They emit each time the selection is modified. Those exposed selectors have a business meaning.
Selector example:
userAge$ = this.store.select(state => state.userDetails.birthdayDate)
.pipe(
map(birthdayDate => getAge(birthdayDate))
);
The domain should be independent of the framework used as much as possible. We should be able to migrate the domain logic from the angular project to any other TypeScript project with very few efforts. Of course, there will always be the @Injectable
and @NgModule
decorators, but you get the idea.
Components are there for two reasons:
The user inputs are interpreted and trigger a dedicated action from the domain layer. If required, additional data can be sent in argument of this action.
The information displayed comes from the selectors exposed. The Observables are used in the template and no business logic should be applied to them.
Those two only responsibilities make the components stateless, no logic is executed from information handled by the component.
The infrastructure layers are dependencies needed by the domain layer to perform its actions. It can be web clients, communications with Local storage, or any other dependency that can be abstracted from the domain layer.
The infrastructure layer contains services with very simple code that is only related to the usage of the dependency. Optionally, it can contain mappers if needed.
Client service example:
@Injectable()
export class ItemClient {
constructor(private httpClient: HttpClient){}
postItem(item: Item): Observable<Item> {
return this.httpClient.post<Item>(API_PATH, item);
}
}
The store is composed of:
It should only contain store-related logic. No business logic and no call to another infrastructure layer.
The store can be implemented using any technology. You can either use a library like NgRx or NGXS. Another solution is to create your own store using a BehaviorSubject
.
Let’s come back to the store and how to implement it. When choosing the store you will use, you need to select the one that respects your architecture. You need to make sure that this store is hidden behind an abstraction to not pollute your domain layer with store-related code.
NgRx could be your choice, but it’s maybe not the right one.
Here is the state management proposed by NgRx:
NgRx comes with a concept that breaks our architectural rules: the effects. Using the concept of effects, you execute business logic in the store layer and directly contact the client layer. You end up with logic being executed in two places: the domain layer and the store layer.
NgRx is great, but it comes with its proper architecture which is not compatible with our SCA architecture. Except if you remove the use of effects.
Another solution could be to create a simple custom store.
State, actions, selectors and reducers, those 4 store concepts can be built around a BehaviorSubject
. This kind of observable is a great help to play with data.
Here is an example of a custom state:
import {BehaviorSubject, distinctUntilChanged, map, Observable} from "rxjs";
export class Store<T> {
private state$: BehaviorSubject<T>;
constructor(initialState: T) {
this.state$ = new BehaviorSubject<T>(initialState);
}
/**
* This method provides an observable representing a part of the state.
* @param selectFn defines a method that select a subpart U the state T
*/
public select<U>(selectFn: (state: T) => U): Observable<U> {
return this.state$.asObservable().pipe(map(selectFn), distinctUntilChanged());
}
/**
* This method is used to update the state
* @param reduceFn defines a method that transform the state to another state
*/
public update(reduceFn: (state: T) => T): void {
this.state$.next(reduceFn(this.state$.getValue()));
}
}
The select
method will be used by the selectors. The update
method will reduce the state according to the action.
But it’s not enough, this store needs to be instantiated and used by a service:
@Injectable()
export class ItemStore {
store = new Store<Item[]>([]);
items$ = this.store.select(items => items);
item$ = (itemId: number) =>
this.store.select(items => items.find(item => item.id === itemId) as Item);
initialize(initialItems: Item[]): void {
this.store.update(_ => initialItems);
}
remove(id: number): void {
this.store.update(state => state.filter(item => item.id !== id));
}
add(item: Item): void {
this.store.update(state => [...state, item])
}
}
The service exposes selectors and actions that have a meaning for the domain layer. It is then ready to be used by the domain layer.
I made a little playground application In Angular and Spring Boot. The frontend part uses the SCA architecture as described above. Take a look to see a concrete example!
If we take a look at the graph of the SCA architecture, we notice that the dependencies lead from the components to the infrastructure layers. Meaning that it’s possible to inject a client into a component.
Of course, this is wrong, but there is a solution to avoid that: The inversion of dependency. Those of you who follow my blog know that I have made a little POC of how to apply the Onion Architecture on a SpringBoot project. The next step is to apply the same principles to the SCA architecture.
So hold on and prepare yourself for the next article: The SCAO architecture: the Simple and Clean Angular Onion architecture.
Happy coding!