Skip to content

Hexagonal Architecture

Denys Poltorak edited this page Mar 5, 2026 · 9 revisions

Trust no one. Protect your code from external dependencies.

Known as: Hexagonal Architecture, or originally as Ports and Adapters.

Variants:

By placement of adapters:

  • Adapters on the external component’s side.
  • Adapters on the core side.

Examples – Hexagonal Architecture:

Examples – Separated Presentation:

Examples – Cell:

  • Basic Cell / Cluster [DEDS],
  • Full-Featured Cell / Domain.

Structure: A monolithic business logic extended with a set of (adapter, component) pairs that encapsulate external dependencies.

Type: Implementation.

Benefits Drawbacks
Isolates business logic from external dependencies Suboptimal performance
Facilitates the use of stubs/mocks for testing and development The vendor-independent interfaces must be designed before the start of development
Allows for qualities to vary between the external components and the business logic
The programmers of business logic don’t need to learn any external technologies

References: Herberto Graça’s chronicles is the main collection of patterns from this chapter. Hexagonal Architecture has the original article and a brief summary of its layered variant in [LDDD]. Most of the Separated Presentation patterns are featured on Wikipedia and there are collections of them from Martin Fowler, Anthony Ferrara and Derek Greer.

Hexagonal Architecture is a variation of Plugins that aims for total self-sufficiency of business logic. Any third-party tools, whether libraries, services or databases, are hidden behind adapters [GoF] that translate the external module’s interface into a service provider interface (SPI) defined by the core module and called port. The core’s business logic depends only on the ports that its developers defined – a perfect use of dependency inversion – and manipulates interfaces that were designed in the most convenient way. The benefits of this architecture include the core’s cross-platform nature, easy development and testing with stubs or mocks, support for event replay and protection from vendor lock-in. It also allows for replacement of any external library at late stages of the project. The flexibility is paid for with a somewhat longer system design stage and lost optimization opportunities. There is also a high risk to design a leaky abstraction – an SPI that looks generic but whose contract matches that of the component it encapsulates, making it much harder than expected to change the component’s vendor.

Stubs and mocks are test doubles – simplistic replacements for real-world components. They are used to run the business logic in isolation – without the need to deploy any heavyweight libraries or services the logic may depend upon. A stub supports a single usage scenario in a single test case while a mock is more generic – its behavior is programmed on a per test basis.

Performance

Hexagonal Architecture is a strange beast performance-wise. The generic interfaces (ports) between the core and adapters stand in the way of whole-system optimization and may add context switching. Still, at the same time, each adapter concentrates all the vendor-specific code for its external dependency, which makes the adapter a perfect single place for aggressive optimization by an expert or consultant who is proficient with the adapted third-party software but does not have time to learn the details of your business logic. Thus, some opportunities for optimization are lost while others emerge.

In rare cases the system may benefit from direct communication between the adapters. However, that requires several of them to be compatible or polymorphic, in which case your Hexagonal Architecture may in fact be a kind of shallow Hierarchy. Examples include a service that uses several databases which are kept in sync through Change Data Capture (CDC) or a telephony gateway that interconnects various kinds of voice devices.

Dependencies

Each adapter breaks the dependency between the core that contains business logic and an adapted component. This makes all the system’s components mutually independent – and easily interchangeable and evolvable – except for the adapters themselves, which are small enough to be rewritten as need arises.

Applicability

Hexagonal Architecture benefits:

  • Medium-sized or larger components. The programmers don’t need to learn details of external technologies and may concentrate on the business logic instead. The code of the core becomes smaller as all the details of managing external components are moved into their adapters.
  • Cross-platform development. The core is naturally cross-platform as it does not depend on any (platform-specific) libraries.
  • Long-lived products. Technologies come and go, your product remains. Always be ready to change the technologies it uses.
  • Unfamiliar domain. You don’t know how much load you’ll need your database to support. You don’t know if the library you selected is stable enough for your needs. Be prepared to replace vendors even after the public release of your product.
  • Automated testing. Stubs and mocks are great for reducing load on test servers. And stubs for the SPIs which you wrote yourself are easy as a pie.
  • Zero bug tolerance. SPIs allow for event replay. If your business logic is deterministic, you can reproduce your user’s bugs in your office.

