Implement your own Rule-Engine (Java8 + SpringBoot + MVEL)

Ramesh Katiyar
12 min readNov 15, 2019

Rule-Engine is an expert-system program, which runs the rules on the input data and if any condition matches then it executes the corresponding actions. There are many rule engine implementations available in the market. But if you want to create your own and simple rule-engine, which satisfies most of your requirements, then here we go. It might not satisfy all requirements but you can get an idea of rule-engine implementation and can modify it as per your necessities. For the full source code of this rule-engine, find the git link here. Below we have mentioned the implementation framework, requirement’s example, flow and few source code for better understanding.

But, before starting the implementation, you should have some idea about the rule-engine that how it’s work, what is inference engine, and what problem it’s solving. If you don’t know, then please learn about it before implementation or you can read my previous blog What is Rule-Engine? on this.

Tech Stack:

For implementing a rules-engine, we are going to use the following tech stacks:

  • SpringBoot 2.0.5.RELEASE: To create a rules-engine project as a rest API.
  • Maven: Project Framework.
  • MVEL 2.4: Expression language to write rules.
  • Java 8: Programing language.
  • PostgreSQL 9.4 Database: To store the rules.
  • Spring JPA: For DB Connection

Implementation Framework:

Below block diagram shows the implementation framework of rules-engine.

Rule Engine Implementation Framework

We follow the above implementation framework and implement Rule-Engine step by step.

  • Here we implement rule-engine as a Rest API with SpringBoot and Maven framework.
  • For writing the rules, we use expression language (MVEL) and some domain-specific keywords.
  • We store the rules in DB (Postgre).
  • We implement an abstract Inference Engine, which use to implement the different domain-specific Inference Engine. For example, we can write both the Loan and Insurance related rules engine in one application.
  • We implement a keyword resolver to resolve all domain-specific keywords.
  • We implement two parsers. One for parsing MVEL expression and one for DSL.
  • The testing we do with postman for API test.

Let’s begin the implementation!

1. Rule Language:

The rule is a set of the condition followed by the set of actions. The rules are mainly represented in the if-then form.

In this implementation, for writing the rules, we use the MVEL expression language + Domain Specific Language.

Rule’s Language = MVEL + DSL

DSL or Domain Specific Language means, It’s not a part of MVEL. It is created by our own for a specific domain. In place of writing multiple rules or expressions, we can combine them into one and can map with a keyword. The DSL keyword makes it easy to use and for reusable purposes.

For example, to approve the loan for any person, if we have to check that the target of a bank for the current year is done or not? then, for mentioning it in rule, we might need to write multiple statements or expressions in MVEL. But if we create one keyword like “bank.target_done” then, it will return a simple true or false. And behind this keyword, we do all computation to get the current year’s target status. Now we can use it anywhere in rules writing.

Here in this rule engine, we use the following format for DSL which is different from MVEL.

$(resolver-keyword.sub-keyword)

Resolver keyword could be considered as a keyspace of all sub keyword. For example, the bank is a resolver keyword and all bank related keyword like interest_rate, targe_done or branch, etc are sub-keywords which return some values after some calculation. This formate is used to combine all the same related keywords together.

Except this other syntax will be the same as the MVEL expression. To make it more clear let’s take an example of the Loan-Rule-Engine.

Example:

Suppose our requirements are like, we have to create a rule engine for Loan application and the following are the rules for approving the loan.

Domain: LoanRule 1: A person is eligible for home loan?
if:
1. He has monthly salary more than 50K.
2. And his credit score is more than 800.
3. And requested loan amount is less than 40L.
4. And bank's current year target not done for home loan.
then:
1. Approve the home loan.
2. Sanction 80% of requested loan amount.
Rule 2: A person is eligible for home loan?
if:
1. He has monthly salary more than 35K and less than 50K.
2. And his credit score is less than 500.
3. And requested loan amount is less than 20L.
4. And bank's current year target not done for home loan.
then:
1. Approve home loan.
2. Sanction 60% of requested loan amount.

