DEV Community

Cover image for Hexagonal Architecture with Angular
aurelien-alet
aurelien-alet

Posted on • Updated on

Hexagonal Architecture with Angular

Summary

  1. Introduction
  2. A complete, working example
  3. Implementation choices
  4. Hexagonal architecture benefits in Angular
  5. When to use Hexagonal architecture in Angular
  6. Conclusion

1 - Introduction

For a long time, Model View Controller seemed to be the favorite architecture of software developers. It was used in back-end, as well as in front-end code. But with the growing interest of the community for Domain-Driven Design, this architecture has been challenged by its cousin, the "hexagonal" (or "ports and adapters") architecture.

As MVC, hexagonal architecture uses the separation principle, but also more abstraction, and domain code is central to its architecture.
If you want more information about hexagonal architecture, here is a complete article, written by its designer, Alister Cockburn.

Currently hexagonal architecture is mostly used in back-end code, and there are poor resources about it in front-end code, especially for Angular.

How to adapt hexagonal architecture to Angular ? Would it be beneficial ? If you are also interested in these questions, you should read this article.

2 - A complete, working example

All the following explanations will be based on an exemple app I developed, and made available on Github. This app is based on Angular's tour of heroes. If you launch the app, the displayed interface is the same as in the Angular tutorial, but there are great differences in the code structure. As a reminder, the principle of this small app is to display a list of heroes and to manage (create, delete, modify) them. The angular-in-memory-web-api module is used to simulate external API calls.

This is an overview of this example's architecture:

Image description

And the associated code organization:

Image description

Domain

In hexagonal architecture, the entire domain related code is isolated. The tour of heroes app has the following purposes: display a list of heroes, display details on a specific hero, and display logs of the actions made by the user. The domain related classes are central to the architecture: HeroesDisplayer, HeoresDetailDisplayer and MessagesDisplayer.

Ports

As you can imagine, domain related code is not lonely in our heroes app. There is also a user interface related code, corresponding to Angular components, and external APIs calls, corresponding to Angular services. In every hexagonal architecture, domain related code doesn't interact directly with all of this code. Instead, objects called ports are used, and they are implemented by interface classes. This weakens the coupling between the elements of our architecture.

In our heroes app, HeroesDisplayer and HeoresDetailDisplayer need to interact with an external service, which stores heroes related interactions. For this purpose, they will expose an IManageHeroes port. For each of our domain classes, we want to keep track of every user's interactions. That is why they also have an IManageMessages port.

The users realize actual actions in our app through display interfaces. These interfaces can be divided in several categories, according to their purpose. To ensure a faithful comparison with the Angular tour of heroes app, we should have interfaces displaying heroes (a list of heroes and a dashboard), an interface displaying hero details, and an interface displaying messages. Therefore, the related ports should be respectively IDisplayHeroes, IDisplayHeroDetail and IDisplayMessages.

Adapters

Now that our ports are defined, we have to plug adapters on them. One of the benefits of hexagonal architecture is the ease when switching between adapters. For example, the adapter plugged to IManageHeroes could be an adapter calling a REST API, and we could replace it easily by an adapter using a GraphQL API. In our case, we want our app to be identical to Google tour of heroes app. So we implement an angular service, HeroAdapterService, calling an in-memory web API, and another, MessageAdapterService, storing messages locally.

The adapters for the three other ports are user interface related adapters. In our app, they will be implemented by Angular components. As you can see, the IDisplayHeroes port is implemented by three adapters. Details will be available in the following.

As explained above, there is an asymmetry in our adapters, due to their nature. The architecture diagram represents it in the following way: architecture left-side adapters are designed for users interactions, whereas right-side adapters are designed for external services interactions.

3 - Implementation choices

Because hexagonal architecture was designed for back-end applications, some arrangements have been made in code implementation. These choices are explained in the following part.

Angular related objects in domain code

