In this article, we are going to describe the hexagonal architectural pattern. We will explain its structure, where it came from, and its advantages and disadvantages. Furthermore, we will demonstrate its implementation in C#.

To download the source code for this article, you can visit our GitHub repository.

Let’s dive in!

What Is the Hexagonal Architectural Pattern?

Before we describe the hexagonal architectural pattern, let’s briefly define what architectural patterns are. An architectural pattern is a general and reusable solution to standard and reoccurring problems in software design. It aims to prescribe the software system’s structure, characteristics, and elements to address common issues and provide a blueprint for solving them.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

The General Idea Behind the Hexagonal Architectural Pattern

The main idea of the hexagonal architectural pattern is to divide the system into loosely coupled components. The components are loosely coupled when there is a weak association between them. That means that changes in one of the components affect other components as little as possible, and ideally, not at all. 

Generally, we achieve that when one component has little knowledge or use of other components. Additionally, each component should depend on only a few other components. 

Let’s look at how the hexagonal architectural pattern proposes achieving that.

The Graphical Presentation of the Pattern

We will start with a graphical presentation of the hexagonal architectural pattern:

Hexagonal Architectural Pattern

As we can see, the hexagonal pattern deals with three main terms: application core, ports, and adapters. With this in mind, let’s describe each of them.

The Terminology

The application core is a central part of our application. The core consists of application core logic and entities. Thus, it implements the main functionality and rules of the application. Overall, it should be completely independent of external components, such as storage, UI, communication protocols, etc. Other names by which we refer to the application core are domain model, business services, and business model. 

The ports are the interfaces that the application core defines for external components. They describe how the external world can communicate with the core. In general, we can divide them into two categories:

  • primary or driving ports – operations that external systems or users can initiate (API interface, user interface, etc.)
  • secondary or driven ports – operations that the core needs from the external systems to perform its function (database, messaging systems, etc.)

We define the ports in the application core, usually as the interfaces or contracts, defining the external functionality we need to enable the core to work. That way, we decouple the external dependencies from the core logic.

The ports use the Inversion of Control (IoC) principle and Dependency Injection. In other words, we inject the adapters through the ports from outside the core application. To learn more about IoC and DI be sure to read Dependency Injection in ASP.NET Core. 

The adapters are the components that implement the interface defined by the ports. They implement the communication and exchange with the application core. There could be one or more implementations of an adapter for each port. Similarly to the ports, we can have primary or driving adapters and secondary or driven adapters. The adapters implement the logic of external dependencies (databases, UI, message bus, etc). This logic is unknown to the core application and completely decoupled from it.

Similar Architectural Patterns and Their Influence

The onion architectural pattern is very similar to the hexagonal pattern. It further decomposes the application core into several concentric rings. The onion architecture was proposed by Jeffrey Palermo in 2008. This architecture further decomposes the application into layers. The application core is divided into domain, domain services, and application layer. The external dependencies are pushed further into the outer ring. To learn more about the onion architectural pattern, please read our article about the Onion Architecture in ASP.NET Core.

Robert C. Martin proposed the clean architectural pattern in 2012. It combines and further refines the principles from the hexagonal and the onion architectural patterns. It isolates the external dependencies into additional components in the outer layers. The inner circles are reserved for domain entities and use cases, which can be roughly approximated with application services. Additionally, this approach prescribes that dependencies should exist from an outer layer to the inner layer.

Some consider the hexagonal architecture to actually be the origin of the microservices architecture. 

The Hexagonal Architectural Pattern in C#

Let’s create a simple ASP.NET application that implements the described architectural pattern for demonstration.

The Demo Project

Our demo project deals with writing articles. We will implement features for adding, deleting, and reading authors and articles. Additionally, we will implement features that move the article through different stages. 

Here is the general structure of our project: 

Hexagonal Architecture Project Structure

Let’s briefly describe each of the projects, and following that, we will examine each of them more closely.

HexagonalArchitecturalPatternInCSharp.Core is the application core. Here, we will implement application logic and domain entities.

After that, we have HexagonalArchitecturalPatternInCSharp.Messaging and HexagonalArchitecturalPatternInCSharp.Persistence, which are the projects where we will implement driven adapters for sending messages to some concrete implementation of the message broker and storing our entities in the database of our choice. 