Let’s convert these rules in MVEL + DSL form to store in DB.

Rules in MVEL + DSL form:

#Loan Rule 1:
Condition:
input.monthlySalary >= 50000
&& input.creditScore >= 800
&& input.requestedLoanAmount < 4000000
&& $(bank.target_done) == false
Action:
output.setApprovalStatus(true);
output.setSanctionedPercentage(90);
output.setProcessingFees(8000);
#Loan Rule 2:
Condition:
input.monthlySalary >= 35000 && input.monthlySalary <= 50000
&& input.creditScore <= 500
&& input.requestedLoanAmount < 2000000
&& $(bank.target_done) == false
Action:
output.setApprovalStatus(true);
output.setSanctionedPercentage(60);
output.setProcessingFees(2000);

In this example, $(bank.target_done) is a domain-specific keyword. Here it will be resolved and return the bank status about the loan that, this year's target for a home loan is done or not (true/false)? Except for this, others are in the form of MVEL expression.

Also, here input and output keywords represent the two java objects. The input object is passed as input data into rule-engine and it executes all loan rules on this data. And In the end, it will return the result in the form of the output object.

In this example we have UserDetails is an input object and LoanDetails is an output result object.

Input Data:

public class UserDetails {
Double monthlySalary;
Integer creditScore;
Double requestedLoanAmount;
//Few other variables
}

Output Result:

public class LoanDetails {
Boolean approvalStatus;
Float sanctionedPercentage;
Double processingFees;
//Few other variables
}

2. Store Rules In DB

Hereafter writing these rules in MVEL+DSL form, we store them in Postgres DB. You can use any other DB as per your feasibility.

If you don’t have Postgres then download and install it in your system or server. If you already have Postgres then, create the database rulebase and inside it create a table called rules . In this table, we mentioned a few more column-like priority, rule id, etc. for specific use (See in section 4: “Create a model class for Rule”).

CREATE DATABASE rulebase;CREATE TABLE rules (
rule_namespace varchar(256) not null,
rule_id varchar(512) not null,
condition varchar(2000),
action varchar(2000),
priority integer,
description varchar(1000),
PRIMARY KEY(rule_namespace, rule_id)
);

Now insert the above two rules into the Postgres database with the help of the following queries.

LOAN RULE 1
INSERT INTO rules
(rule_namespace , rule_id, condition,
action, priority, description)
VALUES (
'LOAN',
'1',
'input.monthlySalary >= 50000 && input.creditScore >= 800 && input.requestedLoanAmount < 4000000 && $(bank.target_done) == false',
'output.setApprovalStatus(true);output.setSanctionedPercentage(90);output.setProcessingFees(8000);',
'1',
'A person is eligible for Home loan?'
);
LOAN RULE 2
INSERT INTO rules
(rule_namespace , rule_id, condition,
action, priority, description)
VALUES (
'LOAN',
'2',
'input.monthlySalary >= 35000 && input.monthlySalary <= 50000 && input.creditScore <= 500 && input.requestedLoanAmount < 2000000 && $(bank.target_done) == false',
'output.setApprovalStatus(true);output.setSanctionedPercentage(60);output.setProcessingFees(2000);',
'2',
'A person is eligible for Home loan?'
);
Rules In Table After Insertion

3. Create a Maven Project

First, create a maven project and add the following dependencies of spring-boot, Postgres, and MVEL, etc. in the pom file.

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.5.RELEASE</version>
</parent>
<properties>
<java.version>1.8</java.version>
<junit.version>4.12</junit.version>
<postgresql.version>9.4-1206-jdbc42</postgresql.version>
<mvel.version>2.4.4.Final</mvel.version>
</properties>
<dependencies>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.mve</groupId>
<artifactId>mvel2</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
</dependencies>

4. Create a model class for Rule

