Enterprise Java

A complete tutorial on the Drools business rule engine

As always we share the code presented in the tutorial in a companion repository: EmailSchedulingRules.

Business rules work very well to represent the logic for certain domains. They work well because they result intuitive and close to the way of thinking of many types of domain experts. The reason for that it is that they permit to decompose a large problem in single components. In this way the user has not to deal with the orchestration of all the single rules: this is the added value provided by the business rule engine.

In this article we will discuss one specific example of application written by using business rules. We will write the rules to decide which email to send to the subscribers to a newsletter. We will see different types of rules and how we could express them using the Drools Rule Language. We will also see how to configure Drools (spoiler: it will be easy) and have the system elaborate the rules to produce a result we can use.

I think that business rules are quite interesting because they permit to look at problems in a different way. As developers we are very used to the imperative paradigm or functional paradigms. However there are other paradigms, like state machines and business rules, which are not so commonly used and which can be a much better fit in some contexts.

As always we share the code presented in the tutorial in a companion repository: EmailSchedulingRules.

What problem we are trying to solve

Let’s consider the domain of email marketing. As marketers we have an email list of persons interested in our content. Each of them may have demonstrate interest in a specific topic, read some of our articles and bought certain products. Considering all their history and preferences we want to send to them at each time the most appropriate content. This content may be either educative or proposing some deal. The problem is that there are constraints we want to consider (i.e., not sending emails on sunday or not sending emails promoting a product to someone who already bought it).

All these rules are simple per se, but the complexity derives by how they are combined and how they interact. The business rule engine will deal with that complexity for us, all we have to do is to express clearly the single rules. Rules will be expressed in the terms of our domain data so let’s focus on our domain model first.

The model of our domain

In our domain model we have:

  • Emails: the single emails we want to send, described by their title and content
  • Email Sequences: groups of emails that have to be sent in a specific order, for example a set of emails representing a tutorial or describing different features of a product
  • Subscribers: the single subscriber to the mailing list. We will need to know which emails we sent to him, what things he is interested in, and which products he bought
  • Products: the products we sell
  • Purchases: the purchases subscribers have made
  • Email Sending: the fact we sent or are about to send a certain email, on a certain date to a certain subscriber
  • Email Scheduling: the plan for sending an email, with some additional information

The latter two elements of our domain model could seem less obvious compared to the others, but we will see in the implementation for which reasons we need them.

business rule engine

What our system should do

Our system should execute all the rules, using the Drools engine, and to determine for each user which email we should send on a specific day. The result could be the decision to not send any email, or to send an email, selecting one among many possible emails.

An important thing to consider is that these rules may evolve over time. The people in charge of marketing may want to try new rules and see how they affect the system. Using Drools it should be easy for them to add or remove rules or tweak the existing rules.

Let’s stress this out:

these domain experts should be able to experiment with the system and try things out quickly, without always needing help from developers.

The rules

Ok, now that we know which data do we have, we can express rules based on that model.

Let’s see some examples of rules we may want to write:

  • We may have sequences of emails, for example the content of a course. They have to be sent in order
  • We may have time sensitive emails that should either be sent in a specific time window or not sent at all
  • We may want to avoid sending emails on specific days of the week, for example on the public holidays in the country where the subscriber is based
  • We may want to send certain type of emails (for example proposing a deal) only to persons who received certain other emails (for example at least 3 informative emails on the same subject)
  • We do not want to propose a deal on a certain product to a subscriber who has already bought that product
  • We may want to limit the frequency we send emails to users. For example, we may decide to not send an email to a user if we have sent already one in the last 5 days

Setting up drools

Setting up drools can be very simple. We are looking into running drools in a standalone application. Depending on your context this may or may not be an acceptable solution and in some cases you will have to look into JBoss, the application server supporting Drools. However if you want to get started you can forget all of this and just configure your dependencies using Gradle (or Maven). You can figure out the boring configuration bits later, if you really have to.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
buildscript {
    ext.droolsVersion = "7.20.0.Final"
 
    repositories {
        mavenCentral()
    }
}
 
plugins {
    id "org.jetbrains.kotlin.jvm" version "1.3.21"
}
 
apply plugin: 'java'
apply plugin: 'idea'
 
group 'com.strumenta'
version '0.1.1-SNAPSHOT'
 
repositories {
    mavenLocal()
    mavenCentral()
    maven {
    }
}
 
