16

Let me begin by first apologizing for the length of the post, but I really wanted to convey as much detail up-front so I don't take your time going back and forth in comments.

I am designing an application following a DDD approach and am wondering what guidance I can follow to determine whether an Aggregate Root should contain another AR or if they should be left as separate, "free-standing" ARs.

Take the case of a simple Time Clock application that allows Employees to clock themselves in or out for the day. The UI allows them to enter their employee ID and PIN which is then validated and the current state of the employee retrieved. If the employee is currently clocked-in, the UI displays a "Clock Out" button; and, conversely, if they are not clocked-in, the button reads "Clock In". The action taken by the button corresponds to the state of the employee as well.

The application is a web client that calls a back-end server exposed via a RESTful service interface. My first pass at creating intuitive, readable URLs resulted in the following two endpoints:

http://myhost/employees/{id}/clockin
http://myhost/employees/{id}/clockout

NOTE: These are used after the employee ID and PIN have been validated and a "token" representing the "user" is passed in a header. This is because there is a "manager-mode" that allows a manager or supervisor to clock-in or out another employee. But, for the sake of this discussion, I'm trying to keep it simple.

On the server, I have an ApplicationService that provides the API. My initial idea for the ClockIn method is something like:

public void ClockIn(String id)
{
    var employee = EmployeeRepository.FindById(id);

    if (employee == null) throw SomeException();

    employee.ClockIn();

    EmployeeRepository.Save();
}

This looks pretty straight-forward until we realize that the Employee's time card information is actually maintained as a list of transactions. That means each time I call ClockIn or ClockOut, I am not directly changing the state of the Employee but, instead, I am adding a new entry into the Employee's TimeSheet. The current state of the Employee (clocked in or not) is derived from the most recent entry in the TimeSheet.

So, if I go with the code shown above, my repository has to recognize that the persistable properties of the Employee have not changed but that a new entry was added into the Employee's TimeSheet and perform an insert into the data store.

On the other hand (and here's the ultimate question of the post), TimeSheet seems like it is an Aggregate Root as well as it has identity (the employee ID and period) and I could just as easily implement the same logic as TimeSheet.ClockIn(employeeId).

I find myself debating the merits of the two approaches and, as stated in the opening paragraph, wonder what criteria I should be evaluating to determine which approach is more suited for the problem.

1
  • I've edited/updated the post so the question is clearer and uses a better scenario (hopefully). Apr 17, 2012 at 13:15

3 Answers 3

4

I would be inclined to implement a service for time tracking:

public interface ITimeSheetTrackingService
{
   void TrackClockIn(Employee employee, Timesheet timesheet);

   void TrackClockOut(Employee employee, Timesheet timesheet);

}

I don't think that it's time sheet's responsibility to clock in or clock out, neither it's employee's responsibility.

3

Aggregate roots should not contain each other (though they may contain IDs to others).

First, is TimeSheet really an aggregate root? The fact that it has identity makes it an entity, not necessarily an AR. Check out one of the rules:

Root Entities have global identity.  Entities inside the boundary have local 
identity, unique only within the Aggregate.

You define the TimeSheet's identity as employee ID & time period, which suggests that TimeSheet is a part of Employee with time period being the local identity. Since this is a Time Clock application, is the main purpose of Employee to be a container for TimeSheets?

Assuming you do have to ARs, ClockIn and ClockOut seem more like TimeSheet operations than Employee ones. Your service layer method could look something like this:

public void ClockIn(String employeeId)
{
    var timeSheet = TimeSheetRepository.FindByEmployeeAndTime(employeeId, DateTime.Now);    
    timeSheet.ClockIn();    
    TimeSheetRepository.Save();
}

If you really need to track clocked-in state in both Employee and TimeSheet, then have a look at Domain Events (I don't believe they are in Evans' book, but there are numerous articles online). It would look something like: Employee.ClockIn() raises EmployeeClockedIn event, which an event handler picks up and in turn calls TimeSheet.LogEmployeeClockIn().

1
  • I see your points. However, there are certain rules that constrain if and when an Employee can be clocked in. These rules are mostly driven by the current state of the Employee such as whether they are terminated, already clocked-in, scheduled to work that day or even the current shift, etc. Should the TimeSheet possess this knowledge? May 10, 2012 at 16:09
3

I am designing an application following a DDD approach and am wondering what guidance I can follow to determine whether an Aggregate Root should contain another AR or if they should be left as separate, "free-standing" ARs.

An aggregate root should never contain another aggregate root.

In your description, you have an Employee entity, and likewise a Timesheet entity. These two entities are distinct, but could include references to each other (ex: this is Bob's timesheet).

That much is basic modeling.

The aggregate root question is slightly different. If these two entities are transactionally distinct from each other, then they can be correctly modeled as two distinct aggregates. By transactionally distinct, I mean that there is no business invariant that requires knowing the current state of Employee and the current state of TimeSheet simultaneously.

Based on what you describe, Timesheet.clockIn and Timesheet.clockOut never need to check any data in Employee to determine if the command is permitted. So for this much of the problem, two different AR seems reasonable.

Another way of considering the AR boundaries would be to ask what kinds of edits are permitted to happen simultaneously. Is the manager allowed to clock the employee out while at the same time HR is fiddling with the employee's profile?

On the other hand (and here's the ultimate question of the post), TimeSheet seems like it is an Aggregate Root as well as it has identity (the employee ID and period)

Identity only means it is an entity -- it's the business invariant that drives whether it should be considered a separate aggregate root.

However, there are certain rules that constrain if and when an Employee can be clocked in. These rules are mostly driven by the current state of the Employee such as whether they are terminated, already clocked-in, scheduled to work that day or even the current shift, etc. Should the TimeSheet possess this knowledge?

Maybe -- the aggregate should. That doesn't necessarily mean that the Timesheet should.

Which is to say, if the rules for modifying a Timesheet depend on the current state of the Employee entity, then Employee and Timesheet definitely need to be part of the same aggregate, and the aggregate as a whole is responsible for ensuring that the rules are followed.

An aggregate has one root entity; identifying it is part of the puzzle. If an Employee has more than one Timesheet, and they are both part of the same aggregate, then Timesheet is definitely not the root. Which means that the application cannot directly modify or dispatch commands to the timesheet - they need to be dispatched to the root object (presumably the Employee), which can delegate some of the responsibility.

Another check would be to consider how Timesheets get created. If they are created implicitly when an employee clocks in, then that's another hint that they are a subordinate entity in the aggregate.

As an aside, it is unlikely that your aggregates should have their own sense of time. Instead, time should be passed to them

employee.clockIn(when);

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.