Hexagonal Architecture is not good for:

  • Small components. If there is little business logic, there is not much to protect, while the overhead of defining SPIs and writing adapters is high compared to the total development time.
  • Write-and-forget projects. You don’t want to waste your time on long-term survivability of your code.
  • Quick start. You need to show the results right now. No time for good architecture.
  • Low latency. The adapters slow down communication. This is somewhat alleviated by creating direct communication channels between the adapters to bypass the core.

Relations

Hexagonal Architecture:

Variants by placement of adapters

One possible variation in a distributed or asynchronous Hexagonal Architecture is the deployment of adapters, which may reside adjacent to the core or with the components they adapt:

Adapters on the external component side

If your team owns the component adapted, the adapter may be placed next to it. That usually makes sense because a single domain message (in the terms of your business logic) tends to unroll into a series of calls to an external component. The fewer messages you send, the faster your system is.

This resembles Sidecar [DDS] and Open Host Service [DDD].

Adapters on the core side

Sometimes you need to adapt an external service which you don’t control. In that case the only real option is to place its adapter together with your core logic. In theory, the adapter can be deployed as a separate component, maybe in a Sidecar [DDS], but that may slow down communication.

This approach resembles Ambassador [DDS] and Anticorruption Layer [DDD] and is usually found in Cells.

Examples – Hexagonal Architecture

Hexagonal Architecture protects business logic from all its dependencies. It is simple and unambiguous. It does not come in many shapes:

Hexagonal Architecture, Ports and Adapters

Just like MVC it is based on, the original Hexagonal Architecture (Ports and Adapters) does not care about the contents or structure of its core – it is all about isolating the core from the environment. The core may have layers or modules or even plugins inside, but the pattern has nothing to say about them.

DDD-Style Hexagonal Architecture, Onion Architecture, Clean Architecture

As Hexagonal Architecture built upon the DDD’s idea of isolating business logic with Adapters, it was quickly integrated back into DDD [LDDD]. However, as Ports and Adapters appeared later than the original DDD book, there is no universal agreement on how the thing should work:

  • The cleanest way is for the domain layer to have nothing to do with the database – with this approach the application asks the repository (the database adapter) to create aggregates (domain objects), then executes its business actions on the aggregates and tells the repository to save the changed aggregates back to the database.
  • Others say that in practice the logic inside an aggregate may have to read additional information from the database or even depend on the result of persisting parts of the aggregate. Thus it is the aggregate, not the application, which should save its changes, and the logic of accessing the database leaks into the domain layer.
  • Onion Architecture, one of early developments of Hexagonal Architecture and DDD, always splits the domain layer into a domain model and a domain services. The domain model layer contains classes with business data and business logic, which are loaded and saved by the domain services layer just above it. And the upper application services layer drives use cases by calling into both domain services and domain model.
  • There is also Clean Architecture which seems to generalize the approaches above without delving into practical details – thus the way it saves its aggregates remains a mystery.

Examples – Separated Presentation

Separated Presentation protects business logic from a dependency on Presentation Layer [DDD] (interactions with the system’s user via a window, command line, or web page). There is a great variety of such patterns, commonly known as Model-View-Controller (MVC) alternatives. They are derived from Hexagonal Architecture by omitting every component not directly involved in user interactions and make three structurally distinct groups:

  • Bidirectional flow – the view (user-facing component) both receives input and produces output and there is often an extra adapter between it and the main system, resulting in Layers.
  • Unidirectional flow – the controller receives input while the view produces output, forming a kind of Pipeline.
  • Hierarchical with multiple models, discussed in the Hierarchy chapter.

All of them aim at making the business logic presentation-agnostic (thus cross-platform and developed by an independent team), but differ in their complexity, flexibility and best use cases.

Model-View-Presenter (MVP), Model-View-Adapter (MVA), Model-View-ViewModel (MVVM), Model 1 (MVC1), Document-View

MVP-style patterns pass user input and output through one or more presentation layers. Each pattern includes:

  • View – the interface exposed to users. In Hexagonal Architecture’s terms it is an adapter for the OS GUI framework.
  • An optional intermediate layer that translates between the view and model, beneficial for when the internal representation of the domain in the model diverges from the way it is presented to users by the view. It is this component which differentiates the patterns, both in name and function.
  • Model – the whole system’s business logic and infrastructure, now independent from the method of presentation (CLI, UI or web).

