Service Development Using Clean Architecture Approach

Huseyin Kutluca
Software Architecture Foundations
7 min readSep 22, 2021

You may have heard of Clean Architecture (Bob Martin), Onion Architecture (Jeffrey Palermo), and Hexagonal Architecture or Port and Adapter (Alistair Cockburn et al.) Although these are approaches put forward by different industry leaders, they basically describe similar things in different terms. Even though, there are principles developed mostly for enterprise software, these principles also apply to mission-critical and algorithm-intensive applications.
I can summarize the common point of the architectural approaches mentioned above as follows: Domain objects and rules do not change very often. On the other hand, the frameworks, middleware technologies, databases changes more frequently. If we are going to build an architecture that will work for a long time, it is necessary to separate the domain logic from technology. Your domain class classes should never be dependent on the classes you use technology and libraries for. In this context, a reverse dependency is suggested.
Almost all articles and examples in Clean/Onion architecture, are in web based It applications. Engineers who wants to use Clean Architecture principals in mission-critical applications (such as Telecom, Automotive, Defense) think that the philosophy is good, but don't know how do I adapt it to their area. In this article, I explained the philosophy of the clean architecture and and explained mission-critical project with a sample project structure.

Layers of Clean Architecture

Domain Entities: These objects form the core of your design and are at the center.
You can identify these business entities and the business use cases with the Domain Driven Design (DDD) approach. Here we are talking about objects such as Personnel, Order, Product, Call, Delivery, Aircraft etc.. If you are using a database, these objects are often your tables. In other cases, you share between applications and store it in list or tree. We design these objects as data objects only, independent of the business domain logic. Even if we are using an object-oriented approach, it is important to keep business objects as separate as possible from classes with complex algorithms. This way we can easily handle these objects in different classes.
Since these domain entities will be used by more than one service in large projects, it would be appropriate to design them as a separate module. In this case, these entity modules will be managed separately with giving versioning and will be the vital part of system integration.
Domain Services (Use Cases): Business logic consists of codes such as creating, updating, processing, sharing between services.
In this layer, business rules and algorithms are implemented. For example, in banking transactions, loan calculation and interest calculation take place here. Calculating the shortest path in a delivery system will likewise be included in the business area logic.
The service you develop usually includes one or more sub-business areas. These sub-businesses represent different use cases. In a good design, it is recommended to identify the sub domains within the service and develop them as separate modules.
When we design in-service structures with these principles, business modules only include programming language features. It is a module that is not dependent on the communication method, the technology used, whether the system is distributed or monolithic, or the user interface infrastructure. Beyond that, in an ideal design, these modules do not include components such as thread, timer or socket. These modules contain mainly passive algorithms and related data structures. It can easily test these algorithms with unit tests (google test etc.). This is actually the most critical gain in terms of writing a good service. Because your critical algorithms can be well tested, you’ll have fewer problems when your service goes to customer site. When you make a change to the system, you will see that you did not break another place by running the unit tests again. In systems that are not designed in this way, the system is tested as a whole by sending data with an external tool, and since the tests are mostly carried out only on the main flows, major problems are experienced at customer site and confidence in the product is reduced from the very beginning.
Application Layer: It determines how we interact with business space use cases. This is the module where the main control and flow operations are located, including the data input from middleware. Here, we receive data with communication technologies such as REST, MQTT, AMQP, gRPC (see Footnote) and provide it to business area services. Operations such as managing the relevant technology, transforming the data into an object from a format such as json or Google Protocol Buffer are done.
It is desired that the domain components are not dependent to this details. So what should we do if we are going to transmit a message to the other services or if we are going to save data to database. We define an interface class in the business area and call this interface from the business area services. In languages ​​like Java we define it as an interface, while in C++ we define it as an abstract class. We then develop the implementation class of this interface in the application layer. Now that we are in the application layer, we can have technology, middleware or database dependencies as we want. With a similar logic, we can make calls from the subdomain module_1 to the subdomain module_2. This approach is actually is a port and adapter pattern.

Example Clean Architectural Structure

The drawing above shows the clean architecture of a C++ service’s folder structure.
The Coremodule includes additional OS wrappers and data structures that are not directly provided by programming language. The more mature the standard libraries provided by the programming language, the less the need for such additional libraries.
In order not to spread the different technologies and libraries used in the project to more than one class and to manage them better, classes can be written to wrap these libraries. The modules where these classes will be placed are located under InfraModules. In the example, a middleware or communication technology named Gamma is wrapped under GammaProtocolInterface. The advantage of this approach is keeping the related library dependencies in one place, configuring, reading and writing for the respective protocol together.

In some large projects, components under both these InfraModules and CoreModules are developed and managed by a separate team and provided as libraries to more than one service. Thus, reusability increases and architectural compatibility between services are better.

Domain use cases are located under Modules. Alpha and Beta modules are given as an example. These are considered to develop algorithms for two different sub-business areas. These modules are dependent neither on each other nor on the technologies used in the project. They may only depend on CoreModule. As you can see in this sample project, both Alpha and Beha modules have a Test folder under them. Unit tests are located under this folder and can test related modules without the need for any technology or communication library. These Alpha and Beta modules can be compiled as libraries for the main project. With this structure, it can be used by other services when needed. In such a case, it is possible to manage the relevant source code here and deliver the version to the other service as a library. In addition, this module can be easily turned into a separate project by moving it to a separate workspace according to the need. In summary, these business area use case modules, which are the most important components of the project, are designed in a very modular structure. Abstract interface classes that we call EventInterface can be included in these services in order to transfer the data produced in this project to other modules or to send them to other services over the communication layer. For example, the AlphaEventInterface class in the Alpha module is an abstract class. Although only one is given in the example, there may be more than one such abstract interface depending on the complexity of the module.

The application layer of the project is the module we specified as MainModule in the drawing. This module is often called the service itself. In this module, first of all, there is the “main” where the main program entry is made. Apart from this, the project also includes tasks(threads) , classes for data receive and send via middleware or communication libraries. If a database connection is required, it is done in this module. One of the most critical components in this module is the development classes of the abstract EventInterface classes defined under Modules. For example, the AlphaEventInterfaceImpl class that will do the main operation of the AlphaEventInterface class in the above example is located under this MainModule. In this way, communication between modules and other technologies is moved out of business logic. Each module modules has its own defined interface (API) class. These modules are accessed via this interface class. In languages ​​such as java that provide interface definition, this interface is defined abstractly and the development details are seen in MainModules. For C++, we may develop an abstract interface with FactoryPattern (ModuleFactory.GetAlhpaImplementation) or PIML (Pointer To Implementation) method can be preferred.

Evaluation

As a result, Clean/Hexagon/Onion Architecture presents an architectural approach that is independent of Framework software, testable, independent of database or user interface, independent of other external units, and distinguishes between business domain objects and business domain logic. Clean Architecture leads to easy-to-maintain, sustainable, high quality software services.

Footnote: Terms

REST: REpresentational State Transfer — An approach that enables data sharing over WEB technology
MQTT: A middleware standard in Message Queuing Telemetry Transport — A Middleware standard in the Publish Subscribe architecture
AMQP:Advanced Message Queuing Protocol — A middleware standard in the Publish-Subscribe architecture
GRPC: Google Remote Procedure Call — Remote procedure call approach

--

--

Huseyin Kutluca
Software Architecture Foundations

Highly motivated Software Architect with hands-on experience in design and development of mission critical distributed systems.