3

I've been digging into DDD for the first time the last few weeks. I'm developing an HR application and am really struggling with the idea of Aggregate Roots.

So the current entities would be:

  • Company
  • Employee

Now given the lifecycle of an Employee, it makes sense at first to include it as a child entity on Company. An Employee can be added to a Company (new hire or existing) and then that Employee could resign or be fired - at which point that Employee record would likely be updated with some kind of status, so it can be differentiated from active Employees.

In regards to domain logic, every Employee at a Company must have a unique email. So if a Company always contained a list of all its Employees, I imagined that could be modeled as:

company.AddEmployee(employee) - This method would contain logic that makes sure the email is unique. In addition, it would update the size property of the Company, based on how many Employees they have. (<100 is small, <500 is medium, etc)

Now the biggest issue I have seen people discuss is concerning large Aggregates. I think this use case would fall under that concern, as a Company could have 10k+ Employees in this HR application. If I'm adding a single Employee at a time, it seems really wasteful to gather all 10k+, even if it's just their emails.

Am I doing the right thing here by making the Company the Aggregate Root, or is there a better way?

Nick N
  • 49
  • 5
  • Employee email uniqueness is more of an application level rule. If you run the entire business on pen and paper, email is just an employee detail. – Yorro Feb 11 '22 at 07:28
  • I was starting to think the same, yeah. – Nick N Feb 11 '22 at 15:09

4 Answers4

3

Modeling aggregates is not so much about parent-child hierarchies. As already been stated it is about transaction boundaries. Also, consider aggregates provide the public APIs for performing transactions to your domain model.

It is easier to set aside parent-child hierarchy thinking and, at first also performance considerations. But rather to think:

Are there use cases to perform transactions (changes) on this entity without the need to apply domain rules that can only be adhered by some encapsulating root entity? That means, can this entity contain all domain logic (business rules) on its own to perform transactions on it?

Applying this kind of thinking in your case could have the following reasoning:

There are use cases where I want to modify an Employee entity where it does not make sense for the Company to check business invariants. Like, the main phone number contact information of an employee must never be empty or have invalid format.

This is of course an artificial example but it shows that by that reasoning such a transaction does not require business invariants that would be rather located in the Company entity. By that logic it can make sense to make Employee an aggregate on its own.

Also, for me, one crucial key learning from tactical DDD (or basically also from object-oriented programming) is to put the logic where the data is. If employee has all the data to execute logic that is required to keep transactional consistency without the need to ask Company, it can be a good candidate for an aggregate on its own.

Note: Of course, making an entity which is part of an aggregate an aggregate on its own can also make sense based on performance requirements (in case child entity collections would get too large). But I would not start modeling aggregates based on performance requirements, but rather ask yourself the questions outlined above.

Now to the topic of checking for e-mail uniqueness.

Where is the data for that? You could say if the Company knows about all the employees it would also know about all the emails of employees in that company so far. But just making the Employee a child of the Company does not seem to be the right reason.

If you model an Employee as an aggregate on its own, there is usually a special domain service that maintains collections of employee aggregates and provides access capabilities for retrieval and modification of such aggregates that make sense in the domain of your business - the aggregate repository.

This is also a good place to put logic for questioning, is there already an employee with that email address? Because the repository has the data to provide that logic.

The remaining question would of course be what code/component should than call this logic on the repository?. You could, e.g. have a special employee service that orchestrates employee creation. But depending on the situation that might also not be suitable. To help with this kind of decisions it is good to understand the implications of the DDD trilemma.

To get further hints concerning such decisions you can have a look at this Q&A on stackoverflow which also discusses domain services for a similar problem.

Andreas Hütter
  • 3,288
  • 1
  • 11
  • 19
  • Thanks for the response. The transaction boundary concept is starting to make sense. To your point, I can see scenarios where you could update an Employee without relying on Company logic. But that does still leave me with the edge case of a Company having unique Employee emails as a rule. Another person mentioned domain events which I've been looking into. I'm using Golang, though, which doesn't have quite the same robust event sourcing options as .NET, for example. – Nick N Feb 10 '22 at 22:20
  • Uniqueness rules across aggregate instances can of course not be located inside such an aggregate itself. Therefore, if strong consistency is required, domain services make sense (a repository is also a domain service). If eventual consistency is okay, domain events are suitable that allow reaction to such changes and also allow for compensation events. Please also note that domain event handling does not need a specific framework and can be implemented with every language. It's based on the publish subscriber pattern... – Andreas Hütter Feb 10 '22 at 23:15
  • Understood. It just seems way easier in .NET because of the strong Mediatr library. But like you said, still very doable in Go. You mentioned a repository could be a domain service. At least for this app, I have treated the repository as an interface for getting data, which in this case is SQL. From all the examples I've seen, these repos are not supposed to contain any logic. We can go a layer up, and then I suppose the question becomes, is the email uniqueness a domain rule or just an application rule? – Nick N Feb 10 '22 at 23:45
  • I updated my answer to also address the email uniqueness topic. – Andreas Hütter Feb 12 '22 at 06:15
  • I'm glad you linked that Vladimir Khorikov blog. I actually stumbled across that on Friday and he was talking about almost the exact same use-case, which was really helpful. Sounds like no matter what, you have to give something up. But I can't really think of any other Employee changes that would require a Company aggregate, so I think domain service or something similar is the way. Thanks for the help. – Nick N Feb 14 '22 at 17:08