Document-View [POSA1] and Model 1 (MVC1) skip the intermediate layer and connect the view directly to the model (document). These are the simplest Separated Presentation patterns for UI and web applications, respectively.

In a Model-View-Presenter (MVP), the presenter (Supervising Controller) receives input from the view, interprets it as a call to one of the model’s methods, retrieves the call’s results and shows them in the view, which is often completely dumb (Passive View). A complex system may feature multiple view-presenter pairs, one per UI screen.

A Model-View-Adapter (MVA) is quite similar to MVP, but it chooses the adapter on a per session basis while reusing the view. For example, an unauthorized user, a normal user, and an admin would access the model through different adapters that would show them only the data and actions available with their permissions.

A Model-View-ViewModel (MVVM) uses a stateful intermediary (ViewModel or Presentation Model) which resembles a Response Cache, Materialized View, Reporting Database or Read Model of CQRS – it stores all the data shown in the view in a form which is convenient for the view to bind to. Changes in the view are propagated to the ViewModel which translates them into requests to the underlying application (the true model). Changes in the model (independent or resulting from user actions) are propagated to the ViewModel and, eventually, to the view.

All those patterns exploit modern OS or GUI frameworks’ widgets which handle and process mouse and keyboard input, thus removing the need for a separate (input) controller (see below).

Model-View-Controller (MVC), Action-Domain-Responder (ADR), Resource-Method-Representation (RMR), Model 2 (MVC2), Game Development Engine

When your presentation’s input and output diverge (raw mouse movement vs 3D graphics in UI, HTTP requests vs HTML pages in websites), it makes sense to separate the presentation layer into dedicated components for input and output.

Model-View-Controller (MVC) [POSA1, POSA4] allows for cross-platform development of hand-crafted UI applications (which was necessary before universal UI frameworks emerged) by abstracting the system’s model (its main logic and data, the core of Hexagonal Architecture) from its user interface containing platform-specific controller (input) and view (output):

  • The controller translates raw input into calls to the business-centric model’s API. It may also hide or lock widgets in the view when the model’s state changes.
  • The model is the main UI-agnostic application which executes controller’s requests and notifies the view and, optionally, controller when its data changes.
  • Upon receiving a notification, the view reads, transforms, and presents to the user the subset of the model’s data which it covers.

Each widget on the screen may have its own model-view pair. The absence of an intermediate layer between the view and model makes the view heavyweight as it has to translate the model's data format into something presentable to users – the flaw addressed by the 3-layered MVP patterns discussed above.

Both Action-Domain-Responder (ADR) and Resource-Method-Representation (RMR) are web layer patterns. An action (method) receives a request, calls into a domain (resource) to make changes and retrieve data and brings the results to a responder (representation) which prepares the return message or web page. ADR is technology-agnostic while RMR is HTTP-centric.

Model 2 (MVC2) is a similar pattern from the Java world with integration logic implemented in the controller.

A game development engine creates a higher-level abstraction over input from mouse / keyboard / joystick and output to sound card / GPU while more powerful engines may also model physics and character interactions. The role is quite similar to what the original MVC did, with a couple of differences:

  • Games often have to deal with the low-level and very chatty interfaces of hardware components, thus the input and output are at the bottom side of the system diagram.
  • The framework itself makes a cohesive layer, becoming a kind of Microkernel.

Another difference is that while MVC provides for changing target platforms by rewriting its minor components (view and controller), you are very unlikely to change your game framework – instead, it is the framework itself that makes all the platforms look identical to your code.

Examples – Cell

Application of the isolation principle behind Hexagonal Architecture to a distributed subsystem results in a Cell (according to WSO2, not Amazon) – an encapsulated cluster of Services which implements a subdomain and is usually deployed as a single unit.

In the simplest implementation a Cell’s contents are hidden from the Cell’s clients by a Cell Gateway which acts as an Open Host Service [DDD], allowing for anything inside the Cell to be changed at will with no effect on the outside world.

Better developed Cells employ Adapters and Plugins for outgoing requests to build an Anticorruption Layer [DDD] that protects the Cell’s contents from changes in its environment.

