TL;DR
In this blogpost I find a way to make the rules part of the domain. Next to that I develop some code so I can store them on disk. Find the resulting code at GitHub.
Making the rules part of the domain
As I value my privacy I don’t want to leak my personal finances to the internet. Although if you look closely you probably can guess one of my banks 😉. In the examples I use some fake transaction file and fake categorization rules. The code creates all rules programatically. Next to that I hardcoded the transaction file. It frustrates me every time I want to use my tool that I need to copy paste the rules I have personally to my codebase. After that I comment the ‘demo’ code and run the tool. Furthermore I noticed in my previous blogs that the rules are bolted on. So I think it is time to make the categorization rules part of the domain.
Let’s rephrase quickly what I want to do with this tool. I want to get financially independent at some point in the future. In order for me to reach this I create an application that shows me my progress. Next to that this is my favorite pet project to learn new things. Originally this project was meant to learn Domain Driven Design.(DDD). In good DDD fashion I created some ubiquitous language to guide the goal of my project:
To achieve financial insight the application needs to import and categorize financial transactions. I want to set budgets for a year and compare them to the actual amounts. Furthermore based on the actual numbers the savings rate and years till financial independence must be calculated. In order to calculate the years till financial independence there is something to assess my savings and investments.
New language?
If I make the rules part of my domain I probably need to adjust the Ubiquitous Language a bit. Yes the language includes categorizing transactions, but I chose a way to do this: a rule based approach. Maybe something allong the lines of:
To achieve financial insight the application needs to import and categorize financial transactions using Categorization Rules. I want to set budgets for a year and compare them to the actual amounts. Furthermore based on the actual numbers the savings rate and years till financial independence must be calculated. In order to calculate the years till financial independence there is something to assess my savings and investments.
Making the rules part of the domain
Let’s go to work. As mentioned the copying and pasting of rules is my main issue in really using the application. One way to prevent this would be marchalling these rules to disk. Next to to that I think these rules are an important part of the domain. First I take a look at the current class hierarchy relevant to rules:
Apparently I named the interface CategoryRule
, however in the ubiquitous language I called it Categorization Rules. That must be in sync. So I rename the interface to CategorizationRule
to reflect this. Next to that I rename the other classes as well. A simpel but important change.
As I have many types of CategorizationRule
and even more instances of it, lets focus on these first. So for now I ignore the lonely CategorizationRules
class. The first question I ask myself: Does the CategegoryRule belong to one of the existing aggregates. As a recap of an earlier blog post, currently I have the following aggregates: Category
, Budget
. To me the CategorizationRule does not belong to Budget. It has really nothing to do with it. Maybe you could argue that the rules lead to a Category and therefore it should belong there. Another option would be to create a new aggregate completely separate. A CategorizationRule
assigns a Transaction
to a Category
. And as both Transaction
and Category
are part of the aggregate Category
it makes sense to first try to see if I can make it work within this aggregate.
CategorizationRule as part of Category
How can I make this part of the domain. Lets look at the DDD toolbox how we can do this. The following basic building blocks are available to me:
- Entities
- Value Objects
- Services
Which one has the right characteristics given my needs. Lets discuss them one by one.
Entities
In the DDD book a nice summary is given for Entities:
Some objects are not defined by their attributes. They represent a thread of identity that runs through time and often across distinct representations. Sometimes such an object must be matched with another object even though attributes differ. An object must be distinguished from other objects even though they may have the same attributes. Mistaken identity can lead to data corruption.
Value Objects
Similary the book covers a summary for Value Objects:
Many objects have no conceptual identity. These objects describe some characteristic of a thing.
Services
For services the book also has a summary:
Sometimes it just isn’t a thing. In some cases, the clearest and most pragmatic design includes operations that do now conceptually belong to any other object. Rather than force the issue, we can follow the natural contours of the problem space and include
Services
explicitly in the model.
Conclusion
Looking at the 3 options the rule typically doens’t change over time, It is more an object than a operation, thus I think a value object would be best. So the equals methods and hascode need to be refactored. I won’t explain how that is done as it is quite easy.
How to fit the CategorizationRule into the aggregate
If you look at the semantics a CategorizationRule
is used to add a financial transaction to a category. So first lets look at the current model:
Currently the Category.add(...)
method is responsible for adding Transactions
to a specific Category
. This method has no logic whatsoever and is just a simple add to a list. Can I plug the rules into this processing? A possibility is to attach the all the instances of CategorizationRule
that will add transactions to a category to the category itself. And then changing the inner workings of the add method to check if indeed the transaction adheres to the rules before it is added. Lets try to build this and see how it works using unit tests.
@Test
void transactionsAreOnlyAddedWhenRuleMatches() {
final Category category = new Category("category");
final CategorizationRule rule = new DescriptionCategorizationRule(category, DESCRIPTION2);
category.addCategorizationRule(rule);
final Transaction transaction1 = new Transaction(ACCOUNT_NUMBER, JANUARI_01_2017, CONTRA_ACCOUNT, COUNTER_PARTY, EURO_10, DESCRIPTION);
final Transaction transaction2 = new Transaction(ACCOUNT_NUMBER, DECEMBER_31_2017, CONTRA_ACCOUNT, COUNTER_PARTY, EURO_11, DESCRIPTION2);
category.addTransactions(transaction1, transaction2);
assertThat(category.transactions(), not(hasItems(transaction1)));
assertThat(category.transactions(), hasItems(transaction2));
}
So the Category
has 1 description rule attachted. Therefore only the second transaction should be added to the Category
. It is still a bit strange that the Category is part of the Rule for now. As this is an experiment I’ll ignore that for now. In order to make this pass I change the implementing code. Relevant Category
code looks as follows:
public class Category {
public void addTransactions(final Transaction... transactions) {
for (Transaction transaction: transactions ) {
if(shouldCategorizeTransaction(transaction)) {
this.transactions.add(transaction);
EventBus.getInstance().publish(new CategoryUpdated(this));
}
}
}
private boolean shouldCategorizeTransaction(Transaction transaction) {
for (CategorizationRule categorizationRule : rules) {
boolean categorized = categorizationRule.(transaction);
if (categorized) {
return true;
}
}
return false;
}
}
This model does not scale
So this works for transactions belonging to 1 category, categorized by 1 Categorization rule. How would the code look like when we have multiple categories, many CategorizationRules and even more Transactions. I don’t think this will scale. The main problem I see is that in the old situation the rules were ordered. The UncategorizedRule
would accept everything. Next to that I have a other Category
taking all left over small amounts. If one of both rules would end up very early in the rule chain it would mess up the complete insight. So this wouldn’t work without ordering the categories themselves and then adding transactions to these categories. Making sure that the other and uncategorized Category
are processed last. I don’t want to change the characteristics of categories in such a way. For reference this code can be found on GitHub as well. Al be it on a dead branch.
Back to the drawing board
Ok the CategorizationRule
s are cannot be tied to 1 Category as shown above. Can I use the CategoryRepository
for this? Re-reading the repository chapter it seems that this is not a good fit. The responsibility of a repository is to retrieve and delete aggregate roots. Maybe we can design a Service. Lets try to design such a service. I think it would be great to have a service that takes a transaction and then calculates the resulting category it belongs to:
public interface Categorization {
Category categorize(final Transaction transaction);
}
I don’t remember the exact specifics of a service. So I read the relevant chapters again and I find:
A good service has 3 characteristics:
- The operation relates to a domain concept that is not a natural part of an
ENTITY
orVALUE OBJECT
.- The interface is defined in terms of other elements of the domain model.
- The operation is stateless
The first 2 characteristics are a clear match. The third one is not a 100% match. As I need state, the rules, to do the categorization. Reading somewhat further:
Statelessness here means that any client can use any instance of a particular
SERVICE
without regard to the individual instance’s individual history. The execution of aSERVICE
will use information that is accessible globally, and may even change the global information. (…) But the service does not hold state of its own that effects its own behavior, as most domain objects do.
So I need to be able to instantiate multiple versions of the same service and the results should be identical. I think I can make that work. Because on any moment I’m categorizing this is true. In time more rules may be added or removed. If I factor the rules itselve outside of the service I adhere to the official spec. So I decide to give it a try and see if this will work for me in a practical manner.
Trying the service to make the rules part of the domain
Then the question arises is it an application service, a domain service or an infrastructure service. It is definitely not the latter. Application service and domain service is a bit harder to decide. The main difference seems to be if it is part of the Ubiquitous Language and thus if it is important concept in the domain. To me it is. So lets design this as a domain service.
Ok I have a basic interface. How will it look in practice. I need to build the implementation to see where it leads. I need 2 things for this to work. A way to get the rules. And to get the Category from a categorization rule.This looks something like:
public class Categorization {
public List<CategorizationRule> rules;
public Optional<Category> categorize(final Transaction transaction) {
for(CategorizationRule rule : rules) {
if(rule.matches(transaction)) {
return rule.category();
}
}
return Optional.empty();//nothing matches
}
}
}
CategorizationRule as a separate Aggregate in the domain
Probably the CategorizationRule
should be its own Aggregate, a small one, but a specific new concept. As determined earlier it is defined by its properties and thus a ValueObject. Retrieving existing aggregates should happen in a repository. So I build that one as well. Next to that I remove the old categorization code after I checked that I got the same results given the same rules. The new model looks as follows:
How about storing the rules
The main problem I started this post with is not solved yet 🙃. How can I read and store the CategorizationRule
s. The easiest way is to use the CSV reader and store the rules. In order to make that work I need some reflection and a small change to the code. Almost all of the rules use a second constructor argument to initialize them. E.g. in DescriptionCategorizationRule
we need a description to match and CounterAccountCatergorizationRule
needs a counter account. This field needs to be stored as well. So I make it part of the CategorizationRule
. The interface now looks as follows:
public interface CategorizationRule {
boolean matches(final Transaction transaction);
Category category();
Object constructorValue();
}
The resulting code is kinda trivial, more details at GitHub.
Conclusion
After making al these changes I can easily store and read all parts of my administration. Next to that I fixed a long standing problem of the CategorizationRule
s being bolted on. As the new year is well on his way, probably my next post will be about budgets for 2023 as that is the part I can use to reach financial independence. As always the code is available at GitHub.