Determining aggregate roots when invariants exist in a list

I am making a family day care application and I thought I would try DDD / CQRS / ES, but I have problems designing the units. The domain can be described quite simply:

  • The child receives registration
  • Baby can arrive
  • Child can leave

The goal is to track visit times, create invoices, write notes (for example, what was done for lunch, injuries, etc.) against visits. These other actions will certainly be the most common interaction with the system, as the visit starts once a day, but something interesting happens all the time.

The invariant I'm struggling with is:

  • A child cannot arrive if he is already here.

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

1. The single root of the Child aggregate

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

It seems simple, but since I want every event to be associated with a visit, this means that the visit will be the essence of the Child aggregate, and every time I want to add a note or something else, I have all the visits for this children have ever been. It seems ineffective and rather irrelevant - the child himself and every other visit simply have nothing to do with what the child has for lunch.

2. Aggregate Roots for Child and Visit

Child will only generate ChildEnrolled , and Visit will be the source of ChildArrived and ChildLeft . In this case, I do not know how to preserve the invariant, except that Visit accepts the service for this purpose, which I saw is not recommended.

Is there any other way to force an invariant to apply to this design?

3. This is a false invariant.

I suppose this is possible, and I have to protect against several people signing up for the same child at the same time, or latency, which means that the user clicks the "login" button several times. I do not think this is the answer.

4. I miss something obvious

It seems most likely - of course, this is not some special snowflake, how is it normal? I can hardly find examples with multiple ARs, not to mention lists.

+7
domain-driven-design aggregateroot cqrs event-sourcing
source share
4 answers

Placeholders

You are talking mainly about Visits and what happened during this Visit , so it looks like an important domain concept of your own.
I think you will also have a DayCareCenter , which will include all Children .

So, I would go with these combined roots:

  • Daycarecenter
  • Child
  • Visit homepage

By the way: I see another invariant:
"A child cannot be in several kindergartens at the same time"

"Holds down the Login button several times."

If each command has a unique identifier that is generated for every intentional attempt - not generated by every click (unintentional) , you can buffer the last n received command identifiers and ignore duplicates.

Or maybe your communications infrastructure (service bus) can handle this.

Create Visit

Since you are using multiple aggregates, you need to request some (reliable, consistent) store to see if the invariants are satisfied.
(Or, if the collision is rare and the ā€œcancellationā€ of the invalid Visit manually is reasonable, perhaps a consistent reading model will work too ...)

Since a Child can have only one Visit current, Child stores only a small amount of information (event) about the last run Visit .

Whenever you need to start a new Visit , a ā€œsource of truthā€ (recording model) is requested for any previous Visit and it is checked whether Visit ended or not.

(Another option would be that Visit can only end with the Child aggregate, while preserving the ā€œcompletionā€ event in Child again, but this seems not very good for me ... but this is only a personal opinion)

Part of the request (check) can be performed through a special service or simply passed to the repository to the method and directly requests it - this time I go with the second option.

Here is some Cse-compiled pseudo-code with brain compilation 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-ac-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); } } 

Notes:

  • RaiseEvent(...) calls Apply(...) instantly behind the scenes
  • writeSideRepository.Save(...) actually saves events
  • LurchTable used as MRU list of fixed sizes of command identifiers
  • Instead of transferring the entire event store, you can make a service for it if you benefit you

Denial of responsibility:
I am not a famous expert. This is exactly how I approach him. During this answer some patterns may suffer.;)

+2
source share

Looks like ā€œhereā€ in your invariant ā€œthe child cannot arrive if they are already hereā€, there may be an idea for the totality. Maybe Location or DayCareCenter . From there, it seems trivial to guarantee that the Child cannot arrive twice, unless they have left earlier.

Of course, then this unit would be quite durable. You can then consider an aggregate for BusinessDay or something similar to limit the initial count of arrivals and departures of children.

Just an idea. Not necessarily a way to solve this problem.

0
source share

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

I assume that they use a notebook or a printed list and start every day with a new sheet, writing down the date today, and then writing down for each child about arrival, lunch, etc. The case of children who stay overnight should not be a problem - check on day 1 and check on day 2.

The aggregated root should be focused on the process (in your case, daily / night care for the child), and not on the student’s data objects (visit, child, parent, etc.).

0
source share

I'm missing something obvious

This; although I would think about whether this is obvious.

A ā€œchildā€ should probably not be considered a collection in your domain model. This is an object that exists outside of your model. In other words, your model is not a "record book" for this object.

The invariant I'm struggling with is:

A child cannot arrive if he is already here.

Right This is a struggle because your model does not control when children come and go. It keeps track of when this happens in some other domain (in the real world). Therefore, your model should not reject these 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 is arriving. You unload the children, view their barcodes and insert them into the playroom. At the end of the day, you change the process - scanning their codes when downloading them to the bus. When the scanner tries to check a child who has never registered, the child does not disappear.

Best suited since you cannot prevent this ā€œinvariant violationā€ in order to detect it.

One way to track this is through 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 listens for these child events. It uses the identifier of the child (which is still an entity and not an aggregate) to find the correct process instance and notify about this event. A process instance compares an event with its own state, generates new events to describe changes in its own state, and emits them (a process manager instance can be rewetted from its own history).

Therefore, when the process manager knows that the child is verified at location X, and receives an event requiring the child to be registered at location Y, it records the QuantumChildDetected event to track unforeseen circumstances.

A more sophisticated process manager will also act on ChildEnrolled events so that your staff knows to quarantine these children instead of the playroom.

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

0
source share

All Articles