Revisiting cargo tracking application using Clean DDD

George
Technical blog from UNIL engineering teams
21 min readJul 4, 2022
Photo by Roger Hoyles on Unsplash

In this article we shall take the well-known DDDSample (cargo tracking) application and we shall try to redesign and to reimplement some of its functionality by following the Clean DDD approach. I find this to be a very nice exercise since it allows to compare different architectural approaches while having a familiar domain and similar tooling.

Here is a link to the source code used in this article.

Exploring original DDDSample

Original DDDSample application is a Spring Boot application which can be run from an IDE or as an executable JAR. It uses an in-memory HSQL DB by default. It is helpful to be able to see the contents of the database at runtime. For this, we can configure and start an embedded database engine web server together with the original application. See README.md file in the GitHub repository for detailed instructions.

Using HSQL DB Manager to explore in-memory persistence model used in DDDSample

DDDSample application has several entry points. Here is the welcome screen for “Cargo Booking and Tracking”.

DDDSample application: Cargo Booking and Routing screen

We can proceed to book a new cargo, for example, from Dallas, USA to Melbourne, Australia to arrive no later then 28 June 2022.

DDDSample application: book new cargo

There is also the “Incident Logging Application” for entering handling events (via JMS service). It can be downloaded from this link.

Cargo tracking domain model

DDDSample is a very well structured and nicely designed application which showcases all the main principles of DDD in action. I wanted to use somewhat simplified version of the original domain model and to try to implement some of the use cases in order to illustrate Clean DDD approach.

I’ve used a popular PlantUML modeling tool to create an approximate UML diagram of the Cargo tracking domain (the source file for the diagram is available in the GitHub repository as well).

Original cargo tracking domain

As we can see, there are four aggregates with roots: Cargo , HandlingEvent , Voyage , and Location . We can see that there are some complex value objects, like Delivery , for example. In fact, a large part of business logic for DDDSample actually resides in Value Objects. Another, interesting observation, is that entities and value objects of one aggregate only refer to the root (entities) of other aggregates.

Use cases, principal building blocks of CA

One of the main ideas of Clean Architecture (CA) is that Controller does not know anything about Presenter. Controller limits itself to obtaining an instance of a specific use case and transferring the control to the use case together with any “Request Model” (as a parameter), to use Robert. C. Martin’s terminology, reflecting user’s input. It is the responsibility of the use case to decide what presentation logic (if any) will be executed.

There is one important reason why this separation of Controller — use case — Presenter needs to be enforced. In a use case driven application with a non-trivial logic, a use case logic may result in a process control flow with many branches (if-then-else statements, exception handling, etc.) each of them requiring different presentation logic, and, possibly different “Response Models” (as Robert C. Martin calls them) to be presented.

In a typical MVC architecture, Controller will just call a use case (as a function) processing the returned result (or catching any errors) by calling the appropriate Presenter. So the great deal of sophisticated business logic (related to the process flow of the use case) leaks into Controller. This is not desirable, since this logic belongs to the core of the application where it can be isolated and unit tested.

So we should be able to just “fire-and-forget” a use case from the controller, transferring the control of the execution to it completely. We’ll try to use the same UI framework: Spring MVC with Thymeleaf for our implementation as the original DDDSample application. So we need to see how we can tweak Spring MVC a bit to allow for a “cleaner” separation between Presenter and Controller.

Spring MVC, controllers, presenters, and use cases

The snippet below shows an implementation of the controller’s logic compatible with the principles we have outlined above: it processes user’s input, creates a “Response Model” (a DTO), retrieves an instance of a specific use case from the application context, calls the use case and simply returns. Notice ResponseBody annotation on the request handling methods it signals to the framework that the HTTP request was fully processed by the current thread at the end of the controller’s execution.

At this point, what remains to be seen, is how does Presenter actually trigger the model-and-view processing required by Spring MVC. In a Spring MVC application, it is org.springframework.web.servlet.DispatcherServlet which is responsible for rendering a view. The problem is that render() method in the dispatcher servlet is not publicly accessible. So the trick is to create a local override of the dispatcher servlet, the one which will process all requests and which can then wire into our Presenter. Here is how such Presenter may look like:

Here is a link to a full example of such approach which uses Spring MVC together with Thymeleaf.

Repository, persisting and querying aggregates

Before we look at our first use case, we need to set up our persistence gateway. I chose not to use JPA with Hibernate, but to implement persistence using Spring Data JDBC framework. Following CA principles, we should not have any Hibernate proxies leaking into the core of the application. So, we would have to perform the mapping between our database entities and our model entities ourselves. This is the responsibility of the persistence gateway (secondary adapter) which will be called through the output port in the use case when a use case needs to perform any C(r)UD or query operations.

For automated mapping between entities and model we can use an an excellent MapStruct library, which will do most of the mapping for us.

This approach, while certainly following the Repository pattern, has some advantages and drawbacks. The advantage of simply eager-loading of the aggregate roots (with Spring Data JDBC) and mapping them to the domain objects manually (with the help of MapStruct) is that we control the process completely so we can be as flexible as we want.

For example, we can take advantage of MapStruct’s Lombok binding to be able to map LocationDbEntity row to the immutable Location model instance (using the built-in builder).

The disadvantage is, of course, since we are not using Hibernate, we cannot rely on lazy loading of related entities. All aggregate roots will be loaded and mapped completely (eagerly) by the gateway before returning to the use case. This will not be a problem if we take care to load a single aggregate instance or when we are loading a bound collection of simple aggregate instances. For use cases which require information from several aggregates we shall use “read-models” approach (Lev Gorodinski) and this only during query processing.

What is left now, before we can implement our first use case, is to set up a table and insert the data for Location aggregate. We do this using Flyway database migration tool which integrates seamlessly with Spring Boot.

Booking new cargo use case

Let’s now look at our first use case: booking a new cargo. As could be seen from the screenshot above, we need a list of all locations so that we can present the user with the choice of locations for the origin and the destination of the new cargo. So BookingUseCase will actually have two methods (sub-cases): one will prepare all the data needed to display the booking form, and another one to actually process new booking (after the submission).

The basic outline of a use case is always the same:

  • Validate “Request Model”, parameters to the use case (more on this later).
  • Retrieve any of the domain entities (aggregate roots) needed for the execution of the use case via one or several output ports.
  • Perform use case logic constructing and validating any new domain entities or modifying existing entities. Important point here is to try to offload, as much as possible, the business logic to the domain (value objects and/or aggregate roots) itself, following the best practice of DDD.
  • Persist the new/modified aggregates or perform any other side-effects through the output ports.
  • Present any relevant results through the presenter (the IDs of new entities, any data queried from the gateway, etc.). There can be different presenters wired into the use case, which will present different outcomes in different ways.
  • Any errors that may have happened during the use case’s execution will trigger some relevant side-effect (through output ports), like logging or sending a message, and/or will be presented the same way as any normal outcome: by calling one of the presenter’s methods (with a message to be displayed to the user, for example).

Here is an example of saving new cargo use case, also in BookingUseCase class:

Error handling and validation

When using use case driven approach, it’s important to treat as many business aspects of the application as possible within the scope of a use case. This is especially true with respect to the validation, since it is only in a use case that we can decide which error “presentation” (error handling) logic we should invoke depending on the type of the error.

As you can see in the gist for BookingUseCase above, I prefer to validate the input to the use case in the method implementing the use case itself. The other way, would have been to validate the inputs in the controller right before invoking the use case. It is also a valid approach, but I prefer to perform the validation in the use case, so that I can implicate different presenters depending on the outcome of the validation. This way we can also reuse the error presentation logic from the use case.

Validation itself is a perfect example of a domain service, which is wired into the use case together with the presenter and any output ports. In our implementation, we use JSR-303 bean validation (Hibernate validator) based on annotations. But we can easily add any custom validation logic, as well.