dependencies {
    compile "org.kie:kie-api:${droolsVersion}"
    compile "org.drools:drools-compiler:${droolsVersion}"
    compile "org.drools:drools-core:${droolsVersion}"
    compile "ch.qos.logback:logback-classic:1.1.+"
    compile "org.slf4j:slf4j-api:1.7.+"   
    implementation "org.jetbrains.kotlin:kotlin-stdlib"
    implementation "org.jetbrains.kotlin:kotlin-reflect"
    testImplementation "org.jetbrains.kotlin:kotlin-test"
    testImplementation "org.jetbrains.kotlin:kotlin-test-junit"
}

In our Gradle script we use:

  • Kotlin, because Kotlin rocks!
  • IDEA, because it is my favorite IDE
  • Kotlin StdLib, reflect and test
  • Drools

And this is how our program will be structured:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
fun main(args: Array<String>) {
    try {
        val kbase = readKnowledgeBase(listOf(
                File("rules/generic.drl"),
                File("rules/book.drl")))
        val ksession = kbase.newKieSession()
        // typically we want to consider today but we may decide to schedule
        // emails in the future or we may want to run tests using a different date
        val dayToConsider = LocalDate.now()
        loadDataIntoSession(ksession, dayToConsider)
 
        ksession.fireAllRules()
 
        showSending(ksession)
    } catch (t: Throwable) {
        t.printStackTrace()
    }
}

Pretty simple, pretty neat.

What we do in, details is:

  • We load the rules from file. For now we just load the file rules/generic.drl
  • We setup a new session. Think of the session as the universe as seen by the rules: all data they can access is there
  • We load our data model into the session
  • We fire all the rules. They could change stuff in the session
  • We read the modified data model (a.k.a. the session) to figure out which emails we should send today

Writing the classes for the data model

We have previously seen how our data model looks like, let’s now see the code for it.

Given we are using Kotlin it will be pretty concise and obvious.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
package com.strumenta.funnel
 
import java.time.DayOfWeek
import java.time.LocalDate
import java.util.*
 
enum class Priority {
    TRIVIAL,
    NORMAL,
    IMPORTANT,
    VITAL
}
 
data class Product(val name: String,
                   val price: Float)
 
data class Purchase(val product: Product,
                    val price: Float,
                    val date: LocalDate)
 
data class Subscriber(val name: String,
                      val subscriptionDate: LocalDate,
                      val country: String,
                      val email: String = "$name@foo.com",
                      val tags: List<String> = emptyList(),
                      val purchases: List<Purchase> = emptyList(),
                      val emailsReceived: MutableList<EmailSending> = LinkedList()) {
 
    val actualEmailsReceived
            get() = emailsReceived.map { it.email }
 
    fun isInSequence(emailSequence: EmailSequence) =
            hasReceived(emailSequence.first)
                    && !hasReceived(emailSequence.last)
 
    fun hasReceived(email: Email) = emailsReceived.any { it.email == email }
 
    fun hasReceivedEmailsInLastDays(nDays: Long, day: LocalDate)
            : Boolean {
        return emailsReceived.any {
            it.date.isAfter(day.minusDays(nDays))
        }
    }
 
    fun isOnHolidays(date: LocalDate) : Boolean {
        return date.dayOfWeek == DayOfWeek.SATURDAY
                || date.dayOfWeek == DayOfWeek.SUNDAY
    }
 
    fun emailReceivedWithTag(tag: String) =
            emailsReceived.count { tag in it.email.tags }
 
}
 
data class Email(val title: String,
                 val content: String,
                 val tags: List<String> = emptyList())
 
data class EmailSequence(val title: String,
                         val emails: List<Email>,
                         val tags: List<String> = emptyList()) {
 
    val first = emails.first()
    val last = emails.last()
 
    init {
        require(emails.isNotEmpty())
    }
 
    fun next(emailsReceived: List<Email>) =
        emails.first { it !in emailsReceived }
}
 
data class EmailSending(val email: Email,
                        val subscriber: Subscriber,
                        val date: LocalDate) {
    override fun equals(other: Any?): Boolean {
        return if (other is EmailSending) {
            this.email === other.email && this.subscriber === other.subscriber && this.date == other.date
        } else {
            false
        }
    }
 
    override fun hashCode(): Int {
        return this.email.title.hashCode() * 7 + this.subscriber.name.hashCode() * 3 + this.date.hashCode()
    }
}
 
