1

We're completely remodelling a system at the company for which I am currently working. We're applying DDD and for the very first time I have actually got someone on my team who has some prior experience with DDD as well (yay!)

This new system is extremely user-centric, i.e. pretty much every operation within the system comes from the end-user. Some of these operations may be:

  1. Create an Account. A non-paying user is limited to one active account, pro-members can have as many accounts as they want.
  2. Add a Transaction to an Account, but only if a User has access to said Account.
  3. Reorder Divisions in an Account, but only if a User has access to said Account.

There are a lot of these rules, all point to a single user and this is where me and my colleague came into argument.

My opinion is, aggregates should be as small as necessary, as cohesive, this makes their testing and understanding easier. E.g. if I model the first rule, I would create a simple aggregate, which could look like this:

class UserWithActiveAccounts {

    private UserId id;
    private NonNegativeNumber countOfActiveAccounts;
    private MembershipType membershipType;

    public Account createNewAccount(AccountId accountId, NonEmptyString accountName) {
        if (
            MembershipType.FREE.equals(membershipType) &&
            countOfActiveAccounts.GT(NonNegativeNumber.fromValue(1))
        ) {
            throw AccountLimitExceeded.forFreeUserTooManyAccounts(id);
        }

        return Account.withOwner(accountId, accountName, id);
    }
}

This aggregate contains only count of currently active accounts and user's membership type, because that's what the business rule when creating a new account cares about.

Then, if I had to model the other 2 user cases, I would need another aggregate, such as UserWithAccessibleAccounts, which would internally contain a List of AccountId values, which I could iterate upon adding a Transaction with a specific AccountId to check, whether the operation is allowed or not.

class UserWithAccessibleAccounts {

    private UserId id;
    private List<AccountId> accessibleActiveAccounts;

    public Transaction addTransaction(
        TransactionId transactionId,
        AccountId toAccountId,
        Number value
    ) {
        if (hasAccessToAccount(toAccountId)) {
            throw CannotAddTransaction.noAccessToAccount(id, toAccountId);
        }

        return Transaction.byUserInAccount(transactionId, value, id, toAccountId);
    }

    private bool hasAccessToAccount(AccountId accountId) {
        return accessibleActiveAccounts.contains(accountId);
    }
}

Obviously, with my approach you will have a lot of small aggregates each responsible for a tiny piece of business rule.

What my colleague would like is to have a large class, such as User, which would contain all operations. Which I am against. I believe it would lead to a god messy class, where it would be much more difficult to track bugs. Also, as this class would grow, each time you would want to run any of the operations on the user, you would need to load a lot of properties which are unrelated to the operation that the user is currently trying to do (such as loading the List of AccountId values when a simple count of them is necessary, in this very simplified example).

We're basically trying to solve the problem of:

  • either having to come up with a lot of class names for all those small aggregates,
  • deal with scalability issue and a humongous class due to pulling all the data for each operation, no matter how small.

I am inclined to the small aggregates approach, but perhaps I am not looking at it from the correct angle and there's something about the approach my colleague is suggesting I am simply not seeing.

  • Why createNewAccount and addTransaction return something? – Constantin Galbenu Feb 13 '18 at 9:08
  • @ConstantinGalbenu Both an account and a transaction require an id of the user who initiated the operation. It's returning an entity because this id is populated through the constructor and does not have to be exposed in the command handler. – Andy Feb 13 '18 at 9:42
  • sorry, I don't get it. – Constantin Galbenu Feb 13 '18 at 9:44
  • @DavidPacker code readability and cohesion are not the only aspects to consider when designing your aggregates. Transactional anlaysis is also important. You have to find a compromise between aggregate size and the amount of concurrent access to it, with eventual consistency being a good mitigator/plan B. You might want to research on aggregate as consistency boundary for insight on this. – guillaume31 Feb 14 '18 at 14:00
2

First of all. If you want to follow DDD then one of the rules for creating aggregates is that the transactions should not cross aggregate boundaries. But in your approach if a UserWithActiveAccount has a new accessible account then you will have to delete UserWithActiveAccount and create a new UserWithAccessibleAccount in the same transaction. Which is breaking one of the rules of designing aggregates in DDD.

I can see why you might think it is better to create different user entities. But be aware that you are now creating entities that doesn't even exist in your domain. And this can easily go wrong at one point. You can easily end up with a big number of user classes that doesn't exist in the ubiquitous language and then the amount of cognitive load (information needed to understand the code) will be huge.