On the other hand, the HexagonalArchitecturalPatternInCSharp.API project is the driving adapter. Here, we expose our endpoints so consumers of our service can call them. 

And finally, the HexagonalArchitecturalPatternInCSharp is the application itself, where everything is wired together and run. 

The Application Core

In the application core, we define our entities. Based on the project description, we need an Author class:

public class Author
{
    public Guid Id { get; set; }

    public required string Name { get; set; }

    public double Balance { get; set; }
}

And an Article class: 

public class Article
{
    public Guid Id { get; set; }

    public required string Title { get; set; }

    public string? Description { get; set; }

    public Difficulty Difficulty { get; set; }

    public WritingStatus WritingStatus { get; set; }

    public Guid? AuthorId { get; set; }
}

Besides these classes, there are two simple Enum types, the Difficulty enumeration for determining the article’s complexity and the WritingStatus enumeration for describing the stage at which the article is currently. Lastly, there is the Message class, with the encapsulation text we want to send to the message broker.

After that, in the folder Ports, we implement our driven and driving ports. On the driven side they are simple interfaces for respective repositories and message publishers. On the driving side, we expose the interfaces of our services, and the classes implementing application logic and modifying our entities. 

In the last folder, Services, we have a concrete implementation of services implementing the domain logic. The ArticleService class implementation is: 

public class ArticleService : IArticleService
{
    private readonly IArticleRepository _articleRepository;

    public ArticleService(IArticleRepository articleRepository)
    {
        _articleRepository = articleRepository;
    }

    public async Task AddAsync([FromBody] Article article)
    {
        await _articleRepository.AddAsync(article);
    }

    public async Task DeleteAsync(Guid id)
    {
        await _articleRepository.DeleteAsync(id);
    }

    public async Task<IEnumerable<Article>> GetAllAsync()
    {
        return await _articleRepository.GetAllAsync();
    }

    public async Task<Article?> GetByIdAsync(Guid id)
    {
        return await _articleRepository.GetByIdAsync(id);
    }
}

The ArticleService class implements the GetByIdAsync() method to fetch the article by its id,  or GetAllAsync() to fetch all articles in our database. Along with that, we have the AddAsync() method for adding a new article and the DeleteAsync() method for deleting an article from the database. 

The AuthorService class is logically similar to the ArticleService class, so we won’t repeat that code here. However, the WritingService class implements some slightly more exciting logic. Here, we assign the article to the author and trace the article’s progress.:

public class WritingService : IWritingService
{
    private readonly IAuthorRepository _authorRepository;
    private readonly IArticleRepository _articleRepository;

    public WritingService(IAuthorRepository authorRepository, IArticleRepository articleRepository)
    {
        _authorRepository = authorRepository;
        _articleRepository = articleRepository;
    }

    public async Task ChangeArticleStatusAsync(Guid articleId, WritingStatus writingStatus)
    {
        var article = await _articleRepository.GetByIdAsync(articleId);
        if (article == null)
        {
            return;
        }

        article.WritingStatus = writingStatus;
        await _articleRepository.UpdateAsync(article);
    }

    public async Task StartWritingAsync(Guid authorId, Guid articleId)
    {
        var author = await _authorRepository.GetByIdAsync(authorId);      
        if (author == null)
        {
            return;
        }

        var article = await _articleRepository.GetByIdAsync(articleId); 
        if (article == null) 
        {
            return;
        }

        article.AuthorId = author.Id;
        await _articleRepository.UpdateAsync(article);
    }
}

The StartWritingAsync() method assigns the existing author to the existing article. That marks the start of the work on a particular article. As the author progresses, we can move the article status to editing and then to publishing with the ChangeArticleStatusAsync() method. 

The Driven Adapters

As we already mentioned, there are two ports for driven adapters, so we implement them here. 

Like most applications, we want to persist our entities’ state. With that in mind, we use the Microsoft.EntityFrameworkCore package to abstract our database. In this demo, we use only in-memory databases, but with straightforward changes, we can use any database for which there is an Entity Framework provider. First, we define our DbContext: 

public class WritingDbContext : DbContext
{
    public DbSet<Author> Authors { get; set; }

    public DbSet<Article> Articles { get; set; }

