Clean Domain-Driven Design

George
Technical blog from UNIL engineering teams
9 min readJan 4, 2022
Photo by Max Kleinen on Unsplash

Introduction

This post will present an opinionated approach to building applications using DDD and Clean Architecture. By “opinionated” I mean that I will argue for a particular way of addressing several well-known concerns in design and architecture of an application. This, of course, does not mean that this is the only correct way of going about implementation of these concerns. It is however a fruit of a long though process and research in the area of DDD and architecture. Here is my motivation.

There numerous posts and questions about concepts central to DDD on the web. Here is just one example of a question on SO, which asks about aggregates and the place of the business logic which enforces the rules spanning several aggregates. It is this post which gave me the idea for the domain I will use through out my post and my example application. A lot of people seem to be very unclear about such things as the exact aggregate boundaries, aggregate roots, navigation between entities (inside the aggregates), aggregate persistence (with or without an ORM) and where to put the logic dealing with cross-aggregates concerns and operations. And it is understandable, these are indeed complicated topics very dependent on the exact nature of the problem domain.

On the other hand, there have been interesting developments in the area of architecture and design which seem to complement DDD approach. I am talking here about approaches like Clean Architecture and CQRS. Here is a very interesting discussion about similarities and differences between these approaches. So, thinking about all this, made me wonder if I can come up with a relatively simple way of combining the very best of the ideas from several of these approaches to showcase a way of building a robust application which is easy to understand and to maintain.

Example application

The example application is available in this GitHub repository. It deals with very simple domain: there are two entities: Student and Course . A student can enroll in one or several courses. Each course keeps track of the number of students enrolled in it and each student keeps track of the courses where she is enrolled in. Obviously, the application must allow creation and editing of new Course or a new Student independently of each other. Enrolling of a student in a course, however, must guarantee that if, and only if, a student has successfully registered her enrollment in a course, that course’s number of enrolled students is incremented accordingly.

