How to combine DDD, Hexagonal, Onion, Clean, CQRS

Tram Ho

The article is translated from the source

This article is part of the Annals of Software Architecture , part of a series of articles on Software Architecture . In those articles, I write about what I’ve learned about Software Architecture, how I think about it, and how I use it. The content of this article will make more sense if you have read the previous articles in the aforementioned series.

Basic components of a system

I will start from EBI and Ports & Adapters architectures. Both of these architectures explicitly distinguish between what is internal and what is external and what is used to connect internal code & external code .

Furthermore, Port & Adapters also define 3 basic components of a system:

  • User interface
  • Business logic or application core – used by the UI to perform the business.
  • Infrastructure – connects the application core to tools like DB, or search engines or 3rd party APIs.

000-explicit-architecture-svg

Application core is what we should pay the most attention to. It is the component that allows our code to perform the main business of the system. It may use some user interface (CLI, API, …) but its functionality remains the same and it doesn’t need to care how the user interface triggers it.

As you can imagine, the typical flow of that application is going from the user interface, through the application core, to the infrastructure code, back to the application core and finally returning the response to the user interface as shown below.

010-explicit-architecture-svg

Tools

Going further than the application core, we have tools that the application will use such as the database engine, search engine, Web server or CLI console.

020-explicit-architecture-svg

Putting the CLI console in the same “bucket” as the DB engine may seem odd, but in reality it’s the tools used by the application.

The main difference here is that while the CLI console and web server are used to tell the application to do something , the database engine is told by the application to do something .

Connect tools and put mechanisms into Application core

Code units that connect tools to the application core are called adapters . Adapters implement code that allows business logic to interact with a tool and vice versa.

Adapters that tell the application to do something are called Primary or Driving Adapters while adapters that are told by the application are called Secondary or Driven Adapters .

Ports

The Adapters themselves are not randomly generated, they are created for the purpose of matching Application Core inputs, here Port . Port is like a specification of how the tool uses the application core or how the application core uses the tool.

In practice these ports will be:

  • Interface
  • Combination of interfaces
  • DTOs

One important thing here is that Ports (interfaces) belong inside the business logic while Adapter will be outside the business logic.

Primary or Driving Adapters

The Primary (Driving) Adapter will wrap the port and “command” the Application core to “work”. The Adapter will be responsible for converting everything from the delivery mechanism to the method call in the Application Core.

030-explicit-architecture-svg

In other words Driving Adapters are Controllers or Console commands, these Adapters will inject objects (which are instances of classes that implement the ports – interfaces that the controller needs).

More specifically, the Port here can be Service Interface or Repository Interface that the controller needs. Port implementations are then injected into the controller.

Alternatively, the port can also be Command Bus Interface or Query Bus Interface .

Secondary or Driven Adapters

Unlike Driving Adapter (which will wrap ports), Driven Adapter will implement port – interfaces are injected into Application core

040-explicit-architecture-svg

For example, suppose we have an application that needs to save and delete data. We will create an interface that meets the above needs with two methods, save and delete . Application when needing to save or delete data just “request” the object to be an instance of the class that implements that interface in its constructor.

Suppose initially, we use MySQL to manage the DB, but then we want to switch to PostgreSQL or MongoDB, then we just need to implement the interface above, modifying Driven Adapter so that it uses the correct functions. preset of PostgreSQL or MongoDB, then inject it into the application core’s constructor.

Inversion of control

One of the notes about this pattern is that adapters depend on specific tools and ports (because they will implement port – interface) but business logic depends only on port (interface), the port itself is also designed. To match what the business logic needs, the port itself won’t depend on a particular adapter or tool.

050-explicit-architecture-svg

This means that the dependency relationship will point to the heart of the system – this is called inversion of control at the system architecture level.

Another thing to note here is that:

Ports were created to meet the needs of Application Core, not simply reimagining tools APIs.

Application Core organization

Application Layer

Usecase is a “process” – it will be triggered inside Application Core of the system with some elements like the external User Interface, …

Usecase is defined inside the Application Layer – this is the first layer provided by DDD and used by the Onion Architecture.

060-explicit-architecture-svg

