Now that we have our Custom Field system that is capable of storing any customer data we could need, the next building block we should think about is a Condition System. The basic idea of this system is to move customer data-driven logic out of your application code and into the data of our application. That may sound scary, but in reality it’s pretty simple.
Just like the Custom Field System, many applications in the wild that must deal with complex use-cases have this kind of tool built in. Doing a quick Google search resulted in Zendesk’s Condition System.
Zendesk’s condition system is extremely similar to what I’ve worked with in my career. Each condition has a left side, an operator, and a right side. Individually, the value of the left side is computed, the right side is computed, and then the computed values are compared using the operator. Each condition either returns a true or false boolean value.
Individual conditions are combined together using a “Meets ALL Conditions” (a logical AND), or “Meets ANY conditions” (a logical OR). The result of the entire evaluation results in either a true or false boolean value. You can use this evaluation to make decisions in your codebase based on arbitrary business logic without ever polluting your actual codebase with this logic.
Example Usage
Let’s look at an example using the scenario introduced earlier in the book. As a reminder, we have special code that must be executed if the order was placed for Michigan. Our code might originally look something like this:
As we discussed earlier, this is not ideal since we have this customer-specific logic in our codebase. It’s not bad by itself, but imagine if we did this 1,000 times. Things could get extremely messy. Fortunately, there is a better way! Let’s see what this would look like with the Custom Field and Conditions Systems.
First, we would add a new Custom Field for this customer attached to our order system. That record may look something like this:
Now that we have our custom field, we can set each Order that is placed to have an “Is In Michigan?” value of either true or false. Then, we would set up our condition as something that looks like this:
Under the hood, this would be using Liquid on the left and right sides (even though the right side is a constant “true” value, it should still be evaluated through Liquid). The Liquid representation of this condition would look like this:
Now, our Conditions System can evaluate this. It will start with the left value, which let’s assume that when evaluated by Liquid will return a value of “true”. Then, we can evaluate the right side of the condition. Since there is nothing dynamic about it, it will return a value of “true”. Then, the application can compare the results using the middle operator. Since “true” == “true” is true, our condition evaluation results in a true value.
Going back to our code example, now we can change that code to be something like this:
When the `conditions_pass?` method (or function) runs, it evaluates the conditions that store the business logic that has been configured. As you can see now, our messy customer-specific logic is no longer in our code base and instead lives as data in our database.
In the future, let’s say that this customer has another crazy request. They now want the special functionality only if the person’s name is “Adam” and the person’s employee_id is “25B”. You can instantly see how the customer specific logic would have grown over time. With our Custom Field and Conditions Systems, this ask is trivial.
First, we add our new Custom Fields:
User’s Name Custom Field (In many systems, this may already be modeled in the database)
User’s Employee ID Custom Field
Now, we’ll add our new conditions to the existing Conditions. This will end up looking like this:
Meet ALL of the Following Conditions
Under the hood, we are still using Liquid:
Guess what? Our code doesn’t change at all! That’s the power of the conditions system. You can introduce new customer specific business logic without changing any code. Now, when our “conditions_pass?” method is called, it will evaluate the new logic and behave accordingly.
This is extremely powerful because it gives your application the ability to be highly adaptable to customer’s logic without becoming an unmaintainable mess. Going back to our original goal, customer logic can be configured by a configuration team, rather than the software engineering team.
What About No Conditions?
When you go to evaluate your conditions, what happens if there isn’t anything there? This is an important decision to make because it will impact how your application behaves. Personally, I think that there are advantages to making conditions return true when they don’t exist. This means that when the configuration team doesn’t add any condition, behavior application-wide will be enabled by default.
You can use Conditions to control this in cases where you must disable functionality. In the worst case, if the configuration team needs to disable a feature for a customer, they can enter a condition that is never true, such as this condition.
Meets ALL Conditions
Since 1 is never equal to 2, the condition will always fail and the functionality will be disabled.
Short Circuiting
Since the conditions System is using logical operations, there is room here to get clever about how your system handles evaluation. One way that I’ve dramatically improved the performance of a condition system in the past has been to implement short circuiting.
If you’re unfamiliar with how short circuiting works, here is an example. Let’s say that I tell you, I will give you $100 if “The sky is purple” AND “the day is Tuesday” AND “Your name is Bob”. Well, since we are doing an AND operation, as soon as you hear “The sky is purple”, it doesn’t matter what the rest of the conditions are, the entire statement will always be false since the sky isn’t purple.
This works in the opposite way for OR statements. With OR statements, as soon as a single condition is true, then the entire statement is true. Going back to the same example, let’s say I will give you $100 if “The sky is blue” OR “your name is Bob” or “The day is Tuesday”. Well, in this example, I will owe you $100 after the first condition since the sky is blue.
All production programming languages take advantage of this. You can too! This is a great way to improve the performance of your application.
Condition Weights
I remember one time solving a bug when we had a condition (one of many) that would be way more computationally expensive than all the rest. Our team had optimized the performance as much as possible under the hood, but there was only so much that could be done. The work that was being computed was always going to be more expensive than other operations.
When you have short circuiting, you can be clever about ordering your condition evaluation. Let’s say that you have the following conditions (with weight added in the third column).
Meets ALL Conditions
It is in the best interest of your application to evaluate these conditions in the following order if possible.
The reason this is the ideal order is because if the cheap condition fails, then we may short circuit and know that the entire result will be false without ever actually doing the work to evaluate Condition 2 or 3, which are the most expensive. This performance gain may be trivial for a single condition. However, in applications with thousands of conditions, this performance improvement can really add up to have an impact.
In reality, this can be extremely hard to pull off. It’s often difficult to know how expensive an operation will be beforehand. However, this is just one example of how you can use a simple Logic hack to improve your application!