Managing Taxation Rules With Rulette
Managing Taxation Rules With Rulette
In this article, we discuss how we can use Rulette with Java to better manage business logic in enterprise-level applications.
Join the DZone community and get the full member experience.Join For Free
This is a deep dive into using Rulette for modeling business rules. It is a simplified (slightly) version of a real-world use case that I have used Rulette for in production. We will try to model the tax that must be paid when certain types of items are sold by manufacturers located in different states.
In the Beginning...
It starts with the forging of the great tax laws, where the government mandates that the tax that must be paid by a manufacturer on a sale depends on the state in which his facility is located (West bengal, Punjab...), the type of the item (T-shirt, shoes, bags etc), the material (silk, leather, gold...) and the price of the product.
The product managers confer with the financial and legal team and put together all combinations of these attributes in an excel sheet. The whole thing is a layered structure built on default rules with more specific rules applied on top of them (Karnataka charges 5% on shoe sales, unless they are made of leather and priced above Rs. 5000, in which case it charges 10%). Something like this.
We, the dev team, now need to build a system to store these rules and determine what should be the applicable taxes on a sale. There are a lot of rules, so the system should be able to handle that, but the expectation is that they will be changed infrequently but read very regularly. Of course, we would like to make the management of all these rules as easy as possible and their evaluation as fast as possible. Let's say we already have a Java-based tax system, and we want to enhance that to handle the new tax regime.
With this, let's jump into the solution(s).
Any set of rules can be expressed as a combination of if-else conditions - that's what all rule management is. So the naive way would be to hardcode all of these rules and return the corresponding tax values as the output. If there is a particular dimension we don't care about in a rule (e.g. Any MRP), we will represent it with the string "Any".
This will work, but it will turn the codebase into the perfect hell for man and beast. This is difficult to understand, change, and test for correctness. If anyone asks for tax rules in our system, there is no way but to open code — which makes conversations with product managers and business teams very difficult.
They have no visibility into the technical representation of the rules. Additionally, from a computing perspective, this is a brute force lookup when trying to evaluate any outcome. We are iterating over all the combinations of all the inputs. The structure of the problem indicates that this could be improved if we were to use some tree/graph-based solution.
A huge step up from this situation would be to capture the rule inputs and the operators (equals, greater than, etc.) in some sort of configuration or DSL which we can parse at start-up to build the entire set of if-else combinations. This will make it easy to manage and share the rules outside of code because we can transform the product managers excel sheet into the configuration independently and then reload them into the tax system.
This will make the code look simpler, since the heavy lifting has moved from implementing the code to implementing the transformation of an excel sheet to DSL. This does not remove the computational complexity — we still evaluate all combinations to find out which rules apply. The combinations are just generated using config.
Using Rulette for Modeling Rules
Rulette encompasses the learnings for the previous two attempts and presents them in a single, compact solution. We see that the most intuitive configuration/DSL to model the rules is the same format in which they were shared — a 2D matrix with each column representing a "rule input" (source state, item type, material, and price) and each row representing a "rule", i.e a combination of these inputs mapped against an output value.
We also see that the relation between a column of the sheet (in a single row) is of "AND" type (also seen in the code above), and that any "OR" relationships are modeled as multiple rows (as shown by the else-if conditions in the code above). Rulette carries over these modeling insights into the heart of the application so that the dev and business teams are talking in the same language when it comes to rule modeling.
Setting Up Rulette
Let's assume that we take the Excel sheet and dump it into a MySQL table called 'tax_rule_system' in the 'tax' schema. You don't necessarily have to do this (Rulette has an extensible data loading model which allows you to plug in any source of rules), but MySQL is officially supported out-of-the-box so we will use that for this example. This table is nearly identical to the Excel sheet and now contains all our tax rules.
Let's create two metadata tables in the 'tax' schema (or any other schema the tax application has read access to). These tables help Rulette in making sense of the tax rules we stored earlier.
The first table models the rule system as an entity and allows us to locate where the rules of a rule system are stored. In this example, we can perform the following mapping to map a rule system name "tax_rule_system" to the data stored in our 'tax_rule_system' table. It also identifies that that unique identifier for rules in that table is called "id" (the primary key) and that the output tax values live in the column named "rule_output_id".
The second table interprets the columns of our rule system. Each row in this table defines the name of the input (source state, material, item type, mrp), its type (VALUE or RANGE), its data types (String/Number/Date), its priority (the order in which inputs are matched during evaluation). You can learn more about types and data types here and about how rule evaluation happens in Rulette here.
For now, just focus on the priorities being set for each input. Since the MRP value is a RANGE input, it is physically stored as two columns ('min_mrp', 'max_mrp') in the 'tax_rule_system' table, which are mapped as the `range_lower_bound_field_name` and `range_upper_bound_field_name` columns in the last INSERT statement.
All of this setup is included in the sample SQL script in the Rulette examples module. It also contains a bunch of sample rules that you can insert into the tax_rule_system table to play around with this case study. That's it! Rulette is now set up for use. Let's look at the code side now.
Using Rulette in Code
The first step is to add Rulette library to our tax system. In Maven, we can do this:
The first dependency is for the Rulette evaluation engine, and the second is for MySQL data loading plugin, which we will use to connect with the database setup we just did. Now, we need to tell Rulette how to connect to the database. Rulette uses the Hikari connection pool, which is highly configurable. For this, create a properties file where it can be found and loaded by your application. A sample file looks like this. Now we can initialize the tax rule system as follows:
This will first load the rule system definitions from our meta-data tables and then load all the rules for the "tax_rule_system" in the memory in a trie format optimized for evaluation. Make sure that you instantiate each rule only once because each new RuleSystem object contains all the rules from the database in memory.
If you mistakenly start instantiating a new object for every evaluation request, you will quickly run out of memory! We are now ready to start evaluating rules. Let's say we need to know what tax rate is applicable generally in the state of Karnataka. We can do the following.
What about finding out how items made of leather are taxed in the state of Karnataka?
Note that the values of the map should exactly match the values of the database, so in an actual application, you might be doing some data normalization before invoking the
getRule method. Putting it all together, here is what the new code in the tax system might look like:
Notice the syntax of how the output of the applicable rule is being accessed. As a library, Rulette does not understand what the output value "means" in any use-case — it simply returns the best-matching rule. This business-agnosticism can be used to build another level of indirection where we do not store the actual tax value inside the tax_rule_system table but rather a reference to an external data source somewhere else.
This can be useful in case we have already some data in our system and we need a rule system to map different use-cases to them. The Rulette part will continue to work in exactly the same way while the using application can now use the returned output as a reference in other systems.
This compact piece of code shown above all the complexity of modeling the business rules and evaluating input data against it in a blazingly fast manner. Using Rulette also means that you can talk to the business teams in their own language (anyone can understand the tax_rule_system table) and that you can add more rules without having to change your code.
More of the ways in which Rulette can be used to manipulate MySQL based rules are outlined in the examples module.
This case study demonstrates how to model business rules using Rulette. But we all know that rules never stop evolving. In the next post, we will see how we can easily evolve this rule system to incorporate changes to the tax regime.
Published at DZone with permission of Kislay Verma . See the original article here.
Opinions expressed by DZone contributors are their own.