Exceptions should be as domain specific as possible and should form a hierarchy of sub-types relevant to the nature of the errors (errors from validation, domain objects manipulation, output port errors, etc.). The actual error presentation will depend on the specifics of the UI layer of the application. In our example, we redirect to a special Thymeleaf view with a session parameter containing the message of the error.

Discussion

By no means, what we have seen so far is a complete implementation of the cargo domain from the original application. The idea, is to show one or two use cases with all the relevant implementation details and leave the rest of the implementation as an exercise for the reader interested in the Clean DDD approach. Among other things which can be further explored are:

  • Testing use cases, see BookingUseCaseTest for an example
  • How to use security, especially ABAC or domain object security, hint: think of a domain service injected into the use case
  • How to reuse some configuration information in the use cases logic
  • How to make all our domain objects (not only value objects) immutable

Here is a diagram showing the relationships among principle parts of processing “new cargo booking” use case. It is so arranged as to instantly remind the user the similar diagram from the blog article by Robert C. Martin.

Cargo booking use case

As you can see, our implementation adheres closely to the principles of Clean Architecture. In particular, a special attention was taken to separate Controller and Presenter. Controller remains very “thin” it looks up the appropriate use case from the application context and invokes it. Other interesting points discussed in this article:

  • Business logic is encapsulated in a particular use case (focused on specific business need)
  • Repository pattern is used with object-to-object framework (MapStruct) helping to map from database entities to aggregate roots (using Spring Data JDBC framework)
  • Validation, exceptions and error handling are all relevant to use case and processed there
  • All domain objects: entities and value objects are immutable
  • Simple data structures are passed though to the use case (from the controller)
  • Simple data structures or immutable domain objects are passed through to the presenter

This last point is a small deviation of the Clean Architecture principles which states that only plain objects (DTOs or “Response Models”) are passed from the use case to the presenter. I think that, as long as our domain entities and value objects are immutable, we should be able to pass them through to the presenter. In my opinion, it is precisely the presenter which is responsible for constructing one or another “Response Model” depending on the view it is presenting.

I hope this was a useful presentation of the principles behind Clean DDD approach.

Update v1.0 (natural IDs, aggregate references)

I decided to go deeper and implement some more use cases. This means that some of the Gists above will not reflet the actual code. So, please, do check the source code for the most recent implementation. I will also try to tag different working versions, so that we can follow the evolution of the codebase.

