Nowadays, it’s often necessary to set up specific business rules that automatically trigger particular tasks over time, usually under particular conditions. The conditions of triggering such rules should be modifiable in a flexible way, preferably by non-technical users, and the result of such tasks should be available for any further processing, possibly as an input for another rule.

To be precise, this is called a business rule engine (BRE). I am not going to describe the differences between BREs and workflow engines or the difference between a business rule engine and business process engine, as these questions are very common, and answers to them can be found in many articles. Instead, I will just focus on requirements regarding BRE and try to implement them in further blog posts. Without further ado, let’s dive into today’s subject.

As I have already stated, the desirable situation is when a business user can define rules through a simple user interface. In addition, this should help to avoid logical mistakes. I should also be able to constrain rules by a specific set of multiple conditions, preferably complex ones. Let’s break this down – I need four components to achieve this:

  • Set of tasks which can be executed
  • Set of conditions which can be applied to these tasks, preferably as reusable as possible
  • Rule engine to execute the above. An easy-to-use user interface for business users to plan our rule’s behaviour

It turns out that to introduce processing of rules on an existing system, we only need to define tasks and their possible conditions for this system, as the rule engine itself and UI remain unchanged between distinct systems once implemented. These available tasks and conditions become a kind of simple language that can be used by business users to build their rules as if they were building them with LEGO blocks. This language can be infinitely extended by programmers implementing new tasks or conditions when the business needs it.

Let’s check out some case scenarios to see what such a language could look like and what its advantages and limitations are.

First steps

First of all, let’s suppose I’m an e-commerce shop owner. There could be many kinds of discounts based on offered product types, customer activity or location. A simple flat rule could look like this:

As you can see, this rule can be separated into specific linguistic components:

1. One task:

  • Set customer discount to [X], where [X] is its parameter, made from two combined values – a percentage rate of discount and one of our shop’s categories for that discount

2. Three conditions:

  • Customer has registered more than [X months ago], where X is a certain timeframe
  • Customer is from [Continent], where [Continent] is one of the predefined dictionary values
  • Customer is from [Country], where [Country] is one of the countries in which we sell products

Each of them operates on some subject. In this case, it’s a customer.

Let’s summarise this in an image:

Let’s dig into each of the above.

The task can do anything we want: send an email immediately, publish a post on Facebook, write or update something in our database, etc. In this case, we’re assigning specific discounts in a specific category to our customer. Typically, we’d like a discount to be applied to a customer only once, though such a task could be implemented in any way we want, i.e. accumulating smaller and smaller discounts each year in a non-linear fashion.

The task is always operating on some subject, and its possible conditions (if they are necessary) will also be needed. From now on, we will always underline the subject of the task or condition to make things clearer.
Conditions indicate the circumstances under which we want our task to be executed.

Each condition can have parameters specific to it. Certain data from our database could be used as a parameter, i.e. a specific country from the set of countries in which we offer sales, or unrelated immediate values, like 10%. There could be any number of parameters or none at all; we will mark parameters using square [brackets].

Conditions can also work according to the current (condition check/running) time, so that they can be true within a specific date range or within a specific time offset to a certain date.

They can be joined with logical operators (AND, OR) to form more complicated groups (trees).

For now, business users can achieve three main advantages:

  • They can create their own rules, which are built from tasks, and set their conditions
  • They can parameterize both tasks and conditions
  • They can join conditions with logical operators (AND, OR) and control their order with parentheses

More tasks, more conditions

Let’s have a quick brainstorm about what other tasks or conditions we could imagine in e-commerce.

Ideas of possible tasks:

  • Send an e-mail to a customer with the message body of [“Hello {name} {surname}, we have new products in the [tools] category”]
  • As we have a subject in this context (the customer in this case), we can pass its data to achieve a specific result. Here, we’re injecting the name, surname, and e-mail address of the processed user into the e-mail they will receive.
  • Apply a [10%] discount to an order
  • Send notification to a warehouse about low stock of [product]
  • Set discount on product to [50%] of its profit margin
  • Set discount on [product] to [50%] of its profit margin

The last one is interesting, because the product is a subject and parameter at the same time, so we can force a task to operate precisely on a specific subject. We will get back to this later.

