Defining aggregate roots when invariants exist within a list
Asked Answered
D

4

7

I'm doing a family day care app, and thought I'd try DDD/CQRS/ES for it, but I'm running into issues with designing the aggregates well. The domain can be described pretty simply:

  • A child gets enrolled
  • A child can arrive
  • A child can leave

The goal is to track the times of the visits, generate invoices, put notes (eg. what was had for lunch, injuries etc.) against the visits. These other actions will be, by far, the most common interaction with the system, as a visit starts once a day, but something interesting happens all the time.

The invariant I'm struggling with is:

  • A child cannot arrive if they are already here

As far as I can see, I have the following options

1. Single aggregate root Child

Create a single Child aggregate root, with the events ChildEnrolled, ChildArrived and ChildLeft

This seems simple, but since I want each other event to be associated with a visit, it means the visit would be an entity of the Child aggregate, and every time I want to add a note or anything, I have to source all the visits for that child, ever. Seems inefficient and fairly irrelevant - the child itself, and every other visit, simply isn't relevant to what the child is having for lunch.

2. Aggregate Roots for Child and Visit

Child would source just ChildEnrolled, and Visit would source ChildArrived and ChildLeft. In this case, I don't know how to maintain the invariant, besides having the Visit take in a service for just this purpose, which I've seen is discouraged.

Is there another way to enforce the invariant with this design?

3. It's a false invariant

I suppose this is possible, and I should protect against multiple people signing in the same child at the same time, or latency meaning the use hits the 'sign in' button a bunch of times. I don't think this is the answer.

4. I'm missing something obvious

This seems most likely - surely this isn't some special snowflake, how is this normally handled? I can barely find examples with multiple ARs, let alone ones with lists.

Dore answered 13/6, 2015 at 3:43 Comment(7)
If Child and Visit are ARs, why can't Child hold a childPresenceState and then calling child.acknowledgeArrival() enforces the invariant before raising a ChildArrived event that would trigger the creation of a Visit AR. Does that make sense?Aristides
How would the presence state be updated? If the child arrived event is for the visit AR, the child won't know about it.Dore
Well, I do not know your overall design, but couldn't the command for marking a child as present be on Child? Like child.AcknowledgeArrival(currentDate) and child.AcknowledgeDeparture for instance?Aristides
Oh I get that, but I'm doing event sourcing, so the aggregate state should only be changed via events, not via commands. So I'd need 2 events (one to mark the child as present, and another to create the visit AR), which is in dodgy-land again.Dore
There's always a command that generates an event with ES. Then it makes sense to have 2 events because they are targeting different ARs. There's no problem there. You cannot use the same event to be applied on many ARs.Aristides
Maybe this is a concept I'm missing, but isn't that prone to getting into an invalid state, unless you use a unit of work/transaction pattern when persisting events? I thought one of the benefits of ES is not having to deal with this. But if, say, the power goes out before the 2nd AR's events are saved, the system will be in a bad state.Dore
It's eventual consistency. Your messaging infrastructure should be durable and redeliver undelivered messages.Aristides
G
3

Aggregates

You're talking heavily about Visits and what happened during this Visit, so it seems like an important domain-concept of its own.
I think you would also have a DayCareCenter in which all cared Children are enrolled.

So I would go with this aggregate-roots:

  • DayCareCenter
  • Child
  • Visit

BTW: I see another invariant:
"A child cannot be at multiple day-care centers at the same time"

"Hits the 'sign in' button a bunch of times"

If every command has a unique id which is generated for every intentional attempt - not generated by every click (unintentional), you could buffer the last n received command ids and ignore duplicates.

Or maybe your messaging-infrastructure (service-bus) can handle that for you.

Creating a Visit

Since you're using multiple aggregates, you have to query some (reliable, consistent) store to find out if the invariants are satisfied.
(Or if collisions are rarely and "canceling" an invalid Visit manually is reasonable, an eventual-consistent read-model would work too...)

Since a Child can only have one current Visit, the Child stores just a little information (event) about the last started Visit.

Whenever a new Visit should be started, the "source of truth" (write-model) is queried for any preceeding Visit and checked whether the Visit was ended or not.