data class EmailScheduling @JvmOverloads constructor(val sending: EmailSending,
                           val priority: Priority,
                           val timeSensitive: Boolean = false,
                           var blocked: Boolean = false) {
    val id = ++nextId
 
    companion object {
        private var nextId = 0
    }
}

Nothing surprising here: we have the seven classes we were expecting. We have a few utility methods here and there but nothing that you cannot figure out by yourself.

Writing a rule to schedule an email

It is now time to write our first business rule. This rule will state that, given a sequence and given a person, we will schedule the first email of the sequence to be sent to a person if that person is not already receiving an email from that sequence.

01
02
03
04
05
06
07
08
09
10
11
dialect "java"
rule "Start sequence"
   when
      sequence : EmailSequence ()
      subscriber : Subscriber ( !isInSequence(sequence) )
 
   then
      EmailSending $sending = new EmailSending(sequence.getFirst(), subscriber, day);
      EmailScheduling $scheduling = new EmailScheduling($sending, Priority.NORMAL);
      insert($scheduling);
end

In the header of the rule we specify the language we are using for writing the clauses. In this tutorial we will consider only Java. There is another possible value: mvel. We will not look into that. Also, while in this example we specify the dialect on the rule it can be instead specified once for the whole file. There is even a better option: not specifing the dialect at all, as Java is the default anyway and the usage of mvel is discouraged.

The when section determines on which elements our rule will operate. In this case we state that it will operate on an EmailSequence and a Subscriber. It will not work just on any person but only on a person for which the condition !isInSequence(sequence) is satisfied. This condition is based on a call to the method isInsequence that we will show below:

1
2
3
4
5
6
7
8
9
data class Subscriber(...) {
 
    fun isInSequence(emailSequence: EmailSequence) =
            hasReceived(emailSequence.first) &&
                !hasReceived(emailSequence.last)
 
    fun hasReceived(email: Email) =
            emailReceived.any { it.email == email }
}

Let’s now look at the then section of our rule. In such section we specify what happens when the rule is fired. The rule will be fired when elements satisfying the when section can be found.

In this case we will create an EmailScheduling and add it to the session. In particular we want to send to the considered person the first email of the sequence, on the day considered. We also specify the priority of this email (NORMAL in this case). This is necessary to decide which email effectively to send when we have more than one. Indeed we will have another rule looking at these values to decide which emails to prioritize (hint: it will be the email with the highest priority).

In general you may want to typically add things into the session in the thenclause. Alternatively you may want to modify objects which are part of the session. You could also call methods on objects which have side-effects. While the recommended approach is to limit yourself to manipulate the session you may want to add side effects for logging, for example. This is especially useful when learning Drools and trying to wrap your head around your first rules.

Writing a rule to block an email from being sent

We will see that we have two possible types of rules: rules to schedule new emails and rules to prevent scheduled emails to be sent. We have seen before how to write a rule to send an email and we will now see how to write an email to prevent an email from being sent.

In this rule we want to check if an email is scheduled to be sent to a person who has received already emails in the last three days. If this is the case we want to block that email from being sent.

1
2
3
4
5
6
7
8
9
rule "Prevent overloading"
   when
      scheduling : EmailScheduling(
            sending.subscriber.hasReceivedEmailsInLastDays(3, day),
            !blocked )
 
   then
      scheduling.setBlocked(true);
end

In the when section we specify that this rule will operate on an EmailScheduling. So, every time another rule will add an EmailScheduling this rule could be triggered to decide if we have to block it from being sent.

This rule will apply to all scheduling which are directed to subscribers who have received emails in the last 3 days. In addition to that we will check if the EmailScheduling was not already blocked. If that is the case we will not need to apply this rule.

We use the setBlocked method of the scheduling object to modify an element which is part of the session.

At this point we have seen the pattern we will use:

  • We will create EmailScheduling when we think it makes sense to send an email to the user
  • We will check if we have reasons to block those emails. If that is the case we will set the blocked flag to true, effectively removing the EmailScheduling

Using a flag to mark elements to remove/invalidate/block is a common pattern used in business rules. It can sound a bit unfamiliar at the beginning but it is actually quite useful. You may think that you could just delete elements from the session, however doing so it becomes easy to create infinite loops in which you create new elements with some rules, remove them with others and keep recreating them again. The block-flag pattern avoids all of that.