    public WritingDbContext(DbContextOptions options): base(options)
    {        
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseInMemoryDatabase("WritingDb");
    }
}

After that, we add the extension for the WritingDbContext class to seed our memory database with some data for testing purposes: 

public static class WritingDbContextExtensions
{
    public static void AddSeedData(this WritingDbContext dbContext)
    {
        var author1 = new Author
        {
            Id = Guid.NewGuid(),
            Name = "Author 1",
            Balance = 0,
        };

        var author2 = new Author
        {
            Id = Guid.NewGuid(),
            Name = "Author 2",
            Balance = 0,
        };

        dbContext.Authors.Add(author1);
        dbContext.Authors.Add(author2);

        dbContext.SaveChanges();

        var article1 = new Article
        {
            Id = Guid.NewGuid(),
            Title = "Title 1",
            Description = "Description 1",
            Difficulty = Difficulty.Beginner,
        };

        var article2 = new Article
        {
            Id = Guid.NewGuid(),
            Title = "Title 2",
            Description = "Description 2",
            Difficulty = Difficulty.Intermediate,
            AuthorId = author1.Id,
        };

        var article3 = new Article
        {
            Id = Guid.NewGuid(),
            Title = "Title 3",
            Description = "Description 3",
            Difficulty = Difficulty.Expert,
        };

        dbContext.Articles.Add(article1);
        dbContext.Articles.Add(article2);
        dbContext.Articles.Add(article3);

        dbContext.SaveChanges();
    }
}

In a similar project, we implement repositories to change our stored entities. Let’s look at the ArticleRepository class as the AuthorRepository class is logically the same: 

public class ArticleRepository : IArticleRepository
{
    private readonly WritingDbContext _dbContext;

    public ArticleRepository(WritingDbContext dbContext)
    {
        _dbContext = dbContext;
    }
    public async Task AddAsync(Article article)
    {
        await _dbContext.Articles.AddAsync(article);   
        await _dbContext.SaveChangesAsync();
    }

    public async Task DeleteAsync(Guid id)
    {
        var article = _dbContext.Articles.FirstOrDefault(x => x.Id == id);

        if (article != null)
        {
            _dbContext.Articles.Remove(article);
            await _dbContext.SaveChangesAsync();
        }
    }

    public async Task<IEnumerable<Article>> GetAllAsync()
    {
        return await _dbContext.Articles.AsNoTracking().ToListAsync();
    }

    public async Task<Article?> GetByIdAsync(Guid id)
    {
        return await _dbContext.Articles.FirstOrDefaultAsync(x => x.Id == id);
    }

    public async Task UpdateAsync(Article article)
    {
        _dbContext.Articles.Entry(article).State = EntityState.Modified;
        await _dbContext.SaveChangesAsync();
    }
}

All the classes, the DbContext override, and two repositories include standard use of the Entity Framework. To learn more about Entity Framework, check out our Entity Framework Core Series.

Another driven adapter is the MessagePublisher class. We don’t have a concrete implementation in this demo project, but rather just a placeholder for a true implementation:

public class MessagePublisher : IMessagePublisher
{
    public Task PublishMessageAsync(Message message)
    {
        return Task.CompletedTask;
    }
}

The Driving Adapters

In this demo project, we implement only one driving adapter in the form of WebAPI. It consists of three controllers where we expose endpoints to the consumers. Within them, we are calling our application logic. In this case, the application logic is driven by these APIs. 

Let’s take a look at the WritingController implementation: 

[ApiController]
[Route("[controller]")]
public class WritingController : ControllerBase
{
    private readonly ILogger<WritingController> _logger;
    private readonly IWritingService _writingService;

    public WritingController(IWritingService writingService, ILogger<WritingController> logger)
    {
        _logger = logger;
        _writingService = writingService;
    }

    [HttpPost]
    public async Task<ActionResult> StartWriting(Guid authorId, Guid articleId)
    {
        await _writingService.StartWritingAsync(authorId, articleId);
        _logger.LogInformation("Author {authorId} started to write article {articleId}", authorId, articleId);

        return StatusCode(201);
    }

    [HttpPut]
    public async Task<ActionResult> ChangeStatus(Guid articleId, WritingStatus writingStatus)
    {
        await _writingService.ChangeArticleStatusAsync(articleId, writingStatus);
        _logger.LogInformation("Status of the article {articleId} change to {writingStatus}", 
                               articleId, writingStatus);
        return NoContent();
    }
}

