Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Defining aggregate roots when invariants exist within a list

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.

like image 225
XwipeoutX Avatar asked Jun 13 '15 03:06

XwipeoutX


People also ask

How do you determine the aggregate root?

When choosing an aggregate root you choose between Transactional Consistency and Eventual Consistency. When your business rules allow you would rather favour Eventual Consistency.

Can an entity be part of multiple aggregates?

As you rightly pointed out, Entity instances shouldn't be shared between aggregates, as one aggregate wouldn't be aware of changes to the entity made through another aggregate and couldn't enforce its invariants.

What is aggregate root in event sourcing?

An Event Sourced Aggregate Root The aggregate root can be thought of as a number of functions that take value objects as arguments, execute the business logic and return events. Important: This means we never have any getters or expose the aggregate root's internal state in any way!

Can an aggregate root reference another aggregate root?

What you cannot do is reference anything inside the other aggregate root.


1 Answers

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. ;)

like image 182
David Rettenbacher Avatar answered Oct 16 '22 15:10

David Rettenbacher