The session

Rules operate on data which is part of the session. Data is typically inserted into the session during the initialization phase. Later we could have rules inserting more data into the session, potentially triggering other rules.

This is how we could populate the session with some example data:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
fun loadDataIntoSession(ksession: KieSession,
                        dayToConsider: LocalDate) {
    val products = listOf(
            Product("My book", 20.0f),
            Product("Video course", 100.0f),
            Product("Consulting package", 500.0f)
    )
    val persons = listOf(
            Subscriber("Mario",
                    LocalDate.of(2019, Month.JANUARY, 1),
                    "Italy"),
            Subscriber("Amelie",
                    LocalDate.of(2019, Month.FEBRUARY, 1),
                    "France"),
            Subscriber("Bernd",
                    LocalDate.of(2019, Month.APRIL, 18),
                    "Germany"),
            Subscriber("Eric",
                    LocalDate.of(2018, Month.OCTOBER, 1),
                    "USA"),
            Subscriber("Albert",
                    LocalDate.of(2016, Month.OCTOBER, 12),
                    "USA")
    )
    val sequences = listOf(
            EmailSequence("Present book", listOf(
                    Email("Present book 1", "Here is the book...",
                            tags= listOf("book_explanation")),
                    Email("Present book 2", "Here is the book...",
                            tags= listOf("book_explanation")),
                    Email("Present book 3", "Here is the book...",
                            tags= listOf("book_explanation"))
            )),
            EmailSequence("Present course", listOf(
                    Email("Present course 1", "Here is the course...",
                            tags= listOf("course_explanation")),
                    Email("Present course 2", "Here is the course...",
                            tags= listOf("course_explanation")),
                    Email("Present course 3", "Here is the course...",
                            tags= listOf("course_explanation"))
            ))
    )
    ksession.insert(Email("Question to user",
            "Do you..."))
    ksession.insert(Email("Interesting topic A",
            "Do you..."))
    ksession.insert(Email("Interesting topic B",
            "Do you..."))
    ksession.insert(Email("Suggest book",
            "I wrote a book...",
            tags= listOf("book_offer")))
    ksession.insert(Email("Suggest course",
            "I wrote a course...",
            tags= listOf("course_offer")))
    ksession.insert(Email("Suggest consulting",
            "I offer consulting...",
            tags= listOf("consulting_offer")))
 
    ksession.setGlobal("day", dayToConsider)
 
    ksession.insert(products)
    persons.forEach {
        ksession.insert(it)
    }
    sequences.forEach {
        ksession.insert(it)
    }
}

Of course in a real application we would access some database or some form of storage to retrieve the data to be used to populate the session.

Global objects

In rules we will not only access elements which are part of the session but also global objects.
Global objects are inserted in the session using setGlobal. We have seen an example in loadDataIntoSession:

1
2
3
4
5
fun loadDataIntoSession(ksession: StatefulKnowledgeSession, dayToConsider: LocalDate) : EmailScheduler {
    ...
    ksession.setGlobal("day", dayToConsider)
    ...
}

In the rules we declare the globals:

01
02
03
04
05
06
07
08
09
10
package com.strumenta.funnellang
 
import com.strumenta.funnel.Email;
import com.strumenta.funnel.EmailSequence;
import com.strumenta.funnel.EmailScheduling
import com.strumenta.funnel.EmailScheduler;
import com.strumenta.funnel.Person
import java.time.LocalDate;
 
global LocalDate day;

At this point we can refer to these globals in all rules. In our example we use day value to know which day we are considering for the scheduling. Typically it would be tomorrow, as we would like to do the scheduling one day in advance. However for testing reasons we could use any day we want. Or we may want to use days in the future for simulation purposes.

Global should not be abused. Personally I like to use them to specify configuration parameters. Others prefer to insert this data into the session and this is the recommended approach. The reason why I use globals (carefully and rarely) is because I like to distinguish between the data I am working on (stored in the session) and the configuration (for that I use globals).

Writing the generic rules

Let’s now see the whole set of generic rules that we have written. By generic rules we mean rules that could be applied to all email schedulings we want to do. To complement these rules we may have others for specific products or topics we are promoting.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
package com.strumenta.funnellang
 
