The Rules Engine Pattern

The Rules Engine Pattern

As software engineers, whether professional or aspiring, our primary responsibility is to translate business rules into logic that a computer can understand. This often involves modeling problem domains—typically using classes—and writing business logic that mirrors real-world rules. However, as business rules evolve, our code must adapt, which introduces significant complexity.

The Issue with Changing Business Rules

A common sentiment among software engineers is that good code should be easy to modify. Let's consider a simple example: calculating shipping costs for an e-commerce platform. The current rules might look like this:

  • Domestic shipping: £19.99
  • International shipping: £49.99
  • Free shipping applies if the basket total is £100 or more for domestic orders or £200 or more for international orders.

Now, imagine the business introduces a new rule: domestic shipping is half-price during December. A quick and straightforward approach might involve adding another condition to the existing if statement.

At first glance, this may seem manageable. However, if the business later extends the discount to international orders, the code becomes cluttered and harder to maintain. This is a clear sign that we need to refactor the code.

The Open-Closed Principle

If you've participated in a technical interview in the last decade, you're likely familiar with the SOLID principles. One of the most important, yet often misunderstood, principles is the Open-Closed Principle (OCP). It states that a system should be open for extension but closed for modification. In simple terms, you should be able to add new functionality without altering the existing code.

Does our current shipping calculation code adhere to this principle? Unfortunately, no. Every time a new rule is introduced, we modify the existing logic, increasing the risk of introducing bugs. Moreover, such code is rarely well-covered by up-to-date unit tests, making changes even riskier.

Introducing the Rules Engine Pattern

To address this issue, we can apply the Rules Engine pattern to improve our shipping logic.

Defining a Rule Interface

We start by defining an interface that outlines what a rule should look like in our system. This interface will contain two key methods:

  • checkEligibility: Determines if the rule applies based on the basket contents.
  • calculate: Returns the applicable shipping cost.

Article content

The decision to separate the eligibility check from the calculation is based on personal preference and may vary. If you find it more practical to combine both into a single method, that's perfectly fine. My preference is to keep them separate because it allows the calculate method to safely assume that the eligibility check has passed, erasing the overhead of providing fallback values.

Implementing the Rules Engine

Next, we create a Rules Engine that processes a collection of rules and determines the final shipping cost.

Article content

Defining Shipping Rules

We now define each shipping rule as an individual class, for example:

  • DomesticShippingRule
  • InternationalShippingRule
  • BasketTotalRule

Article content

With this approach, we gain several benefits:

  • A clear contract (ShippingCalculatorRule) that all rules must follow.
  • Concrete rule implementations for each scenario (InternationalShippingRule, DomesticShippingRule, etc.).
  • An engine (ShippingCalculatorRulesEngine) that evaluates the rules and computes the final shipping cost.

The Benefits of Modularity: Adding New Rules

With the Rules Engine in place, adding new rules becomes a breeze. For example, suppose the business introduces a birthday discount that halves the free shipping threshold. We can easily add a new rule without modifying the existing ones.

Article content

Since these rules have been written as pure functions (they have no side effects), they are easy to test, which makes our system more maintainable and less prone to bugs. Utilising a rule engine isn't a guarantee that you have function purity, but writing your rules with purity in mind will significantly aid testability.

Rule Evaluation Strategies

The implementation described here is relatively simple and designed for a basic problem domain. Depending on the complexity of the problem you're solving, the strategy for rule evaluation may vary.

In my case, I opted for a "lowest applicable price" evaluation strategy, where all rules are evaluated and the lowest eligible price is selected. However, there are other strategies you might consider, such as aggregating the outputs of all applicable rules (e.g., calculating surcharges) or running rules in a specific order or priority.

Conclusion

By implementing the Rules Engine pattern, we can model complex business logic as a series of modular rules. This reduces cyclomatic complexity and makes our code more maintainable. While design patterns should be applied with care, this approach is highly beneficial when your code:

  • Contains nested if-else statements.
  • Is frequently updated with new business rules.
  • Calculates a returned value based on complex conditions.

Making your code easier to work with reduces friction, accelerates development, and speeds up delivery—sometimes making the difference between success and failure for a business.


To view or add a comment, sign in

More articles by Carlton Upperdine

  • Is specialising worth it?

    The decision to specialise or not is one that can have a tremendous effect on the trajectory of a person's career. This…

    2 Comments

Others also viewed

Explore content categories