Hexagonal Architecture

Ports and Adapters, Onion Architecture, Clean Architecture, Functional Core / Imperative Shell

Definition
Ports and Adapters provides application logic without dependencies on external services. The dependencies are replaceable at runtime.

The main aim of this architecture is to decouple the application's core logic from the services it uses (database, email, time), and the services that use it (user interface, framework). This allows different services to be "plugged in", and it allows the logic to be run without these services.

The application logic, of an application consists of the algorithms that are essential to its purpose. They implement the use cases that are the heart of the application. The external services are not essential. They can be replaced without changing the purpose of the application. Examples: database access and other types of storage, user interface components, e-mail and other communication components, hardware devices. In a strict sense of this architecture even the application's framework is a set of services. The core logic of an application should not depend on these services in this architecture (so that it becomes "framework agnosic").

Hexagonal architecture diagram

The number of ports depends on the application. The shown number of 6 ports nicely matches the name of the architecture. But the point of the architecture's name is not that the number six matters, but that the core logic is at the center. A realistic number of ports is about 2 to 4.

A port is an entry point, provided by the core logic. It defines a set of functions.

Primary ports are the main API of the application. They are called by the primary adapters that form the user side of the application. Examples of primary ports are functions that allow you to change objects, attributes, and relations in the core logic.

Secondary ports are the interfaces for the secondary adapters. They are called by the core logic. An example of a secondary port is an interface to store single objects. This interface simply specifies that an object be created, retrieved, updated, and deleted. It tells you nothing about the way the object is stored.

An adapter is a bridge between the application and the service that is needed by the application. It fits a specific port.

A primary adapter is a piece of code between the user and the core logic. One adapter could be a unit test function for the core logic. Another could be a controller-like function that interacts both with the graphical user interface and the core logic. The primary adapter calls the API functions of the core logic.

A secondary adapter is an implementation of the secondary port (which is an interface). For instance, it can be a small class that converts application storage requests to a given database, and return the results of the database in a format requested by the secondary port. It can also be a mock database object needed to unit tests certain parts of the core logic. The core logic calls the functions of the secondary adapter.

How does it work?

The application can be used by different types of users. Each of these can create their own variant of the application, by plugging in custom adapters.

  • An instance of the application is created, as well as the adapters.
  • The secondary adapters are passed to the core logic (dependency injection).
  • The primary adapters receive a link to the core logic. They start to drive the application.
  • User input is processed by one or more primary adapter(s) and passed to the core logic.
  • The core logic interacts with the secondary adapters only.
  • Output of the core logic is returned to the primary adapters. They feed it back to the user.

Variants

The Onion architecture emphasis the layers in the pattern and doesn't stress the ports and adapters. Inner layers define interfaces. Outer layers implement interfaces.

The Clean architecture is similar to the onion architecture and integrates the Entity-Control-Boundary pattern

Functional Core / Imperative Shell is also similar and consists of a core that is purely functional: it is immutable and has no side-effects.

Where does it come from?

Alistair Cockburn invented it in 2005. It is a response to the desired to create thoroughly testable applications. As Cockburn says: "Allow an application to equally be driven by users, programs, automated test or batch scripts, and to be developed and tested in isolation from its eventual run-time devices and databases."

When should you use it?

A strong argument for this pattern is testability. Code that can be run without external dependencies like time, randomness, database, or sending email is easier to test.

Problems

  • It takes a significant mental effort to keep the application logic free of dependencies. Writing unit tests for the code forces you to deal with them, and implementing a Dependency Injection Container or a Service Locator.

Common implementation techniques

  • Dependency injection is used to pass the adapters to the application logic
  • Ports are implemented as interfaces. Adapters implement these interfaces.

Links