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:
- Create an Account. A non-paying user is limited to one active account, pro-members can have as many accounts as they want.
- Add a Transaction to an Account, but only if a User has access to said Account.
- 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.
createNewAccount
andaddTransaction
return something?