In the controller, we inject the appropriate services to execute the application logic exposed by the particular endpoint. We also log the call and return the response. To explore more about creating a WebAPI, have a look at our ASP.NET Core Series.

The ASP.NET Core Application

And finally, to wire it all up, in the application project, we add our services to the IoC container:

public static class ServiceExtensions
{
    public static void AddPersistence(this IServiceCollection services)
    {
        services.AddDbContext<WritingDbContext>(opt => opt.UseInMemoryDatabase("WritingDb"));
        services.AddScoped<IAuthorRepository, AuthorRepository>();
        services.AddScoped<IArticleRepository, ArticleRepository>();
    }

    public static void AddBusinessServices(this IServiceCollection services)
    {
        services.AddScoped<IAuthorService, AuthorService>();
        services.AddScoped<IArticleService, ArticleService>();
        services.AddScoped<IWritingService, WritingService>();
    }

    public static void AddMessaging(this IServiceCollection services)
    {
        services.AddScoped<IMessagePublisher, MessagePublisher>();
    }
}

The Program class configures the middleware and runs the application. 

The Advantages, the Disadvantages, and Possible Use Cases

As with every predefined pattern, the hexagonal architectural pattern brings some advantages but isn’t a magic bullet for all scenarios. Let’s explore some of the pros and cons.

The Advantages of the Hexagonal Architectural Pattern

In the center of this pattern is the extracting external dependencies from core application logic. This brings some benefits:

  • decoupling – separation of concerns between different parties
  • scalability – loose coupling between components enables more effortless application scalability 
  • testability – clear boundaries make testing each component easy 
  • adaptability – an adapter for a particular port can be easily exchanged 
  • maintainability – changes in one layer have minimal (and ideally no) impact on other layers

The Disadvantages of the Hexagonal Architectural Pattern

The introduction of new terms and the different approaches to software development bring also some drawbacks:

  • increased complexity – the code base introduces several concepts that don’t exist in a traditional approach
  • harder debugging – having parts of the logic in different components makes the application more complex to debug
  • entity translations – entities are transferred between different layers and must be adapted to it, which requires some translations
  • steeper learning curve – additional ramp-up may be required to grasp the new architectural concepts

Potential Use Cases

Considering the above advantages and disadvantages, here are some sample project types for which the described pattern may be a good fit:

  • Complex applications with excessive interaction with external components – the pattern helps manage these interactions cleanly. We treat each external component as a separate adapter, promoting clean separation.
  • Applications requiring high scalability and maintainability – the separation of core logic from external concerns makes the system more maintainable and easier to understand. It also allows parts of the system to scale or update independently.
  • Projects needing high testability – testing becomes more accessible because the core logic can be tested in isolation from external services. You can use mock implementations or stubs for the ports while testing, especially in unit testing.
  • Projects with frequent changes in external components – if your application frequently adapts to different external systems or the implementations of these systems are subject to change, the hexagonal architecture can insulate the core business logic from these changes.
  • Legacy project refactoring scenarios – when refactoring legacy systems for better maintainability and testability, the hexagonal architecture can be a valuable approach to gradually decouple business logic from external dependencies.
  • Applications with clear domain logic – if your application has a rich domain model or complex business rules, Hexagonal Architecture helps isolate this logic, making the system more coherent and easier to reason about.
  • Systems with different user interfaces – if your application needs to support multiple types of user interfaces (e.g., web, mobile, CLI), Hexagonal Architecture can allow you to plug in these different interfaces as separate adapters.
  • Integration-heavy applications – for applications that integrate with numerous external services (e.g., payment gateways, third-party APIs), managing these integrations as adapters helps keep the core logic clean and focused.

Conclusion

The hexagonal architectural approach pattern was one of the first patterns to address component dependencies in an application. This concept paves the way for other approaches and thinking about how to make software easier to understand, extend, and maintain.

When designing software, we should consider the benefits of using a given pattern versus the problems the pattern brings to our solution with its specific requirements. Also, being a pattern, we are free to adapt it to our specific needs and requirements.

We hope this introduction to the Hexagonal Architectural pattern motivates you to try this approach in your next .NET project!

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!