And some ideas of conditions for these tasks:

  • If customer age is above [10 years]
  • If [customer] age is above [10 years]
  • If customer has ever bought anything from the [tools] category
  • If total order value is above [$100]
  • If product stock in a warehouse is lower than [X] items
  • If product is in the [tools] category
  • If product is exactly [product]

It turns out that, by combining the last condition with the next to last task, we can achieve the same rule as in the last task:

This means that the same rules can be achieved in more than one way.

All right, so we have extended the set of tasks and conditions, but some of their combinations won’t make sense, as you can see in the example below:

bre-send-notification

The problem with this is that these two domains are entirely unrelated. It could have some statistical meaning – like “customers above 50 led to low stock of screwdrivers” – but we’re only analyzing data flow. This means that the tasks are always operating on certain subjects, and available conditions of these tasks should just address those subjects. In this case, the task is dependent on the stock of screwdrivers, so any conditions we attach should be directly or indirectly related to it.

  • A direct condition could be “if the product stock is lower than five items …”. This way, we are directly using the task’s subject – the product stock
  • An indirect condition could be “if the product is in the cars category…”. With this one, there is a short link in our data domain that connects the product category with the stock of product. In other words, it’s easy to access the product’s category once we’re given a product stock, so the condition is also usable

Now let’s get to our example when such indirections link subjects that are strongly separated, such as stock level vs customer. Some problems start to appear:

  • The condition starts becoming fuzzy; it’s harder to say what it is doing, as it utilizes some heavy linking logic under the hood
  • Such conditions will be slower in execution and harder to maintain by developers than simple conditions

How to cope with that?

First of all, we should block using indirect conditions.

The defined condition should always be related to a certain subject, and it can be assigned only to tasks which are operating on this subject.

This limitation can also be forged into an advantage, as it forbids the creation of illogical rules and strengthens the reusability of conditions. In other words, once we have some condition for a product, we can use this condition in every task that uses a product as its subject.

Okay, but how can we deal with linking of such distant subjects?

Well, as I have said earlier, the same rule can be achieved in many ways, so we can divide a rule into individual chunks that are smaller and easier to deal with. It’s time to introduce chaining.

Chaining

We will start by extending our case scenario to:

 

What do we expect it to do?

As before, I’m going to give a discount of 10% on tools to customers with accounts older than six months who are from Europe or the USA.

Additionally, when such a customer purchases tools within 3 months upon being assigned the discount, we’ll give them an additional discount of 5% (15% in total) on tools and notify them by sending an e-mail.

This way, we will provide an additional reward to customers who use discounts. This will make them feel special.

How does chaining work?

Well, some actions in our systems leave a persistent trace in databases. This could be, for example, a new order, new customer, discount assignment for a customer in a specific category – these are some of the more obvious examples.

The ability of chaining is built on top of that persistence. Every persistent piece of data can be treated as a task input, and tasks themselves can generate data as output. If something is not directly persistent, we can consider whether it should be, or we can pretend it is.

Let’s see what the inputs, subjects and outputs of each task are:

bre-full-task-name

Tasks that are the basis for our rule to start are root tasks, and they do not take any inputs. For example, the “notify a customer of the discount via e-mail” task wouldn’t make sense as a root task because we need the context of about the discount of which we’d like to notify the user. Although, we can (but don’t have to!) nest some other tasks within root tasks. Of course, only if the root task returns a specific data context that applies to its nested tasks.

Tasks that do not have outputs cannot have any other tasks nested inside them. That makes sense, since that would make us lose track of the context of which task it operates on. This could be true, i.e. when we’re sending an e-mail through an SMTP server and don’t track further information about it (send and forget).

In the middle, there are tasks which take inputs and generate outputs. These are the most complicated, most common and most usable ones.

Now, let’s introduce another constraint.

Nesting tasks within other tasks is possible only when the outer task output matches the input of its inner task.

This is entirely okay, because it allows the data context to be passed down, and if there is no match, then it means one of two things: such a task would be illogical, and we don’t need it in our language, or the opposite – we should consider creating it.

It’s the same situation as with conditions – it comes with some reusability in exchange. Let’s take another look at our example table.

