logoThe Neverland
back iconBACK TO HOME
Hire Me

Domain Driven Design Is Hard But... - Part 2

By Wú Xùdōng
Sunday, February 28, 2021
Wú XùdōngFullstack Software Engineer
I am a veteran software engineer with extensive experience in fields such as aerospace, financial services and consulting, now looking for opportunities to share my knowledge to help a company in need. And I always welcome challenges, enjoy solving complex and exciting problems, and would love to be involved in any startups. Please contact me here for more information.

Recap

In Part 1,I have introduced the three building blocks of Domain-driven design - Entity, Value Object and Service. Entities are stateful objects that are identified by a unique ID whereas Value Objects are stateless objects that are immutable and are defined by their attributes. Services are to model the processes between objects and they are usually recognized by verbs in the ubiquitous language. In part 2, I will continue to touch the basics of the other three building blocks - Aggregate, Factory and Repository. 

Aggregate

In Part 1 we mentioned that making the association between objects uni directional helps minimize the complexity of relationships; however, when applications grow bigger and more objects are added into the domain model, things shall tend to get out of control. Several tough scenarios can be: 

  1. Invariants are hard to enforce among a closely related group of objects;
  2. Object references are present and scattered in many other objects due to hacking and quick fixes. 

To share my own experience, I once worked with a legacy project. I was assigned to add a new functionality to this feature, it seemed relatively easy and timesaving to add a new object reference to another object to achieve this feature; so, I did it at that time. Later on when I became more familiar with the code base, I realised that I had assumed wrongly to add that object reference though miraculously it worked for that feature. Imagine that developers do this often, the relationship between objects shall become a net which is even harder to maintain and slows down future development.

Aggregate is a mechanism suggested in the book to counter this problem.What if we group some of the objects together and assign a leader to the group ? All the communications from outside have to go through this leader and any member of the group that wants to talk to the outside world has to pass the conversation through the leader. It sounded familiar ? It is like applying a network gateway in front of enterprise networks. So here proper definition goes: 

an Aggregate is a cluster of associated objects that we treat as a unit for the purpose of data changes.

Each aggregate has a root and a boundary. An aggregate root is a single entity that has global identity and it is responsible for checking and ensuring the invariants between objects within the aggregate are enforced throughout the lifecycle of the aggregate. Only the aggregate root is allowed for the outside object to hold references to; however, within the aggregate, objects can hold references to each other. As a corollary to this rule, only the aggregate root can be constructed directly with database queries and all the other objects shall be found through traversal of associations. Since aggregates are very coupled concepts and aggregate root has enforced some important invariants, deleting of aggregate root should lead to deleting of all the objects in that aggregate.  Below is an example of aggregate for Student in the academic system,

As the example above shows that the aggregate has grouped some closely concepts of a college student's academic progress. What can be the invariants here inside the aggregate?

  1. A student should not take more than 6 modules for each term;
  2. A student should take N number of core modules for a selected major;
  3. A student should not take more than 40 modules for the entire study.

And there are a lot more invariants that need to be enforced; but I will keep it simple here. By grouping them together, it gives us clear boundaries of these objects and how they interact with each other. Below shows the java implementation of this example.

public abstract class AggregateRoot<ID extends Identifier> implements Entity<ID> {
   private final ID id;
   protected AggregateRoot(ID id) { this.id = id; }
   public ID getId() { return id; }
}

public class StudentId implements  Identifier {
   private final String id;
   public StudentId() { id = UUID.randomUUID().toString(); }
}

public class Student extends AggregateRoot<StudentId> {
   private final ModuleHistory moduleHistory;
   private final ActiveModules activeModules;
   private final Major major;

   public Student(Major major) {
       super(new StudentId());
       this.moduleHistory = new ModuleHistory();
       this.activeModules = new ActiveModules();
       this.major = major;
   }

   public StudentId getStudentId() { return getId(); }

   public void register(Module newModule){
       int currentEnrolled = activeModules.getNumOfCurrentEnrolledModules();
       int taken = moduleHistory.getNumOfModulesTaken();
       if (taken + 1 > 40) {
           throw new UnsupportedOperationException("Registered Modules have exceeded the maximum number(40) in total");
       }
       if (currentEnrolled + 1 > 6){
           throw new UnsupportedOperationException("Registered Modules have exceeded the maximum number(6) for current semester");
       }
       activeModules.register(newModule);
   }
}

