In part one of this series, we looked at ground-up modelling, storage, and evaluation of taxation rules using Rulette. By the end of that article, we had a Rulette based system storing all rules in MySQL and we were able to evaluate the applicable tax values for various combination of rule inputs. In this post, we will look at how we can evolve this rule system to accommodate more evaluation criteria.
Rules are meant to change
Let us consider that the government changes the tax law to state that taxes to be paid on a sale now depend not only on the state where the manufacturer is located but also on the state where the customer is located. And this change of rules will come into affect a week from now, i.e. May 6, 2020.
Everyone goes into a huddle again, and out comes an updated excel sheet which has lots more rules than our previous sheet (because we need to model all to-from combinations of states). e.g
becomes
The other part of the problem is that the new rule set should only become live at 00:00 on May 6. There should ideally be no downtime to the system. How do we achieve this change using Rulette? Let us focus first on solving the cutover on May 6 problem.
Achieving cutover on May 6
We will consider two straightforward ways of doing this.
Create a new rule system
We can create a fresh rule system by storing the new set of rules in a new table `tax_rule_system_v2` and mapping it in the rule_system and rule_input tables in exactly the same way as we had in the previous example. For this rule system, the rule input table will get one additional entry for the destination_state input.
We now need to make a code change saying that for all evaluation dates prior to May 6, 2020, we should use one rule system, and for dates after May 6 2020 we should use another.
This is a simple enough solution, but it leaves room for improvement on two fronts.
- If tax rules change often, creating a new table every time is a lot of overhead. Not only do we have to set up the changed rules, we also have to copy over all the unchanged rules for completion. Then we end up loading all these rule systems into our application.
- Another layer of decision-making is introduced for deciding which rule system to pick. What’s worse, this layer lives in code and changes are, therefore, intrusive.
Let’s look at a more elegant solution.
Enhance the existing rule system
What if we incorporate the date based decision making step inside the current rule system? Dates and ranges are both supported out-of-the-box in Rulette, and we can therefore augment the current rule system with the time dimension. This will remove the need to maintain an extra step of decision making and our rules would themselves become time aware.
Let’s add two more columns to the ‘tax_rule_system’ table called ‘effective_from’ and ‘effective_to’. Since all the current rules have been effective from the beginning and are valid till May 6, we can set all the rows to have NULL (NULL represent ‘Any’ or min value of range in Rulette) as ‘effective_from’ and ‘2020-05-06 00:00:00’ (YYYY-MM-DD HH:mm:SS is the default date format in MySQL) as ‘effective_to’.
Now we need to make sure that everyone using the tax rule system is passing in a parameter called ‘effective_date’ which is the date as of which the tax data will be returned. Since this will need code change from our users, we will have to wait till everyone migrates. We can also assume a default value of “now” to make this a non-breaking change for our users.
Finally, we map a new rule input called ‘effective_date’ in the rule_input table to indicate that this rule system should have a new input of data type DATE and input type RANGE
INSERT INTO rule_input
(`name`, `rule_system_id`, `priority`, `rule_type`, `data_type`, `range_lower_bound_field_name`, `range_upper_bound_field_name`)
VALUES
('effective_date', 1, 5, 'RANGE', 'DATE', 'effective_from', 'effective_to');
Reloading the rule system after this query will make our effective date related changes live. If anyone happens to try to find tax rate applicable on May 7, 2020, they will get nothing as we have no rules defined for that time.
Modelling Destination State
With either of the above solutions, adding this new dimension is very simple.
If we created a new rule system, it already contains this dimension and looks like Fig-2.
If we take the second option of adding the time dimension to the existing rule system, we want our rule system to finally look like this.
To get there, we repeat the process for adding a new rule input we just performed for adding effective_date.
- Add a column called to the tax_rule_system table called ‘destination_state’.
- Since this value is immaterial pre-May 6, set NULL (‘Any’) as the value for all rows.
- Wait for users to change their code and start sending destination_state in their tax queries
- Insert new rows in tax_rule_system table to represent the new destination specific rules. These rules will be effective from ‘2020-05-06 00:00:00’ to ‘NULL’ (‘Any’ aka forever). The blue row in Fig-4 is the older rule applicable before May 6, and the other rows are the. new rules available on and after May 6. Notice the way effective_from and effective_to columns are set.
- Insert a row in rule_input table to represent that the destination_state column contains values for a new VALUE input of data type STRING.
INSERT INTO rule_input
(`name`, `rule_system_id`, `priority`, `rule_type`, `data_type`, `range_lower_bound_field_name`, `range_upper_bound_field_name`)
VALUES
('destination_state', 1, 6, 'VALUE', 'STRING', NULL, NULL);
- Reload the rule system.
My personal preference is to enhance the current system instead of creating a new one because what we are modelling is essentially an evolution of the existing tax regime. If a completely new law arose about a new notion of tax ,e.g. environment tax, then that should be built and evolved independently.
Code Changes
Regardless of the approaches taken from the above two options, one thing is sure – our system is now time aware, and hence effective date is now a mandatory input. If we want, we can now expose a version 2 of our getTax API. This is, of course, outside of the Rulette modelling process.
public class RuletteBasedTaxManager {
private final RuleSystem taxRuleSystem;
// Constructor
public RuletteBasedTaxManager(String dpPropertiesFilePath, String ruleSystemName) {
File f = new File(dpPropertiesFilePath);
IDataProvider dataProvider = new MysqlDataProvider(f.getPath());
taxRuleSystem = new RuleSystem(ruleSystemName, dataProvider);
}
// API version 2
public Optional<String> getTaxV2(Map<String, String> inputMap) {
if (!inputMap.contains('effective_date')) {
throw IllegalArgumentException("Effective date is mandatory");
}
return getTax(inputMap);
}
// Older API
@Deprecated
public Optional<String> getTax(Map<String, String> inputMap) {
Rule applicableRule = rs.getRule(inputMap);
if (applicableRule != null) {
return Optional.ofNullable(applicableRule.getColumnData(rs.getOutputColumnName()));
} else {
return Optional.empty();
}
}
}
I hope this case study has demonstrated how Rulette is powerful not only for starting a fresh rules modelling effort but also in evolving an existing use case elegantly. We just reduced a major change in business environment to largely a matter of changing configurations. And that is the intent behind Rulette – our time as developers should be spent in problem solving, not in writing/maintaining tedious bits of code. Reducing rules to externally modifiable configuration allows us to deliver business value ever faster.
Rulette is open source on Github, so pop over, show some love and spread the word. Code review, bug reports/fixes, and any other forms of contribution are more than welcome.
In follow-up posts, I will show how we can expose our rules over a REST API so that non-JVM clients can also access Rulette’s powerful feature set and we can expose taxation as a generally available capability in the organization’s larger ecosystem.
If you liked this, subscribe to my weekly newsletter It Depends to read about software engineering and technical leadership