Basically, rules are in if-then form. It contains basically two parts, condition and action. But we have to define some more information to implement it. Those are:

  • Namespace: Use to identify or club the same types of rules of a domain into one.
  • Id: Each rule could be identified with some unique Id.
  • Condition or pattern: Rule’s condition in the MVEL + DSL form.
  • Action: Rule’s action in the MVEL + DSL form.
  • Priority: It will use to decide which rule should execute first on input data.
  • Description: Show the description of the rule.

To store all these pieces of information, we use Rule Model.

public class Rule {
RuleNamespace ruleNamespace;
String ruleId;
String condition;
String action;
Integer priority;
String description;
}

Describe the domain (Rule namespace) of the rules in the form of Enums.

public enum RuleNamespace {
LOAN,
INSURANCE
}

5. DB Connection service

Here for the DB connection, we use the Spring Data JPA. Now first add the application.properties in resources directory and write the PostgreSql URL and credentials.

spring.datasource.url=jdbc:postgresql://localhost:5432/rulebase
spring.datasource.username=<your postgre username>
spring.datasource.password=<your postgre password>
spring.jpa.generate-ddl=true

Now create the database model and repository to access the data from Postgres DB. To implement it we use Spring Data JPA. For this, we create a repository RulesRepository, DB model RuleDbModel, and service KnowledgeBaseService.

DB Model: Get the data from DB into RuleDbModel form.

@Table(name = "rules")
public class RuleDbModel {
@Id
@Column(name = "rule_namespace")
private String ruleNamespace;
@Column(name = "rule_id")
private String ruleId;
@Column(name = "condition")
private String condition;
@Column(name = "action")
private String action;
@Column(name = "priority")
private Integer priority;
@Column(name = "description")
private String description;
@Data
static class IdClass implements Serializable {
private String ruleNamespace;
private String ruleId;
}
}

Repository: Define database queries or function in RulesRepository class.

@Repository
public interface RulesRepository extends JpaRepository<RuleDbModel, Long> {
List<RuleDbModel> findByRuleNamespace(String ruleNamespace);
List<RuleDbModel> findAll();
}

KnowledgeBase Service: Define methods to access rules as per our requirements.

@Service
public class KnowledgeBaseService {
@Autowired
private RulesRepository rulesRepository;
public List<Rule> getAllRules(){
//See complete code on git.
}
public List<Rule> getAllRuleByNamespace(String ruleNamespace){
//See complete code on git.
}
}

6. DSL Parser:

To resolve the DSL related keyword, we need to implement a keyword resolver.

DSLResolver interface:

public interface DSLResolver {
String getResolverKeyword();
Object resolveValue(String keyword);
}

This interface will be implemented by each domain-specific keywords. For example, BankResolver for getting the current interest rate and info of target done or not.

Let’s assume here resolver keyword is bank and sub-keywords are interest and target_done. Then the code for BankResolver:

public class BankResolver implements DSLResolver {
private static final String RESOLVER_KEYWORD = "bank";
private static final String INTEREST = "interest";
private static final String TARGET_DONE = "target_done";
@Override
public String getResolverKeyword() {
return RESOLVER_KEYWORD;
}
@Override
public Object resolveValue(String keyword) {
if (keyword.equalsIgnoreCase(INTEREST)){
//Code to calculate the current variable interest rates.
return 9.0;
}
if (keyword.equalsIgnoreCase(TARGET_DONE)){
//Code to see the bank target of giving loan for this current year is done or not.
return false;
}
return null;
}
}

DSL Keyword Resolver:

After that, we write a service DSLKeywordResolver to resolve all implemented keywords. Here getResolver(String keyword) method resolves the resolver-keyword and returns the object reference of that keyword resolver (Example: BankResolver).

public class DSLKeywordResolver {
Map<String, DSLResolver> dslKeywordResolverList;
@Autowired
public DSLKeywordResolver(List<DSLResolver> resolverList) {
dslKeywordResolverList = resolverList.stream()
.collect(Collectors.toMap(DSLResolver::getResolverKeyword, Function.identity()));
}
public Optional<DSLResolver> getResolver(String keyword) {
return Optional.ofNullable(dslKeywordResolverList.get(keyword));
}
}

