In this blog, we will explore the power behind the BlockingState abstraction inside Kill Bill.
Overview:
The BlockingState events were introduced in version 0.6.x, so while they are certainly not new, they were mostly used for internal purpose inside Kill Bill. We recently enriched our apis to make them fully available to plugins.
Let’s take a simple use case where a user subscribes to a monthly recurring Plan on January 1st 2016 (and catalog was configured to bill in advance). Each 1st of the month, the invoicing system takes as an input the subscription state (subscription events) and the user is billed for a full month. This works quite well, but how can we address the following questions:
- Entitlement: The ability to separate what the user is billed for and the service that she receives (e.g if user cancels mid month, we may want to terminate the service right away and keep the billing until the end of the month)
- Overdue: The user does not pay its bills. We want to allow to introduce some states that will define a gradual degrade of the service until the user pays
- Pause/Resume: Ability to arbitrarily pause the billing (and/or its associated entitlement) for a period of time.
The BlockingState events allow to introduce arbitrary states associated to subscriptions, and allow to modify the behavior of the system to block billing or entitlement periods, solving the original use cases we had and more…
Internals:
Before looking at their usage, let’s review what they contain:
- stateName : A string that is to be interpreted by the service inserting such event
- blockingStateType : An enum (ACCOUNT, BUNDLE or SUBSCRIPTION) that defines the scope on what it applies to
- blockedId : The ID of the matching account, bundle or subscription object
- effectiveDate : The date at which it becomes effective
- service : The name of the service inserting this event
- isBlockEntitlement : A boolean indicating whether the entitlement should be blocked (If blockingStateType=ACCOUNT, this is for all subscriptions attached to this account, if blockingStateType=BUNDLE, this is for all subscriptions attached to this bundle, …)
- isBlockBilling: A boolean indicating whether the billing should be blocked (works in a similar fashion as isBlockEntitlement)
Since BlockingState includes both flags that will modify the behavior of the system and states that are opaque to the system, they can be used as a number of ways (with a mix of those options):
- Ability to manipulate the billing event stream during invoice creation: Each time the system computes an invoice associated with an account (either through a future notification associated with a recurring subscription or because of an explicit api call to trigger such invoice), the system will pull all the billing events through a submodule called Junction. Junction, is the conduit which knows how to pull all events associated with all subscriptions for this account, and which also knows how to insert additional events (the subset of BlockingState events for which isBlockBilling transitions from true <-> false). Junction knows how to computes the full event stream required by the the Invoice submodule.
- Ability to control the entitlement state (ACTIVE, BLOCKED, CANCELLED) for each subscription: The entitlement (ability for user of a subscription to receive the associated service) for a specific subscription is computed by looking at the various BlockingState (more specifically their isBlockEntitlement flag) for that subscription, and its associated bundle/account. For example let’s assume a subscription S with its matching bundle B and account A:
- T0: isBlockEntitlement=true for B => entitlement(S) = BLOCKED
- T1: isBlockEntitlement=false for B => entitlement(S) = ACTIVE
- T2: isBlockEntitlement=true for S => entitlement(S) = BLOCKED
- Ability to insert non intrusive events (user event) that can be interpreted by that same service (plugin) to perform additional actions. An example would be for instance to notify a customer before a subscription renews: The plugin would insert a BlockingState event (stateName=’RENEW’, service=’SVC’,…), let’s say 10 days before the renewal date, and then that event becomes effective the plugin is notified, and understand this is one of its event, and knows how to interpret the state to perform the required action. Any arbitrary state and associated action could be taken for any chosen time.
Semantics:
There are 2 dimensions associated with these events, and because both the flags isBlockEntitlement and isBlockBilling modify the state of the system, we need to define the semantics:
- service: BlockingState associated to different services are completely independent for each other
- blockingStateType: BlockingState with different types (ACCOUNT, BUNDLE or SUBSCRIPTION) are also treated independently. That is there is no hierarchy across types
Let’s look at some examples for which we a subscription S with its matching bundle B and account A:
- T0: {service=’SVC1′, blockingStateType=SUBSCRIPTION, isBlockEntitlement=true}, T1: {service=’SVC1′, blockingStateType=SUBSCRIPTION, isBlockEntitlement=false} => At T1, S is ‘ACTIVE‘
- T0: {service=’SVC1′, blockingStateType=SUBSCRIPTION, isBlockEntitlement=true}, T1: {service=’SVC2′, blockingStateType=SUBSCRIPTION, isBlockEntitlement=false} => At T1, S is ‘BLOCKED‘ because second event is for a different service so does not unblock first event
- T0: {service=’SVC1′, blockingStateType=BUNDLE, isBlockEntitlement=true}, T1: {service=’SVC1′, blockingStateType=SUBSCRIPTION, isBlockEntitlement=false} => At T1, S is ‘BLOCKED‘ because although second event is for the same service, it applies at the SUBSCRIPTION level and does not impact the first event at the BUNDLE level
Use Cases:
There are already two use cases internal to Kill Bill:
- The Entitlement module uses those BlockingState to implement pause, resume, cancel operations by using a service=’entitlement-service‘, stateName=ENT_BLOCKED|ENT_CLEAR|ENT_CANCELLED and setting the isBlockEntitlement=true|false
- The Overdue module also makes use the the api to insert BlockingState that were defined in the overdue xml configuration.
The Overdue module uses both the isBlockEntitlement and isBlockBilling flags to implement the actions specified in the XML, but does not interpret the value associated with the stateName.
For example, an overdue xml configuration could define two overdue states such as a WARNING state where no action is taken by the system and a CANCELLED state where subscriptions should become cancelled. The first BlockingState would have a stateName=’WARNING’ and both isBlockEntitlement and isBlockBilling would be set to false. The second BlockingState would have a stateName=’CANCELLED’ and both isBlockEntitlement and isBlockBilling would be set to true. It is important to understand that the overdue state would be visible to the outside world but would not be interpreted by the system as such.
In addition to those, plugins can now manipulate BlockingState associated to accounts, bundles, subscription. They can interpret them as they wish, and tweak the isBlockEntitlement and isBlockBilling flags to obtain desired results. For example one could provide arbitrary 10 days discount logic for a specific subscription each time there is a blue moon:
- At time=’T_blue_moon’, insert a BlockingState=’BLUE_MOON‘ with isBlockBilling=true
- At time=’T_blue moon+10_days’, insert of BlockingState=’BLUE_MOON_DISCOUNT_END‘ with isBlockBilling=false
The invoicing code would not invoice that subscription for the time period [T_blue_moon,T_blue_moon+10_days)
Conclusion:
We have seen an overview of what BlockingState are, and looked at their purpose and semantics. We have presented two internal use cases and a somewhat less serious example to illustrate the flexibility behind them. While manipulation of BlockingState may seem like a rather advanced topic, the reality is that we keep seeing new use cases of our system pretty much every day and those are a validation for the flexibility offered by Kill Bill.