2

I assume the app you are developing handles many organisations, otherwise there would not be a need for a separate "Company" aggregate.

An aggregate should preserve its transaction boundary. There is always a tradeoff when you establish this boundary. E.g. you can make the whole system an aggregate. But then you will only be able to process all the requests strictly sequentially with a lot of locking and waiting.

Or you can make a Company an aggregate to handle Employees. Then all the Employees of a given company will be processed sequentially. That may not work well at the scale of big company.

Or you can make a Company an aggregate to handle organisation-wide stuff. And make Employee an aggregate to handle personal stuff. In this case you will need some job to check after the fact if the e-mail is uniquie, or if some other properties are Ok (making a request to some external security system, for example), and updating this Employee to "verified" or "active" status.

The last approach will work well in a highly concurrent setting.

So, as a system designer you should pick one of the approaches and accept the trade-offs.

iTollu
  • 999
  • 13
  • 20
  • Could you expand on that first part please? In the future, we will likely have multiple orgs with Companies underneath them. But at launch, it will just be our single org with all of our customers as the Companies. But each Company should still manage Employees separately. And yeah, I figured some of this would come down to subjectivity. I just get paralyzed with those decisions, since I always wanna make the best one. As we all know, some times it's really hard to change the design of an app 6 months later. – Nick N Feb 10 '22 at 14:23
  • 1
    There is no universal best decision in these things, because the optimum is shaped by the domain requirements (hence why this approach to design is called domain-driven). – Levi Ramsey Feb 10 '22 at 14:32
1

You can keep Company as the aggregate root, but I'd use domain events to guard against duplicate emails.

My approach would be along the following lines:

class Company
{
    List<Employee> Employees = new List<Employee>();
    public int EmployeeCount { get; private set; }

    public void AddEmployee(Employee employee)
    {
        Employees.Add(employee);

        EmployeeCount++;

        AddDomainEvent(new EmployeeCreated(employee));
    }
}
  1. Retrieve Company without existing employees. Just retrieve the EmployeeCount property.

  2. Call the AddEmployee method:

    • Add the employee to collection
    • Increment EmployeeCount
    • Add a domain event for the new employee.
  3. Before committing the Unit of Work, process all domain events stored on your entities in the unit of work. In this case that will be the one EmployeeCreated event.

  • That event will include the proposed email of the new employee.
  • Your domain event handler can make a dB query that returns a scalar True/False value if the proposed email address has been used.
  • If it has been used, throw exception.
  1. If no errors thrown, then commit the unit of work.

  2. To prevent concurrency issues, add a concurrency token on Company, so that we don't have two simultaneous calls to AddEmployee resulting in two new employees in the dB but only a single increment of the Employee count.

https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/domain-events-design-implementation#the-deferred-approach-to-raise-and-dispatch-events

Neil W
  • 7,670
  • 3
  • 28
  • 41
  • Thank you for the answer. I have a few follow-up questions: 1.What does it look like to request the Company object without Employees? I like to have a complete object if I can help it. And it seems like if you had a Company object with no Employees on it, that might tell the user/consumer that Company has no Employees, which wouldn't be the case here. 2.I haven't explored domain events too much, so thank you for the link. My concern here is moving more of the domain logic out of the object itself and into those handlers. Unless the handler actually exists on the domain object? – Nick N Feb 10 '22 at 14:11
  • Ideally, your domain model is built for Commands (Use Cases). You should not be using that domain model for returning data to client. You can create other classes, 'Views' of the entities, for that purpose. When returning a Company view, you can happily retrieve all Employees and attach to that View, if you so desire - that depends on what level of info you want to provide the client on their GUI page. If you try to build your domain entities to support both commands and queries at the same time, you will definitely find it heavy going. Look up CQRS. – Neil W Feb 10 '22 at 18:38
  • To your point about "moving logic out of the domain object", the scenario you discuss (i.e. a large aggregate) forces compromises in this area. Don't hold up all-logic-must-be-in-entity-class as sacrosanct. Again, you will find it heavy going. Use Domain Events when it solves the problem you have encountered. Yes, default to putting logic as close to the entity itself, but sometimes the cost of that is too high ... and Domain Events come to the rescue. – Neil W Feb 10 '22 at 18:41
  • That helps, thank you. I was stuck on domain models having to be returned from all repositories, even for queries. But I see that just returning a ViewModel type of object creates a lot more flexibility. I will have to keep thinking on the Company aggregate on the command side. – Nick N Feb 10 '22 at 18:56
1

"Company" aggregate root makes sense if its only responsibility is to handle "Human Resources" related concerns.

Regarding the "Email Uniqueness". This is an application layer concern. If you run the business on pen and paper, the business side wouldn't care if two employees have the same email.

Yorro
  • 11,445
  • 4
  • 37
  • 47