Decision service business rules in JBoss Rules

28 December 2009

Peter Hilton

by Peter Hilton

This article describes the kinds of business rules that you might implement in a decision service, with a simple example; this is essentially a functional design.

This article is part 2 of a series:

  1. Decision service architecture with JBoss Rules - what a decision service is and gave a high-level technical overview how you can use the JBoss Rules Execution Server to build one

  2. Decision service business rules in JBoss Rules - this article

  3. How to build a decision service using JBoss Rules Execution Server - how to get this working, with example rules - a RESTful decision service with no Java code required.

Functional requirements

For this example we are going to write business rules for desktop PC configuration, to determine which components can be selected when building a custom PC. The decision service will implement the following business rules:

  • The user must select a motherboard type, a processor type.

  • The user must select memory modules with specific sizes.

  • An empty selection will result in an error message.

  • The selected motherboard type and processor type must be present in a pre-defined list.

  • The selected memory module size must be present in a pre-defined list.

  • The selected processor’s socket type must match the motherboard’s processor socket type.

  • The selected number of memory modules must not exceed the motherboard’s number of memory sockets.

  • Memory modules must be selected in pairs of matching sizes.

  • If the selection violates one of these rules, a message must be generated.

  • The decision service will provide lists of available components: motherboards, processors and memory modules.

  • The lists of available components will only include components that are compatible with any existing selections.

Decision service functionality

The requirements above mean that the decision service will need to provide the following functionality.

  • Define lists of available components.

  • Generate lists of the remaining available components.

  • Generate result messages.

  • Validate that each selection is not empty.

  • Validate that each selection is in the list of available components.

  • Filter available components based on previous selections.

  • Perform additional ad-hoc validations.

Data model

For this example, we shall use the following data model. This includes the input data for the user selections, the reference data for the items in the lists of available components, and additional output data.

Domain model diagram

Selection has properties for the selected motherboard type and processor type. In addition, there is a separate MemorySelection for each memory module selected.

The reference data types Motherboard, Processor and MemoryDimm each represent a different version of each component, with properties for their different characteristics. The Motherboard and Processor type properties are the identifiers specified in the Selection, while each type of MemoryDimm is identified by size, matching the size in the MemorySelection.

The output data consists of text messages. The reference data will also be included in the output data, to represent the lists of available components.

Business rules in the Drools Rule Language

Lists of available components

Now it is time to look at the rules code, starting with the lists of available components.

First, I am going to assume that the reference data objects have already been inserted into the rules session’s working memory, to provide the facts that the rules will use to reason about the user’s selection. In this example, we can use additional rules to insert the facts from within the rules session:

rule "Insert motherboards"
   when
      not Message(text == "Motherboards inserted")
   then
      Motherboard integrated = new Motherboard();
      integrated.setType("integrated");
      integrated.setSocketType("none");
      integrated.setMemorySockets(0);
      insert(integrated);

      Motherboard standard = new Motherboard();
      standard.setType("standard");
      standard.setSocketType("pga");
      standard.setMemorySockets(2);
      insert(standard);

      insertLogMessage(drools, "Motherboards inserted");
end

In practice, however, you would be more likely to do this using the rule session’s Java API:

final List<Command> commands = new ArrayList<Command>();

