Clean Architecture in Frontend Applications with Angular.

If you think good architecture is expensive, try bad architecture. - Brian Foote

Clean Architecture in Frontend Applications with Angular.

Photo by Alex wong on Unsplash

Today companies require developers to be proficient in Clean Architecture, Domain Driven Design, TDD, BDD, and other principles. Obviously these corporations still need to evolve their software architecture and keep learning how to create systems that do not self-destruct (sabotage) over the months. But many points are still always associated with the backend, because it is the heart of the software. But we cannot ignore that we have more and more complexity in the frontend, this includes mobile applications as well. We know that business rules should not live on the client side. But depending on the business model your frontend application may involve a lot of complexity.

That's why we should worry about having a clean architecture, when planning the design of components, communications between them and other aspects in the frontend. Let's understand some important concepts of clean architecture and then how we can apply it on the web using Angular.

Clean Architecture

Clean Architecture is a standard for structuring an application in general. It was originally introduced by Robert C. Martin, also known as Uncle Bob, who also published many articles and books about writing clean and reusable code. In the book Clean Architecture we have many of his thoughts and suggestions on how to improve the structure of your application.

In this years of research on architecture design, it was discovered that the most popular architecture principles (for example, just to name a few: Hexagonal Architecture, Onion Architecture, or Screaming Architecture) have one main goal; to build some kind of layer into the application and create a separation of interest along that path. Uncle Bob noted that every architecture tries to at least separate the business logic from the rest of the application and that these architectures produce systems:

  • Framework Independent: Your application should not be driven by the framework that is used.

  • Testable: Your application and business logic should be testable and contain tests.

  • User Interface Independent: The user interface can be changed easily, without changing other points of the system. A web user interface can be replaced by a mobile interface, for example, without changing the business rules.

  • Database Independent: Your application should not be designed by the type of database you are using. Your business logic doesn't care whether your data entities are stored in documents, relational database, or in memory.

  • Independent of any external agent: Your business rules should only care about their own task and not about anything else that might reside in your application.

Why should you consider using Clean Architecture on the frontend?

It all depends on the context of the product, I'm not saying that you should always build your frontend apps based on clean architecture, after all every solution has a certain degree of complexity. But one of the main reasons is that the frontend is the gateway to every digital product. Think of an app that loses more and more money as new features are built. How is this possible? Just think of the example that Uncle Bob comments on in his clean architecture piece. Imagine that each feature demanded by an actor in the system (a group of users), takes 1 month to be implemented. After that new features continue to be demanded, but the development team takes longer to implement features that seemed simple or not so complex. The cost of maintaining operations and hiring more developers to maintain legacy code and to create new features exceeds management budgets. Stakeholders understand that it is not viable to continue investing money in this software because it does not keep up with the competition and cannot innovate, much less generate profits for the company. The users, dissatisfied with the countless bugs, also start to abandon the platform.

This is the recipe for failure when developing software. There is no point in having a well-planned backend and built with the best development practices, if the frontend takes too long to render the main content or there are always unexpected bugs that make the user experience frustrating, among other negative points that can occur.

Concepts such as Clean Architecture, TDD, DDD and other development principles are not a waste of time. On the opposite, they help a lot to develop quality software that is reliable and stable.

A good architecture makes the system simple to understand, quick to develop, simple to maintain, and friendly to deploy. The ultimate goal is to minimize the system's lifetime cost and maximize the programmer's productivity. - Robert C. Martin, Clean Architecture.

Let's go to a practical example to make clear the advantages of clean architecture.

Have you ever needed to build presentation components? In the frontend we need to have views. See below how a view would look without a clean architecture:

@Component({
  selector: 'app-edit',
  templateUrl: './edit.component.html'
})
export class EditComponent extends ProductBaseComponent implements OnInit {

  pictures: string = environment.picturesUrl;

  @ViewChildren(FormControlName, { read: ElementRef }) formInputElements: ElementRef[];

  base64: any;
  picturePreview: any;
  pictureName: string;
  pictureOriginalSrc: string;

  constructor(private fb: FormBuilder,
    private productService: ProductService,
    private router: Router,
    private route: ActivatedRoute,
    private toastr: ToastrService) {

    super();
    this.product = this.route.snapshot.data['product'];
  }

  ngOnInit(): void {

    this.productService.getSuppliers()
      .subscribe(
        suppliers => this.suppliers = suppliers);

    this.productForm = this.fb.group({
      supplierId: ['', [Validators.required]],
      name: ['', [Validators.required, Validators.minLength(2), Validators.maxLength(200)]],
      description: ['', [Validators.required, Validators.minLength(2), Validators.maxLength(1000)]],
      picture: [''],
      value: ['', [Validators.required]],
      isActive: [0]
    });

    this.produtoForm.patchValue({
      supplierId: this.product.supplierId,
      id: this.product.id,
      name: this.product.name,
      description: this.product.description,
      isActive: this.product.isActive,
      valor: CurrencyUtils.DecimalToString(this.product.value)
    });

    this.pictureSrc= this.pictures + this.product.picture;
  }