This dichotomy is similar to that between Separated Presentation (indirection between the system and its users or clients) and Ports and Adapters (full isolation of the business logic from its environment).

In practice there are three kinds of outgoing traffic: responses to incoming client requests, pub/sub notifications, and requests to external services. In a Cell, responses are sure to pass through or be generated by the Cell Gateway. Indeed, a response usually reuses the request’s transport, therefore if a request arrives at the Gateway, the corresponding response should also start there. Notifications are harder to pinpoint: on one hand, they are a part of the Cell’s API which the Gateway takes care of. However, that means that the Cell’s internals must be aware of the Gateway’s existence to use it for notifications, thus creating a dependency that violates the normal order for a layered system. Finally, outgoing requests bypass the Cell Gateway whose role is limited to the *Cell’*s API.

Cells facilitate recursive decomposition by subdomain. They are the building blocks for the following patterns:

Basic Cell, Cluster

A Cell (WSO2 definition) or Cluster [DEDS] may naturally emerge when a subdomain service becomes too large for comfortable development, which usually means that at least its domain layer (already limited to a single subdomain) is to be subdivided into sub-subdomain components. If other layers remain intact, this leads to a Sandwich, otherwise the result is a subsystem of Services or a Pipeline.

As it is undesirable to let the new components appear at the top system level because that would increase the system’s integration complexity:

  • The newly created subservices are hidden behind a Cell Gateway (which coincides with the topmost layer in case of a Sandwich) so that everything outside of the Cell remains ignorant of what is within it, and the Cell contents will thus retain the freedom to change.
  • Everything within a Cell is deployed and scaled together to avoid the overhead of versioning and keep the number of system components small (the entire Cell is operated as a single service).
  • As a rule [DEDS], the communication inside a Cell remains synchronous, allowing for complex orchestrated use cases that involve many tightly coupled Cell components. Contrariwise, there is usually asynchronous messaging between loosely coupled Cells, which make a choreographed system.

As a matter of fact, forming a Cell tackles the complexity of an overgrown service without leaking any implementation details to its clients or committing to irrevocable architectural decisions. We will be able to make gradual changes to our Cell’s code and structure as our clients see only the Published Language [DDD] of our Cell’s API via its Cell Gateway, which thus becomes an Open Host Service [DDD].

Another way a Cell can arise is when architects have overcommitted themselves to Microservices, gradually introducing hundreds of them and turning their project into a Microservice Hell. Now they need to group their services to:

  • Have a clear high-level picture of what is going on in the system.
  • Cut accidental dependencies between their services and the teams behind them.
  • Improve latency by co-locating the services that interact intensely.

Basic Cells have a downside: even though we have protected our clients from changes in our implementation, our code is still vulnerable to changes in any external services which it uses. To fix that, we will need to add a layer of indirection for outgoing communication as well:

Full-Featured Cell, Domain

Regardless of how a Cell emerges, the isolation of its contents can be improved through the addition of Adapters for any external services used by the Cell. Please note that, unlike in Ports and Adapters, there is no Adapter for a Cell’s database(s) as it is inside the Cell’s perimeter.

Another improvement, popularized by Uber’s Domains, is the use of Ambassador Plugins which run other services’ business logic inside your Cell. That both avoids slow intercell calls and boosts the system’s fault tolerance as each Cell can now operate independently.

In Uber the service responsible for a driver’s status accepts Plugins from other services, such as safety checks or compliance, which can block the driver from appearing in the system and responding to ride requests.

The Adapters and Plugins make the Cell’s Anticorruption Layer [DDD], completing the Cell’s boundary that allows for the Cell’s logic to be implemented and tested in isolation from the external environment.

Summary

Hexagonal Architecture isolates a component’s business logic from its external dependencies by inserting adapters between them. It protects from vendor lock-in and allows for late changes of third-party components but requires all the APIs to be designed before programming can start and often hinders performance optimizations.

<< Plugins ^ Implementation metapatterns ^ Microkernel >>

Table of Contents:

Introduction
Foundations of software architecture
Basic metapatterns
Extension metapatterns
Fragmented metapatterns
Implementation metapatterns
Analytics
Appendices

Fast Navigation:

Websites about Patterns:

Clone this wiki locally