This layer will include:

  • Application Services (and their interfaces) – as a kind of “first-class citizen”.
  • Port & Adapters interfaces (may include ORM interfaces, search engine interfaces, messaging interfaces).
  • Can contain Handlers for Command and Query Buses.

Application Services covers usecase implementation logic. Specifically, their duties are:

  1. Use the repository to find one or several entities.
  2. Ask entities to perform domain logic.
  3. Use the repository to store entities or save data changes.

Command Handlers can be used in 2 different directions:

  1. They may include logic to implement the use case.
  2. They can also be used as “communication” wires in the system, receiving Commands and triggering logic located in the Application Service.

Which approach to choose depends on our situation:

  • Do we already have the Application Service and now just need to add the Command Bus ?
  • Whether the Command bus allows specifying a certain class / method as a handler or extending the available classes or interfaces to serve its purpose.

This layer can also include triggering Application Events . These event trigger logic is the side-effect of the usecase, for example:

  • Send email.
  • Send notifications to 3rd party APIs.
  • Push notifications.
  • Or start/call a usecase that belongs to another component in the application.

Domain Layer

Objects in this layer include data editing logic according to different logical domains. It is independent of the usecase (business logic).

070-explicit-architecture-svg

Domain Services

Sometimes we will see domain logic calling multiple entities but not belonging to a particular entity, we will feel that the logic is not the responsibility of any one entity.

So our first reflex is to put them in the Application Service layer, but in reality, moving them to another layer will make those logical domains cannot be reused in different usecases, therefore:

Keep the domain logic away from the application layer.

The solution here is to create a Domain Service, which takes in a set of entities and executes some business logic on them. Domain service belong to Domain layer so they don’t know anything about Application layer classes like Application Services or Repositories .

In addition, Domain Service can also use other Domain services as well as other Domain Model objects .

Domain Model

Deep inside the system center, independent of anything but it – will include business objects representing logical domains, typical examples here are Entities , Value-Objects .

The domain model is also where Domain events exist, these events are fired when a certain data set is changed. In other words when the entity changes, the Domain Event is fired and it takes care of the change in the value of the properties. These Events will be very useful especially when used in Event Sourcing .

Components

In the previous sections, we have mentioned splitting code based on layers. Referring to code splitting, we also have split by feature (Package by feature) or split by component (Package by component) and split by layer (Package by layer) as above.

These divisions are explained in great detail in the blog Package by component and architecturally-aligned testing by Simon Brown.

Screen Shot 2023-03-08 at 8 34 23

Personally, I prefer “Package by component”, here is a diagram for this approach taken from Simon Brown’s article.

Packaging 4_3

The code will divide components into layers one by one as shown above. We see that components can be Billing, User, Review or Account but they are always domain related. Bounded context like Authorization or Authentication should be seen as external tools to serve when we create adapters and hide them behind ports.

080-explicit-architecture-svg

Decoupling components

Similar to dividing code into units such as (classes, interfaces, mixins, …), reducing interdependencies between components will also bring us significant benefits.

To separate classes from each other, we can use Dependencies Inject – by injecting dependencies into the class instead of instantiating it, making the class depend on abstractions (interface or abstract classes) instead of classes specifically. Therefore, the class that injects dependencies does not know anything about the class implementing the interface or the abstract class.

Likewise, separating components will also make these components ignorant of each other. It also means that Dependency Injection and Dependency Inversion are not enough to separate components so we need to refactor the architecture. We may need shared kernel, eventual consistency and even discovery service.

100 - Explicit Architecture

Trigger logic in other components

When one of our components (component B) needs to perform a task if an event occurs in component A, we cannot make a direct call from component B to component A because then these two components will depend on each other.

But we can also proceed for component A to dispatch an event and the event listener inside component B will listen to the above event, when receiving the event, it will proceed to trigger the logic that needs to be done inside. component B. That means that component A will depend on event dispatcher but is completely separate from component B.

Also, if the event itself “exists” in A then this means that B will know about the existence of A so it will depend on A. To remove this dependency we will create a library with a set of application core functionality that will be shared between components – we call it Shared Kernel . This means that the components will depend on Shared Kernel but are “independent” of each other.