import com.strumenta.funnel.Email;
import com.strumenta.funnel.EmailSequence;
import com.strumenta.funnel.EmailScheduling
import com.strumenta.funnel.EmailSending;
import com.strumenta.funnel.Subscriber
import java.time.LocalDate;
import com.strumenta.funnel.Priority
 
global LocalDate day;
 
rule "Continue sequence"
   when
      sequence : EmailSequence ()
      subscriber : Subscriber ( isInSequence(sequence) )
 
   then
      EmailSending $sending = new EmailSending(sequence.next(subscriber.getActualEmailsReceived()), subscriber, day);
      EmailScheduling $scheduling = new EmailScheduling($sending, Priority.IMPORTANT, true);
      insert($scheduling);
end
 
rule "Start sequence"
   when
      sequence : EmailSequence ()
      subscriber : Subscriber ( !isInSequence(sequence) )
 
   then
      EmailSending $sending = new EmailSending(sequence.getFirst(), subscriber, day);
      EmailScheduling $scheduling = new EmailScheduling($sending, Priority.NORMAL);
      insert($scheduling);
end
 
rule "Prevent overloading"
   when
      scheduling : EmailScheduling(
            sending.subscriber.hasReceivedEmailsInLastDays(3, day),
            !blocked )
 
   then
      scheduling.setBlocked(true);
end
 
rule "Block on holidays"
   when
      scheduling : EmailScheduling( sending.subscriber.isOnHolidays(scheduling.sending.date), !blocked )
 
   then
      scheduling.setBlocked(true);
end
 
rule "Precedence to time sensitive emails"
   when
      scheduling1 : EmailScheduling( timeSensitive == true, !blocked )
      scheduling2 : EmailScheduling( this != scheduling1,
                !blocked,
                sending.subscriber == scheduling1.sending.subscriber,
                sending.date == scheduling1.sending.date,
                timeSensitive == false)
   then
      scheduling2.setBlocked(true);
end
 
rule "Precedence to higher priority emails"
  when
     scheduling1 : EmailScheduling( !blocked )
     scheduling2 : EmailScheduling( this != scheduling1,
               !blocked,
               sending.subscriber == scheduling1.sending.subscriber,
               sending.date == scheduling1.sending.date,
               timeSensitive == scheduling1.timeSensitive,
               priority < scheduling1.priority)
 
   then
      scheduling2.setBlocked(true);
end
 
rule "Limit to one email per day"
  when
     scheduling1 : EmailScheduling( blocked == false )
     scheduling2 : EmailScheduling( this != scheduling1,
               blocked == false,
               sending.subscriber == scheduling1.sending.subscriber,
               sending.date == scheduling1.sending.date,
               timeSensitive == scheduling1.timeSensitive,
               priority == scheduling1.priority,
               id > scheduling1.id)
 
   then
      scheduling2.setBlocked(true);
end
 
rule "Never resend same email"
  when
     scheduling : EmailScheduling( !blocked )
     subscriber : Subscriber( this == scheduling.sending.subscriber,
            hasReceived(scheduling.sending.email) )
   then
      scheduling.setBlocked(true);
end

Let’s examine all these rules, one by one:

  • Continue sequence: if someone started receiving an email sequence and he did not receive the last email yet, then he should get the next email in the sequence
  • Start sequence: if someone did not yet receive the first email of a sequence he should. Note that technically speaking this rule alone would cause everyone who has finished a sequence to immediately restart it. This does not happen because of the Never resend same email rule. However you could decide to rewrite this rule to explicitly forbidding someone who has already received a certain sequence to be re-inserted in it.
  • Prevent overloading: if someone has received an email in the last three days then we should block any email scheduling directed to that person
  • Block on holidays: if someone is on holidays we should not send emails to them
  • Precedence to time sensitive emails: given a pair of email schedulings directed to the same person on the same date, if only one of the two is time sensitive we should block the other
  • Precedence to higher priority emails: given a pair of email schedulings directed to the same person on the same date being both time sensitive or both not time sensitive, we should block the one with lower importance
  • Limit to one email per day: we should not schedule to send more than one email per day to the same person. If this happens we have to pick one somehow. We use the internal ID to discriminate between the two
  • Never resend same email: if someone has already received a certain email he should not receive it again in the future

Writing the rules specific to the book emails

Our marketing experts may want to write specific rules for specific products or topics. Let’s assume they want to create a set of emails to promote and sell a book. We could write these rules in a separate file, perhaps maintained by the marketing expert in charge of selling that book.

