Software Architectures examples: Hexagonal Architecture

What is this architecture about, and what`s the difference from others more traditional like MVC?

If we go back to 2005, according to Alistair Cockburn, the creator of this term, this architecture tries to solve the problem of Avoid infiltration of business logic into the user interface code.

The most important thing of this architecture is to protect the most valuable code of any application, the domain.

So, we can summarize it as an isolation strategy of the domain. And isolated code can be tested better.

Based on the same concept, there are other related architectures like Clean architecture or Onion architecture that have comon strategies and bring some topics to the table like the application layer or the interface adapters. In the next example I will try to ilustrate a combination of te core concepts of this architectures:

Example

Following this aspnetcore example, consider the next code distribution:

As you can see, we already got a music application. In this case, we got an API called MyMusic.Api. We had decided for this example to separate the code in folders according to each layer:

  • Domain with the domain models PlayList and Track.
  • Application.Core with the application use cases, models as services. This layer tells us what the application do, in this case: GetPlayList, CreatePlayList, AddTrackToPlayList, etc. Also we have the ports, wich are the contracts wich describe the behaviour that the application need.
  • Adapters are the infrasctucture implementations of the contracts, for example to read/write from a database like PLayListPostgreSQLAdapter or just call another api using http like TraksSpotifyApiAdapter.

Layers dependency flow

To maintain this isolation, the layers of the architecture must follow a directional dependency flow: Inside layers should not know anything about outside layers.

As you can see, if we look at the project dependency tree, we can confirm this dependency flow:

Working like this have some benefits:

  • The domain is isolated, so rules and models are not affected from infrastructure decisions.
  • The code is more easily testable. For example, the application layer can be tested separately without infrastructure dependencies.
  • Responsibilities are separated. Now each layer and code component follows the Single Responsibility Principle (SRP), This increases code reusability.
  • The code is more ready to change and to grow, following the Open/Closed Principle (OCP). For example, if you want to change any infrastructure implementation, you only need to follow a defined contract, without affecting the whole application.

How is this possible?

This awesome dependency graph is possible thanks to the Dependency Inversion Principle (DIP).

  • High-level modules should not depend on low-level modules. Both should depend on abstractions

In this case, we reach this using the ports abstraction, making application code only depends on an interface for example PlayListPersistencePort:

public interface PlayListPersistencePort {
    PlayList GetPlayList(string playlistId);
    List<PlayList> GetAllPlayList();
    void Persist(PlayList playList);
}
  • Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions

We reach this making adapters implement interfaces. This interfaces are just interfaces with models objects in signatures. So, all the implementations details stay in the adapters. In this case PLayListPostgreSQLAdapter implements PlayListPersistencePort:

public class PLayListPostgreSQLAdapter : PlayListPersistencePort {
    
    public PlayList GetPlayList(string playlistId) {
        //This should be read from PostgreSQL DB
        var trackList = new List<Track> {
            new Track("D7D0BF31-CC98-44EA-B983-C8C37FA95A59", "Hakujitsu", "King Gnu",261000),
            new Track("560D59E0-0487-4DF5-90C6-95C5594F244A", "Era - Ameno (The Scientist Remix)", "The Scientist DJ", 202200)
        };
        return new PlayList(playlistId, "Example PlayList", PlayListStatus.Active, trackList, "https://imageUrl.com");
    }

    public List<PlayList> GetAllPlayList() {
        var trackList = new List<Track> {
            new Track("D7D0BF31-CC98-44EA-B983-C8C37FA95A59", "Hakujitsu", "King Gnu",261000),
            new Track("560D59E0-0487-4DF5-90C6-95C5594F244A", "Era - Ameno (The Scientist Remix)", "The Scientist DJ", 202200)
        };
        return new List<PlayList> {
            new PlayList("B6AED672-2663-49BA-85B0-0DDB02D59C1B", "Example PlayList", PlayListStatus.Active, new List<Track>(), "https://imageUrl.com"),
            new PlayList("916A3E10-7AF2-4D54-BD78-48364F783F78", "Example PlayList 2", PlayListStatus.Active, trackList, "https://imageUrl2.com"),
            new PlayList("BF2D7788-D1FE-4772-B362-6D89686D895A", "Example PlayList 3", PlayListStatus.Archived, new List<Track>(), "https://imageUrl3.com"),
        };
    }

    public void Persist(PlayList playList) {
        //This should persist in PostgreSQL DB
    }
}

Code dependency flow

To understand how powerfull this approach can be, we need to understand how code dependencies are connected. If we pay atention to layer connections, we can notice that code dependency flow can be represented like this:

Controller: PlaylistsController

[HttpPost]
public ActionResult CreatePlayList([FromBody]CreatePlayListRequest request) {
    var service = playListServiceCreator.CreateCreatePlayListService();
    var result = service.Execute(request.PlayListName);
    return this.BuildResponseFrom(result);
}

Service: CreatePlayListService

public class CreatePlayListService {
    private readonly UniqueIdentifiersPort uniqueIdentifiers;
    private readonly PlayListPersistencePort playListPersistence;
    private readonly PlayListNotifierPort playListNotifier;
    
    public CreatePlayListService(UniqueIdentifiersPort uniqueIdentifiers,PlayListPersistencePort playListPersistence, PlayListNotifierPort playListNotifier) {
        this.uniqueIdentifiers = uniqueIdentifiers;
        this.playListPersistence = playListPersistence;
        this.playListNotifier = playListNotifier;
    }

    public Either<DomainError, ServiceResponse> Execute(string playListName) {
        var newPlayListId = uniqueIdentifiers.GetNewUniqueIdentifier();
        var playList = PlayList.Create(newPlayListId, playListName);
        playListPersistence.Persist(playList);
        playListNotifier.NotifyPlayListHasBeenCreated(playList.Id, playListName);
        return ServiceResponse.Success;
    }
    
}

Domain: PlayList

public static PlayList Create(string id, string name) {
    return new PlayList(id, name, PlayListStatus.Active, new List<Track>(), null);
}

Did you notice the magic? Well, let me explain a little about what is going on here:

PlayListPersistencePort It`s just an abstraction, so playListPersistence.Persist(playList) application layer is saying “I want to persist this playlist“, but it doesn’t care how.

