Corporate Wellness Programs, programs sponsored by employers that reward employees for activities like exercising, eating nutritiously, and engaging with their doctor, have demonstrable positive impacts on employee health. At Rally, we’ve developed a system to more efficiently configure and manage these programs, reducing administrative costs significantly. Importantly, in the case where imperfect employee data leads to dissemination of erroneous rewards, our system provides a mechanism to “roll-back” the state of an employee’s program and recoup unspent rewards on behalf of the employer. This document describes the salient aspects of our system as they pertain to roll-back and the roll-back algorithm which guarantees that the state of employee’s program is consistent after a roll-back event. A complete working example is provided here.

This document is organized as follows:

  • Section 1 describes corporate wellness programs in more detail.
  • Section 2 gives a high level overview of Drools.
  • Section 3 describes the Rally Incentives Engine, the component of our system responsible for determining when an employee is eligible for a reward.
  • Section 4 introduces an example incentives plan and walks the reader through how the Incentives Engine would operate for an employee.
  • Section 5 builds on the previous example to demonstrate the roll-back algorithm. A proof of correctness for the roll-back algorithm is provided in the appendix.

Corporate Wellness Programs

A Corporate Wellness Program (CWP), consists of a number of activities that an employee may undertake to redeem rewards. Examples of activities include:

  • Complete a Health Assessment Survey (1)
  • Get a physical examination this year
  • Obtain a body fat percentage of 27
  • Connect with a nutritionist
  • Participate in a company-sponsored fitness event

Example rewards include:

  • Deposit to employee’s Health Savings Account (HSA) or Flexible Spending Account (FSA)
  • Gift Card sent to employee

CWPs often impose contextual restrictions on when a reward can be disseminated for the completion of its associated activity. For example, referring to the activities listed above, the CWP may stipulate that an employee can earn $100 (as a reduction on his health insurance premiums or as a deposit into his HSA or FSA) for a physical examination only after he completes the Health Assessment Survey. As another example, there may be a requirement that in order to earn a $200 benefit for connecting with a nutritionist, the employee must have failed to achieve a body fat percentage of 27. Finally, it’s often the case that the employer imposes a cap on the total dollar value in benefits that an employee may earn in a given period, the value of which is less than the total reward amount in the CWP. To illustrate, the five activities listed above may each be linked to a $100 reward totaling $500, but the cap on the CWP may be $400.

Drools

Drools is a Business Rules Management System with a wide range of features and functionality of which we use a small subset. (2) Specifically, we leverage the core business rules engine and the Drools Rule Language. For simplicity, we will refer to these two components collectively as “Drools”.

Drools is invoked with a collection of objects, K, called a knowledge base, representing the state of some system, and a set of rules, R, that may mutate the knowledge base in various ways iff their preconditions are satisfied. These mutations may in turn lead to the execution of more rules which will continue the mutation process until no more rules can fire. For example, K may state that the completion of a physical examination is tied to a $100 reward and that the employee, Donald Fagen, received such an examination on 2/1/2017. If R contains the following rules:

  • IF an employee completes an activity linked to a reward THEN deposit the associated reward into the employee’s HSA or FSA
  • IF an employee receives a deposit into his HSA or FSA THEN send a notification to the employee

Then calling Drools with arguments (K,R) will update K with two additional pieces of information. Namely, that $100 dollars has been deposited into Donald Fagen’s HAS or FSA and that Donald Fagen has been notified of this deposit via email.

In general, Drools allows rules to mutate the knowledge base in a number of ways:

  • A rule may insert one or more new objects into the knowledge base
  • A rule may remove one or more existing objects from the knowledge base
  • A rule may change (update) the state of one or more existing objects in the knowledge base

In our system, we impose two restrictions on the behavior of rules:

  • A rule may not mutate an existing object
  • A rule may not retract (remove) an object from the knowledge base

Said another way, the only allowable operation of a rule on a knowledge base is the insertion of a new object.

These restrictions enable the development of a simple, provably correct roll-back algorithm that does not require the specification of any additional business logic. Before describing this algorithm in detail, we outline how Drools (as described above) fits within the Rally Incentives Engine; we introduce some additional definitions and notation to ease exposition; and we provide a working example that aims to highlight some of the complexity associated with rolling back inferences in a sufficiently rich domain.

Rally Incentives Engine

The Rally incentive engine invokes drools with a number of facts, represented as objects, about an employee. Facts can be roughly divided into three types: background facts, which are statements about the composition of an employee’s wellness plan; events, which are actions undertaken by the employee; and inferences, which are the consequences of rules in the engine. Like inferences, background facts and events are never retracted or modified internal to Drools.