I first tried to follow the DB schema from DDDSample as close as possible, but then I changed my mind and tried something different. Here are couple of observations:

  • Location aggregate has a “natural” and unique ID: UnLocode (standard UN localization code)
  • Cargo aggregate (root) also has a natural ID: TrackingId (unique for each instance of Cargo

So I’ve thought: “Why not use these natural IDs as the primary keys in the tables for the aggregate roots as well?”

Spring Data JDBC can distinguish between the states of a persistent entity (new or existing) via Version annotation. And we do not have to manipulate it ourselves: we just need to make sure that it is mapped though to and from our domain models.

So I decided to change Cargo root andRouteSpecification value object to reference any location by its UnLocode instead of Location . It does work and it gives the following advantages (at this point, it may cange in the future).

  • No more SERIAL (auto-incremented) identifiers added to the DB entities. Much cleaner SQL. Domain entities are always valid — they always have non-null identifier.
  • The foreign keys for Location used in other tables are actually readable as is: USDAL , AUMEL .

This brought me to another consideration. As you have noticed, in DDDSample, Cargo aggregate root references Location aggregate root directly and not by UnLocode (ID). Cargo contains an instance ofLocation as its origin, for an example. But there is a rule in DDD which states that aggregates should not reference other aggregates by object references but by their IDs, is there not? On the related note: it is interesting that in DDDSample we have very complex value objects, see Delivery for example. I noticed this after I read Paul Rayner’s article which specifically addresses this issue.

Update v1.1 (query use case)

We shall look at the way we can use what Lev Gorodinski calls “read-models”. This pattern calls for a use of a read-only (Value Object or DTO) structures created by issuing custom queries against the persistence store and which can be used to present some information in the UI usually spanning several aggregates. This is an idea borrowed from CQRS world: it’s about creating a projection of our domain. It is also the idea behind “the fast-track” by Sandro Mancuso.

To showcase this functionality I invented a little use case: we shall report the number of expected arrivals by the destination city. The report must specify the actual names of cities. This is not as trivial as it looks. One way of doing this would be to simply load all Cargo s from a use case, then load Location for each destination UnLocode in RouteSpecification so that we can have access to the name of the city. Then it would be simple to create the response model by grouping by cities and counting the total number of matching destinations. This is a classical problem: we have to access several aggregates: Cargo and Location , but we only need a partial information from each aggregate and for read-only processing. This is exactly where query type use cases from Clean DDD shine.

Here is the little DTO which can be used together with Spring’s JDBC template (BeanPropertyRowMapper , for example) to query for all the pieces of the information we need from the database.

Notice how we collect just enough information from the tables cargo and location to be able to create our model value object which will be provided by the gateway to the use case: ExpectedArrivals . Then there is just a little help from MapStruct and here we have the implementation of the projection in our gateway:

What’s left is to implement a simple use case and a presenter which will list ExpectedArrivals in a tabular form.

This gives me an opportunity to stress a very important point. The idea behind this type of “query” or “read-model” use cases is not to offload a lot of business logic to the SQL. We are still in the DDD world, we should query just enough information to be able to create the models (with all relevant processing logic attached to them) specifically crafted for the particular use case at hand. That’s the difference between the JOIN query in ExpectedArrivalsQueryRow and the “read-model” ExpectedArrivals used in ReportUseCase . As always, it is about Ubiquitous Language: the naming and the packaging of value objects, use cases must reflect the domain problem at hand: expected arrivals, city, report, etc.

Update 1.2 (external service, DTOs)

I’ve implemented another use case: RoutingUseCase . This one is interesting because it involves the use of an external serivice — path finding or graph traversal service for determining a suitable route for the cargo. For this to work, I repackaged the service from the original DDDSample in a separate JAR which can be used with Maven system type dependency. The idea, same as in the original application, is to map the external API objects: com.pathfinder.api.TransitPath to the objects from our domain. This example of Context Mapping in the strategic DDD design (or, more specifically, the Anti-Corruption Layer).

Of course, MapStruct can help us with the mapping again: take a look at TransitPathMapper .

The use case itself has two parts: selectItinerary , where we present the user a list of possible routes which could be assigned to the cargo, and assignRoute , where we actually assigning an itinerary to the cargo based on the route selected by the user. Here are a couple of interesting things to pay attention to, in my opinion.

ExternalRoutingService is modeled as an output port, exactly as any other output port (the port for the persistence gateway). This brings uniformity to the way the use case is connected to the interface adapters layer.

The logic of actually assigning new itinerary to cargo is still in the Cargo model itself. This is important, that’s where we have the most delicate point of this whole architectural approach. How to divide the business logic between use case and a domain entity? Use case should prepare the inputs needed for the aggregate root (a domain entity) to change its state according to the needs of the business scenario. These inputs may come from the output ports or they themselves may be other domain entities (from other aggregates) or value objects. Here is how RoutingUseCase initiates the assignment of a new route:

Notice how we prepare the value object, Itinerary , from the DTO representing the route selected by the user, we retrieve the needed cargo from the repository and, we simply delegate the actual state change of the Cargo aggregate to the aggregate itself. It is the responsibility of the aggregate root, Cargo , to verify the invariants (check if the itinerary satisfies the route specification) and to perform the actual assignement.

Update 1.3 (handling events, tracking)

Here we really delve into intricacies of Clean DDD. In this update we’ll look at how we can implement cargo tracking per se. As we saw in the original application, once a cargo is booked and routed, it’s delivery progress is updated via processing of handling events. In the original application, the events are processed asynchronously. They are entered, via a special helper application. For our implementation, I’ve decided to use a simple REST interface for registering handling events for a cargo. Here is how the Swagger UI for this endpoint looks like:

Swagger UI for REST endpoint to register a handling event
Swagger UI for REST endpoint to register a handling event

So the handling events processing will consist of calling a use case once for registering an event and once to update the cargo’s delivery (based on the updated history of the handling events associated with the cargo).

See how we have avoided using a compound use case, and at the same time kept updateDeliveryAfterHandlingActivity use case separate, so that we can possibly use it from somewhere else. Of course, there is no logic in the controller — everything interesting is happening in the use case: HandlingUseCase . What remains, is the usual way to separate Controller and Presenter for REST service.

Here is the implementation of the use case which updates Delivery of a cargo after some handling activity took place (a handling event was registered).

Updating cargo’s delivery after a handling event was processed.

Here is where Clean Architecture and DDD really shine. There is a lot going on in the method above, and yet it remains simple, comprehensible, and focused on the use case at hand. All of the intricacies are abstracted nicely behind the output ports (loading a Cargo from the gateway, etc.) and a lot of logic is actually in the models. Here we just reuse the logic from the original application.

  • HandlingHistory is a value-object responsible for keeping all HandlingEvent s sorted by their completion time and without any duplicate events.
  • Cargo knows how to derive an updated instance where Delivery value-object is updated to reflect the changes to the HandlingHistory .

Another interesting point comes when we look at the “tracking” use case. Here we need to present the current status of the cargo to the user, which includes a last-known location of the cargo. That information is stored in the lastKnownLocation attribute of Delivery . But, unlike the original application, we have only the identifier ( UnLocode ) of the location, and not the reference to the corresponding Location aggregate. This is because we have decided to model our Cargo following the advice from Vaughn Vernon to keep the IDs of related aggregate roots in our aggregate and not the actual references.

This has an advantage of allowing us to obtain Cargo instance from the persistent storage very efficiently. However, the disadvantage is: we do not readily have the related aggregates, which we may need to present to the user, as is the case with the current status of the cargo where we would like to show the actual name of the city when presenting the last known location for the cargo (and not just UnLocode for it). So what we need to do is to look this information ( Location aggregate) from trackCargo use case and pass it together with the Cargo instance to the presenter.

Update 1.4 (date-time value object)

Let’s review and refactor the way the application handles the dates (date-time objects). The original “DDDSample” used a standard date-picker in the Thymeleaf template for new cargo booking UI. I’ve kept this mostly as is:

The particularity of this widget is that it works with java.util.Date object which is also used in the model in the original application. I decided to use a more advanced java.time.ZonedDateTime to track date-times in the model. So this warrants a first conversion: from the view (Date) to the model (ZonedDateTime). And then there has to be a conversion from ZoneDateTime to java.time.Instant which is used in the Database entity beans for transporting date-time information to be stored as a timestamp in Postgres by JDBC.

At first, I’ve used a conversion helpers to convert from Date to ZonedDateTime each time specifying the timezone configured from the constants. This was a bit tedious. I’ve also thought that it makes sense for the domain to work exclusively with UTC date-times. So I decided to create a Value Object: UtcDateTime which wraps around a ZonedDateTime instance fixed at UTC timezone.

As seen from the Gist above, UtcDateTime delegates actual date-time operations to the wrapped ZonedDateTime object. This makes it a very useful object, as any operation available on ZonedDateTime is also available to UtcDateTime with a help of a simple straight-forward conversion method. The model stays exclusively with UtcDateTime and all conversion logic (to formatted strings and Date objects) is encapsulated in one place.

Update v1.5 (validation)

Let’s refactor validation in our model. We start with validation as a Domain Service pattern. There are multiple JSR 303 annotated fields in our model objects, like: NotNull and NotBlank . And we use Hibernate validator to validate model instances at some specific points of execution: during the creation from use cases, during the creation from a persistent entity (by the gateway), and during any modifications from the use case logic.

This works fine, but I think there is a better pattern to use for validation. What we are going to do is to make sure that all our model objects are always in a valid state. It is actually simpler and more straightforward than it seems. For this we need to follow these rules, which we should already be following if we are doing DDD:

  • Enforce construction of valid objects by asserting all necessary parameters passed to the constructor. This usually means that we shall check that none of the mandatory parameters are null (or blank, or empty) throwing a DomainObjectInvalidError otherwise.
  • During execution of any business method of any domain entity, assert that the final state of the modified entity (or the state of the returned entity, if we are using unmodifiable entities) is also valid. Throw DomainObjectInvalidError if any inconsistencies are detected.

To illustrate this technique, let’s look at Delivery entity. Here is the constructor used by the mapper to map delivery from a persistent object.

Constructor used to load Cargo (Delivery) from the database

Here we are asserting that TransportStatus , as well as, RoutingStatus of any Cargo instance (even the ones which have not yet been routed) must not be null. We are not making any other assumptions here. This guarantees that we will always have a valid Delivery instance returned from the gateway.

Let’s look at what is happening when we need to update the delivery progress of a Cargo aggregate. At this point, we are creating a new Delivery instance using a dedicated private constructor (from the original application).

Constructor used to update delivery progress of a Cargo

Notice how we are making sure here too that TransportStatus and RoutingStatus are not null? I think this pattern is well suited for DDD (if not required) and works better than using a dedicated domain service for validation. In fact, this way most of the validation comes from the original “DDDSample” application — it is already there because it follows the principles of DDD modeling so well.

Update v1.6 (security)

Here is the article were I go into details about how security is implemented in the application.

Update v1.7 (domain events)

Before this update, when a manager registered a HandlingEvent using the provided OpenAPI Swagger UI, HandlingController used to execute two distinct use cases (methods) from HandlingUseCase.java synchronously, by calling them one after another. This is not desirable, because Controller should limit itself to handling a single use case for each request. As I explain in this article, we should not come back to the controller after execution of a use case.

But it may happen that a use case needs to be executed automatically after a successful execution of a previous use case. What we should do in such cases, is to employ Domain Events pattern from DDD. Concretely, we introduce a secondary adapter which is capable or dispatching application-level events. Then, at the end of the first use case, we call the port of the event dispatcher to send an event encapsulating the information about the result of the use case’s execution. The event is processed by a dedicated primary adapter which executes the second use case depending on the type or the payload of the domain event.

In this update I’ve implemented Domain Event dispatching and handing. Here are the important parts:

  1. HandlingEvent becomes a subclass of CargoEvent : a generic domain event handled by the application.
  2. CargoSpringEventDispatcher is the dispatcher of the events using Spring’s ApplicationEventPublisher . It could be called from any use case to publish events via EventDispatcherOutputPort .
  3. CargoSpringEventProcessingAdapter is the primary adapter responsible for processing domain events. It will react to a published HandlingEvent and execute the use case for updating delivery progress for the cargo mentioned in the event.

This way two use cases are cleanly separated and the flow of control in and out of the hexagon is respected. There are couple of caveats which need to be kept in mind:

  1. Event handling adapter will cannot call the use case on the behalf of a user of the application. So there cannot be any security assertion in the use case. It must be executed as a system-level (all security granted) operation.
  2. For the same reason, the use case executed from the event dispatcher should not use UI presentation (web, for example) destined for a end-user. It may, however, use system-level presenters (loggers, message queues, etc.)
  3. Another important point is that event publishing must proceed if and only if the publishing use case has successfully completed all of its transactional operations (i.e. persistence in the gateway). In Spring, we can use the excellent TransactionalEventListener annotation, specifically designed to work with the transactional context. You can read more about handling of events with Spring in this article.

References

  1. GitHub, cargo-clean, source code
  2. DDDSample application, GitHub, source code for original implementation with Spring Boot
  3. Previous home of DDDSample, lots of interesting information: architecture overview, DDD principles
  4. The Clean Architecture, by Robert C. Martin
  5. Question about use cases and MVC
  6. GitHub, usecases-mvc, example application
  7. Interesting discussion about aggregates, Value Objects, by Paul Rayner
  8. Read-models in DDD, by Lev Gorodinski
  9. Clean DDD, another article discussing principles of Clean DDD
  10. Sandro Mancuso, presentations
  11. “Spring Events” by Baeldung, how to work with events in Spring

--

--