This is Responsibility for the infrastructure.

If we go back to the controller we can see playListServiceCreator.CreateCreatePlayListService() method call. With is the infrastructure part of code that is really caring about how data will be persisted.

PlayListServiceCreator

public CreatePlayListService CreateCreatePlayListService() {
    var pLayListDatabaseAdapter = new PLayListPostgreSQLAdapter();
    var musicCloudApiHttpAdapter = new PlayListSpotifyApiAdapter();
    var uniqueIdentifiersInMemoryAdapter = new UniqueIdentifiersInMemoryAdapter();
    return new CreatePlayListService(uniqueIdentifiersInMemoryAdapter, pLayListDatabaseAdapter, musicCloudApiHttpAdapter);
}

This class is responsible to inject a concrete implementation, adapter, in this case PLayListPostgreSQLAdapter.

This tecknike is also knowed as dependency inyection (DI).

And.. what about testing?

Here another big improvement comes in to play, because now application layer and domain can be tested alone.

You do not need any infrastructure code in your application layer tests like test databases. Now we have fastest test covering the core rules.

[Test]
public void create_a_play_list() {
    var aPlaylistId = APlaylist.Id;
    var aPlaylistName = APlaylist.Name;
    uniqueIdentifiers.GetNewUniqueIdentifier().Returns(aPlaylistId);
    
    var result = createPlayListService.Execute(aPlaylistName);
    
    result.IsRight.Should().BeTrue();
    VerifyPlayListHasBeenPersistedWith(aPlaylistId, aPlaylistName, PlayListStatus.Active);
    playListNotifierPort.Received().NotifyPlayListHasBeenCreated(aPlaylistId, aPlaylistName);
}

For example, now you can check that paramenters that are sending to your port are correct.

private void VerifyPlayListHasBeenPersistedWith(string aPlaylistId, string aPlaylistName, PlayListStatus status) {
    playListPersistence.Received().Persist(Arg.Is<PlayList>(playlist =>
        playlist.Id.Equals(aPlaylistId)
        && playlist.Name.Equals(aPlaylistName)
        && playlist.Status.Equals(status)
    ));
}

On the other hand, now you can let your adapters implementations tests to only cover infrastucture logic and not domain rules.