Using JBoss Drools to Implement a E-commerce Promotion Rule Engine – Part 2

In my last post, I outline several common E-commerce promotion rules to be implemented with JBoss Drools Expert. I also describe the domain objects for representing the facts used by the rule engine as well as set up the unit test harness. I will finish off with this post by providing details on how each rule can be defined with the purpose of demonstrating some capabilities of Drools.

Drools Rules

Rule 1: Large order discount

Rule file

// 1. Apply discount if order is over a certain amount
 rule "$50 discount for order over $1000 but less than $2000"
 no-loop
 when
      $order : Order(
           $order.lineTotal >= 1000 &&
           $order.lineTotal < 2000
      )
 then
      modify($order) {
           setOrderDiscountAmount(50);
      }
      System.out.println("Apply $50 discount for order over $1000:");
      System.out.println($order);
 end
rule "200 discount for order over $2000"
 no-loop
 when
      $order : Order(
           $order.lineTotal >= 2000
      )
 then
      modify($order) {
           setOrderDiscountAmount(200);
      }
      System.out.println("Apply $200 discount for order over $2000:");
      System.out.println($order);
 end

Note

  1. 2 rules are defined here. The first one applies when the order total (lineTotal) is between $1000 and $2000. The second rule applies for any order over $2000.
  2. The modify keyword in the action (then) clause tells the rule engine that the rule modifies the fact, in this case the order. This would have triggered the same  rule again. Drools use the rule attribute no-loop to stop this from happening

Unit Test

@Test
 public void testLargeOrderDiscount_over1000() {
      // Add facts to working memory
      Order order = new Order();
      order.addOrderLine(new OrderLine("SKU 123", 1, 1200));
      ksession.insert(order);
      assertEquals(1, ksession.fireAllRules());
 }
Output:
Apply $50 discount for order over $1000:
 Order total(discount): 1200.0(50.0)
 [SKU 123] x1: 1200.0(-0.0) = 1200.0

Rule 2: Clearance products – 10% of list of defined products

Rule File

// 2. Clearance products
 rule "Clearance products"
 no-loop
 when
      $order : Order()
      $item : OrderLine() from $order.lines
      ClearanceProductList(skus contains $item.getSku())
 then
      System.out.println("Apply discount for clearance product " + $item.getSku() + ":");
      $item.setLineDiscountAmount(10);
      System.out.println($order);
 end

Note

  1. Note the use of keyword from in the when clause to iterate list of order items from the order
  2. Each item found in (1) is set in variable $item, which is then used to check in the next line ClearanceProductList(…) whether the line item is one of the discounted product list skus by using the contains keyword.
  3. Discount is applied to the item in the then clause  $item.setLineDiscountAmount(10);

Unit Test

@Test
 public void testClearanceProductDiscount() {
     // Add facts to working memory
     Order order = new Order();
     order.addOrderLine(new OrderLine("SKU 123", 1, 100));
     order.addOrderLine(new OrderLine("SKU 456", 1, 200));
     ClearanceProductList productList = new ClearanceProductList();
     productList.add("SKU 123");
     productList.add("SKU ABC");
     ksession.insert(order);
     ksession.insert(productList);
     assertEquals(2, ksession.fireAllRules());
}
Output:
Apply discount for clearance product SKU 123:
 Order total(discount): 290.0(0.0)
 [SKU 123] x1: 100.0(-10.0) = 90.0
 [SKU 456] x1: 200.0(-0.0) = 200.0

Rule 3: Time based sales – 10% off from a list of defined products within a certain date range, e.g. Christmas sales between 1/12 to 31/12.

Rule File

// 3. Time based sales
rule "Time based sales"
no-loop
 when
      $order : Order()
      $item : OrderLine() from $order.lines
      $sales : TimeBasedSales(
           fromDate < LocalDate.now().toDate(),
           toDate > LocalDate.now().toDate(),
           products contains $item.getSku()
     )
 then
      $item.setLineDiscountAmount($item.getUnitPrice() * $item.getQty() * 0.1);
      System.out.println("Apply time based discount for product " + $item.getSku() + ":");
      System.out.println($order);
end

Note:

  1. The date comparisons for fromDate and toDate in the last lines of the when clause.

Unit Test

@Test
 public void testTimeBasedSales() {
      // Add facts to working memory
      Order order = new Order();
      order.addOrderLine(new OrderLine("SKU 123", 1, 100));
      order.addOrderLine(new OrderLine("SKU 456", 1, 200));
      TimeBasedSales sales = new TimeBasedSales(
      LocalDate.now().minusDays(5).toDate(),
      LocalDate.now().plusDays(5).toDate());
      sales.addProduct("SKU 456");

      ksession.insert(order);
      ksession.insert(sales); 
      assertEquals(2, ksession.fireAllRules()); 
 }