  ngAfterViewInit(): void {
    super.validateForm(this.formInputElements);
  }

  editProduct() {
    if (this.productForm.dirty && this.productForm.valid) {
      this.product = Object.assign({}, this.product, this.productForm.value);

      if (this.base64) {
        this.product.imagemUpload = this.base64;
        this.produto.picture = this.pictureName;
      }

      this.product.value = CurrencyUtils.DecimalToString(this.product.value);

      this.product.updateProducts(this.product)
        .subscribe(
          success => { this.processSuccess(success) },
          fails => { this.processFail(fails) }
        );

      this.noChanges = false;
    }
  }

  processSuccess(response: any) {
    this.productForm.reset();
    this.errors = [];

    let toast = this.toastr.success('Product edited successfully!', 'Success!');
    if (toast) {
      toast.onHidden.subscribe(() => {
        this.router.navigate(['/products/list']);
      });
    }
  }

  processFail(fail: any) {
    this.errors = fail.error.errors;
    this.toastr.error('An error has occurred!', 'Oops :(');
   }

  upload(file: any) {
    this.pictureName = file[0].name;

    var reader = new FileReader();
    reader.onload = this.manipulateReader.bind(this);
    reader.readAsBinaryString(file[0]);
  }

  manipulateReader(readerEvt: any) {
    var binaryString = readerEvt.target.result;
    this.base64 = btoa(binaryString);
    this.picturePreview = "data:image/jpeg;base64," + this.base64;
  }
}

The component shown above is a clear violation of several S.O.L.I.D principles and it is not clean. See how many responsibilities are assigned, headers manipulation, form validation, uploading and receiving by injection in the constructor a service that has the implementations to make API calls. Now imagine the work to test this component, in addition to the work of having to perform maintenance and even add new features. It would be a long and unforeseen task.

When we put in practice clean architecture the components are well separated, we know just by analyzing the structure of folders and files that each one has its responsibility. See a code example of a component that follows good practices:


@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.scss']
})
export class LoginComponent implements OnInit {

  form: FormGroup;
  isLoading: boolean;

  constructor(
    private fb: FormBuilder,
    private authService: AuthService,
    private snackBar: MatSnackBar,
    private router: Router,
    private notification: NotificationService,
    private userController: IUserController
  ) { }

  ngOnInit() {
    this.createForm();
  }

  private createForm() {
    this.form = this.fb.group({
      username: ['', Validators.required],
      password: ['', Validators.required]
    });
  }

  login() {
    this.isLoading = true;

    this.userController
    .login(this.form.value)
    .pipe(finalize(() => {
      this.isLoading = false;
    }))
    .subscribe(
      (user: UserEntity) => this.loginResponse(user),
      (err: ValidationError[]) => this.notification.open(err)
    );

  }

  loginResponse(usuario: UserEntity) {
    if (usuario) {
      this.authService.credentials = user;
      this.router.navigateByUrl('/home');
    } else {
      this.snackBar.open('Invalid username or password.', null, {
        duration: 2000
      });
    }
  }

}

See that even the file gets much smaller! It only takes care of the responsibilities that pertain to the login and nothing more. We have no validations in the form regarding the number of characters, like minLength, maxLength or if the password is meeting all the business rule requirements. This is not the responsibility of the presentation layer. The most that the component does is to determine that that field is required in the FormGroup, this is a more syntactic rule for this type of context for the Login component, because every user should see on the screen some indicator that those fields are required. So we can say that it is a basic input validation, not business logic!

You may have noticed that it gets in the constructor the IUserController. We have controllers that play the role of making the calls to the UseCases, but see how simple it is:

@Injectable({
  providedIn: 'root'
})
export class UserControllerService implements IUserController {

  constructor(
    private userUseCase: IUserUseCase
  ) { }


  login(param: UserEntity): Observable<UserEntity> {
    return this.userUseCase.login(param);
  }

  logout(): Observable<boolean> {
    return this.userUseCase.logout();
  }
}

We don't have strong coupling. There is a well defined flow in the clean architecture that makes it easy to create components and cascade calls towards the domain of our application.

I hope these code examples have made it clear how important it really is to bring the clean architecture and its principles to the frontend.

Layers in the Frontend

The Clean Architecture diagram and rules are very general, let's try to break it down and put it more in the context of web application development. Take a look at the image below that may be very familiar to you if you have studied the subject further:

Captura de Tela 2022-08-21 às 17.59.04.png

The illustration shows some rules that exist about how these layers should interact with each other:

  • Core Entities (Domain): are data models, which are essentially required to represent our core logic, build a data flow, and make our business rule work.

  • Use cases: are built on top of the main entities and implement all the application's business logic. Use cases should "live in their own world" and do only what is necessary to perform a given task.

  • Dependency Rule: Each layer should only have access to the layer below it, according to the Dependency Rule, which states that the dependency between layers can only point inside the application, from the outside in. For example, the Data layer should only use entities specified in the entity layer and the controller should only use use use cases from the layer below.

  • Interface adapters: To get an architecture beyond the edge of each layer, the best way is to work with interface adapters from the core (entity and use case layer) to ensure a homogeneous structure (shows similarity of structure) in all designs, that fits together like a puzzle in the end.