Now write the DSLParser, which takes given rule expression and resolve the DSL keyword and replace the value in the rule’s expression with the corresponding keyword.

Example:

condition: “input.monthlySalary >= 50000 && input.creditScore >= 800 && input.requestedLoanAmount<4000000 && $(bank.target_done) == false”

To:

condition: “input.monthlySalary >= 50000 && input.cibilScore >= 800 && input.requestedLoanAmount<4000000 && false == false”

public class DSLParser {@Autowired
private DSLKeywordResolver keywordResolver;
public String resolveDomainSpecificKeywords(String expression){
Map<String, Object> dslKeywordToResolverValueMap = executeDSLResolver(expression);
return replaceKeywordsWithValue(expression, dslKeywordToResolverValueMap);
}
}

7. MVEL Parser

After resolving the DSL keywords, the expression will be full of MVEL expression. Now we create an MVELParser to resolve this MVEL expression.

public class MVELParser {public boolean parseMvelExpression( String expression, Map<String, Object> inputObjects){
try {
return MVEL.evalToBoolean(expression,inputObjects);
}catch (Exception e){
log.error("Can not parse Mvel Expression : {} Error: {}", expression, e.getMessage());
}
return false;
}
}

Here the inputObjects contain the map of input data and output result objects with respect to input and output keys.

8. RuleParser

RuleParser is a wrapper of MVELParser and DSLParser. It is used to parse the rule’s condition and action. Condition execute on input data and action execution returns the output result.

Rule parser parses the rule expression in two steps:

  • Step 1) Resolve domain-specific keywords first: $(resolver-keyword.sub-keyword)
  • Step 2) Resolve MVEL expression.
public class RuleParser<INPUT_DATA, OUTPUT_RESULT> {@Autowired
protected DSLParser dslParser;
@Autowired
protected MVELParser mvelParser;
private final String INPUT_KEYWORD = "input";
private final String OUTPUT_KEYWORD = "output";
public boolean parseCondition(String expression, INPUT_DATA inputData) {
String resolvedDslExpression = dslParser.resolveDomainSpecificKeywords(expression);
Map<String, Object> input = new HashMap<>();
input.put(INPUT_KEYWORD, inputData);
boolean match = mvelParser.parseMvelExpression(resolvedDslExpression, input);
return match;
}
public OUTPUT_RESULT parseAction(String expression, INPUT_DATA inputData, OUTPUT_RESULT outputResult) {
String resolvedDslExpression = dslParser.resolveDomainSpecificKeywords(expression);
Map<String, Object> input = new HashMap<>();
input.put(INPUT_KEYWORD, inputData);
input.put(OUTPUT_KEYWORD, outputResult);
mvelParser.parseMvelExpression(resolvedDslExpression, input);
return outputResult;
}
}

9. Inference Engine

The inference engine is the core part of the rule-engine. It executes the rule on input data mainly in three steps.

  1. MATCH: Match the facts/conditions and data against the set of rules. It returns the set of satisfied rules.
  2. RESOLVE: Resolve the conflict set of rules and give the selected one rule.
  3. EXECUTE: Run the action of the selected one rule on given data and return the resulting output.

We implement all three steps or methods in the Inference engine’s abstract class and call them in the run method.

