Patterns to be used judiciously 3: Hexagonal Onion
Hexagonal onion architecture as a layered architecture. Graham Berrisford 2023

Patterns to be used judiciously 3: Hexagonal Onion

A solution architect works in the gap between enterprise architects and software architects. This is one of several articles written to help solution architects understand the software architecture of enterprise applications, and to supplement the syllabuses covered in our courses to industry certificates for architects of all threee kinds.

The article summarises what I understand of the application architecture described by Herberto Graca, which he calls Explicit Architecture (shown above as a layered architecture). It combines several patterns and ideas, including what people call hexagonal and onion architectures, and domain-driven design.

Contents: Basic principles and patterns. User interface layer. Application layer. Domain model layer. Componentization into microservices. Postcripts on decoupling and constraints.

Remember: "The common, costly mistake is to build an infrastructure and use tools much heavier weight than are needed, or to undertake a sophisticated design approach that you aren’t committed to carrying all the way."

“Domain-Driven Design: Tackling Complexity in the Heart of Software” Erik Evans 2002

I welcome comments from professional software developers about their experience.

Basic principles and patterns

This section outlines three ideas

  • Layered architecture
  • Hiding implementations behind interfaces
  • Inversion of control

The layered architecture

As in domain-driven design, the middle layer of the classic three layer architecture is divided in two:

User interface (presentation) layer: presents information to users; receives input, recognises user commands and queries and passes then to the

  • Application layer: progresses the state of use cases, coordinates tasks to complete them, delegates business logic work to domain objects in the
  • Domain model layer: progresses the state of business domain objects, and applies business rules to them.

Infrastructure layer: provides technical services, e.g., persistence, security, and communications with other systems.

Hiding implementations behind interfaces

The four layers are defined by, and encapsulated behind, the interfaces they provide and require. The same is true of components within a layer. This helps you change or replace the implementation of a layer, or a component, with minimal impact on whatever depend on operations in its interface.

Adapters are components that connect the core application to tools in layers above and below it. Driving adapters tell the application what to do; they specify how user interfaces can use an application. Driven adapters are told by the application what to do; they specify how application can use infrastructure tools.

Graca calls the point where an adapter connects to the application layer a port. In most languages, and in its simplest form, a port is an interface. For simplicity, although a port might actually be composed of several interfaces and DTOs, I use the term interface in this article.

For this pattern to work as it should, interfaces must be created to fit the particular application's needs - not simply mimic the infrastructure tools’ APIs. While the application layer contains its own provided and required interfaces, the adapters sit outside of it (as the graphic shows).

Aside for coders

The driving adapters are controllers or console commands “injected in their constructor” with an object that implements the interface.

  • “An interface can be a repository interface that a controller requires. The concrete implementation of the repository is then injected and used in the controller. Alternatively, an interface can be a command or query bus interface. In this case, a concrete implementation of the command or query bus is injected into the controller, who then constructs a command or query and passes it to the relevant bus.”

A driven adapter implements an interface, and is then injected into the application layer, where and whenever the interface is required.

  • “For example, let’s suppose that we have a naive application which needs to persist data. So we create a persistence interface that meets its needs, with a method to save an array of data and a method to delete a line in a table by its ID. From then on, wherever our application needs to save or delete data we will require in its constructor an object that implements the persistence interface that we defined.
  • Now we create an adapter specific to MySQL which will implement that interface. It will have the methods to save an array and delete a line in a table, and we will inject it wherever the persistence interface is required. If at some point we decide to change the database vendor, let’s say to PostgreSQL or MongoDB, we just need to create a new adapter that implements the persistence interface and is specific to PostgreSQL, and inject the new adapter instead of the old one.”

Inversion of control