(Another option would be that a Visit could only be ended through the Child aggregate, storing again an "ending"-event in Child, but this feels not so good to me...but that's just a personal opinion)

The querying (validating) part could be done through a special service or by just passing in a repository to the method and directly querying there - I go with the 2nd option this time.

Here is some C#-ish brain-compiled pseudo-code to express how I think you could handle it:

public class DayCareCenterId
{
    public string Value { get; set; }
}
public class DayCareCenter
{
    public DayCareCenter(DayCareCenterId id, string name)
    {
        RaiseEvent(new DayCareCenterCreated(id, name));
    }
    private void Apply(DayCareCenterCreated @event)
    {
        //...
    }
}

public class VisitId
{
    public string Value { get; set; }
}
public class Visit
{
    public Visit(VisitId id, ChildId childId, DateTime start)
    {
        RaiseEvent(new VisitCreated(id, childId, start));
    }
    private void Apply(VisitCreated @event)
    {
        //...
    }

    public void EndVisit()
    {
        RaiseEvent(new VisitEnded(id));
    }
    private void Apply(VisitEnded @event)
    {
        //...
    }
}

public class ChildId
{
    public string Value { get; set; }
}
public class Child
{
    VisitId lastVisitId = null;

    public Child(ChildId id, string name)
    {
        RaiseEvent(new ChildCreated(id, name));
    }
    private void Apply(ChildCreated @event)
    {
        //...
    }

    public Visit VisitsDayCareCenter(DayCareCenterId centerId, IEventStore eventStore)
    {
        // check if child is stille visiting somewhere
        if (lastVisitId != null)
        {
            // query write-side (is more reliable than eventual consistent read-model)
            // ...but if you like pass in the read-model-repository for querying
            if (eventStore.OpenEventStream(lastVisitId.Value)
                .Events()
                .Any(x => x is VisitEnded) == false)
                throw new BusinessException("There is already an ongoning visit!");
        }

        // no pending visit
        var visitId = VisitId.Generate();
        var visit = new Visit(visitId, this.id, DateTime.UtcNow);

        RaiseEvent(ChildVisitedDayCenter(id, centerId, visitId));

        return visit;
    }
    private void Apply(ChildVisitedDayCenter @event)
    {
        lastVisitId = @event.VisitId;
    }
}

public class CommandHandler : Handles<ChildVisitsDayCareCenter>
{
    // http://csharptest.net/1279/introducing-the-lurchtable-as-a-c-version-of-linkedhashmap/
    private static readonly LurchTable<string, int> lastKnownCommandIds = new LurchTable<string, bool>(LurchTableOrder.Access, 1024);

    public CommandHandler(IWriteSideRepository writeSideRepository, IEventStore eventStore)
    {
        this.writeSideRepository = writeSideRepository;
        this.eventStore = eventStore;
    }

    public void Handle(ChildVisitsDayCareCenter command)
    {
        #region example command douplicates detection

        if (lastKnownCommandIds.ContainsKey(command.CommandId))
            return; // already handled
        lastKnownCommandIds[command.CommandId] = true;

        #endregion

        // OK, now actual logic

        Child child = writeSideRepository.GetByAggregateId<Child>(command.AggregateId);

        // ... validate day-care-center-id ...
        // query write-side or read-side for that

        // create a visit via the factory-method
        var visit = child.VisitsDayCareCenter(command.DayCareCenterId, eventStore);
        writeSideRepository.Save(visit);
        writeSideRepository.Save(child);
    }
}

Remarks:

  • RaiseEvent(...) calls Apply(...) instantly behind the scene
  • writeSideRepository.Save(...) actually saves the events
  • LurchTable is used as a fixed-sized MRU-list of command-ids
  • Instead of passing the whole event-store, you could make a service for it if you if benefits you

Disclaimer:
I'm no renowned expert. This is just how I would approach it.
Some patterns could be harmed during this answer. ;)

