SpringBoot: Rule Engine For Classifying Celestial Objects

George Berar
6 min readJan 18, 2022

--

In today’s article I will talk about the Rules design pattern and how can we implement a simple rule engine for classifying celestial objects.

Photo by Donald Giannatti on Unsplash

Before we start you can find the entire code here.

Background

Imagine the case when you have to implement a discount calculator in order to give the proper discount to a customer based on different criteria like date of birth, date of last purchase, is his first purchase, is veteran etc. The most straightforward and easy way to implement a solution is with if/else statements and this works well if you have a small number of simple conditions that you have to check. The problem appears when new complex conditions need to be checked and you start creating new if/else statements or modifying the existing ones which slowly increases the cyclomatic complexity of your code to a degree where is hard to follow and maintain. Furthermore it will be harder and harder to support new rules over time and it will be too complex to understand for a pair of fresh eyes.

Working with complex conditional statements can become a pain really quick because they are always subject to change and new ones can appear like mushrooms after rain when you expect less.

Rules Pattern to the rescue!

This design pattern has an important role when it comes to flexible ways of decomposing large decision logic and helps the developer to introduce a separation of concern among the conditions by treating them as individual rules which are completely decoupled. The class diagram looks like this:

Rules Design Pattern class diagram

Basically, we have an interface which defines two methods:

  • shouldRun — whether to run or not the rule
  • evaluate — the business logic behind the rule which runs only if shouldRun method returns true

Each rule must adhere to this interface and provide an implementation for the methods. Simple as that!

Among the advantages this pattern brings, we have:

  • reduced cyclomatic complexity
  • adding, updating or removing rules is easier
  • separation of concern
  • we can reuse rules somewhere else if we want
  • unit testing is easier since we have decoupled and smaller pieces of code which can be tested individually
  • dynamic composition of rule engines

Note: the method names from the interface are subject to change and you will probably see some variations but it’s the same concept.

The Problem

Let’s suppose we landed a job at NASA and our first assignment is to build an engine which can classify newly discovered celestial objects based on some input data and rules.

This engine should be exposed using a REST API endpoint which accepts the following input data:

Input data format

where:

  • name — the name of the newly discovered celestial object; e.g. Kepler
  • mass — the mass of the object expressed in kg; e.g. 3.65e29
  • equatorialDiameter — the diameter expressed in meters; e.g. 9.94e8, 18450
  • surfaceTemperature — the surface temperature expressed in Kelvin; e.g. 5800

The engine should respond with the following output:

Output data format

where type represents the type of the celestial object computed by the engine and can have the following values: PLANET, STAR, BLACK_HOLE or UNKNOWN.

The set of rules we need to follow are:

Rules for classifying celestial objects

The Planet and Star rules are mutually exclusive while (Planet and Black-Hole) or (Star and Black-Hole) rules can be satisfied simultaneously. In such a case the rule with the lower precedence wins. For example, if our input data triggers Planet and Black-Hole rules, the type of the object will be given by the Black-Hole rule because has the lower precedence (0 instead of 1 from Planet rule). If none of the rules are satisfied the celestial object is classified as UNKNOWN.

The main challenge for us is to create the engine in such a way that it can be extended at any point in time since our Universe is vast and new types of celestial objects can appear. The engine must be flexible enough to allow us change the precedence or logic of the rules whenever we want.

How can we build this engine in order to satisfy the requirements?

The Solution

The solution, as you might’ve guessed already, implies the Rules design pattern.

Step 1. Defining DTOs

The first thing we need to do is defining the DTOs for input and output data.

Input DTO

Output DTO

Note: I’m using Lombok to generate constructors, getters/setters, builders, toString, equals and hashCode methods and this is the reason you see those annotations at class level.

Step 2. Defining Rules

In this step we define our three individual rules which will be used later on by our engine.

First, we create the interface:

The Rule interface

As you can see we defined the methods a little bit different to match our use case:

  • shouldRun — accepts the input data also in order to decide if we run the rule or not
  • evaluate — returns a result which contains the celestial object type and precedence:
Evaluation Result

Each of the individual rules will return its own EvaluationResult with the correct celestial object type and precedence inside. Let’s see how they look like.

Planet Rule

Star Rule

Black-Hole Rule

The shouldRun method from each rule represents the implementation of a certain condition from our rules table and evaluate method returns the result.

Let’s take for example the Planet Rule. If the input mass is at most 13 times the mass of Jupiter the shouldRun method returns true and the evaluate method will be executed returning an EvaluationResult(type=PLANET, precedence=1). The same applies for the rest.

Step 3. Defining The Engine

After we defined and implemented the rules is time to put everything together and define our complete rule engine which will receive the input data, execute the rules and return an output. The implementation looks like this:

Rule Engine full picture

As you can see we initialize our engine with the set of rules and inside the classify method we make sure to deal with the precedence in case multiple rules were triggered by the input data. The final output of our method represents the DTO we defined in Step 1 and contains the type of the celestial object.

Now, let’s do some testing!

Test

Scenario 1 — input data for Planet

Scenario 2 — input data for Star

Scenario 3 — input data which triggers 2 rules

In this scenario I’m using an input data which triggers the Planet and Black-Hole rules but the output is given by the one with the lowest precedence, the Black-Hole.

From the logs we can see the rules which were triggered:

More test data here.

That’s it!

Conclusion

As always please keep in mind this approach might or might not suit your project context or needs and I’m not in the position to say there’s no other way to do it differently or better. I really hope you enjoyed it and had fun reading it.

Stay safe and remember you can find the code here.

--

--

George Berar

Senior Software Engineer • Freelancer • Tech Enthusiast