A good practice in hexagonal architecture is to keep domain related code independent of any framework, in order to ensure it is functional for any kind of adapter. But in our code, the domain is highly dependent on Angular and rxjs objects. In fact, we can assume that we won't use several typescript or JavaScript frameworks, in order to keep interface coherence. Also, the angular dependency injection system is very useful to achieve the inversion of control principle. However, it should be possible to use JavaScript promises instead of rxjs observables, but we would have to write a lot of boilerplate code in our classes.

Observable return type in left-side ports

Since the logic behind the code is handled in the domain, one could wonder why returning Observable objects in IDisplayHeroDetail, IDisplayHeroes and IDisplayMessages ports. Indeed, each object returned by the services is handled inside the domain code using pipe and tap methods. For example, the hero detail save result returned by HeroAdapterService is managed directly in the HeroDetailDisplayer:

hero-detail-displayer.ts

askHeroNameChange(newHeroName: string): Observable<void> {
    [...]
    const updatedHero = {id: this.hero.id, name: newHeroName};
    return this._heroesManager.updateHero(updatedHero).pipe(
        tap(_ => this._messagesManager.add(`updated hero id=${this.hero ? this.hero.id : 0}`)),
        catchError(this._errorHandler.handleError<any>(`updateHero id=${this.hero.id}`, this.hero)),
        map(hero => {if(this.hero){this.hero.name = hero.name}})
    );
}
Enter fullscreen mode Exit fullscreen mode

Still, returning an empty observable from the askHeroNameChange method is interesting if we aim at enabling the interface adapters to know when the data is loaded. For example, when the hero detail changes are effective, we can go back to the previous page:

hero-detail.component.ts

changeName(newName: string): void {
    this.heroDetailDisplayer.askHeroNameChange(newName).pipe(
        finalize(() => this.goBack())
    ).subscribe();
}
Enter fullscreen mode Exit fullscreen mode

The drawback of this implementation choice is the need of subscribing to each domain function call inside left-side adapters:

heroes.component.ts

this.heroesDisplayer.askHeroesList().subscribe();
Enter fullscreen mode Exit fullscreen mode

HeroesDisplayer class instantiated twice

In our app, dependency injection is handled in app.module.ts. We use injection tokens to make domain classes accessible inside Angular components. For instance the injection of IDisplayHeroDetail into the HeroDetail component is done this way:

app.module.ts

import HeroDetailDisplayer from '../domain/hero-detail-displayer';

providers: [
    [...]
    {provide: 'IDisplayHeroDetail', useClass: HeroDetailDisplayer},
    [...]
}
Enter fullscreen mode Exit fullscreen mode

Sets HeroesDetailDisplayer instance as a IDisplayHeroDetail implementation

hero-detail.component.ts

import IDisplayHeroDetail from 'src/app/domain/ports/i-display-hero-detail';

export class HeroDetailComponent implements OnInit {
    constructor(
        @Inject('IDisplayHeroDetail') public heroDetailDisplayer: IDisplayHeroDetail,
        [...]
    ) {}
}
Enter fullscreen mode Exit fullscreen mode

Injects HeroDetailDisplayer inside HeroDetailComponent

However, there is a subtlety somewhere in the code: two different injection tokens are generated for the HeroesDisplayer class. Moreover, HeroesComponent and DashboardComponent share the same injection token, whereas HeroSearchComponent component uses another token.

app.module.ts

import HeroesDisplayer from '../domain/heroes-displayer';

providers: [
    // Used in HeroesComponent and in DashboardComponent 
    {provide: 'IDisplayHeroes', useClass: HeroesDisplayer},
    // Used in HeroSearchComponent
    {provide: 'IDisplayHeroesSearch', useClass: HeroesDisplayer},
]
Enter fullscreen mode Exit fullscreen mode

This is because HeroesComponent and DashboardComponent can share the same instance of HeroesDisplayer: they display the same list of heroes. On the other hand, if HeroSearchComponent had this same instance, each search would affect the displayed heroes, since the heroes attribute is modified by the askHeroesFiltered method in HeroesDisplayer. Sharing the same token for the three components would change our app's behavior:

Image description

4 - Hexagonal architecture benefits in Angular

The main essence of hexagonal architecture consists in having interchangeable adapters allowing our app to be driven equally by a human, a system, or by tests. In our app, we are highly tied to the Angular framework, which means we do not make a whole benefit from this architecture asset. However, I found some promising insights from experiencing it in front-end code.

Decoupled presentational layer, core layer and external services calls

Domain code, corresponding to our core layer, is clearly separated from the interface adapter, namely the presentational layer, by ports. Thanks to these same ports, the risk of adding unwanted code into external service calls is reduced. All the core logic is handled into domain classes.

heroes.component.ts

constructor(
    @Inject('IDisplayHeroes') public heroesDisplayer: IDisplayHeroes
) { }
Enter fullscreen mode Exit fullscreen mode

Imports domain class, corresponding to code layer

heroes.component.html

<li *ngFor="let hero of heroesDisplayer.heroes">
    [...]
</li>
Enter fullscreen mode Exit fullscreen mode

Uses heroes information handled by domain code inside view, corresponding to presentational layer

Code factorization

If you look at the original tour of heroes application, the main purpose of the HeroesComponent, of the HeroSearchComponent and of the DashboardComponent are very close. They all display the list of heroes, but the possible interactions differ depending on components. Therefore the related core code, mapping services returns to displayed information, should be factorized. In our code, the domain related code for the three components is factorized: we take advantage of hexagonal port re-usability.

Testing

Sometimes, Angular tests can be very painful, even more if core code is mixed with presentational code inside components. This code grows as your application evolves. Keeping our display components, our domain code, and our services isolated from each other make the tests more straightforward. You can easily mock other layers, and focus on testing the current class.

hero-detail.component.spec.ts

beforeEach(async () => {
    spyIDisplayHeroDetail = jasmine.createSpyObj(
      'IDisplayHeroDetail', 
      ['askHeroDetail', 'askHeroNameChange'],
      {hero: {name: '', id: 0}}
    );
    spyIDisplayHeroDetail.askHeroDetail.and.returnValue(of());
    spyIDisplayHeroDetail.askHeroNameChange.and.returnValue(of());
    [...]
}
Enter fullscreen mode Exit fullscreen mode

Hero details display tests: domain class and methods can be mocked easily

5 - When to use Hexagonal architecture in Angular

Even if we can't totally compare it with back-end code, hexagonal architecture can have some great benefits in some front-end applications. Some use cases seem particularly adapted to it.

Profile based apps

As we isolated the presentational layer, apps where the same logic is used inside different interfaces, like profile based apps, are good candidates for our architecture. The admin-panel branch gives an example of what the app would look like if we added an admin panel interface. This interface, designed for admin users, allows them to do every administrative action inside a single view: adding, changing, deleting or searching for heroes. Only the AdminPanelComponent is added to the heroes app, no changes inside domain code or services, showing their reusable attribute.

To launch the administrator interface, run npm run start:admin on admin-panel branch.

Apps calling multiple external services

Angular hexagonal architecture is also adapted if you have to contact several external services serving the same purpose. Once again, domain code re-usability simplifies. Let's say we want to call an online service instead of our in-memory heroes service: the superhero api by Yoann Cribier. Adding a SuperheroApiAdapterService is the only thing required, as you can see in the superhero-api branch.

To make the app communicate with superhero-api, run npm run start:superhero-api on the superhero-api branch. Observation: in our example, heroes modifying and deletion aren't implemented by the online service.

6 - Conclusion

This tiny app shows it's possible to adapt hexagonal architecture to an Angular app. Some problems, which haven't been raised by the tour of heroes tutorial app, can be solved using it.

Thank you for reading !
I hope you enjoyed this article, and I'm very interested in any feedback of this architecture adoption.

I also would like to thank Leila for correcting this article.

Oldest comments (1)

Collapse
 
vicrider profile image
Vic-rider

Thanks for sharing πŸ€πŸΎπŸ‘ŠπŸΏ