Ghostly answered 14/6, 2015 at 17:50 Comment(5)
Thanks for the detailed response, I will give your ideas (or a variation thereof) a go when I get back home. First glance though, it looks like Child.VisitDayCareCenter is violating CQS. And ought those calls to writeSideRepository.Save be in a transaction?Dore
Transaction: Yes. VisitsDayCareCenter strictly speaking is violating CQS. But is it problematic? It doesn't return some information about internal state, instead it acts as a factory-method for another aggregate-root tightly related to this one. Some renowned experts like Greg Young support this kind of aggregate-factory-methods on aggregate-roots: groups.google.com/d/topic/dddcqrs/B6kxs7FK8_I/discussionGhostly
...Otherwise a more complex setup would be needed. A simple domain-service doing exactly this work. Or if you need to have it decoupled - to follow "Save 1 AR in 1 command" - a saga listening for the ChildVisitedDayCenter event and creatinga new Visit would be needed, obscuring the strong direct relationship. And as it'd be decoupled you'd need to implement "undo"-logic if something fails (i.e. ChildVisitedDayCenter is created but not VisitCreated due to some exception...) IMHO - in this case - striclty following this pattern just adds complexity but your mileage my vary.Ghostly
The example given by your link is a little different - the PlaceOrder method wouldn't be changing any of the state on Customer, just returning a new AR ready to be saved. You're probably right about the CQS violation not being particularly problematic, I would have thought this was a fairly common invariant that had a simple, almost boiler-plate solution. Guess not!Dore
You're right: Customer is not changing in their example - a little deviation ;) Child (silently) storing the information of created visits is - let's admit it - a little trick to enable enforcing the given invariants in an easy way. In my experience in DDD you almost always have many options for how to solve a problem - differing in complexity. So more often you will hear the famous phrase "it depends" ... which unfortunately describes the situation very well: you have to choose what is the simplest but still most fitting solution to your current (and foreseeable probable future) needs...Ghostly
T
0

It sounds like the "here" in your invariant "A child cannot arrive if they are already here" might be an idea for an aggregate. Maybe Location or DayCareCenter. From there, it seems trivial to ensure that the Child cannot arrive twice, unless they have previously left.

Of course, then this aggregate would be pretty long-lived. You may then consider an aggregate for a BusinessDay or something similar to limit the raw count of child arrivals and departures.

Just an idea. Not necessarily the way to solve this.

Trivalent answered 13/6, 2015 at 15:42 Comment(1)
I hadn't thought of having a business day as an aggregate, but it seems kinda dodgy, and the case where kids stay the night wouldn't be accounted for.Dore
P
0

I would try to base the design on reality and study how they solve the problem without software.

My guess is they use a notebook or printed list and start every day with a new sheet, writing today's date and then taking notes for each child regarding arrival, lunch etc. The case with kids staying the night shouldn't be a problem - checking in day 1 and checking out day 2.

The aggregate root should focus on the process (in your case daily/nightly per-child caring) and not the participating data objects (visit, child, parent, etc.).

Pimental answered 2/7, 2015 at 12:58 Comment(0)
P
0

I'm missing something obvious

This one; though I would quibble with whether or not it is obvious.

"Child" probably should not be thought of as an aggregate in your domain model. It's an entity that exists outside your model. Put another way, your model is not the "book of record" for this entity.

The invariant I'm struggling with is:

A child cannot arrive if they are already here

Right. That's a struggle, because your model doesn't control when children arrive and leave. It's tracking when those things happen in some other domain (the real world). So your model shouldn't be rejecting those events.

Greg Young:
    The big mental leap in this kind of system is to realize that 
    you are not the book of record. In the warehouse example the 
    *warehouse* is the book of record. The job of the computer 
    system is to produce exception reports and estimates of what 
    is in the warehouse

Think about it: the bus arrives. You unload the children, scan their bar codes, and stick them in the play room. At the end of the day, you reverse the process -- scanning their codes as you load them onto the bus. When the scanner tries to check out a child who never checked in, the child doesn't disappear.

Your best fit, since you cannot prevent this "invariant violation", is to detect it.

One way to track this would be an event driven state machine. The key search term to use is "process manager", but in older discussions you will see the term "saga" (mis)used.

Rough sketch: your event handler is listening to these child events. It uses the id of the child (which is still an entity, just not an aggregate), to look up the correct process instance, and notifies it of the event. The process instance compares the event to its own state, generates new events to describe the changes to its own state, and emits them (the process manager instance can be re-hydrated from its own history).

So when the process manager knows that the child is checked in at location X, and receives an event claiming the child is checked in at location Y, it records a QuantumChildDetected event to track the contingency.

A more sophisticated process manager would also be acting on ChildEnrolled events, so that your staff knows to put those children into quarantine instead of into the playroom.

Working back to your original problem: you need to think about whether Visits are aggregates that exist within your domain model, or logs of things that happen in the real world.

Poole answered 29/4, 2016 at 13:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.