public class ModuleHistory implements ValueObject {
   private final Map<String, List<Module>> moduleHistories;
   public ModuleHistory() { this.moduleHistories = new HashMap<>(); }
   public int getNumOfModulesTaken() { return moduleHistories.size(); }
}

public class ActiveModules implements ValueObject {
   private final List<Module> modules;
   public ActiveModules() { modules = new ArrayList<>(); }
   public int getNumOfCurrentEnrolledModules() { return modules.size(); }
   public void register(Module newModule) { modules.add(newModule); }
}

public class Major implements ValueObject {
   private final List<Module> coreModules;
   private final String name;
   public Major(List<Module> coreModules, String name) {
       this.coreModules = coreModules;
       this.name = name;
   }
}

The first two invariants are enforced in the register method of the student class. Every new registration of a module shall go through the check of invariants. Only when the invariants are enforced, a new module will be added to the student's active modules of the current term. 

Factory

Now I have briefly discussed the concept of Aggregate, I now would like to introduce a pattern called Factory which operates on Aggregates. It sounds similar to the two design patterns: Factory Method and Abstract Factory. As the word "factory" implies, they are to help produce objects. While these two factory patterns explore the polymorphism aspect of producing objects , the factory in DDD focuses on a different concern: that when the complexity of invariants and associations aggregates grows, it becomes hard to create an aggregate from the client's perspective.

Factory is to provide a way to help create complex aggregates and to ensure the invariants for the client without exposing the concrete detailed objects to the client. 

Imagine that there is a Graduation Service that is checking students who are graduating in the coming term. When a freshman gets admitted into the college, the Graduation Service has to know that to create an aggregate Student requires creating Module History, Module Taken Current Term, Major and etc. If Graduate Service interacts with other aggregates such as Graduation Ceremony,  Diploma and etc., it has to know all the internal structure of those aggregates as well. This becomes extremely unscalable and also hard to test the functionality of Graduation Service. Thus, Factory is a good mechanism to offload that work from the clients Graduation Service and ensure integrity of the aggregate. The diagram below shows the functionality of a Factory. 

In the Graduation Service example above, the process of creating a freshman can be modelled as below:

Let's also look at how the Student Factory can be represented in code:

public class StudentFactory {
   private final static int DEFAULT_MAX_PER_TERM = 6;
   private final static int DEFAULT_MAX_PER_DEGREE = 40;

   public static Student create(String name, String majorName) {
       Major major = new Major(majorName);
       List<Module> coreModules = Collections.emptyList();
       major.addCoreModules(coreModules);
       Student student = new Student(name, major);
       student.addTermPolicies(MaximumModulePerTerm.ofNewPolicy(DEFAULT_MAX_PER_TERM));
       student.addDegreePolicies(MaximumModulePerDegree.ofNewPolicy(DEFAULT_MAX_PER_DEGREE));
       student.addDegreePolicies(MajorQualifiedByCoreModules.ofNewPolicy(coreModules));
       return student;
   }
}

I have refactored the previous code base to make the invariants more explicit as domain models. Now if the Graduation Service would like to call this Student Factory to add a freshman, it just have to know the literal name of the student and major and this factory will return a newly created student with a unique student number (UUID) and three invariants of maximum modules allowed per term, maximum modules allowed per degree, and module requirement for a major. 

I also would like to share why I did the refactoring for the invariants. Firstly, in the Aggregate section, I enforce the invariants inside the register method with two "if-statements". However, these invariants are also very important concepts in the Ubiquitous Language. By modelling them as objects, they become explicit and part of the rich models. Secondly, from a clean code perspective, if there are many more invariants, the register method will end up with a lot of if-else statements and this is not readable and scalable in the long run. Here is how I made it become more explicit.

This is more intention revealing and it is easier to add more rules regarding modules. For example, modules taken in the current term will be constrained by prerequisites,  there should be more than 4 core modules for freshman and sophomore terms, etc..