In principle, Clean Architecture is a general standard that should work across different platforms and can vary from backend, mobile and web devices. This means that there is no best way to implement it. But it is important to understand that the architect needs to ensure clarity and decoupling of the layers, avoiding making any kind of maintenance or development of a new feature difficult.

Ok, but how can all this fit into a frontend application? And even more so using Angular? Let's apply these points to the framework:

  • Framework Independent: Our logic and business rules should not be affected by any functionality or structure of the Angular framework. This gives us more flexibility when upgrading to new versions or switching to a completely different framework - we don't need to depend on any framework-specific library. But in Angular we have useful features and functionality to facilitate development, such as RxJS, Router, ReactiveForms and others. This is why it is important that you carefully analyze everything surrounding your application and which features you will or will not use. The important thing is to ensure that your components and code are flexible.

  • Testing: It is important to test our layer and business rules. When we have a clean design this makes unit and integration testing much easier. To test the core functionality of our application. We can use Jasmine for this purpose and Cypress.

  • Independent of the user interface: Business rules do not care how they are triggered or controlled. Therefore no business logic should be placed inside any user interface controller. We will see this throughout the article.

  • Database Independent: In general web applications are not concerned with the use of databases because they are mainly fed through APIs. But looking at the repository pattern most applications do some kind of CRUD operation by calling the API endpoints. We can abstract the calls to the backend to repositories. This avoids passing that responsibility to another layer that shouldn't know how the JSON is sent to the endpoint that is in the API.

  • Independent of any external agent: As already mentioned, our core application logic should not be totally dependent on the Angular framework. But again, it all depends on the real needs of your business and how you will divide the layers of your application. Remember that in Angular we have facilities to modularize the whole application and thus avoid coupling. But the important thing is to avoid being strongly coupled to versions of the framework and to resources that would make it totally difficult to change the framework in the future. Especially when we touch on the subject of third-party libraries, these should be very restricted, be studied and planned to be as decoupled as possible from your app and web system.

Remember that only the upper layers are allowed to access the lower layers, not vice versa.

In our example we will have the following layers:

  • Data: This layer handles the implementation of the actual data interfaces, repositories and mappers. It ties the logic of saving specific data from the framework, for example saving data to a storage location or sending it to an API, to an adapter for our use case specified in the Domain layer.

  • Domain: Aggregates main entities, use cases and interface definitions, because they are closely linked. The logic validation rules live here. So if I need to validate that a user has submitted a name with a valid character format, it should be done here. But don't be fooled into thinking that everything that concerns validation should stay here, we are talking about specific validation rules that we know are part of Domain. For example, if you need to validate that the date is in the correct format, you can do that outside this layer, closer to the input layers. But if you need to validate that the date is greater than or equal to another date that you have received from an API response, this must be done in the Domain. This was commented on by Robert C. Martin, in a live broadcast from TheWiseDev, the link is at the end of the article.

  • Presentation: This layer contains the standard structure with components and views as we already know them. Each component can now use the previously defined use cases to populate the components with business logic. We can also divide into two folders within presentation with views and controllers. Views receive only what is needed to display the information on the screen, via dependency injection in the constructor. The controllers, on the other hand, receive the injections in their constructor of the contracts from the Use Cases, so they can make the calls for the flow to continue without any strong coupling between them.

  • Shared: Here we can assign it as more of a directory than a layer itself, this is at the developer's judgement. This folder can be used a lot by the presentation layer, we know very well that in the frontend we have a great need to reuse presentation components, to avoid the repetition of classes, CSS and HTML. Again if you think it is necessary you can have a folder within the presentation layer that has these components ready for reuse or use it as a layer, since it is necessary to create a module for Shared. This is just a suggestion. We just have to be careful not to put anything related to validations that are related to logic rules in this folder/layer. When we have a shared folder we don't want it to contain too much complexity, especially knowing how to fully validate a CPF, but just help us centralize HTML, Pipes, Directives and other types of components that help in the presentation of the view.

  • Core/Infra: A directory, I don't see it as a layer, that is more related to common services. The services defined in the Core module are instantiated once. We can have specific services that have nothing to do with business rules, but with HTTP communications, Interceptors, Internationalization and whatever else is related to common services.

Below is a picture of a personal study project, applying the main concepts of clean architecture:

Captura de Tela 2022-08-21 às 18.04.21.png

Conclusion

To conclude we can quote Uncle Bob's words when referring to the role of architects, developers within systems. This sentence makes us reflect a lot about the role of clean architecture and its importance:

The goal of software architecture is to minimize the human resources required to build and maintain the system needed. - Robert C. Martin

I hope this article has helped to shed some light on this very important subject! The base project is not yet complete, so the repository link on Github is not yet available.

References:

Otavio Lemos: youtube

Rodrigo Manguinho: youtube

DDD talk in frontend by Manfred Steyer

Clean Architecture with Robert Martin (a.k.a. Uncle Bob) | theWiseDev-chat