DEV Community

Cover image for A better architecture for your Angular projects
Jérôme Navez
Jérôme Navez

Posted on • Edited on

A better architecture for your Angular projects

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:

Why focus on the frontend architecture?

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.

Javascript Frameworks

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 concepts

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:

  • State management to enhance the single source of truth;
  • Asynchronous code to not block your UI;
  • Combining UX modularity and Code modularity.

Backend vs Frontend architecture

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.

Refactoring frontend code is way harder

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.

Base principles

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:

  • The business logic tends to be split across multiple components;
  • There is a risk of data being contained in multiple places, increasing the chance to use outdated or unsynchronized data.

Given those objectives and issues, the following architecture put its prioritizes on:

  • Stateless components;
  • Single source of truth;
  • Separation of concern;

The SCA architecture

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:

SCA architecture

The architecture is composed of 4 big parts:

  • Components
  • Domain
  • Store
  • Infrastructure (most of the time, the clients)

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.

Domain

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))
    );


Enter fullscreen mode Exit fullscreen mode

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

Components are there for two reasons:

  • Display information in a beautiful way;
  • Catch the user inputs.

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.

Clients & infrastructure

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);
    }
}


Enter fullscreen mode Exit fullscreen mode

Store

The store is composed of:

  • The state of the application;
  • Some reducer methods that can modify the state;
  • Some selectors, being Observables that emit the modifications of the state.

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 focus on the store

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.

The issue with NgRx

Here is the state management proposed by NgRx:

NgRx State Management

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.

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()));
  }
}


Enter fullscreen mode Exit fullscreen mode

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])
  }
}


Enter fullscreen mode Exit fullscreen mode

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.

Proof of Concept Project

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!

Task Manager Playground

Next step

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!

Top comments (0)