Hexagonal Architecture in Action

I’ve been an advocate of the single-responsibility principle for a long time. I’ve used it effectively on several projects to make sure that each individual class or function has a singular purpose. It’s definitely kept me from making an unholy mess out of some of the more complicated projects I’ve worked on.

However, particularly with large projects, I kept feeling like I needed another layer of abstraction. Something that would help organize all these simple, tiny functions into a cohesive whole. Something that would help guide the structure of the app as new features were added. That’s roughly where my head was at when I began to read about hexagonal architecture.

Hexagons to the Rescue!

I’ve heard and read about hexagonal architecture several times over the years, sometimes by its other name “Ports and Adapters.” I also think that the concept of “Functional Core, Imperative Shell” is closely related. The concept seemed like it might provide the next layer of structure I was seeking.

Recently, I started on a project that seemed like a particularly good candidate for hexagonal architecture. It featured a handful of unrelated dependencies. Additionally, it was both large enough to benefit from hexagonal architecture and small enough to provide a good testing ground. In this post, I’ll discuss my experience and results.

Case Study

In my case, I’m working on a cross-platform mobile app using Xamarin that tracks data from a Bluetooth device. You can think of it like the app for FitBit, though in my case, the actual device is quite different.

Basically, the app has to do three things:

  1. Connect to the device via Bluetooth and gather data from messages initiated by the device
  2. Track the data from the device in an internal database
  3. Render a visualization of recent data to the user

Here’s how I structured the app, using hexagonal architecture to guide the organization:

  • Inner layer: Common data structures and core business rules.
  • Middle layer: Submodules for Bluetooth, data persistence, and UI rendering and events. Each submodule can utilize common data structures, but they don’t talk to each other.
  • Outer layer: Thin layer for handling events from the middle layer, generally by instrumenting the use of one or more submodules.

Let’s examine each layer in detail:

Inner layer: common data structures and core business rules

In the innermost layer, I define the core data structures to be used by the rest of the app. These are generally tightly coupled to the business domain. I’m using a Redux-like pattern in my app, so I also define the structure of the global state as well as my actions and reducers. Finally, any business rules that can be severed from external dependencies can live here, as well.

All those rules are defined in a functional way, free of side effects. Consequently, this layer is trivial to reason about, and trivial to test.

Middle layer: submodules for Bluetooth, data persistence, and rendering the view

The common data structures act as a lingua franca for the layers in the middle. Each of these submodules exposes one main interface to consumers, which may include event hooks for actions triggered from within the module. For example, the Bluetooth interface may provide a hook for when a new Bluetooth message is received, or the rendering/UI interface may provide a semantically meaningful hook when a certain UI element is clicked.

Each interface provides a single point of access to the submodule and hides implementation specifics. For example, to store our data, we are using an internal SQLite database, but the interface does not expose the database or any data types which are specific to the database (e.g. ORM classes).

When defining the interface, submodules use either the common data structures or specific types defined by the submodule. Because of this, changes to implementation do not require a change to interface. For example, we could change our persistence layer, even to the point of using HTTP to persist to an external back end, and the interface could stay the same. Compare and contrast this with, for example, trying to excise the use of Active Record methods from a typical Rails project.

Central to this pattern is the notion that an external dependency should be contained to one module. If, for instance, I felt the need to add the SQLite library to the Bluetooth module, that would be a strong indicator of a problem in my architectural design.

Because each submodule only deals with one external dependency, this layer is fairly straightforward to reason about and to test, if slightly less so than the inner layer.

Outer layer: thin layer for handling events and instrumenting submodules

So what happens when I need to perform an operation that involves two submodules? That’s where the outer layer comes in. This layer, which I sometimes call the “sagas” layer in honor of redux-saga, handles events triggered by users or by external dependencies, generally by having a short function dispatch to one or more submodules in the inner layer.

Say, for example, that when an event comes in, we want to save the data and then send an ACK back to the Bluetooth layer. That might look something like:


public class MyAppEvents 
{
/* ... * /
    public async Task HandleNewData(EventData data) 
    {
        await _persistence.SaveData(data);
        await _bluetooth.SendAck();
    }
/* ... */
}

Generally, I try to keep code here as short as possible. Otherwise, it will devolve back into the same inter-connected mess I am trying to move away from. That said, even if the balance isn’t always perfect, I still have a clear list of the main events in the app and how they are handled, and I can write straightforward system or integration tests for each event.

Considerations

There are a few extra hexagonal architecture considerations I’d like to discuss that don’t quite fit into any one layer.

Detailed input and return types

If you are in a statically typed language, I can’t stress enough the value of defining types whenever you have a specific set of data. That way, your consumers don’t have to guess what part of a return type or an input type is relevant to the function–everything is relevant. Additionally, if your language has algebraic types–or you don’t mind creating them longhand–you can have a function explicitly indicate a list of outcomes.

I think this is much more helpful than relying on try/catch for anything off the happy path, or relying on the consumer to know what your bool return type “means.”

Using submodules to organize around dependencies

I think this is what was missing in previous projects, where I mis-applied the single-responsibility principle to require that each class should have one or two functions, max. I still stand by that opinion for code inside the submodules, or anything in the inner layer. However, I’ve come to the conclusion that the “single responsibility” of a top-level submodule class is to provide a single point of contact between a submodule and the rest of the app.

That said, if you feel you need 40 or 50 functions to provide your “single point of contact,” it still seems like a smell to me. There’s a good chance that a better division of submodules could streamline their responsibilities, and thus the number of functions in each module.

For me, a useful question is: “Do these functions feel like ‘siblings’ in terms of complexity?” That is, are some much more high-level than others? Are some much more generic than others? If so, you may be missing a level of abstraction.

Testing

I’ve stated above how each layer of the hexagonal architecture lends itself to a specific type of test, but it is also worth noting that you could write “unit” tests at high layers, using fakes or mocks for the lower layers. This can be a good fit sometimes if, for example, you want to test a series of side effects in isolation from some complicated business rules.

Scaling

I would say my app is small-to-medium in size. Accordingly, I really can’t offer great guarantees as to how well this approach scales. That said, I love how when I go to add a new feature, I know exactly where each part should go. And while submodules do grow over time, they don’t lose their “focus” quite as easily as code that is constantly intertwining external dependencies. I think I’m starting to get a feel for the “pit of success,” and I like it.

Setting up your project structure for success

This project uses standard .NET organization, including .sln, .csproj, and NuGet for dependencies. While sometimes the .NET build system can seem large and unwieldy, in this case, I actually found it helpful.

I split up my submodules into their own .csproj projects, which requires me to be very explicit about a) external dependencies and b) internal dependencies of one project on another. Once the basic structure is defined, I can’t violate the hexagonal architecture unless I add a project reference or a NuGet package inappropriately. That’s much easier to notice than if everything were in one .csproj, where I am one “using” statement away from breaking the architecture.

Conclusions

At least for the time being, I’m sold on this architectural pattern. It seems to provide smart guardrails as software grows in complexity. I want to try it out on an API server project next, as I feel like the standard back end frameworks provide some challenges in terms of thinking hexagonally. Plus, the overall structure of a back end seems to me to go down and up (top-level HTTP handler -> process business rules & persistence -> return data to HTTP handler), rather than up and down (submodule event -> outer layer handler -> submodule function(s)). So I’m interested to see what adapting the pattern for a server API will be like.

And just in general, if you have any stories of how you’ve implemented hexagonal architecture, I would love to hear them.