Output:
Apply time based discount for product SKU 456:
Order total(discount): 280.0(0.0)
[SKU 123] x1: 100.0(-0.0) = 100.0
[SKU 456] x1: 200.0(-20.0) = 180.0

Note:

  1. I setup the TimeBasedSales object based on current date. Drools supports concept of pseudo system clock which allows you to test time related rules independent of current real time. See here for details.

Rule 4: Special Tuesday – everything 5% off on Tuesday

Rule File

// 4. 5% discount everything Tuesday
rule "5% discount everything tuesday"
calendars "tuesdays"
 when
      $order : Order()
 then
      System.out.println("Apply special Tuesday discount:");
      $order.setOrderDiscountAmount(0.05 * $order.getLineTotal());
System.out.println($order);
end

Note

  1. This rule makes use of attribute calendars to define the pre-condition that the rule will be considered. The value “tuesdays” correspond to the calendar of same name defined in the knowledge base session. Below is the code in the unit test. Note the use of Quartz to define days of week excluded.
private void getTuesdayCalendar() {
      //in this case I'm using a DailyCalendar but you can use whatever implementation of Calendar you want
       org.quartz.impl.calendar.WeeklyCalendar tuesdays = new org.quartz.impl.calendar.WeeklyCalendar();
       tuesdays.setDaysExcluded(new boolean[] {true, true, true, false, true, true, true, true}); // index 1 = Sunday, etc.
      //convert the calendar into a org.drools.time.Calendar
      org.drools.time.Calendar tuesdayCalendar = QuartzHelper.quartzCalendarAdapter(tuesdays);

      //Register the calendar in the session with a name. You must use this name in your rules.
      ksession.getCalendars().set( "tuesdays", tuesdayCalendar);
}

Unit Test

@Test
 public void testSpecialTuesdaysApplied() {
      // Move to next Tuesday
      advanceToDayOfWeek(2);
      // Add facts to working memory
      Order order = new Order();
      order.addOrderLine(new OrderLine("SKU 123", 1, 100));
      order.addOrderLine(new OrderLine("SKU 456", 1, 200));
      ksession.insert(order);
      assertEquals(2, ksession.fireAllRules());
 }

Output:
Apply special Tuesday discount:
Order total(discount): 300.0(15.0)
[SKU 123] x1: 100.0(-0.0) = 100.0
[SKU 456] x1: 200.0(-0.0) = 200.0

Note

  1. I use the psuedo system clock and method advanceToDayOfWeek(2) to move the clock to next Tuesday to trigger the rule.

Rules Interactions

In a real world commerce environment, a promotion engine will have a considerably bigger number of business rules. Importantly, those rules will be interdependent to each other. Typically, only one rule out of a number of different rules can apply. Alternatively, there is a specific order in which certain rules should apply. Rules can potentially be conflicting. This is where a rule engine like Drools comes in handy as it provides a comprehensive set of features to support these and other common scenarios.

salience

This rule attribute is used to define the priority in which a rule is fired in relation to other rules. For example, a large value discount (Rule 1) should only be applied after all the other line item discounts are applied.

activation-group

This rule attribute is used to group a number of related rules so that when one rule in the group fires, the other rules become inactive. For example, if a product is in clearance category, only the clearance discount is applied.

Final Thoughts

I hope this and last posts demonstrate how common ecommerce problem such as promotion can be solved using a business rule engine such as Drools. Obviously, the business rules will be more complex in the real world but this article should illustrate by example the benefits I listed in Part 1, which I explain in more details below:

  1. Declarative Programming – business rules are defined declaratively in pseudo human readable format (when … then) which promotes common understanding between business and developers.
  2. Separation of Data and Logic – all the logics are defined as rules. This is a significant advantage as it scales much better than having to implement the logics in codes. Also, business rules change often in real world and it is advantageous here as the rules can be modified without need of code changes.
  3. Centralization of Knowledge – the set of rules defined declaratively become the knowledge base of the system (and the business), which will faciliate its update and maintainence.
  4. Explanation of outcomes or actions – the rule engine can answer the why-questions as it knows what rules are fired based on what facts. This is considerably more difficult to implement without the Inference Engine implemented by the rule engine.

There are other ecommerce problems that could benefit from Drools. For example, the determination of product pricing based on order, customer, product costs, etc.

As for Drools, I only touch on the basic features. JBoss has good documentation which can be found here. A few features worth exploring include

  1. Decision table – instead of defining rules in drl files as shown here, drools also support writing rules in MS-Excel spreadsheets
  2. Rule template – instead of defining individual rules, one can define a template with variables and then create the individual rules by filling in the values of the variables from data in database or MS-Excel files.
  3. Event processing in the API

That’s all for now.

About Raymond Lee
Professional Java/EE Developer, software development technology enthusiast.

Comments are closed.