1

I’m currently trying DDD with establishments’ opening hours:

  • OpeningHours is an aggregate root; it ensures its openings don’t overlap
  • an Establishment can be set many opening hours (like summer’s, winter’s…)
  • an establishment’s opening hours can change on specified dates. Such a change would be coded as an OpeningHoursChange.

These changes must be limited somehow: at least you shouldn’t be able to ask two opening hours to start the same date. This check can only be done at the establishment level though.

Does that mean Establishment must be an aggregate root, and OpeningHoursChange a child entity of this aggregate? OpeningHoursChange will have to store an OpeningHours’ identity. Would this cause any kind of issue? I didn’t find any mention of this particular case in the literature.

If this were to cause any issue, what would be better alternatives?


It seems I didn’t make clear enough the fact that OpeningHoursChange is itself an entity: it says “these opening hours will take effect for their establishment at said date”. Updating opening hours is out of the scope of the context I wanted to provide to support my question.

3
  • 1
    "When designing aggregates, can a child entity store another aggregate root ID" - Eric Evans (the originator of the idea and the author of the foundational DDD book, link below) explicitly writes that this is allowed (BTW, the "rule" that the references to other aggregates be IDs doesn't come from him, but from a style of DDD popularized by Vaughn Vernon). You just shouldn't have other objects directly reference things within aggregate boundaries (but you can pass around transient references/copies via the root, with the understanding that these are not guaranteed to be consistent). Commented Nov 2, 2023 at 14:20
  • 1
    P.S. Book link: Eric Evans - DDD: Tackling Complexity in the Heart of Software Commented Nov 2, 2023 at 14:20
  • @FilipMilovanović would you mind adding you comment as an answer, possibly quoting the book?
    – MatTheCat
    Commented Nov 11, 2023 at 13:35

5 Answers 5

5

OpeningHours is an aggregate root; it ensures its openings don’t overlap

That is not what an aggregate root is.

Look at it this way: if company A and B have the same opening hours, they have the same opening hours reference, right? So what happens if company A changes its opening hours, must company B now change as well? No. So there's no shared life cycle for opening hours that happen to have the same start/end values, thus they should not be an aggregate root.

Another way to think of it: how could opening hours meaningfully exist without a related establishment? What would be open during these hours? This again suggests that the existence of a particular opening hour schedule only makes sense when it falls under the existence of an establishment which is open at certain times of day.

This check can only be done at the establishment level though.

This further suggests that opening hours are scoped to the establishment, and therefore fall under its aggregate root.

Such a change would be coded as an OpeningHoursChange.

I'm going to assume here that you're talking about the kind of change that constitutes a different schedule, rather than making changes to the one and only schedule.
For example, a store might have different opening hours based on DST, and therefore they store 2 opening hour scheduled, so that the system keeps displaying the correct opening hours at all times even if no one touches the system for years.

What I'm assuming you don't mean is that company A used to always be open on Thursdays, but they've now decided to always close on Thursdays. That would be a change to the existing schedule, rather than introducing multiple schedules between which you want to toggle.

OpeningHoursChange will have to store an OpeningHours’ identity.

Nope, because OpeningHours should not be an aggregate root to begin with.

Once you make OpeningHours an aggregate under the establishment's root, you'll find that OpeningHoursChange is no longer necessary. Anything you'd store in OpeningHoursChange can just be stored in the OpeningHours themselves.


Would this cause any kind of issue? I didn’t find any mention of this particular case in the literature.

Ignoring your particular example, which has already been answered, there is nothing wrong with one aggregate referring to another by its ID. Whether that ID is stored in the root itself or in a subaggregate is irrelevant - the same answer applies in both cases.

For example, you might track a store's owner (person ID on the Establishment aggregate root), but also which person last updated a particular opening hour schedule (which would be a person ID on the OpeningHours subaggregate that falls under Establishment).

11
  • Okay it seems I misunderstood what an aggregate root is. I thought it was the higher level entity able to protect its invariant. In my example opening hours store an establishment identity, because there is no invariant between the two. Anyway thanks for answering the original question!
    – MatTheCat
    Commented Oct 27, 2023 at 9:54
  • @MatTheCat: link "If we can make sure that an entity (or set of them) is only updated through an Aggregate, then we can make an assertion that it should never be in an incorrect state." This statement applies specifically to your intention to validate opening hours "at the establishment level" (as per your question), since the establishment aggregate root is then able to enforce that the opening hours don't conflict with other opening hours the establishment has.
    – Flater
    Commented Oct 27, 2023 at 11:19
  • @MatTheCat: An aggregate root stands on its own. You wouldn't be able to validate it as part of another aggregate root, that goes squarely against what an aggregate root is. Nothing in your question suggests that your opening hours have a meaningful independent lifecycle from the establishment they belong to. As an exercise to yourself (and leaving the door open to me overlooking something): name one benefit (or more, of course) from making opening hours its own aggregate root that you could not enjoy if it were a subaggregate of the establishment itself.
    – Flater
    Commented Oct 27, 2023 at 11:21
  • 2
    @MatTheCat It's a valid objection if Establishment settings are a highly collaborative part of your system. If opening hours are only edited once or twice per week by a couple of admins, I definitely can see Establishment as the aggregate root. As always, you should mitigate guidelines with context and apply best judgment. Commented Oct 27, 2023 at 12:00
  • 1
    @MatTheCat: Don't draw lines for the sake of drawing lines. Whether opening hours are a subaggregate or not should not hinge on how many other unrelated aggegrate roots you have. Separate your roots when necessary, not just to fill your quota of how many roots you want your application to have.
    – Flater
    Commented Oct 27, 2023 at 21:22
2

You already have a few answers with very useful information, I just want to add one heuristic that I sometimes use to determine if a concept should be an Aggregate of its own.

Ask the question: what should happen with the concept if the parent is removed.

In this case, ask your domain experts: if an Establishment burns down, what should happen to the Opening Hours?

If they say that there is no need to keep the opening hours when the establishment doesn’t exist any more, there’s no need to make OpeningHours an Aggregate, it's a child (Entity or ValueObject) of Establishment.

2
  • Never read anything about that heuristic; where did you get it from?
    – MatTheCat
    Commented Feb 6 at 9:26
  • It’s just something that I personally use. I don’t recall where it came from.
    – Rik D
    Commented Feb 6 at 17:35
2

As @Flater has pointed out, Establishment is probably the most suitable aggregate root for child entities OpeningHours and further childs Opening. You do not automatically "end up with a single AR for your entire application" when you model this specific case as an aggregate with two hierarchy levels.

However, for the sake of this example, let us assume you still want OpeningHours to be an AR on its own. Maybe Establishment with it's childs becomes a too large aggregate otherwise, which might cause issues when changing different OpeningHours schedules concurrently (yes, it sounds contrived, but it is just for demonstrating purposes).

Then, you could model it this way:

 Establishment // AR
 {
       List<OpeningHoursForAPeriod> openingHours;
 }

 OpeningHoursForAPeriod   // child of Establishment
 {
       Date StartDate;
       Date EndDate;
       int openingHoursId; // reference to OpeningHours
 }
 
 OpeningHours  // AR on its own
 {
     int Id;
     List<Slot> OpenSlot;   // time slots, as scetched by @Ewan
 }

That way, Establishment can ensure the list of OpeningHoursForAPeriod objects does never ever overlap in dates, and OpeningHours can ensure Slots never ever overlap in hours of a day - both ARs can protect their invariants.

Of course, when you now add another business rule like "total number of opening hours of an establishment per week must not exceed 60 hours", then an Establishment will not be able to able to enforce such a rule directly. But that's not necessarily a disadvantage, since such a kind of rule may require exceptions. Maybe one needs temporarily violations (for example, during planning / editing of an opening schedule could require this, or the 60 hours limit is changed over time during changes in laws, I can think of a lot of such scenarios).

So it is definitely possible to model this problem with two aggregate roots instead of one. It comes for the price of higher complexity: when you delete an Establishment from the system, this may leave some unused OpeningHours behind, and you will probably need some asynchronus clean-up service to get rid of them.

0

I'm going to disagree slightly here with flater,

Opening hours can totally be an AR.

eg.

OpeningHours
{
   List<Slot> OpenSlot

   AddSlot(Slot)
   {
      //check there isn't an overlap of slots within a day
   }
}

Slot
{
   StartTimeOfDay
   EndTimeOfDay
}

Your problem is you haved added a second higher level invariant

Establishment
{
   Dictionary<StartDate,OpeningHours> OpeningHoursThroughTheYear
   AddOpeningHours(OpeningHours, StartDate)
   {
      //make sure openinghours don't overlap over the year!
   }
}

Now you could just go, OK Establishment is now my AR and everything is a child of it.

But! If you do that all the time, you can end up with a single AR for your entire application, which is not good. Also its totally conceivable that two Establishments would use the same OpeningHours (id=WinterCanada) instance with changes affecting the both.

Your other option is to simply reference the opening hours by Id and allow the invariant to be violated, but check for those violation and deal with them in a separate process (Domain Service).

For example you could have:

EstablishmentOpeningHoursAuditor
{
    RunAudit(establishmentId)
    {
      var est = estRepo.get(establishmentId)
      var oh = openingHoursRepo.get(est.openingHoursIdList)

      //loop through and check for problems
      //set establishment status = "ERROR OVERLAPPING OPENING TIMES"
    }
}

This is fine and allows for more complex checks across ARs, saving bad or "editing not finished" states etc

5
  • The domain model should always be valid. 'allow the invariant to be violated' is an anti-pattern in DDD. Introduce separate concepts for things like 'editing not fished states' such as a DraftSomething instead of allowing the invariants of Something to be invalid under certain conditions.
    – Rik D
    Commented Oct 27, 2023 at 11:47
  • Hard disagree. That kind of thing is only realistic for very, lets call it, "Computer Sciencey" objects. Say you implement a builder or a custom set. If you try it for business logicy domain objects you are going the exact opposite direction of DDD, moving away from the natural, business language just to match your OOP computer language style
    – Ewan
    Commented Oct 27, 2023 at 12:05
  • 1
    That's not what I meant. The business is leading in determining these concepts. You have to ask the business how they handle incomplete physical forms. They probably end up on a separate pile, maybe are handled by a separate person and they might even have a name for this pile; you model that.
    – Rik D
    Commented Oct 27, 2023 at 12:21
  • yeah with status=pileX because that's how people think about these things
    – Ewan
    Commented Oct 27, 2023 at 12:24
  • 1
    @RikD: DDD allows eventual consistency of business rules across different aggregates, that's nothing special.
    – Doc Brown
    Commented Oct 27, 2023 at 14:14
0

I'm going to answer my question since it has mostly generated unrelated and seemingly wrong answers.

Evans says in its “Domain-Driven Design” book that

Objects within the aggregate can hold references to other aggregate roots.

Then Vernon says in “Implementing Domain-Driven Design”

Prefer references to external aggregates only by their globally unique identity, not by holding a direct object reference.

So, yes, child entities can store another aggregate rootʼs id.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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