Even though the domain is very simple, it allows us to focus on several important issues. How do we go about modeling this domain in terms of aggregates and aggregate roots? Do we make one of the entities (e.g. Course the aggregate root with dependent entity, Student ? Or vice-versa? Or maybe we need another (root) aggregate, say Enrollment which will contain the other two? Where will the logic of enrollment go, i.e.: where and how should we enforce our business rule about the number of of enrolled students of a course?

Shift the focus from DDD to Clean paradigm

Approach which I advocate for calls for a general shift of focus from entities in a pure DDD sense to Use Cases as described by Robert C. Martin in his very-well known article.

Going from DDD to Clean Architecture approach
Shifting focus from “Entities” to “Use Cases”

Here is a very interesting article by Carmine Ingaldi, where he talks about domain services as a “missing pattern” in DDD. I strongly agree with the main premise of his article and I would further assert that it is exactly the use cases which bring forth this “missing” functionality of domain services even better. In my opinion, use cases are slightly different from domain services: they demarcate a specific buisines scenario by using explicit and focused nomenclature. But the essence of either a domain service or a use case remains the same: they introduce a small piece of inter-aggregate business logic which is inherently procedural in nature.

Recently, I’ve discovered that Sandro Mancuso have been advocating for many similar ideas in his work with “Outside-In Design” and “Interaction Driven Design”. I strongly encourage the reader to check out some of his publications and presentations.

So going back to our example: instead of trying to figure out whether we should model one, two or three separate aggregates, we focus on the possible use cases for our application. We need to be able to create a Course, we need to be able to create a Student , and we should be able to associate a student with a course, in a persistent manner. But first things first, let’s start by looking onto the persistence.

No n-arity relations between aggregates

If you look at the way Student and Course are modeled, you will see that Student has a set of IDs to the courses where she is enrolled in.

Of course, this is simply following a well-known best-practice of not referencing any related aggregates (aggregate roots) other than by their IDs. But there is more, if we now look at how we persist Student entity, we see that the corresponding JPA entity also does not declare any relation to the JPA entity corresponding to Course model.

This is how entity-relation diagram looks in my trusty DBeaver.

Entity-Relations diagram for persistence layer

The idea here is to avoid complicated graph of domain model objects which will inevitably result in a cobweb of OneToMany and ManyToOne relations on the corresponding JPA entities, which in turn will complicate ORM operations. What we should do instead is something similar to what has always worked in the CQRS world. Here is a seminal article by Udi Dahan where he brings attention to an interesting point. (What follows bellow is a paraphrase of his points.) In the section entitled “Whither the domain model?”, he brings the reader’s attention to the fact that the write-side (command) model does not really needs that many complicated relations between aggregates since, if following CQRS, this model will never be queried. Indeed, it is the projections, or the read-side models, de-normalized and specifically tailored to the needs of a particular UI which will be queried directly.

This gives us an idea (also used by Sandro Mancuso with his “fast-track” paths for queries) to separate persistence operations into two parts. For all C(r)UD operations on the model, including possibly simple look-ups for individual entities, we shall use the ORM mapping JPA entities to model aggregates. But for queries involving two or more aggregates, we will use simple JDBC queries with “JOINs” directly done in the SQL and the results communicated to the presenter via dedicated DTOs (more on this later).

Persistence approach inspired by CQRS

The important points here are as follows. First, the granularity of the model aggregates is at its finest: each aggregate regrouping just one entity with few value objects. The relations between aggregates are modeled via collections of pointers (IDs) to other aggregates. Second, the application uses ORM to persist each aggregate individually and in the sequence prescribed by each particular use case. Third, when an information is needed which involves two or more related aggregates, the direct SQL query is issued against the persistence store joining the results and returning a specialized DTO, again, specifically designed for the use case at hand.

You can follow through the two different paths that the example application takes depending on whether we are executing createStudent , createCourse , enroll use cases (“command” path on the diagram above). Or if we are executing findEnrollmentsForStudent use case (“query” path).

Use cases for inter-aggregate logic with transactional demarcation

Let’s look now how do we enforce the inter-aggregate rule which requires that the states of Student and Course are to be updated together if at all. As I have already mentioned, this logic will be implemented in a use case.

You see how in the enroll use case orchestrates the inter-aggregate business logic by successively executing public methods on Student (to add a new course ID to the set of courses IDs where the student is enrolled in) and on Course (to increment the number of the students enrolled). Use case also persist Student and Course entities after the successful execution of these public methods.

Notice javax.transaction.Transactional annotation on the use case, it is very important. If any of the methods executed on any of the entities fails unpredictably, it will ensure that the state of the aggregates is rolled back to the consistent state. It is also the role of the presenter to roll back any live transaction in case of any (handled) error.

It is very important to realize that leveraging use cases for inter-aggregate logic orchestration, as in this example gives us some considerable advantages.

  • Use cases, unlike domain services, can employ any of output ports for any operations required by the business logic. This is particularly useful when obtaining entities from the persistence layer. In a way, use cases in Clean paradigm are the combination of application services and domain services in the classical DDD sense.
  • The use case is procedural in nature, but very focused on the business requirement at hand. This increases understandability of the code. Use cases also named after very specific business scenarios.
  • We have a complete control on the exact sequence of the methods to execute during each particular use case.
  • Transactional demarcation on the level of a use case assures that, in the case of an error or an exception, the state of aggregates will remain consistent.
  • Following the principles of Port and Adapters (Clean) architecture, use cases are very easy to test.

DTOs, Value Objects, and immutable Entities

Domain-Driven design calls for extensive use of such tactical patterns as Value Objects and Entities. This is certainly the case in our application. Moreover, I think, the “query” type use cases should work either directly with the DTOs returned from the persistence layer or map these DTOs to the value objects. This is because, these objects are the representation of several attributes of related aggregates destined to be used in the UI layer for the very specific business scenario. In our example, the use case for a listing of all enrollments for a particular student operates with Enrollment value object mapped from a row of the JDBC query result (DTOs): EnrollmentRow .

Another idea, I would argue for, is for the making domain entities immutable. Here is the example of how we can achieve this for Course entity.

Domain entity can also be immutable

Notice how enrollStudent method does not actually mutate the existing Course instance but rather returns a new instance of Course with the number of students increased by one. Also notice a handy private Builder object (Lombok) which allows us quickly create a copy of any existing instance with only a particular attribute updated.

The main idea behind using immutable entities is that we can pass them from Use Case layer to the outer layers: for example to the Persistence Gateway or the Presenter without worrying that any operations on these instances will impact the business logic performed in the use case.

Conclusion

To summarize, this post argues for a particular way of building applications using a mixture of several paradigms. The approach is illustrated in the example found here. Here are the main points:

  • DDD is used to model the domain entities encapsulating intra-aggregate invariants (validators in constructors).
  • Use cases (from Clean Architecture) are used to orchestrate inter-aggregate business logic in a comprehensible and a targeted way.
  • Transactional demarcation on the level of use cases allows for enforcement of the inter-aggregates rules.
  • An approach borrowed from CQRS paradigm allows for the efficient use of ORM for C(r)RUD operations from “command” type use cases, while simple JDBC queries (with SQL “JOINs”) are used with “query” type use cases requiring access to several related aggregates.

References

  1. GitHub, cleanddd, example application
  2. The Clean Architecture, by Robert C. Martin
  3. DDD/CQRS vs Clean Architecture, discussion
  4. The Domain Driven Design’s Missing Pattern, by Carmine Ingaldi
  5. Sandro Mancuso, several of his publications and presentations
  6. Clarified CQRS, by Udi Dahan

--

--