To write rules regarding a specific topic we will take advantage of tags, a mechanism that will give us a certain amount of flexibility. Let’s see the rules we can write:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.strumenta.funnellang
 
import com.strumenta.funnel.Subscriber;
import com.strumenta.funnel.EmailScheduling;
import java.time.DayOfWeek;
 
rule "Send book offer only after at least 3 book presentation emails"
   when
      subscriber : Subscriber (
          emailReceivedWithTag("book_explanation") < 3
      )
      scheduling : EmailScheduling(
        !blocked,
        sending.subscriber == subscriber,
        sending.email.tags contains "book_offer"
      )
   then
        scheduling.setBlocked(true);
end
 
rule "Block book offers on monday"
   when
      scheduling : EmailScheduling(
        !blocked,
        sending.date.dayOfWeek == DayOfWeek.MONDAY,
        sending.email.tags contains "book_offer"
      )
   then
        scheduling.setBlocked(true);
end
 
rule "Block book offers for people who bought"
   when
      subscriber : Subscriber (
          tags contains "book_bought"
      )
      scheduling : EmailScheduling(
        !blocked,
        sending.subscriber == subscriber,
        sending.email.tags contains "book_offer"
      )
   then
        scheduling.setBlocked(true);
end

Let’s examine our rules:

  • Send book offer only after at least 3 book presentation emails: we want to block any email selling the book if the subscriber did not receive at least three emails explaining the content of the book
  • Block book offers on monday: we want to block book offers to be sent on monday, for example because we have seen that subscribers are less inclined to buy on that day of the week
  • Block book offers for people who bought: we do not want to propose a deal on the book to subscribers who already bought it

Testing the business rules

There are different types of tests we may want to write to verify that our rules behave as expected. On one side of the spectrum we may want to have tests that verify complex scenarios and check for unexpected interactions between rules. These tests will run considering complex data sets and the whole set of business rules. On the other side of the spectrum we may want to write simple unit tests to verify single rules. We will see an example of these unit tests, but most of what we will see could be adapted to test the whole set of rules instead of single rules.

What do we want to do in our unit tests?

  1. We setup the knowledge base
  2. We want to load some data into the session
  3. We want to run the rule business engine, enabling just the one business rule we want to test
  4. We want to verify that the resulting email schedulings are the one expected

To satisfy point 1 we load all the files containing our rules and we verify there are no issues.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
private fun prepareKnowledgeBase(files: List<File>): InternalKnowledgeBase {
    val kbuilder = KnowledgeBuilderFactory.newKnowledgeBuilder()
 
    files.forEach { kbuilder.add(ResourceFactory.newFileResource(it), ResourceType.DRL) }
 
    val errors = kbuilder.errors
 
    if (errors.size > 0) {
        for (error in errors) {
            System.err.println(error)
        }
        throw IllegalArgumentException("Could not parse knowledge.")
    }
 
    val kbase = KnowledgeBaseFactory.newKnowledgeBase()
    kbase.addPackages(kbuilder.knowledgePackages)
 
    return kbase
}

How do we load data into the session? We do that by loading some default data and then giving the possibility to change this data a little bit in each test. In the following piece of code you will see that we can pass a function as the dataTransformer parameter. Such function can operate on the data before we load them into the session. This is our hook to tweak the data in each test.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
fun loadDataIntoSession(ksession: KieSession,
                        dayToConsider: LocalDate, dataTransformer: ((Subscriber, Email) -> Unit)? = null) {
 
    val amelie = Subscriber("Amelie",
            LocalDate.of(2019, Month.FEBRUARY, 1),
            "France")
    val bookSeqEmail1 = Email("Present book 1", "Here is the book...",
            tags= listOf("book_explanation"))
 
    val products = listOf(
            Product("My book", 20.0f),
            Product("Video course", 100.0f),
            Product("Consulting package", 500.0f)
    )
    val persons = listOf(amelie)
    val sequences = listOf(
            EmailSequence("Present book", listOf(
                    bookSeqEmail1,
                    Email("Present book 2", "Here is the book...",
                            tags= listOf("book_explanation")),
                    Email("Present book 3", "Here is the book...",
                            tags= listOf("book_explanation"))
            ))
    )
    dataTransformer?.invoke(amelie, bookSeqEmail1)
 
    ksession.insert(Email("Question to user",
            "Do you..."))
    ksession.insert(Email("Interesting topic A",
            "Do you..."))
    ksession.insert(Email("Interesting topic B",
            "Do you..."))
    ksession.insert(Email("Suggest book",
            "I wrote a book...",
            tags= listOf("book_offer")))
    ksession.insert(Email("Suggest course",
            "I wrote a course...",
            tags= listOf("course_offer")))
    ksession.insert(Email("Suggest consulting",
            "I offer consulting...",
            tags= listOf("consulting_offer")))
 
    ksession.setGlobal("day", dayToConsider)
 
    ksession.insert(products)
    persons.forEach {
        ksession.insert(it)
    }
    sequences.forEach {
        ksession.insert(it)
    }
}

