Adapting to change
Software systems tend to get complicated. One reason for this could be frequent changes to
the business requirements and little time to adapt the software architecture accordingly.
Another reason could be insufficient investment for setting up the software architecture at the
beginning of the project to adapt to frequent changes. Whatever the reason, a software system
could get complicated to the point where it is almost impossible to make a change. Therefore, it
is important to build maintainable software architecture from the beginning of the project. Good
software architecture can adapt to changes easily.
This section explains how to design maintainable applications by using hexagonal
architecture that adapts easily to non-functional or business requirements.
Adapting to new non-functional requirements by using ports
and adapters
As the core of the application, the domain model defines the actions that are required
from the outside world to fulfill business requirements. These actions are defined through
abstractions, which are called ports. These ports are implemented by
separate adapters. Each adapter is responsible for an interaction with another system. For
example, you might have one adapter for the database repository and another adapter for
interacting with a third-party API. The domain is not aware of the adapter implementation, so
it is easy to replace one adapter with another. For example, the application might switch from
a SQL database to a NoSQL database. In this case, a new adapter has to be developed to
implement the ports that are defined by the domain model. The domain has no dependencies on
the database repository and uses abstractions to interact, so there would be no need to change
anything in the domain model. Therefore, hexagonal architecture adapts to non-functional
requirements with ease.
Adapting to new business requirements by using commands
and command handlers
In classical layered architecture, the domain depends on the persistence layer. If you
want to change the domain, you would also have to change the persistence layer. In comparison,
in hexagonal architecture, the domain doesn’t depend on other modules in the software. The
domain is the core of the application, and all other modules (ports and adapters) depend on
the domain model. The domain uses the dependency inversion
principle to communicate with the outside world through ports. The benefit of
dependency inversion is that you can change the domain model freely without being afraid to
break other parts of the code. Because the domain model reflects the business problem that you
are trying to solve, updating the domain model to adapt to changing business requirements
isn’t a problem.
When you develop software, separation of concerns is an important principle to follow. To
achieve this separation, you can use a slightly modified command
pattern. This is a behavioral design pattern in which all required information to
complete an operation is encapsulated in a command object. These operations are then processed
by command handlers. Command handlers are methods that receive a command, alter the state of
the domain, and then return a response to the caller. You can use different clients, such as
synchronous APIs or asynchronous queues, to run commands. We recommend that you use commands
and command handlers for every operation on the domain. By following this approach, you can
add new features by introducing new commands and command handlers, without changing your
existing business logic. Thus, using a command pattern makes it easier to adapt to new business
requirements.
Decoupling components by using the service façade or CQRS
pattern
In hexagonal architecture, primary adapters are responsible for loosely coupling incoming
read and write requests from clients to the domain. There are two ways to achieve this loose
coupling: by using a service façade pattern or by using the command query responsibility
segregation (CQRS) pattern.
The service façade
pattern provides a front-facing interface to serve clients such as the presentation
layer or a microservice. A service façade provides clients with several read and write
operations. It’s responsible for transferring incoming requests to the domain and mapping the
response received from the domain to clients. Using a service façade is easy for microservices
that have a single responsibility with several operations. However, when using the service
façade, it is harder to follow single
responsibility and open-closed principles. The single responsibility principle
states that each module should have responsibility over only a single functionality of the
software. The open-closed principle states that code should be open for extension and closed
for modification. As the service façade extends, all operations are collected in one
interface, more dependencies are encapsulated into it, and more developers start modifying the
same façade. Therefore, we recommend using a service façade only if it’s clear that the
service would not extend a lot during development.
Another way to implement primary adapters in hexagonal architecture is to use the CQRS pattern, which
separates read and write operations using queries and commands. As explained previously,
commands are objects that contain all the information required to change the state of the
domain. Commands are performed by command handler methods. Queries, on the other hand, do not
alter the state of the system. Their only purpose is to return data to clients. In the CQRS
pattern, commands and queries are implemented in separate modules. This is especially
advantageous for projects that follow an event-driven architecture, because a command could
be implemented as an event that is processed asynchronously, whereas a query can be run
synchronously by using an API. A query can also use a different database that is optimized for
it. The disadvantage of the CQRS pattern is that it takes more time to implement than a
service façade. We recommend using the CQRS pattern for projects that you plan to scale and
maintain in the long term. Commands and queries provide an effective mechanism for applying
the single responsibility principle and developing loosely coupled software, especially in
large-scale projects.
CQRS has great benefits in the long term, but requires an initial investment. For this
reason, we recommend that you evaluate your project carefully before you decide to use the
CQRS pattern. However, you can structure your application by using commands and command
handlers right from the start without separating read/write operations. This will help you
easily refactor your project for CQRS if you decide to adopt that approach later.
Organizational scaling
A combination of hexagonal architecture, domain-driven design, and (optionally) CQRS
enables your organization to quickly scale your product. According to Conway’s
Law, software architectures tend to evolve to reflect a company’s communication
structures. This observation has historically had negative connotations, because big
organizations often structure their teams based on technical expertise such as database,
enterprise service bus, and so on. The problem with this approach is that product and feature
development always involve crosscutting concerns, such as security and scalability, which
require constant communication among teams. Structuring teams based on technical features
creates unnecessary silos in the organization, which result in poor communications, lack of
ownership, and losing sight of the big picture. Eventually, these organizational problems are
reflected in the software architecture.
The Inverse
Conway Maneuver, on the other hand, defines the organizational structure based on
domains that promote the software architecture. For example, cross-functional teams are given
responsibility for a specific
set of bounded contexts, which are identified by using DDD and event storming. Those bounded contexts might
reflect very specific features of the product. For example, the account team might be
responsible for the payment context. Each new feature is assigned to a new team that has
highly cohesive and loosely coupled responsibilities, so they can focus only on the delivery
of that feature and reduce the time to market. Teams can be scaled according to the complexity
of features, so complex features can be assigned to more engineers.