0

I have a subdomain which involves tracking user financial data across different financial account types.

For example, users can input data for their:

  • bank accounts,
  • credit cards,
  • loans,
  • lines of credit,
  • real estate,
  • and more...

Now within each individual type, there are more subtypes.

For instance, under loans:

  • personal loans,
  • business loans,
  • mortgages,
  • car loans,
  • and more...

They would each have their own particular invariants, with some unique properties and functionality, and some shared properties and functionality.

I've been approaching this using composition, creating an aggregate for each subtype, and using interfaces and helper interface implementations to share similar logic between aggregates.

However, it appears as though I'm going to end up with dozens of different aggregates when modelling all these different account types. This doesn't feel right.

Alternatives I've considered:

  • have a type property on the loan aggregate, and conditional logic based off the type.
  • create different bounded contexts for each of these types: This feels like overkill, I believe this is all part of the same business subdomain.
  • create aggregates based off shared functionality - eg SecuredLoan and UnsecuredLoan aggregates
  • creating subclasses in the general aggregates to hold the subtype's unique functionality. get some encapsulation of subtype specific logic, with some conditional logic still (eg conditional properties on the aggregate). Not really sure the difference between this and just creating a separate aggregate for each subtype

Tradeoffs seem to be, the more general the implementation, there will end up being a ton of conditional logic, and conditional properties based off the subtype. Versus building specific aggregates for each subtype, the logic per aggregate is simplified, but there ends up being hundreds of commands in the application layer, a lot of them which are basically the same thing but to a different subtype. Additionally, there end up being dozens of repositories.

It feels like I either get an explosion of conditional logic complexity in a general aggregate, or an explosion of the number of aggregates (or contexts) if building one per subtype.

Question - is there a known pattern for dealing with this type of modelling problem? Or is it really just dealing with the above tradeoffs, and finding something which fits best? In that case, is there some precedent I can apply to the decision-making process, as I'm struggling to decide between the above approaches. And is it problematic if there end up being many dozens of aggregates within a given context?

Nelav
  • 45
  • 6

2 Answers2

2

Rather than starting with the data at rest to get your aggregates, consider instead what operations/changes ("commands" one might say) will be performed and how the results of those operations affect future operations is what leads to what the aggregates want to be. Event storming style approaches can be helpful for figuring out these relationships between state changes.

For instance, each of these kinds of loans might have AccrueInterest, DrawPrincipal, and RecordPayment commands which operate on the balances identically (given perhaps configurable rate parameters etc.) and which don't affect and aren't affected by other commands. In that scenario, you can have a Loan aggregate which models the idea that there's a loan with interest and principal balances on which interest accrues and payments are made. An AutoLoan aggregate might then just be managing the collateralization of Loan ABC123 with VIN 1G1234567890.

Levi Ramsey
  • 18,884
  • 1
  • 16
  • 30
  • Right, this would be an approach for sharing the common `Loan` logic, whilst having specific subtype aggregates. Do you have reasons to prefer the approach you described, vs. having an `ILoan` interface with implementation helpers to share the logic? Thinking on your solution, I'm wondering what happens if the `AutoLoan` doesn't adhere exactly to the `Loan` specification, and needs to implement one of its methods internally. I assume we're sending `AccrueInterest` to the `Loan` aggregate, but what if we need a different implementation of `AccrueInterest` in the `AutoLoan` aggregate (eg) – Nelav Jun 05 '22 at 16:55
  • I do like in your approach that we only need to write the `Loan` commands once, as opposed to per each subtype aggregate – Nelav Jun 05 '22 at 16:56
  • 1
    It's possible, especially if taking a more event-based approach (which is perhaps something this style leads to) to have the auto loan aggregate subscribe to changes in the loan it's associated with. This introduces some eventual consistency (in that there's a delay between accruing interest and whatever change in the auto loan results. – Levi Ramsey Jun 05 '22 at 17:41
  • 1
    For example: if there's some aspect of the domain which is "if the total balance (principal plus accrued interest) exceeds the estimated value of the collateral, begin a sort of foreclosure process", that sort of check can be modeled as a process which happens eventually (which typically captures how one would do it without software: a clerk cross-checking auto loan balances with collateral values). – Levi Ramsey Jun 05 '22 at 17:44
1

Sorry for the simple starting answer, but …

Build your aggregates based on your actual use cases.

Aggregate A - Scenario A

Aggregate B - Scenario B

Avoid building aggregates for general conditions, DDD and ubiquitous language is about developing language, aggregates and systems around the use case not general purpose.

General purpose has its use cases and isn’t necessary anti DDD; but the focus is on creating the specifics then abstracting to generality.

mrdnk
  • 656
  • 9
  • 24
  • Thanks! First impression, this would suggest building an aggregate per whatever the user scenario is (track loan vs track mortgage, eg). I guess this would also suggest if there were 25 such scenarios, then 25 aggregates would be reasonable. I suppose shared logic would be a secondary concern – Nelav Jun 05 '22 at 14:50