public abstract class InferenceEngine<INPUT_DATA, OUTPUT_RESULT> {@Autowired
private RuleParser<INPUT_DATA, OUTPUT_RESULT> ruleParser;
public OUTPUT_RESULT run (List<Rule> listOfRules, INPUT_DATA inputData){//STEP 1 MATCH
List<Rule> conflictSet = match(listOfRules, inputData);
//STEP 2 RESOLVE
Rule resolvedRule = resolve(conflictSet);
if (null == resolvedRule){
return null;
}
//STEP 3 EXECUTE
OUTPUT_RESULT outputResult = executeRule(resolvedRule, inputData);
return outputResult;
}
//Here we are using Linear matching algorithm for pattern
protected List<Rule> match(List<Rule> listOfRules, INPUT_DATA inputData){
return listOfRules.stream()
.filter(
rule -> {
String condition = rule.getCondition();
return ruleParser.parseCondition(condition, inputData);
}
)
.collect(Collectors.toList());
}
//Here we are using find first rule logic.
protected Rule resolve(List<Rule> conflictSet){
Optional<Rule> rule = conflictSet.stream()
.findFirst();
if (rule.isPresent()){
return rule.get();
}
return null;
}
protected OUTPUT_RESULT executeRule(Rule rule, INPUT_DATA inputData){
OUTPUT_RESULT outputResult = initializeOutputResult();
return ruleParser.parseAction(rule.getAction(), inputData, outputResult);
}
protected abstract OUTPUT_RESULT initializeOutputResult();
protected abstract RuleNamespace getRuleNamespace();
}

Here another two abstract methods initializeOutputResult() and getRuleNamespace() are implemented in the extended domain-specific inference engine. The initializeOutputResult() method returns the initialized reference of the output result object. The getRuleNamespace() method returns the RuleNamespace of that inference engine.

Example: For creating the rule-engine for Loan application or domain, we have to extend InferenceEngine and need to implement both abstract methods. For this inference engine, INPUT_DATA will be the UserDetails object and OUTPUT_RESULT will be the LoanDetails object.

public class LoanInferenceEngine extends InferenceEngine<UserDetails, LoanDetails> {@Override
protected RuleNamespace getRuleNamespace() {
return RuleNamespace.LOAN;
}
@Override
protected LoanDetails initializeOutputResult() {
return new LoanDetails();
}
}

Similarly, we can create an inference engine for other domains also. For example Insurance.

public class InsuranceInferenceEngine extends InferenceEngine<PolicyHolderDetails, InsuranceDetails> {@Override
protected RuleNamespace getRuleNamespace() {
return RuleNamespace.INSURANCE;
}
@Override
protected LoanDetails initializeOutputResult() {
return new InsuranceDetails();
}
}

Here for the Insurance inference engine, INPUT_DATA will be the PolicyHolderDetails object and OUTPUT_RESULT will be the InsuranceDetails object.

10. RuleEngine REST API

Till now mostly rule engine implementation is done. Now we have to create a rest API or controller to pass the input as JSON and get the response from rule-engine. Here we created two rest controllers, one /get-all-rules for getting all rules and another /loan for passing input data object in JSON format and getting the result of rule execution.

@RestController
public class RuleEngineRestController {
@GetMapping(value = "/get-all-rules")
public ResponseEntity<?> getAllRules() {
List<Rule> allRules = knowledgeBaseService.getAllRules();
return ResponseEntity.ok(allRules);
}
@PostMapping(value = "/loan")
public ResponseEntity<?> runLoanRuleEngine(@RequestBody UserDetails userDetails) {
LoanDetails result = (LoanDetails) ruleEngine.run(loanInferenceEngine, userDetails);
return ResponseEntity.ok(result);
}
}

11. Testing

Now our rule-engine ready. It’s time to test it. To test the rule-engine API, we will use the postman tool.

First, let’s see the controller /get-all-rules and get all the rules from DB.

http://localhost:8050/get-all-rules

Second, let’s post the input object UserDetails in JSON format through postman and see the response.

Input JSON: According to given input data rule 1 should be satisfied and Sanctioned Percentage should be 90 in response.

Input JSON:

{
"creditScore": 900,
"firstName": "Mark",
"lastName": "K",
"age": "25",
"accountNumber": 123456789,
"bank": "ABC BANK",
"requestedLoanAmount": 3500000.0,
"monthlySalary": 70000.0
}

Output Result:

http://localhost:8050/loan

We also have written the SpringBoot JUnit test for the above test. You can check here on git.

--

--

Ramesh Katiyar

Interested in learning and exploring new technologies and skills.