Task no. 1 had nested task no. 2, but it may also have task no. 4 nested!

With the set of tasks given above, we can also create the following rule:

 

The customer will be emailed about the first discount, and after making an appropriate purchase, he will be emailed about the second discount.

Also, the last constraint of our rule language has just appeared.

One task can have multiple child tasks, as long as the nesting requirement is met.

This allows us to create complicated rules that have a tree structure and could have a theoretically infinite depth. We’ll call them rulesets.

There a small catch though – we don’t want the 5% discount to accumulate above 15% each time. The customer makes one order every 3 months, though this is irrelevant to us, as this behaviour can be controlled by a specific task implementation which we will not consider here. For now, we will only mention that, for the sake of avoiding infinite loops and tree cycles, tasks are by default aware of inputs they’d processed before. Although this behaviour can be changed in a specific task implementation.

A question regarding persistence may occur – what if we’d like to use some result context, as in the “category in order” example, that isn’t directly preserved but we want to use it as some input for tasks? We don’t have to worry about this, as the discussed rule engine has its persistence level for such cases and will handle it.

Combining multiple rulesets

Let’s see some more sophisticated examples with multiple concurrent rules. Imagine that we’ve got a medicine R&D department, and some medicines that are being worked on are already in the clinical testing phase. Each patient reports any side effects every day, directly to his/her doctor, who logs them into the system.

It is crucial to track any undesired side effects of patient treatments, mainly when the side effect is appearing in multiple patients, as it increases the chance that the side effect is caused by the tested med and not just some isolated case. Let’s try to create some rules that could be used to track such situations.

Let’s suppose we’re monitoring coughing reactions. First, we want to track if the given med is potentially causing some allergic reactions. Both allergies and colds could cause coughing, so we want to filter out the latter. We can do this if an expert defines specific rules, but to keep it simple, let’s do this by generalising that colds occur with fever symptoms. It’s interesting that this generalisation could even be sufficient, as our further rule will operate on some statistical threshold.

Rule 1

So this rule will keep executing continuously and harvest patient prescriptions that comply with a specific set of conditions, probably defined by experts, and when such a condition occurs, it would add a coughing reaction to this prescribed med.

Let’s add another parallel rule that would be consuming information about reaction and making certain logical decisions on top of that, based on some statistical method. A single coughing reaction could be omitted, but, for example, when it exceeds 10% of test cases in every tested batch, the reaction is meaningful.

Rule 2

This is a big one.

We’re checking all medications containing a certain ingredient (let’s say Alprazolam) if they have caused a coughing reaction in patients they were prescribed to – not in single cases, but when at least 10% of patients displayed the reaction in almost all tested batches.

If this is true, then we’re suspending prescription of this med to patients, and we’re notifying the R&D team about the situation. Moreover, if there is no sign that they received proper notification within a specific timeframe, the system administrator is notified via email and SMS. After that, he should take some steps independently to propagate this vital information.

Additionally, after suspending prescription of the med to patients, their doctors are informed via e-mail.
We won’t talk about the details of each task and condition here; this example is only for demonstrating that the usage of combined rulesets allows us to achieve even more benefits.

Designing rulesets

There are two possible ways of using the mentioned constraints to define rulesets.

The first one is a good old-fashioned meta-programming descriptive language. Unarguably, its primary benefit is its power, but that comes with the requirement of a useful set of skills and knowledge. The second disadvantage is the need to process code through some validation process that filters out any errors, i.e. when one would like to nest two rules that are incompatible.

Another option is to use graphics designer’s skills, and this will probably be the most suitable for business users. This makes it possible to build rulesets without any linguistic errors by providing bricks that cannot be joined incorrectly.

Execution engine

Once rulesets are defined, they should be executed against data in our domain. This subject becomes more technical than theoretical, and because of this, we will cover this topic in a future series.

Fresh software development tips delivered straight to your inbox

Subscribe to our monthly newsletter with useful information about building valuable software products.
Don't worry, we value your privacy and won't spam you with any bussines enquiries!

maciej_halaczkiewicz

Software Developer

 .NET developer focused on web solutions. He had been working mainly on big projects concerning e-learning and HR. Follower of good architectural principles, privately he's interested in rock climbing, carpentry, and video games.