We achieve point 3 by specifying a filter on the rules to be executed:

1
ksession.fireAllRules { match -> match.rule.name in rulesToKeep }

At this point we can simply check the results.

Once this infrastructure has been put in place the tests we will write will look like this:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
@test fun startSequencePositiveCase() {
    val schedulings = setupSessionAndFireRules(
            LocalDate.of(2019, Month.MARCH, 17), listOf("Start sequence"))
    assertEquals(1, schedulings.size)
    assertNotNull(schedulings.find {
        it.sending.email.title == "Present book 1"
                && it.sending.subscriber.name == "Amelie" })
}
 
@test fun startSequenceWhenFirstEmailReceived() {
    val schedulings = setupSessionAndFireRules(
            LocalDate.of(2019, Month.MARCH, 17),
            listOf("Start sequence")) { amelie, bookSeqEmail1 ->
        amelie.emailsReceived.add(
                EmailSending(bookSeqEmail1, amelie,
                        LocalDate.of(2018, Month.NOVEMBER, 12)))
    }
 
    assertEquals(0, schedulings.size)
}

In the first test we expect Amelie to receive the first email of a sequence, given she did not receive yet. In the second test instead we set in the session athat Amelie already received the first email of the sequence, so we expect it to not receive it again (no email schedulings expected at all).

This is the whole code of the test class:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
package com.strumenta.funnel
 
import org.drools.core.impl.InternalKnowledgeBase
import org.drools.core.impl.KnowledgeBaseFactory
import org.kie.api.io.ResourceType
import org.kie.api.runtime.KieSession
import org.kie.internal.builder.KnowledgeBuilderFactory
import org.kie.internal.io.ResourceFactory
import java.io.File
import java.time.LocalDate
import java.time.Month
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import org.junit.Test as test
 
class GenericRulesTest {
 
    private fun prepareKnowledgeBase(files: List<File>): InternalKnowledgeBase {
        val kbuilder = KnowledgeBuilderFactory.newKnowledgeBuilder()
 
        files.forEach { kbuilder.add(ResourceFactory.newFileResource(it), ResourceType.DRL) }
 
        val errors = kbuilder.errors
 
        if (errors.size > 0) {
            for (error in errors) {
                System.err.println(error)
            }
            throw IllegalArgumentException("Could not parse knowledge.")
        }
 
        val kbase = KnowledgeBaseFactory.newKnowledgeBase()
        kbase.addPackages(kbuilder.knowledgePackages)
 
        return kbase
    }
 
    fun loadDataIntoSession(ksession: KieSession,
                            dayToConsider: LocalDate, dataTransformer: ((Subscriber, Email) -> Unit)? = null) {
 
        val amelie = Subscriber("Amelie",
                LocalDate.of(2019, Month.FEBRUARY, 1),
                "France")
        val bookSeqEmail1 = Email("Present book 1", "Here is the book...",
                tags= listOf("book_explanation"))
 
        val products = listOf(
                Product("My book", 20.0f),
                Product("Video course", 100.0f),
                Product("Consulting package", 500.0f)
        )
        val persons = listOf(amelie)
        val sequences = listOf(
                EmailSequence("Present book", listOf(
                        bookSeqEmail1,
                        Email("Present book 2", "Here is the book...",
                                tags= listOf("book_explanation")),
                        Email("Present book 3", "Here is the book...",
                                tags= listOf("book_explanation"))
                ))
        )
        dataTransformer?.invoke(amelie, bookSeqEmail1)
 
        ksession.insert(Email("Question to user",
                "Do you..."))
        ksession.insert(Email("Interesting topic A",
                "Do you..."))
        ksession.insert(Email("Interesting topic B",
                "Do you..."))
        ksession.insert(Email("Suggest book",
                "I wrote a book...",
                tags= listOf("book_offer")))
        ksession.insert(Email("Suggest course",
                "I wrote a course...",
                tags= listOf("course_offer")))
        ksession.insert(Email("Suggest consulting",
                "I offer consulting...",
                tags= listOf("consulting_offer")))
 
        ksession.setGlobal("day", dayToConsider)
 
        ksession.insert(products)
        persons.forEach {
            ksession.insert(it)
        }
        sequences.forEach {
            ksession.insert(it)
        }
    }
 
