How we write backend code — Part 1

Hassan Ibrahim
Taager Tech Blog
Published in
7 min readJul 6, 2022

Clean Architecture (CA) is the system architecture guideline proposed by Robert C. Martin (Uncle Bob) derived from many architectural guidelines like Hexagonal Architecture, and Onion Architecture, among others.

Eric Evans introduced the concept of Domain-Driven Design (DDD). He wrote about it in his book Domain-driven Design in 2004 (aka “The Big Blue Book”).

Domain-Driven Design is an approach to software development that centers the development on programming a domain model with a rich understanding of the processes and rules of a domain.

We at Taager strive to adopt Domain-Driven Design (DDD) in our work, and our mission was to get the most out of DDD and CA If possible. As the adoption grows, we are getting closer and closer to the business and the domain we’re tackling; we start speaking the domain’s ubiquitous language. This article is an excellent start to getting some more knowledge on DDD.

Note: There is no such architecture that fits all. Every software development architecture or pattern has pros and cons; base your decision on the project, the scope, and the team. This article will describe the path we took.

This article will focus on how we structure the code according to DDD and Clean Architecture, so the code also speaks the domain’s ubiquitous language more easily.

First, we split the system into smaller independent parts around the business sub-domains through a few iterations (DDD strategic tools such as event storming, storytelling, and more). Ideally, these parts should be independently deployable (micro-services), but this is not always the case. Often we could have a legacy code that we can’t easily change, so we have to maintain it for a while. In such cases, we have these sub-domains in a single (monolith) project, with each sub-domain in a separate folder or package.

Bounded Context Code Structure

After splitting the extensive domain into smaller parts, also called sub-domains. Then we try to solve each sub-domain; a “bounded context” will implement a sub-domain. Each bounded context might be a separate micro-service or a separate package wrapping this bounded context inside a current service. So let’s talk about this part now, how we design each bounded context, how many high-level layers we have, and how they would communicate together.

Sample Bounded contexts in an E-Commerce system

├───wallet       
├───orderManagement
├───shipping
├───..

Command vs. Query (CQRS)

The first layer we have in each bounded cotext is commands and queries. What do they mean?

Every use case in a system could be considered a Command or Query, where a Command is any use case that changes the system’s current state, while a Query is any use case that fetches the current state WITHOUT changing the current state. Since these two have different concerns, we have decided to use the CQRS pattern (Command Query Responsibility Segregation)

So, we have very high-level folders; each contains the rest of the layers, which we will discuss later on this page to isolate these two concerns.

├───shipping
| ├───commands
| ├───queries

Architecture Layers

Having clear layers in our code where each layer has clear responsibility makes it suitable for us to identify the dependency direction, easily test the code, work in parallel without waiting for each other, and more.

We agreed to have the following layers

  • Domain Layer
  • Application Layer
  • Infrastructure Layer
├───shipping
├───commands
├───application
├───domain
├───infrastructure
├───queries
├───application
├───infrastructure

You probably noticed that the queries sub-folder doesn’t have a domain layer! The following section will explain the reasons why.

Domain Layer

The domain layer is the central part of a bounded context, containing the core domain and all business invariants and logic for this bounded context. It shouldn’t depend on any layer or third-party library or framework. Instead, all layers depend on it. Typical content in this layer is as follows.

├───shipping
├───commands
├───domain
├───models
├───Shipment
├───Order
├───ShippingCompany
├───OrderStatus
├───Receiver
├───services
├───ShippingService
├───events
├───ShipmentCreated
├───ShipmentDelievered
├───contracts
├───ShipmentRepo
├───OrderRepo
├───ShippingCompanyProvider

Domain Components

In a nutshell, we have domain models, domain services, events, and contracts. There are mainly three types of domain models wrapping the business logic:

  • AggregateRoot,
  • Entity, and
  • Value Object.

And any code that doesn’t fit in any of these models should go into a domain service. We also have the events produced by the AggregateRoots whenever something changes in the model. And finally, the contracts/interfaces for any infrastructure implementation domain might need.

For more details about this layer, stay tuned for part 2

Application layer

This thin layer acts as an API that exposes the bounded context functionalities through use cases.

It’s the direct client of the domain model, responsible for task coordination (orchestration) of use case flows. Also, when using an ACID database, the application layer controls transactions, ensuring that the application atomically persists model state transitions.

Note: It is a mistake to consider the Application use case the same as Domain Services. They are not. The contrast should be stark. We should strive to push all business domain logic into the domain model, whether in Aggregates, Value Objects, or Domain Services. Keep Application Services thin, using them only to coordinate tasks on the model. (vaughn vernon)

A typical Application layer consists of two folders as follows:

├───shipping
| ├───commands
| ├───application
| ├───usecases
| ├───CreateShipment
| ├───OrderConfirmedEventHandler
| ├───models
| ├───CreateShipmentRequest
| ├───OrderConfirmedEvent
| ├───queries
| ├───application
| ├───usecases
| ├───ListShipments
| ├───models
| ├───ListShipmentsQuery

We implement a separate class per use case; each class contains a single method called execute something like command pattern

There are three use case types:

  1. Request to do something (CreateShipment, UpdateShipmentStatus)
  2. Query something (GetShipments)
  3. Event Handler (OrderReceivedEventHandler)

Infrastructure layer

The job of the infrastructure is to provide technical capabilities for other parts of our application. It contains any database, IO, or network implementations, such as MongoDB, Postgres, Analytical services, Controllers, File system access, or memory cache.

It is helpful to maintain a Dependency Inversion Principle mentality. So wherever application or domain layers need infrastructure details, we depend on interfaces. So when an Application use case looks up a Repository, it will depend only on the interface from the domain model but using the implementation from the infrastructure.

A simple diagram to illustrate how that works is as follows.

The Application Service depends on the Repository interface from the domain model but uses the implementation class from infrastructure. The packages encapsulate broad responsibilities.

├───shipping
| ├───commands
| ├───infrastructure
| ├───controllers
| ├───services
| ├───repositories
| ├───..

Repositories:

Repositories are classes or components that encapsulate the logic required to access data sources. They centralize common data access functionality, providing better maintainability and decoupling the infrastructure or technology used to access databases from the domain model layer.

For each aggregate or aggregate root, you should create one repository class.

We create one repository class for each aggregate or aggregate root since the aggregate root doesn’t allow access to its child entities directly to enforce aggregate variants. We need to pull the whole aggregate from the database, perform actions, and save it.

That said, be cautious about designing your aggregate to avoid performance penalties. For example, don’t design an aggregate root containing a list of entities that increases over time. You will need to pull a significant portion of data every time you operate on this aggregate. As a result, you might end up with a very slow operation and, even worse, OutOfMemory!.

Something like a Wallet aggregate root with a list of transactions is an example of such a bad design; the transaction of a wallet increases with time. There are multiple solutions for such cases; one is to use event sourcing.

The last point to mention here is that the repository hides different data sources used from the domain layer so that it can use MongoDB and Redis without any changes in the interface.

Controllers:

  • We strive to have a single controller per API. For example, GetUserController and SaveUserController. Single controller per API keeps the controllers smaller and to the point.
  • A controller receives the request, maps it to the Application model, calls the appropriate application use case, and maps the result to the desired response entity.

How does a complete flow look?

The following two diagrams explain the implementation of each typical flow.

Typical Command Flow

Typical Command Flow

Typical Query Flow

Typical Query Flow

Again, this is how we structure the code at Taager, and it has worked well for us so far. However, it might not be the best option in all cases. We’ll focus more on the domain layer in the next part of this series. Stay tuned!

--

--