The third idea, related to the second, is called “inversion of control”. (You may not find that label clear, but we're stuck with it). Adapters depend on a specific tool and the specific interface they implement. For example:

  • the interface provided by an FTP server contain scores of operations
  • the interface required by an application might contain only two or three file-handling operations.

Since the application layer depends only on the interfaces it provides and requires, it doesn’t depend directly on the interface of any particular FTP server, or other infrastructure tool. However, the adapter that sits between the FTP server and the application must understand both interfaces.?

Application Layer

The application layer progresses the state of use cases (required processes), coordinates tasks to complete them, and delegates business logic work to the domain layer.

Users interfaces send commands and queries that trigger use cases/processes in the application layer, which may be specific to one user interface, or shared by several.

Typically, a command handler or application service:

  1. retrieves the data for one or several entities from a persistent data store
  2. acts on that data, or delegates that data processing to the domain layer
  3. restores changed entity data to the data store.

The application layer also triggers other use case outcomes - application events such as sending emails, notifying a 3rd party API, sending a push notification, or even starting a different use case.

The logic to progress and complete a use case is found in command/query handlers and/or application services. A command/query handler can be designed in two different ways:

  • More procedural: it contains the logic of the required process.
  • More object-oriented: it receives a command and triggers logic in one or more application services to complete the required process.

Aside on application modularisation

Even a so-called "monolithic" enterprise application is modularised in some way. It may be modularised in ways that are more object-oriented or more procedural. E.g., Martin Fowler contrasts his Rich Domain and Transaction Script design patterns for enterprise application architecture.

  • “However much of an object bigot you become, don’t rule out Transaction Script. There are a lot of simple problems, and a simple solution will get you up and running much faster.”… Many… scripts act directly on the database, putting SQL into the procedure... The simplest Transaction Scripts contain their own database logic.” Martin Fowler

So, a command or query handler might be coded as a procedure that accesses the persistent data structure, perhaps via a simple interface. And reuse? Where two procedures have the same access path, they may share a subroutine. Where two commands have the same effect on an entity or aggregate entity, they might share a data-centric subroutine.

Systematically factoring out what transactions share by way of common code might produce the most economical application layer code. Graca discusses the more object-oriented modularisation of the kind in domain-driven design. It is not certain this kind of modularisation will be more performant or adaptable, but most do presume it will be more adaptable.

Domain model layer

The domain (model) layer progresses the state of business domain objects, and applies business rules to the updating and generation of data.

Graca says the domain layer is independent of the uses cases or processes that trigger business logic, and independent and unaware of the application layer. So, the domain model depends on nothing outside the domain layer. (For me, a domain model is an entity-event model that includes the input events that update entities, but let that pass.)

The domain model is composed of domain objects that each represent something in the domain. These objects contain data and operations that manipulate that data in ways specific to the business domain.

The primary objects are entities or aggregate entities of interest (such as resource, movement, and location, in the logistics example I use). But the domain model can contain also value [immutable] objects, enums [constant variables] and other objects.

When an entity changes state, the domain model may generate a domain event, conveying the changed state variable values to any subscriber or interested party. A log of these events might be used in “event sourcing".

Some domain logic does not sit happily inside a domain model object. A solution is to create a domain service, which receives a set of entities and performs some business logic on that data. A domain service knows nothing about the application services or data stores, but it can use domain model objects and other domain services.

Componentisation into microservices

The graphic above shows horizontal layers of classes/objects. The dotted lines divide the application/domain layer code vertically into what might be called microservices for sub-domains or bounded contexts. Click on microservices architecture for more about this idea.

The way Graca sees it, a microservice cannot change data it does not “own”, but he says it can query and use data owned by other microservices. If he means to allow a microservice to directly access and query persistent data maintained by another microservices, this will offend those who demand data is encapsulated.

Graca also suggests that, in a use case, when component A needs component B to do something, it cannot make a direct call to a class/method in component B, because that would couple to B to A too tightly. However, components A and B, can be decoupled by inserting a message broker between them, or by inserting a controller over both of them that invokes each in term.

Further reading

Here is a link to Herberto Graca’s article (November 16, 2017) which goes on to discuss the pattern in more detail, and related matters.

Postscript on decoupling and constraints

On decoupling

Graca treats decoupling as the ultimate goal of system design. He says “the goal, as always, is to have a codebase that is loosely coupled and high cohesive, so that changes are easy, fast and safe to make”.

For example, he proposes an interface in front of each data store, and any abstraction (ORM) layer in front of a data store. So you can more easily replace not only the database management system, but also the abstraction layer.

Decoupling is not itself a goal. It can lead to code that is “interfaced to hell”, which complicates things and hampers performance.

As Craig Larman said, in his book on Applying UML and patterns, “It is not high coupling per se that is the problem; it is high coupling to elements that are unstable in some dimension, such as their

  • interface [definition of services provided or required],
  • implementation [vendor-specific technology], or
  • mere presence [availability].”

"If we put effort into future proofing or lowering the coupling when we have no realistic motivation, this is not time well spent. Focus on the points of realistic high instability or evolution.”

Whether you need all the elements of this Explicit architecture is up to you.

On constraints

Traditionally, OO programmers speak of a data server as though it is generic infrastructure tool like any other; and does nothing but persist and retrieve data.

Yet in a structured database (as opposed to a schema-less one) the data structure (as defined in a logical data model or a physical database schema) is a definition of business domain-specific terms, concepts, rules, variable types, other constraints on variable values, and how types relate to each other.

Given a "defensive design" principle, developers might conclude it helpful to use a database management system to impose constraints as close to the stored data as possible, especially rules of following kinds.

  • An order is placed by one and only one customer (a foreign key must exist as a primary key).
  • Deleting a customer will automatically delete its orders. Or else, a customer cannot be deleted until after all its orders have been deleted, or moved to a closed state..
  • A customer must have a telephone number.
  • A telephone number is of a numeric data type.
  • An email address must contain an @ sign.
  • A movement cannot be scheduled on a date when the source or destination locations are in the closed state.

Alternatively, you can minimise constraint rules imposed by the data server, and minimise your dependence on any constraints that do exist, there by coding them (or tighter rules) in the application/domain layer.

Further reading

A solution architect works in the gap between enterprise architects and software architects. This is one of several articles written to help solution architects understand the software architecture of enterprise applications, and to supplement the syllabuses covered in our courses to industry certificates for architects of all threee kinds.


Adail Retamal

CSEP Systems/Software Engineering - Improving the world through better systems

1 年

I liked your highlights on the Domain Model. I learned to think and design using this approach with Peter Coad, and I still use his UML in Color profile when modeling either with classes or tables. One method that relies heavily on domain models is FDD (Feature-Driven Development).

回复

要查看或添加评论,请登录

Graham Berrisford的更多文章

  • On complexity in systems thinking

    On complexity in systems thinking

    "Systems thinking" is not one coherent and consistent science. Different system theorists have different ideas of what…

    2 条评论
  • How we abstract systems

    How we abstract systems

    This article is about how - in System Dynamics, cybernetics, sociology, enterprise architecture and software…

  • How we assess truth

    How we assess truth

    "Nice and well-stated." It is fashionable to say (as Prince Harry did) that "my truth” is as true as any objective…

    1 条评论
  • Who we are

    Who we are

    "I think therefore I am." Descartes.

    1 条评论
  • How we typify things

    How we typify things

    "Very helpful" It is a law of nature that similar things, in similar situations, appear similar and behave similarly…

  • Entity Event Modeling (EEM)

    Entity Event Modeling (EEM)

    An entity event model not only relates entity types, but also specifies events that affect the entities, and so…

  • Determinism and free will

    Determinism and free will

    "A very interesting article, especially about rule violations and AI." One of the complexities a systems thinker must…

    7 条评论
  • On Peirce's categories

    On Peirce's categories

    I have very little to say about logic and linguistics and most of the many things that Peirce wrote about. This article…

    42 条评论
  • The structure/behavior dichotomy

    The structure/behavior dichotomy

    This article discusses how we can describe the state and progress of things by carving the world into discrete entities…

  • Causality

    Causality

    One of the complexities a systems thinker must get their head around (in simple systems, let alone complex ones) is…

    71 条评论

社区洞察

其他会员也浏览了