Domain Driven Design - How do I model a Company and Employee use case?

Viewed 327

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?

4 Answers

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.

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.

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://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/domain-events-design-implementation#the-deferred-approach-to-raise-and-dispatch-events

"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.

Related