    private fun setupSessionAndFireRules(dayToConsider: LocalDate, rulesToKeep: List<String>,
                                         dataTransformer: ((Subscriber, Email) -> Unit)? = null) : List<EmailScheduling> {
        val kbase = prepareKnowledgeBase(listOf(File("rules/generic.drl")))
        val ksession = kbase.newKieSession()
        loadDataIntoSession(ksession, dayToConsider, dataTransformer)
 
        ksession.fireAllRules { match -> match.rule.name in rulesToKeep }
 
        return ksession.selectScheduling(dayToConsider)
    }
 
    @test fun startSequencePositiveCase() {
        val schedulings = setupSessionAndFireRules(
                LocalDate.of(2019, Month.MARCH, 17), listOf("Start sequence"))
        assertEquals(1, schedulings.size)
        assertNotNull(schedulings.find {
            it.sending.email.title == "Present book 1"
                    && it.sending.subscriber.name == "Amelie" })
    }
 
    @test fun startSequenceWhenFirstEmailReceived() {
        val schedulings = setupSessionAndFireRules(
                LocalDate.of(2019, Month.MARCH, 17),
                listOf("Start sequence")) { amelie, bookSeqEmail1 ->
            amelie.emailsReceived.add(
                    EmailSending(bookSeqEmail1, amelie,
                            LocalDate.of(2018, Month.NOVEMBER, 12)))
        }
 
        assertEquals(0, schedulings.size)
    }
 
}

Conclusions

Marketers should be able to experiment and try out their strategies and ideas easily: for example, do they want to create a special offer just to be sent at 20 subscribers per day? Do they want to send special offers to subscribers in a certain country? Do they want to consider the birthday or the national holiday of a subscriber to send him a special message? Our domain experts, marketers in this case, should have a tool to pour these ideas into the system and see them applied. Thanks to business rules they could be able to implement most of them by themselves. Not having to go through developers or other “gate keepers” could mean having the freedom to experiment, to try things and in the end to make the business profit.

There are things to consider: giving the possibility to write business rules could not be enough. To make our domain experts confident in the rules they write we should give them the possibility to play with them and try them out in a safe environment: a testing or simulation mechanism should be put in place. In this way they could try things and see if they translated correctly into code the idea that they had in mind.

Of course business rules are much easier to write compared to typical code. This is the case because they have a predefined format. In this way we can pick an existing rule and tune a little bit. Still, it requires some training for the domain experts to get used to them. They need to develop the ability to formalize their thoughts and this could be easy or hard depending on their background. For example, for marketers it could be doable while for other professionals it could require more exercise. What we could do to simplify their life and make domain experts more productive is to put a Domain Specific Language in front of our business rules.

By creating a simple DSL we could make things easier for our marketers. This DSL would permit to manipulate the domain model we have seen (subscribers, emails, etc) and perform the two actions marketers are interested into: scheduling and blocking emails. We could provide a simple editor, with auto-completion and error checking, and integrate a testing and simulation environment in it. In this scenario marketers would be fully independent and able to design and verify their rules quickly and with very limited supported needed.

Acknowledgments

Mario Fusco (a Java champion) and Luca Molteni, both working on Drools at RedHat, were so very kind to review the article and suggest significant improvements. I am extremely thankful to them.

Thank you!

Published on Java Code Geeks with permission by Federico Tomassetti, partner at our JCG program. See the original article here: A complete tutorial on the Drools business rule engine

Opinions expressed by Java Code Geeks contributors are their own.

Federico Tomassetti

Federico has a PhD in Polyglot Software Development. He is fascinated by all forms of software development with a focus on Model-Driven Development and Domain Specific Languages.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button