Search code examples
c#domain-driven-designcqrsevent-sourcing

How to make aggregate root smaller


What's going on

I'm developing the internal microservice that handles operation confirmations using one-time passwords. I've decided to create good domain model use event sourcing approach.

What I have

I have Confirmation Session for Operation of a certain Operation Type. User can add ask Session for a Confirmation Attempt and specify the Channel by which he wants to recieve Challenge (one-time password). Channel is like SMS, E-Mail, Push, etc.

Here is code excerpt from command handler

var session = _sessionFactory.CreateSession(operationType, userId);

var challengeCreationResult = await _challengeProvider.CreateAsync(
    recipient, channelType, session, addOnFields);

if (!challengeCreationResult.IsSuccess)
{
    return OperationResult<AttemptCreationResult>.Error(
        challengeCreationResult.ErrorCode, challengeCreationResult.ErrorMessage);
}

session.AddAttempt(challengeCreationResult.Data);

After creating the Confirmation Attempt and receiving Challenge, User is able to submit his response for validation.

var session = await _sessionStore.GetAsync(sessionId);

var result = await session.ApplyResponseAsync(response, _responseValidator, actingUserId, operationType);

Both AddAttempt and ApplyResponseAsync result in appropriate events being fired and return operation result to return to User.

What went wrong

I was satisfied with this implementation until I've tried to add business rules that require knowledge of more than one Confirmation Session. These are:

  1. If user gives five wrong answers for single Operation Type within one hour, we need to freeze him for one hour.
  2. User is only allowed to request new Confirmation Attempt if 30 seconds have passed since last Confirmation Attempt that has no correct answer.

Question

How to implement these rules while keeping aggregate root/services/event handlers small and focused?


Solution

  • I think that UserConfirmation looks OK as an aggregate, but not the idea of UserConfirmationHistory, as it is not recommended to have an aggregate with a lot of children, which would be the case here.

    This is a case where the business rule does not naturally fit the aggregate and should be moved up to a domain service, which can operate on a list of UserConfirmation instances, supplied by an Application Service.

    I mean, I don't like having any infrastructure concerns at all in my domain classes (be they aggregates or services), and fetching a list of UserConfirmation entries would require a Query or Repository implementation, which are infrastructure concerns, even if you do abstract them with interfaces.


    What I would do

    • Application Service: holds a reference to an IUserConfirmationRepository and fetches a list of UserConfirmation entries, passing them to a...
    • Domain Service: which will receive a list of UserConfirmation entries and validate logic spanning multiple aggregates

    This way, your business rules are still protected by a domain class.

    But proceed with caution:

    • By doing things this way, you may have just opened a breach. If users of your code are able to create and persist instances of the UserConfirmation entity without going through this Domain Service, then they will be able to bypass this invariant;
    • Then you should find a way to prevent it. Maybe by making the constructor internal in the UserConfirmation aggregate, and make it so that the only possible path to create this aggregate is by going through the Domain Service.

    PS. you mention that things were easier when you just had to do simple INSERTS, but:

    • DDD is not an approach for every project. There's a lot of friction early on in projects with DDD, which is only justifiable if your domain is very complex. Then you'll rip benefits down the road;
    • If DDD really is the right approach for this project, then you'll really benefit from doing things this way later