final List<Motherboard> motherboards = getMotherboards();
commands.add(CommandFactory.newInsertElements(motherboards);

final StatelessKnowledgeSession session = knowledgeBase.newStatelessKnowledgeSession();
session.execute(CommandFactory.newBatchExecution(commands));

This means that the following rule would be activated - for example, there are motherboards facts:

rule "Motherboard reference data loaded"
when
   $motherboard : Motherboard()
then
   System.out.println("Found motherboard: " + $motherboard);
end

To make the lists of these components available as output data, we define queries:

query "motherboards"
   value : Motherboard()
end

Result messages

Another piece of functionality we need is to generate result messages. For this, we define a new JavaBean type inline in the rules file that has properties for the message text, and a message type that we can use to identify which kinds of messages to include in the output:

declare Message
   type : String
   text : String
end

We can now use this new type in rules. For example, the following rule inserts a new message "Found first motherboard" when there is a Motherboard fact in working memory. This only happens once, because the left-hand side also checks that the message itself is not yet in working memory.

rule "First motherboard reference data loaded"
when
   Motherboard()
   not Message(text == "Found first motherboard")
then
   Message message = new Message();
   message.setType("DEBUG");
   message.setText("Found first motherboard");
   insert(message);
end

Since the Message type only has a default constructor, it is somewhat verbose to insert the message; it is more convenient to define a function in the rules file:

import org.drools.spi.KnowledgeHelper

function void insertDebugMessage(KnowledgeHelper drools, String text) {
   Message message = new Message();
   message.setType("DEBUG");
   message.setText(text);
   drools.insert(message);
}

To make a certain type of messages available in the output, we just define another query:

query "messages"
   value : Message(type == "RESULT")
end

Validating user selections

The user selections are String properties in the Selection type. The first validation is simply to check that the selection is not empty:

rule "No motherboard selected"
when
   Selection(motherboardType == null)
then
   insertMessage(drools, "No motherboard selected");
end

In general, a good way to name a rule is to summarise the condition that its left-hand side represents - the same kind of self-documentation as good method names in Java. However, in the previous validation rule this means that the message duplicates the rule name, which is bad. We can easily avoid the duplication by adding another utility function that gets the rule name from the drools helper object:

function void insertRuleNameMessage(KnowledgeHelper drools) {
   insertMessage(drools, drools.getRule().getName());
}

Next, using the new insertRuleNameMessage function, the selection’s motherboardType should match the type property value of an available motherboard:

rule "Selected motherboard type does not exist"
when
   Selection($type : motherboardType != null)
   not Motherboard(type == $type)
then
   insertRuleNameMessage(drools);
end

Filtering available components

So far the validation rules have not been very interesting, in the sense that they would be just as easy to implement in Java. However, things get more interesting if we start changing which facts are in working memory.

In PC configuration, selecting one component may affect what you may choose for another component. In our example, selecting a particular processor rules out motherboards with an incompatible processor socket.

rule "Filter motherboards for selected processor socket type"
when
   Selection($processor : processorType != null)
   Processor(type == $processor, $socket : socketType)
   $motherboard : Motherboard(socketType != $socket)
then
   retract($motherboard);
end

This rule has three left-hand side conditions. First, the selection must specify a processor type, which is bound to the $processor variable. Second, there must be an available processor that has the selected processor type; its socket type is also bound to a variable. Finally, there is a motherboard that has a different socket type, which is also bound to a variable. This rule matches against each such motherboard, and the right-hand side removes the matched motherboard from working memory, filtering the list of available motherboards.

The interesting thing about this rule is that as well as filtering the list of motherboards that are returned by the motherboards query defined above, this affects which motherboards are available for the Selected motherboard type does not exist rule. The selected motherboard type might initially have been in the list of available motherboards before being filtered out, resulting in the message "Selected motherboard type does not exist".

A crucially important thing to consider when implementing these kinds of rules is that you do not have to care about what order these things happen in - you do not have to think about making sure the filtering happens first. This is because when the filtering rule modifies working memory by retracting the motherboard, the rules engine automatically re-evaluates the validation rule’s not Motherboard(type == $type) condition, which may now be true.

In a more realistic example, there would be many more complex dependencies between components, such as powerful graphics cards requiring a second or larger power supply, which in turn means needing a larger physical case.

Ad-hoc validations

Beyond the kinds of basic validations described above, which apply to all kinds of selections, a real-world problem will always have additional validations that do not fit into any kind of pattern. This is where you get the most benefit from using a rules engine, because each special case can just be an additional rule that uses the same working memory data as other rules.

For example, a special rule for memory modules is that they must be selected in matched pairs of the same capacity. In other words, there must be an even number of each size selected. In our model, each individual memory module is a separate MemorySelection fact, so we count them using the built-in collect function:

import java.util.ArrayList

rule "Memory must be selected in matching pairs"
when
   MemorySelection($selectedDimmSize : dimmSize)
   ArrayList($quantitySelected : size) from collect( MemorySelection(dimmSize == $selectedDimmSize) )
   eval($quantitySelected % 2 != 0)
then
   insertRuleNameMessage(drools);
   insertMessage(drools, $quantitySelected + " x " + $selectedDimmSize + "GB DIMMs selected");
end

Again, there are three left-hand side conditions. The first condition matches against a selected memory module, and binds its size to a variable. The second condition uses the collect function to collect all MemorySelection facts that have that size into a java.util.ArrayList, and binds the number of facts in the list (the quantity of selected memory modules) to a variable. The third condition then evaluates a Java expression that is true when the quantity is an odd humber.

The rule inserts the rule name as a validation message, as usual, as well as an additional message that indicates which size was not selected in matched pairs.

One problem with this version of this rule is that it generates duplicate messages. Suppose that the selection includes three MemorySelection facts with size 8GB. The rule’s second condition will get the value 3 and the third condition will be true because three is odd. However, the first condition will cause the rule to be activated three times, once for each of the three MemorySelection facts, which means that the right-hand side will execute three times. One way to solve this would be to add a condition that the message "3 x 8GB DIMMs selected" is not in working memory. Alternatively, in practice, the MemorySelection facts might be ordered in some way so that you can add a condition that only matches on the 'first' one.

Next steps

Once you have written some business rules for your decision service, the next step is obviously to run them and test them. The simplest way to do this is to configure the JBoss Rules Execution Server to load the rules file, so that you can execute the rules using its web services interface.