javaspringspring-bootdroolsdrools-guvnor

Springboot:Can Drools Rule Engine file (.drl) be updated through frontend based on user inputs


I have followed a tutorial on Drools and have implemented the same as well. I am trying to understand the means by which I can alter the values in the .drl file through front end. Below is the Drools file I have used named order.drl.

package KieRule;
import com.example.demo.Order;

rule "HDFC"

when
orderObject : Order(cardType=="HDFC" && price>10000);
then
orderObject.setDiscount(10);
end;

rule "ICICI"

when
orderObject : Order(cardType=="ICICI" && price>15000);
then
orderObject.setDiscount(8);
end;

rule "DBS"

when
orderObject : Order(cardType=="DBS" && price>15000);
then
orderObject.setDiscount(15);
end;

Basically the logic is to establish rules on the percentage of discount to be calculated on each card type for a commodity purchased.

So a post request as below to http://localhost:8080/orders

{
"name":"Mobile",
"cardType":"HDFC",
"price" : 11000
}

gives output as below where the discount has been determined

{
"name": "Mobile",
"cardType": "HDFC",
"discount": 10,
"price": 11000
}

I created the Spring Starter Project and below is my folder structure

Folder structure of Drools Project

Will it be feasible for me to have the values presently hardcoded in the .drl file like for e.g. "HDFC" and "price>10000" to be captured from the JSP or Reactjs front end and updated? I would prefer the admin users of the application to alter the rules as and when they require. I have seen examples where there are $ notations used in .drl but was not able to grasp them fully. Can this we achieved in Java?

Thanks SM


Solution

  • Assuming that what you're trying to do is to keep the basic structure the same and simply vary the constraints, then the two simplest solutions would be to use rule templates or to pass the constraints themselves to the rules alongside your inputs.


    Passing in constraints

    This is the simplest solution. Basically the idea is that the values for your constraints are first class citizens, alongside your rule inputs.

    Looking at your rules that you presented, the two pieces of data that make up your constraints are the card type and the minimum price. These each have a single consequence. We can model that simply:

    class OrderConstraints {
      private String cardType;
      private Integer minimumPrice;
      private Integer discount;
      // Getters 
    }
    

    You would pass these into the rules in objects that look like this:

    {
        "cardType": "HDFC",
        "minimumPrice": 10000,
        "discount": 10
    },
    {
        "cardType": "ICICI",
        "minimumPrice": 15000,
        "discount": 8
    },
    {
        "cardType": "DBS",
        "minimumPrice": 15000,
        "discount": 15
    }
    

    Now all of your use cases can be handled in a single rule:

    rule "Apply order discount"
    when
      OrderConstraints( $cardType: cardType, $minimumPrice: minimumPrice, $discount: discount)
      $order: Order( cardType == $cardType, price > $minimumPrice )
    then
      $order.setDiscount($discount);
    end
    

    (Side note: I cleaned up your syntax. You had a lot of unnecessary semi-colons and oddly located white space in your original rules.)

    The workflow would basically be as follows:

    1. User creates constraints in UI / front end, specifying required information (card type, minimum price, discount).
    2. User's constraints are sent to the server and saved to your persistence layer (database, etc.)
    3. When a new query is made, constraints are read out of the persistence layer and passed into the rules along with the rule inputs.

    Rule templates

    The second solution is to use rule templates (link is to Drools documentation.) The basic idea is that you provide a table of data and a template of a DRL, and the Drools framework will make the data to the template and generate the DRLs for you. This is useful for when you have very repetitive rules such as yours -- where other you're basically applying the same rule with various different constraints.

    Similar to the other scenario, your workflow would be like this:

    1. User creates constraints in UI / front end, specifying required information (card type, minimum price, discount.)
    2. User's constraints are sent to the server.
    3. Server reformats the request into tabular form (instead of JSON or whatever the original format was).
    4. Server uses the data table (step 3) with the template to generate rules.

    Your template might look something like this, assuming columns labelled "cardType", "minPrice", and "discount"):

    template header
    cardType
    minPrice
    discount
    
    package com.example.template;
    import com.example.Order;
    
    template "orderdiscounts"
    
    rule "Apply order discount for @{cardType}"
    when
      $order: Order( cardType == "@{cardType}",
                     price > @{minPrice} )
    then
      $order.setDiscount(@{discount});
    end
    
    end template
    

    The format is pretty straight-forward. First comes the header, where we define the columns in order. The first blank line indicates the end of the header. The package declaration and imports come next, because those are static for the file. Then comes the template. The column values are interpolated using the @{ column name } pattern; note that you need to wrap this in quote marks for a string.

    The Drools documentation is very good so I'm not going to go overly into detail, but you should be able to get the gist of this.


    Design considerations

    Since you're talking about a React front-end, I'm going to assume you're building a modern web application. When you implement your solution, keep in mind the problems of persistence and data integrity.

    If you scale your backend application into multiple instances fronted by a load balanced, you'll want to make sure that the constraints the user applies are propagated to all instances. Further, if you apply changes, they need to become visible/propagate in real time -- you can't have one node or cluster using stale constraints or values.

    While on its face the rule templates seem like they're the perfect built-in solution for this problem, it's not necessarily the case. Rule templates are designed around data tables stored in flat files -- by definition not a very modern or distributed approach. If your user updates the constraints on Node A, and the data table on Node A is updated, you need to make sure that the same data table file propagates to all of the other Nodes. Further if you spin up a new Node, you'll need to design a mechanism by which it is able to get the "current" data table. And then what if something catastrophic happens and you lose all your nodes?

    The "pass constraints into memory" solution is old school, but it has the benefit of being backed by a traditional persistence layer. If you use a distributed data source of some kind, this can be your source of truth. As long as all of your application instances query your constraints out of the database whenever they file the rules (or with some logical caching layer), you won't need to worry about your instances getting out of sync. And if you lose all your nodes, your constraints will be in your data source for when your new application instances are spun up and need to use it. The downside is, of course, that you do need to do these database queries or cache reads, which potentially adds latency. Then you need to pass this data into the rules, which is going to increase your memory usage and potentially CPU (exactly how much depends on how much data you're passing in).

    I've linked you to the Drools documentation and I strongly suggest reading it. It's long, but quite worth it. It's probably my most-accessed bookmark, even though I don't even do drools on a daily basis anymore. There are other solutions available as is documented there -- for example, your user request could generate rules, package them in a kjar, and publish them to a Maven repository which all of your instances could then download and use -- but the right solution for you use case is something you'll need to decide for yourself based on your requirements.