public interface ModulePolicy {
   public boolean isSatisfiedBy(List<Module> modules);
}
public class MaximumModulePerTerm implements ModulePolicy {
   ...
   public static MaximumModulePerTerm ofNewPolicy(int maxModulesPerTerm) {
       return new MaximumModulePerTerm(maxModulesPerTerm);
   }
   @Override
   public boolean isSatisfiedBy(List<Module> modules) {
       return modules.size() < maxModulesPerTerm;
   }
}
public class MaximumModulePerDegree implements ModulePolicy {
   ...
   public static MaximumModulePerDegree ofNewPolicy(int maxModulesPerDegree) {
       return new MaximumModulePerDegree(maxModulesPerDegree);
   }
   @Override
   public boolean isSatisfiedBy(List<Module> modules) {
       return modules.size() < maxModulesPerDegree;
   }
}

public class MajorQualifiedByCoreModules implements ModulePolicy {
   ...
   public static MajorQualifiedByCoreModules ofNewPolicy(List<Module> coreModules){
       return new MajorQualifiedByCoreModules(coreModules);
   }
   @Override
   public boolean isSatisfiedBy(List<Module> taken) {
       return taken.containsAll(coreModules);
   }
}

Now let's look at how register method has been refactored:

public void register(Module newModule) {
   boolean isOkForCurrentTerm = termPolicies.stream().allMatch(activeModules::satisfyPolicy);
   if (!isOkForCurrentTerm)
       throw new UnsupportedOperationException("Registered Modules have exceeded the maximum number(40) in total");
   boolean isOkForDegree = degreePolicies.stream().allMatch(moduleHistory::satisfyPolicy);
   if (!isOkForDegree)
       throw new UnsupportedOperationException("Registered Modules have exceeded the maximum number(6) for current semester");
   activeModules.register(newModule);
}

Now handling the creation of aggregates is encapsulated in Factory. How about the persistence of the aggregates? In the next section, I shall introduce another pattern Repository of handling the persistence of aggregates.

Repository

As I have briefly mentioned in Part 1 that Entities are stateful, which means that it would require persistence throughout its lifecycle. Let's look at how an object life cycle looks like: 

For a value object it is straightforward to create it and since it is immutable(stateless), it will then be deleted after it has been used. However, for an entity or more specifically aggregate root, it is usually required to be persisted throughout its lifecycle.

Repository is a pattern that will separate the persistence logic from the domain models; by doing this, it helps to keep the domain layer clean and focused on modelling Ubiquitous Language. 

The diagram below shows how a repository helped to encapsulate the technology details from the client.

In the example of the Graduation Service, the diagram below helps demonstrate how the Graduation Service interacts with the Student Repository to get a list of students who are studying computer science from the database.

Now Let's look at how we could implement this:

public class StudentRepository {
    public List<Student> satisfiedBy(ICriteria... criterias) {
       Optional<Condition> combined = Arrays.stream(criterias)
               .map(ICriteria::getCriteria)
               .reduce(Condition::and);
       if (!combined.isPresent()) {
           throw new UnsupportedOperationException("One criteria is needed to fetch the students.");
       }
       Select<?> selectStatements = DSL.select()
               .from(table("STUDENT"))
               .where(combined.get());
       return selectStatements.fetch().into(Student.class);
    }
    public void archive(List<StudentId> studentIds) {
        studentIds.stream()
           .map(studentId -> field("ID").eq(studentId))
           .forEach(condition -> DSL.deleteFrom(table("STUDENT")).where(condition));
    }
}

public class GraduationService {
   public List<Student> graduatingStudentByMajor(String major) {
       StudentRepository studentRepository = new StudentRepository();
       String year = "final_year";
       return studentRepository.satisfiedBy(ByMajor.of(major), ByYear.of(year));
   }
   public void graduateStudents(List<StudentId> studentIds){
       StudentRepository studentRepository = new StudentRepository();
       studentRepository.archive(studentIds);
   }
}

By encapsulating the persistence logic, the domain models stay focused on business logic. In practice, Repository is often used together with Factory when reconstituting objects from the database. When Repository gets the query results from the database, it can delegate the work to Factory to instantiate the aggregate roots / objects. 

Conclusion

In this part, I have briefly introduced Aggregate to tackle complex associations between a group of closely related objects and two patterns realized - Factory and Repository to handle the creation and manage the persistence of aggregates. In part 3, I continue to discuss that in large projects, how Bounded Context, different ways of Collaborations among teams, etc.. Stay tuned.

© 2023 Wu Xudong. All Rights Reserved.