Aggregates creating other aggregates

466 views
Skip to first unread message

Alexey Raga

unread,
Aug 15, 2021, 5:47:29 AM8/15/21
to DDD/CQRS
Hi All!

As far as I understand, it is pretty normal for aggregate roots to create other aggregate roots.

So if I, say, have a Task aggregate root I can do something like:

Performance perf = myTask.StartPerformance(userId);

Where Task and Performance are both aggregate roots.

But what if there was a rule that would state that a performance can only be attempted no more than N times per user? The information about that N would be in Task, and Task can pass it to a Performance as a part of StartPerformance..
But not at the second attempt, we should load the existing performance somehow instead creating a new one?

Would it be fine to load/create Performance aggregate root somewhere in AppService/CommandHandler instead of doing it in Task Aggregate?
Who then would "own" and enforce "no more than N times" business rule?

Taking "N" out of Task and validating it a command handler doesn't feel right...

Double dispatch like below doesn't feel right either: 

var performance = PerformanceRepository.Load(perfId);
var task = TaskRepository.Load(taskId);
task.StartPerformance(performance);

Or is it fine? Or how would you approach this?

Regards,
Alexey.

Harrison Brown

unread,
Aug 15, 2021, 8:28:42 AM8/15/21
to ddd...@googlegroups.com
If the limit of how many performances can be done per task is genuinely a domain invariant then you probably do need to get performance into rhe Task aggregate. Aggregates are boundaries of consistency so if you need to synchronously protect the performances per task limit then that needs to be inside the same aggregate boundary, probably.

However, in many cases it’s fine to actually allow what feels like a fixed rule to have some flexibility. What’s the actual business impact if multiple performances are attempted? Could you just eventually disregard any further performances beyond the limit in the 1% of times it might happen and put something in the UI to prevent the rule being broken the other 99%?

Harrison

Sent from my iPhone

On 15 Aug 2021, at 10:47, Alexey Raga <alexe...@gmail.com> wrote:

Hi All!
--
You received this message because you are subscribed to the Google Groups "DDD/CQRS" group.
To unsubscribe from this group and stop receiving emails from it, send an email to dddcqrs+u...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/dddcqrs/7e897065-55ee-42a7-9cc3-9b35ad7eb9b1n%40googlegroups.com.

Alexey Raga

unread,
Aug 15, 2021, 10:45:29 PM8/15/21
to DDD/CQRS
These are valid concerns!

It would be possible to disregard further attempts later, if it happens not too often, I agree. The strict rule is that only N attempts count for asserting the final result. 

So, wouldn't it be the same problem later when we make a decision whether to accept or disregard an attempt to compute a final result of a performance for a given task? At a time of evaluating the results, we would need to know the max allowed number of attempts + the data from the attempts themselves. Results evaluation can be made the responsibility of Task, but then it'd need to access internals of Performance (or double dispatch). And I don't think that I can make Performance aggregate to be aware of max attempts count at creation time because Task may be changed, and there can be thousands "performances" that already exist and that would need to allow more attempts from now on...

Any thoughts?

Regards,
Alexey.

Harrison Brown

unread,
Aug 16, 2021, 4:38:58 AM8/16/21
to DDD/CQRS

Yep, I would find a way to disregard them if they were submitted, and then I’d check how many Performances had been submitted for a given Task in the user interface and hide the controls to add a new Performance. That leaves a very small chance that too many Performances would be submitted and the user will know that they shouldn’t submit more than X performances.

For the evaluation, there’s a few ways to do it. What triggers a Task’s Performances to be evaluated? Does a human press a button, is there a timeout, after the 3rd submission something else entirely?

Harrison

Francisco Javier Estrella Rodriguez

unread,
Aug 16, 2021, 6:46:56 AM8/16/21
to DDD/CQRS
Hello,

You can find the answer in eventual validation of consistent data.

This would be that when you send the command to start the task to execute to an aggregate, you consult the "table" where you see the attempts with user x (not to be confused with the reading model).

When you are going to execute the command N times, as you have a control over the status of attempts you can cause a rejection before it reaches the aggregate.

This is a very common practice.

Greetings.

Alexey Raga

unread,
Aug 16, 2021, 7:44:08 AM8/16/21
to DDD/CQRS
> What triggers a Task’s Performances to be evaluated?

Say, when a user finishes the performance, it gets evaluated (scored, etc.) automatically. So yes, human presses a button. Then, if they are not happy with how an evaluation results are turned out to be, they can make another attempt.
I guess we can imagine an online coding exercise as a close enough illustration: as a user you see the task, you try solving it, you submit your solution, you get scored. If you are not happy - you can try again. But you only get N tries to show your best.

> when you send the command to start the task to execute to an aggregate, you consult the "table" where you see the attempts with user x (not to be confused with the reading model).

Do you mean something like an Application Service consulting a projection? Don't even encode this rule as a part of any aggregate?