Examples of background facts are:

  • There exists a reward R in the incentives plan
  • There exists an activity A in the incentives plan
  • Activity A is linked to reward R

Examples of events are:

  • Employee E completed his Health Assessment Survey on 1/1/2016
  • Employee E achieved a body fat percentage of 20% on 2/1/2016

An example inference is:

  • Employee E is eligible for reward R, which is a consequence of the rule “If an employee completes an activity, A, linked to a reward, R, the employee is eligible for R”

Facts can be expressed in predicate logic syntax and a similar notation is adopted moving forward. For example:

  • “There exists an activity ‘Fitness Challenge’’” is written as Activity(‘Fitness Challenge’)
  • “There exists an employee, Donald Fagen” is written as Employee(‘Donald Fagen’)
  • “Donald Fagen completed the fitness challenge on 1/5/2016” is written as Completed(Employee(‘Donald Fagen’), Activity(‘Fitness Challenge), 1/5/2016)

When string literals and other properties like dates are omitted as in, Completed(Employee, Activity), it should be understood that there exist facts of type Completed that involve Employees and Activities.

Background facts are stored in a database with an associated plan ID representing the wellness plan to which they belong. Events arrive at the Incentives Engine asynchronously from a number of external sources and are persisted with a time of receipt. When the incentives engine is invoked for an employee, relevant background facts and events are loaded into working memory at which point the following procedure is invoked:

Procedure 1

Input: Background facts, events
Output: Inferences
  1. Background facts are bulk inserted into a Drools session
  2. Events are ordered by time of receipt
  3. For each event, e, in order
    a. e is inserted into the session
    b. the drools inference engine is run
  4. The complete set of inferences generated by all events is returned

The resultant inferences are used to drive messaging to employees and to notify downstream systems to disseminate payments.

Working Example

The following describes a simple incentives plan and how such a plan is modeled in the incentives engine.

The plan

“An employee may earn a $500 Health Savings Account deposit for achieving a body fat percentage of 27 or below measured by a physician. If the employee obtains a measurement greater than 27, he has the option to connect with a nutritionist to earn the $500 deposit or to lower his body fat percentage to the required level and have it remeasured.”

The background facts describing the plan are:

  • Let bodyFat = Activity(“Obtain a body fat target”)
  • Let nutritionist = Activity(“Connect with a nutritionist)
  • Let reward = Reward(“$500”)
  • Let target = Target(27%)
  • Let choice = Choice(bodyFat, nutritionist)
  • IsBiometricActivity(bodyFat, target)
  • RequiresFailureOf(nutritionist, bodyFat)
  • Pays(choice, reward)

Choice(Activity, Activity) represents a special kind of activity that an employee may complete by completing either constituent. IsBiometricActivity(Activity, Target) conveys that an activity has an associated biometric target. RequiresFailureOf(Activity, Activity) states that even if the first activity is successfully achieved, that result will be ignored until the second activity has been failed. Finally, Pays(Activity, Reward) associates a reward with an activity. Given these background facts and a set of events for employee e:

  • BodyFatReading(e, Measurement(30), 2/1/2017)
  • Completed(e, nutritionist, 2/10/2017)

Running Procedure 1 will produce the following inferences:

  • Failed(e, bodyFat)
  • Unlocked(e, nutritionist)
  • Eligible(e, reward)

Failed(e, bodyFat) is inferred because the employee’s body fat percentage (30) is greater than the minimum acceptable value of 27. Unlocked(e, nutritionist) is a consequence of Failed(e, bodyFat) and RequiresFailureOf(nutritionist, bodyFat). The Incentives Engine requires that an activity be Unlocked before its associated reward can be paid. Finally, Eligible(e, reward) is inferred because the user has Completed the Unlocked nutritionist activity, which is a constituent of the choice activity, and the choice activity pays a reward.

In the live system, the Incentives Engine transmits Eligible facts to external systems responsible for depositing funds into an employee’s account. It’s sometimes the case, due to bad event data, that an Eligible fact is erroneously sent. For example, in the scenario above, it may have been the case that the employee did not have his body fat percentage measured. Perhaps this event was generated due to some clerical error. Whatever the case, once it’s determined that no such reading took place, there are certain inferred facts that may no longer be true. For instance, in our example, it’s no longer true that the employee failed the body fat activity since he never got a measurement to begin with. Consequently, it’s no longer true that he has unlocked the nutritionist activity. Finally, and most importantly, it’s no longer the case that he is eligible for a payment.