Shared Kernel will include features such as:

  • Application events
  • Domain events

However, we should keep this Shared Kernel as “small” and “light” as possible because any change in this shared kernel will affect all the components that depend on it. Suppose we have a multilingual system, a micro-services ecosystem, for example, each service will be written in a different language, so the shared kernel needs to be written in a language that other services are understandable. We take the example with Event class , it will contain properties like:

  • Event name
  • Event description

is written in JSON , for that reason it can be read and interpreted by different languages ​​of services. You can read more in my following article: More than concentric layers

explicti_arch_layers

This approach works well with both monolithic apps and distributed apps as micro-services ecosystem. Since the event will be fired asynchronously, it is not possible to trigger logic located in other components immediately.

Having component A make an HTTP call to component B directly makes these two components dependent on each other, so to make them separate, we can use discovery service – component A will have to “ask” the discovery service to know the destination address to send requests to, in addition discovery service can also proxy requests to related services and then return responses to the requester.

The above approach will probably make the components depend on discovery service , but it will keep the components from being dependent on each other.

Get data from other components

As we have seen, it is not allowed for one component to change another component’s data. But it is possible for a component to query and use data from another component.

Data Storage shared between components

For example billing component needs to know the name of the client that belongs to account component , now billing component needs to send a data query to shared data storage to get the data it needs. Shared data storage is a collection of many different data, but components that use this data can only be used in read-only mode (ie, used through queries).

Data Storage is separate by component

In this case, each component will have its own storage, each storage will include:

  • A dataset is owned by the component and can only be edited by the component itself.
  • Another data set is a copy of the data of other components, the component that owns this set is only allowed to query for its function, not edit anything, it needs to be updated when the “master” component changes. these star data.

Each component will create its own copy of the data (local form) copied from other components. When other components change data, the component itself will trigger an event, other components that are holding a copy of the data will listen for this event (domain event) and update their local data.

Flow of control

As shown above, our flow will be user → application core → user. But how can classes be “in tune” with each other? Which depends on which? And how to combine them?

When there is no Command/Query Bus

In case we do not use the command bus, the controller will depend on either the Application Service or the Query Object.

4_3 UMLish_1

Query Object will return raw data to the user. Data returned as DTO – will be injected into ViewModel – ViewModel can have some view logic inside it.

Appliation Service – will contain usecase logic, these logic will usually perform data editing instead of just plain data view. The Application Service will depend on Repositories – which return the entites that include the logic that needs to be triggered. In addition, it can also depend on Domain Service – coordinating domain processes between entities.

In addition, after completing the use case logic, the Application service may notify the whole system, so the application service may depend on dispatcher to trigger the event.

In the above figure we use the interface for the persistence engines and the repositories. They have the following purposes:

  • The Persistance interface is the abstraction layer above the ORM so that we can easily change the ORM without affecting the Application Core.
  • The repository interface is an abstraction of the persistence engine. Let’s say we want to switch from MySQL to MongoDB. The Persistence interface will not change, only the class implementing it will change. So whether it’s MySQL or MongoDB, just meet the interface’s mechanism.

When Command/Query Bus

4_3 UMLish_2

Now the controller will depend on the command and the query. Command and query will be initialized and transmitted to the Bus – The bus will be responsible for finding the right handler to handle the commands.

summary

Our ultimate goal is still to create a codebase with low dependencies that can be easily changed and extended.

All of the above information is only conceptual, but if we understand all of them, we can build a “healthy” architecture and system.

But application development is a highly practical field, depending on each case, we will apply our knowledge in the most flexible way, which are the elements that make up the architecture. system .

We need to understand patterns but we also need to think and really understand what our application needs, and how far we can go in separating components.

This decision will depend on a lot of factors be it the functional requirements of the system can also be the life cycle of the product as well as the level of the dev team,…

Above is all that I have systematized.

In addition, I also expand on a few ideas in the article More than concentric layers

So how can you fulfill the above requirements for your code base? That’s exactly the point of my next article: how to apply architecture and domain to code.

Share the news now

Source : Viblo