Think about having more possible states for the user(user with expired account, user with deleted account ...). You will have to create a new class for each one and then what about a user that can be in two states at the same time? Would create a new class for each combination?

DDD is all about designing a model that reflects the domain (business) and in your domain there is a user that can have different states but it is still the same user. So I would suggest to have the same in the code. That means a user entity that can have different states.

Now let's discuss what can go wrong with this approach:

aggregates should be as small as necessary, as cohesive, this makes their testing and understanding easier.

This is not necessarily true. Keeping the functions small and following some of the coding good practices will keep the class simple and easy to understand. The size of the class doesn't necessarily mean it is complicated.

Also, as this class would grow, each time you would want to run any of the operations on the user, you would need to load a lot of properties which are unrelated to the operation that the user is currently trying to do (such as loading the List of AccountId values when a simple count of them is necessary, in this very simplified example).

What is the problem with that? Do you have limited memory on the machine where this code will be executed? will this result in a noticeable decrease in performance? If not then there is no reason to do some premature optimization for problem that you will probably never have to deal with. If all the data is well encapsulated in your class, there is no problem if it contains many properties.

  • But in your approach if a UserWithActiveAccount has a new accessible account then you will have to delete UserWithActiveAccount and create a new UserWithAccessibleAccount in the same transaction. I should have added that this is a stateless API we're programming, so everything is pulled from the database on each user's request anyway and the requests share no data between each other. – Andy Feb 13 '18 at 13:36
  • I understand but still there is some transactions that can involve 2 or more aggregates. The example I thought about is when you have a request that is creating an accessible account (or any other action that can change the user state or type) then you will have to delete a user from one aggregate and create a new one in another aggregate. And all this needs to happen in the same transaction. – Mohamed Bouallegue Feb 13 '18 at 14:22
  • Each command to the system is atomic and affects only a single aggregate, such as creating a new account creates a new Account aggregate, adding a Transaction to an Account creates a new Transaction aggregate, etc. Side-effects of such changes would be propagated through eventual consistency using domain events and managed by command-inducing-sagas, where each command once again affects only a single aggregate. – Andy Feb 13 '18 at 15:07
  • Sorry, I'm not sure if I understood correctly – Mohamed Bouallegue Feb 14 '18 at 9:26
1

I'm not a fan of your smaller aggregate approach. You are over-valuing small at the expense of a large overhead of slightly different classes that repeat themselves a lot. However, I think both of you are too focused on user and aren't fleshing out other domain objects properly.

Based on your three rules user should be an abstract class with free and premium implementations, a single method to create an account. You then have an account object that contains its valid user and has two methods, one for adding transactions and one for reorder divisions(or a more general update method).

Your concern about having to load too much data with each request is a sign your abstraction isn't correct and needs to be tweaked, or you aren't taking advantage of deferred execution/lazy loading. Also, needing to come up with lots of similar names is a sign you aren't using a useful abstraction.

basic account example:

class Account{
  private int AccountId;
  private user ValidUser;
  private TransactionCollection myTransactions;

  public AddTransaction(transaction, User){
      if(validUser==User)
          myTransactions.Add(transaction);
      else
          throw noAccessException;
  }
}
  • Could you perhaps share an actual code example of the Account class? I don't see why the Account should have a method to add a transaction. Based on the business rules, Users may add transactions to accounts they have access to, it's a User who adds a Transaction into an Account, not an Account adding the Transaction by itself. Following the rule of ubiquitous language, shouldn't the methods be on a User aggregate by definition? – Andy Feb 13 '18 at 15:04
  • @DavidPacker It matters how you define objects. A user that has accounts that have transactions means everything has to go through user first. An account that has transactions and has valid users makes the intent far easier to understand. The important interaction is usually between the transaction and the account so that's where the abstraction should focus. forcing everything through user just adds noise. – Ryathal Feb 13 '18 at 20:00
0

From your question title, I'd deduce that yes, it's fine to have separate aggregates for concept from different points of view, since that makes it sound like you've got several bounded contexts. The Account aggregate thus apparently means something from point-of-view X and again something different from point-of-view Y.

However, to me it does not seem like you're dealing with different bounded contexts based on the code snippets you've provided. It's more like you've created aggregates which describe two different states of an Account aggregate.

Taking that stance, I'd say what @Mohamed Bouallegue commented above seems like a fine take home message what you could be doing instead.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.