Rules Pattern

Ionut Ciuta
The Startup
Published in
5 min readOct 18, 2020
Photo by Joshua Miranda from Pexels

A few weeks back, someone tagged me in a company wide “Good Practices” challenge and I ended up writing a long post that I shared on Teams. I enjoyed doing that quite a lot and I decided to share it here as well, in a more detailed format. Here’s what I came up with.

On multiple occasions in my career I was tasked with checking some input data — e.g. an incoming HTTP request — against multiple business rules. Most of the time, my instinct would say “Just drag that thing through some if statements”. However, after doing this a couple of times, I realised that there has to be a better way of implementing this. Managing a big if-else block is not fun especially when the number of conditions you have to check is dynamic and can vary based on business needs. I’m also not very keen on using switch statements for many of the same reasons.

In my quest to become a more productive programmer, I ended up doing a bit of studying and came across the Rule Pattern. All of the code that you’ll see bellow is hosted here on GitHub. It’s mostly Java with a few Spring annotations on top of it.

The problem

Say we have an online shop. We want our shop to be smart so that we can give our customers contextual offers based on the content of their basket and other variables such as the date of the order.

e.g. It’s October, Halloween 🎃 season is in full effect and, soon after that, Christmas 🎄 is just around the corner. We want our shop to have promotions for both holidays. However we want to apply them accordingly. How should we go about this?

Let’s say we have this basket:

Basket basket = new Basket();
basket.addItem(new Item("The Shinning Book", 5.0));
basket.addItem(new Item("Hereditary Bluray", 15.00));
basket.addItem(new Item("The extremely expensive new Iphone", 1400.00));

The easiest way to check what promotions to apply is to just drag the basket through an unpleasant if-else block or even — God forbid — a switch 👻.

Yes, the code above is awful. It breaks many common sense rules. It’s dense, it does too many things and handles too much business. It also resists change. Adding a new rule means adding a new if statement. Also, good luck testing this.

The way I like to design this is by using rules. After I found this pattern, I used this approach on several projects and it proved to be a flexible way of decomposing large decision logic.

The solution

We first need to define what a rule is. A rule should contain the logic that dictates whether or not it can be applied to a target object and the logic that it should apply.

public interface IRule<E> {
boolean matches(E input);
void apply(E input);
}

The rule above is a bit too generic, E is not an actual type. Let’s make it a bit more specific.

public interface BasketRule extends IRule<Basket> {}

Based on this blueprint, we can set up actual rules that apply promotion logic to our basket. Here is an example:

Our HalloweenPromotion gives all of our customers a free skeleton suit 💀 and a 13 (dollars?) discount based on the logic in the apply function. That’s a pretty neat deal if you ask me! However, as per the matches function, this is applicable only if the order was created in October and it’s still October when rule is evaluated.

Let’s add a simpler rule:

This rule is straightforward. If your order is bigger than 1000, you get a discount of 100. Let’s add a 3rd rule that hopefully won’t be applied during Halloween season:

This last rule would give you a free Santa hat 🎅🏻 and a small discount if your order was placed near Christmas. Notice that all the rules described above are annotated with Spring’s Component. This example works neatly with Spring, but you can apply it just as well for plain Java.

@Autowired
private List<BasketRule> promotionRules;

Through some Spring magic and plain old polymorphism, we now have all our rules nicely grouped together. We just need the logic to apply them which again is super smooth and can be as simple as using streams and Lambdas:

promotionRules.stream()
.filter(rule -> rule.matches(basket))
.forEach(rule -> rule.apply(basket));

For the sake of coherence, I’ve put these two pieces of code in the PromotionEngine class.

Conclusion

If you run my demo code, you’ll see the following output:

=== BEFORE Promotions ===
Basket:
Created: 2020-10-18T21:58:50.760277
Name: The Shinning Book
Price: 5.0
---
Name: Hereditary Bluray
Price: 15.0
---
Name: The extremely expensive new Iphone
Price: 1400.0
---
========
Total: 1420.0

Applying promotions
Applying DiscountForBigOrdersPromotion promotion -100.00
Applying HalloweenPromotion promotion -13.00
Promotions applies

=== AFTER Promotions ===
Basket:
Created: 2020-10-18T21:58:50.760277
Name: The Shinning Book
Price: 5.0
---
Name: Hereditary Bluray
Price: 15.0
---
Name: The extremely expensive new Iphone
Price: 1400.0
---
Name: Promo - Creepy Skeleton Suit
Price: 0.0
---
========
Total: 1307.0

Let’s take a look at what just happened:

  • your basket has total value of 1420.0 and was created in October.
  • two promotions were applied: DiscountForBigOrdersPromotion that gave us -100 discount and HalloweenPromotion that gave us a -13 discount.
  • most importantly, you get that free Creepy Skeleton Suit for your socially distant Halloween party 😉💀

“Ok, but isn’t this more complicated?” You might ask. Well, not really. Here’s why:

  • you can easily add new rules or remove old ones
  • rule specific logic is isolated from other rules
  • you can easily test each rule in isolation or you can mock them accordingly
  • the code is much cleaner overall

Related reads

After I posted this on my company’s Teams, a colleague of mine mentioned that there might be a connection between the Rules Pattern and the Chain of Responsibility (CoR) pattern. While I’ve read about CoR in the past, I felt the need to look up some implementation examples and I stumbled across this thorough article. I highly recommend you read through it. I would say that the Rules Pattern is a specialisation of the CoR pattern with some caveats.
In the version I described, the rules — or handlers as per the mentioned article — are unordered and are applied exhaustively meaning the input is processed by all rules in no particular order.

Don’t forget to take a look at the demo code on GitHub.

Thanks for reading! 🙏🏻

--

--