Roll-back

The rollback algorithm requires no modification to the underlying business rules. Generally, the algorithm invokes Procedure1 as a subroutine with different subsets of events to determine what inferences need to be rolled back.

Continuing with the working example above, let: reading = BodyFatReading(e, Measurement(30))

if it’s determined that reading is an error, a special event is sent to the Incentives Engine, Nullify(e,reading), and the ordered event set for the employee is updated:

  • BodyFatReading(e, Measurement(30))
  • Completed(e, nutritionist)
  • Nullify(e,reading)

In this case, the inferences that should be rolled back can be identified by calling Procedure1 twice with two different sets of events, C and C-, and then taking the difference of the resultant inference sets, Inf(C) and Inf(C-).

C is constructed by taking all events, k, prior to the last Nullify, n, such that:

  • k is not of type Nullify
  • There does not exist an event, Nullify(k), before n

which results in

C = {BodyFatReading(e, Measurement(30)), Completed(e, nutritionist)} C- is constructed by taking all events, k, prior to the last nullify, n, such that

  • k is not of type Nullify
  • There does not exist an event, Nullify(k), before n
  • k is not the event nullified by n

Which yields:

C- = {Completed(e, nutritionist)}

Calling Procedure 1 on C produces the set of inferences, Inf(C), from the working example:

  • Failed(e, bodyFat)
  • Unlocked(e, nutritionist)
  • Eligible(e, reward)

Calling Procedure 1 on C- returns no inferences (Inf(C-) = {}). To see why, consider that the employee has no longer Failed to obtain a body fat percentage <= 27. It follows that the nutritionist activity is not Unlocked. As a result, the fact that the employee has connected with a nutritionist is ignored and he is not Eligible for payment.

Taking the set difference, Inf(C) - Inf(C-) results in Inf(C) indicating that all previous inferences need to be rolled back.

In situations where there are multiple Nullify events in an employee’s event set, the above procedure is executed for each Nullify in order of time of receipt. In this way, each rolled back inference can be tied to the Nullify directly responsible for its invalidation.

A proof of correctness for the rollback algorithm is provided in the appendix.

Conclusion

This document describes an event processing system implemented with Drools and Scala applied to Corporate Wellness Program management. In the case of erroneous events, the system provides a mechanism for rolling back associated inferences to ensure an employee’s program is consistent. The simplicity and correctness of our roll-back algorithm hinge on the limited extent to which “state” can be mutated in our system. Specifically, we only allow the insertion of new objects internal to Drools. As such, our algorithm serves as yet another example of how immutability leads to simple, clean solutions that are easy to implement and reason about.

Appendix

Correctness of the roll-back algorithm

Consider an ordered event set consisting of events E = e1, …, en, some of which may be Nullify events.

Construct a set, C, such that c ∈ C iff

  • c ∈ E
  • c is not of type Nullify

Construct a set C- such that c is in C- iff

  • c ∈ E
  • c is not of type Nullify
  • ¬∃ Nullify(c) in E

C- represents all the events that are currently known to be correct.

Note that C- is always a subset of C since C- is constructed with one additional restriction. So, C can be written as C- + D where D is the set of nullified (invalid) events.

Let Inf(C) and Inf(C-) be the inferences generated by running Drools on C and C- respectively

Note that Inf(C-) is always a subset of Inf(C) = Inf(C- + D). This follows because we restrict Drools to only insert (never modify or retract) facts.

Claim: Inf(C) - Inf(C-) is exactly the set of inferences that are invalid

Proof:

Assume there was an inference j that should have been invalidated but was not.

This means that j is in both Inf(C-) and Inf(C- + D)

But if j is in Inf(C-), deducing j did not require any event in D so j must be valid.

Assume there was an inference j that is valid but was invalidated.

This means that j is not in Inf(C-) but is in Inf(C- + D)

But if j is not in Inf(C-) and in Inf(C- + D), j is based on at least one invalid event so cannot be valid

Notes

(1) A Health Assessment Survey is used to gather information about an employee’s health risks and quality of life (2) One specific component of Drools, Drools Fusion, that we do not make use of deserves to be mentioned in this context because it deals with event processing. We don’t use drools fusion for two reasons: first, it doesn’t explicitly support the ability to “roll-back” that state of the system in the case of bad event data and second, it requires the use of more Drools specific machinery which results in a higher learning curve for people not familiar with the Drools ecosystem.

Join the conversation on Reddit!