Alexey.

jest...@teides.com

unread,
Aug 16, 2021, 8:24:42 AM8/16/21
to ddd...@googlegroups.com

> Do you mean something like an Application Service consulting a projection? Don't even encode this rule as a part of any aggregate?

 

Because of data inconsistency, you cannot use a projection or read model.

 

What is done in these cases is to create a support table. That would contain in your case, for example, table attempts (userid, number of attempts) every time you try, you must read this table to see how many attempts you have. Then you must update this table with the data. When you reach the limit of attempts, then what you do is to return the corresponding error because you reached the number of attempts.

 

Where does this logic go?

 

In my case I have done it in the command handler, but you can also do it in an application service.

 

Greetings.

--
You received this message because you are subscribed to a topic in the Google Groups "DDD/CQRS" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/dddcqrs/3YkbuiN-OGg/unsubscribe.
To unsubscribe from this group and all its topics, send an email to dddcqrs+u...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/dddcqrs/83b32ed1-edf9-449f-9228-31170faeec0en%40googlegroups.com.

Message has been deleted

Alexey Raga

unread,
Aug 16, 2021, 7:35:51 PM8/16/21
to DDD/CQRS
> What is done in these cases is to create a support table.
> Then you must update this table with the data.
> In my case I have done it in the command handler

Hmm... I have never seen this approach. Looks like a part of an important logic is being encoded directly into the command handler and is not a part of the domain. Feels like a shortcut or a cheating :)

Alexey.

Francisco Javier Estrella Rodriguez

unread,
Aug 17, 2021, 3:22:11 AM8/17/21
to DDD/CQRS
Premise

Reading an aggregate from another aggregate does not seem to me a good solution, this indicates that you must put a service to communicate between them.

Any action you want to perform on an aggregate is always through a command.

On the other hand, the solution I was asking you.

You should look up the term eventual validation of consistent data.

I understand that you use event sourcing to store the data.

Example of use,

When you have a condition where you cannot repeat the user name, due to the way event sourcing works you cannot rely on looking up the information in the read model or projection to validate that it is not repeated because of data inconsistency.

The solution they have come up with for these cases is to create an alternate table with an index on the username and handle its state outside before you invoke the aggregate.

Translating this behavior to your use case, the solution would simply be as I have suggested, the important thing is that you understand how to solve the eventual validation.

Harrison Brown

unread,
Aug 17, 2021, 5:46:31 AM8/17/21
to DDD/CQRS

Yes, doing something like this does feel somehow ‘wrong’ but when you absolutely have to do set validation having a separate table that you look at is a sensible pragmatic approach, otherwise the only option you’re left with is to put everything into a single massive aggregate which is obviously wrong.

When I do have to do this I do prefer to put it in the domain model rather than in the command handler. I’d use double dispatch something like the pseudo code shown below.

However, as I say you can often find that you don’t actually need to do cross-aggregate validation. There is already an interesting discussion on that topic here which you should read: https://groups.google.com/g/dddcqrs/c/aUltOB2a-3Y/m/0p0PQVNFONQJ (including comments from Greg Young who invented event sourcing)

I’d also recommend you watch this presentation which covers related topics: https://verraes.net/2013/06/unbreakable-domain-models/


Note that even that code doesn’t guarantee that performances cannot exceed the limit because of concurrency. Handling strict concurrency is a pain because you end up having to do locking or something, hence why I tend to avoid attempting to actually do set validation in DDD and rather do looser validation further up the stack (e.g., in the UI or HTTP controller) and then add code to handle the <1% of times one does slip through. In your case, that might mean having some code which listens for a performance that sneaks through your validation and revokes the grade applied to a Task if it was given by an invalid Performance submission.

Doing this does leak some business knowledge out of your domain so you might want to look at the Domain Query pattern or Specification pattern.

namespace MyApp\Domain;

class Task {
    const PERFORMANCE_LIMIT = 3;

    // Using the Task aggregate as a factory for Performances
    // to give an expressive domain model
    public function submitPerformance(
        PerformanceContent $performanceContent,
        PerformanceRepository $performances
    ) : Performance
    {
        if ($performances->countForTask($this) > self::PERFORMANCE_LIMIT) {
            throw CannotSubmitMorePerformancesForTask($this, $performanceContent);
        }

        return new Performance($this->id(), $performanceContent);
    }
}

interface PerformanceRepository {
    public function countForTask(Task $task) : int
}

// .............

namespace MyApp\Infrastructure;

class SqlPerformanceRepository implements PerformanceRepository {
    public function countForTask(Task $task) : int
    {
        return DB::table('performances')
            ->where('task', '=', $task->id()->toString())
            ->count();
    }
}

Harrison

Harrison Brown

unread,
Aug 17, 2021, 5:52:33 AM8/17/21
to DDD/CQRS
Reply all
Reply to author
Forward
0 new messages