Aggregates, entities, domain services

Hi,

I am working with a domain where invariants were identified on different “levels” so to say. The real domain is too complicated to describe, so I’ll come up with a simple-to-understand analogy. Some of the rules might be painfully artificial, but bear with me. They present the same problems as the one I’m facing.

Let’s say we have a system that manages user accounts for an organization. Users can create and increment counters (because it’s good to be able to count on something). There are some rules, like

  1. “The user cannot increment the same counter twice in less than 30 seconds”
  2. “The user must have either a phone number or an e-mail address specified”
  3. “The user can provide up to five e-mail addresses”

And there are some rules that apply to the users as a group.
4. “No e-mail address can be registered twice” (so one user cannot specify an e-mail address another user has specified, not even for a tiny fraction of a second)
5. “There can only be a single supersystemadministratorgod user at any time” (let’s say users have different levels, that can change along the way via promotions by higher level users, but the highest level can only be granted by a quorum of slightly lower level users, demoting the previous god - again, hard consistency requirement)

Users are expected to obsessively manipulate their counters, but it is a rare occassion that they edit their e-mail addresses.

So the problem is there’s rules I can enforce by loading just a user. At the same time there’s rules that I can enforce by loading data related to a large number of users.

Now in a traditional system I think I would handle this by creating an “Aggregate” for User that’s responsible for enforcing 1-3, and a domain service for 4. and 5.
In a system with DDD and ES, I struggle to understand how that would work.

Now in my understanding if we create an aggregate that cares about the details of a single user, it can enforce 1 - 3 but not 4. and 5. On the other hand if we create an aggregate that has the data of all users, all of the rules can be enforced, but it means having a single instance with way too many events happening to it, and with many users clicking “+” at the same time, would lead to performance issues.

So one way I could think of is to create the aggregate “Users” that spans all users but cares about only the rights and the emails, and one that’s for a single “User”, which has the counters, the e-mails, phone number, etc. Requests to edit the e-mail address list (which should be rare), would have to go to Users, which would need to do the validation and the manage the e-mail address list, while incrementing counters would go to aggregates representing individual users.
When a user account is to be created, the first command would have to hit “Users” and there a new entity would be created for the aggregate, saving an event in the stream of “Users”. A process manager would then pick this event up and create a “User” aggregate with its own stream for the events related to counter manipulations and things that do not affect other users.

What do you think about such an approach? Am I overcomplicating? Is there an easier way?

Tamas,

Point 5. We have a very similar requirement, and ended creating a specific stream that indicates which user is currently “god”. It simply holds the user id.
Using eventually consistency, this “GodAllocatedEvent” makes it’s way to User aggregate

Point 4 - funnily enough, we have almost the same issue. Rehydrating 1000’s of events to verify a name / email is not duplicated was a non starter for us, so we played around with using a read model, then eventually settled on a little projection which scanned through a specific stream to find a match (using a string placeholder)

Here is the code we use:

fromStream("$et-OrganisationCreatedEvent")
    .when({
        $init: function (s, e) {
            return {
                total: 0,
                organisationId: 0
            };
        },
        'OrganisationCreatedEvent': function (s, e) {
            if (e.data.organisationName == "{ORGANISATIONNAME}") {
                s.total += 1;
                s.organisationId = e.data.organisationId;
            }
        }
    });

The placeholder {ORGANISATIONNAME} gets overwritten in our C# with the name we are searching for.

Yes, I was thinking of these too. However, if I’m using a projection to figure out whether the e-mail address exists (or that there’s no god user at the moment), I risk having the situation that the same address has just been registered (user promoted), it’s just the read model is not updated yet. If this risk is unaccaptable, I basically need to block processing and make sure that the read model has caught up before issueing the my command - and after success, let others continue with their registrations/promotions. I think this is very inconvenient and I’d prefer if there was an aggregate I could tell “do this” and it says either “yes, SIR” or “the hell you do”. :slight_smile:

What is your approach to this?

Would the projection not help in this scenario. It’s checking the evenstore fairly quick for a duplicate, and if don’t your domain service allows the user to be created with that email address.
Even if you rehydrated all the email address, I can’t see how you could lock the entire system (unless you aggregate was actually ALL users (but that would be very nasty)
Personally, I don’t the think the invariant is on UserAggregate (the duplicate email) and it’s hard to see how a new aggregate could work round the problem.

I added a nasty twist to my requirements by saying that the user can define up to 5 e-mail addresses (not necessarily at creation time), so the no duplicate e-mail rule does not only apply at creation time.

I know in many cases it’s acceptable that the system says ‘OK’ to a request, and then when it turns out some constraint was violated, it informs a client about how sorry it is for choosing it as the victim. But our “client” is not a reasonable human, but another system that wants to be able to count on our response and not introduce complexity by allowing “probably”, “maybe” together with “false” and “true”.

I am trying tot hink of technical ways to achieve this (but probably not nice DDD)
Maybe the email address becomes a stream name itself?
So something like;

Load email aggregate as EmailAggregate-

You could then quickly see if it’s been used.
Of course, you have the issue of small time between checking this, and creating your User.