Long Running Business Logic in Plain Old Code Part 2
In the previous post, we went through the problem domain that we would be implementing - a loyalty system. We also looked at implementing various Temporal components for our problem domain - workers, workflows and activities.
In this post we’ll look at how we can use signal methods and query methods to implement the “earning points” portion of the loyalty system.
Initial Membership Tier
First piece of business logic we need to implement is setting the member’s initial membership tier.
Enum class for the tiers:
enum class Tier(val min: Long, val max: Long, val multiplier: Double) {
MEMBER(0, 199, 1.0),
SILVER(200, 899, 1.5),
GOLD(900, 3499, 2.0),
PLATINUM(3500, Long.MAX_VALUE, 3.0)
}
// Helper function for calculating a member's tier
fun determineTier(pointsAccumulatedInPeriod: Long): Tier {
Tier.values().forEach {
if (pointsAccumulatedInPeriod in it.min..it.max) {
return it
}
}
throw Exception("Invalid points value $pointsAccumulatedInPeriod")
}
Notes:
multiplier
- when a member in this tier performs a transaction, the system will multiply the points earned by this multiplier.
Lets update the workflow method to give the member an initial tier when first enrolling in the system:
class LoyaltyWorkflowImpl : LoyaltyWorkflow {
private var tier: Tier = Tier.MEMBER
...
}
Earning Points
The member needs to perform transactions to earn points. The loyalty system needs to be “informed” each time the member performs a transaction. To do this we need to create a signal method. Signal methods allow workflows to be notified of events in the outside world.
Signal Method
First we need to add the signal method to the workflow interface:
@WorkflowInterface
interface LoyaltyWorkflow {
...
@SignalMethod
fun postTransaction(value: Long)
}
Implementing the Signal Method
class LoyaltyWorkflowImpl : LoyaltyWorkflow {
private var pointsThisPeriod: Long = 0
private var redeemablePoints: Long = 0
private var lastTransactionDateInMillis: Long = 0
override fun postTransaction(value: Long) {
val newPoints = (value.toDouble() * tier.multiplier).toLong()
this.redeemablePoints += newPoints
this.pointsThisPeriod += newPoints
this.lastTransactionDateInMillis = Workflow.currentTimeMillis()
}
}
Notes:
pointsThisPeriod
- points accumulated during the current 6 month period - in part 3 we will use this to determine whether the customer has done enough transactions to maintain his membership tier.redeemablePoints
- points that have not expired or been redeemed. This value will be purged when the the points expire.lastTransactionDateInMillis
- date of the last transaction. We will use this later in part 3 to determine whether we should purge the member’sredeemablePoints
for not performing any transactions in the last 3 months.
Querying Points Accumulated
We want to allow external systems to query the number of points that the member has that are redeemable.
We do this in Temporal by creating a query method.
Update the workflow interface: redeemablePoints()
@WorkflowInterface
interface LoyaltyWorkflow {
...
@QueryMethod
fun redeemablePoints(): Long
}
Implementing: redeemablePoints()
class LoyaltyWorkflowImpl : LoyaltyWorkflow {
...
override fun redeemablePoints(): Long {
return this.redeemablePoints
}
}
Keeping the Customer Enrolled
At the moment the customer is only enrolled in the loyalty programme for a short period of time because there is nothing stopping the workflow method from exiting.
Just to allow us to test the signal and query methods that we just created lets add a sleep
call so that the customer is at least enrolled in the loyalty programme for 15 minutes.
class LoyaltyWorkflowImpl : LoyaltyWorkflow {
override fun enroll() {
...
Workflow.sleep(Duration.ofMinutes(15))
...
}
}
Using the CLI to invoke Signal and Query Methods
Now that we have the signal and query methods in place lets try invoking them.
Start a Workflow
First let’s start a workflow instance. This time we’ll give the workflow a workflow_id, customer:bob
, so that it’s easier to lookup the workflow instance that we want to signal and query.
$ docker run --network=host --rm temporalio/tctl:0.21.1 workflow start --workflow_id "customer:bob" --tasklist LoyaltyTaskList --workflow_type LoyaltyWorkflow_enroll --execution_timeout 400000000 --input '{"id": "71659d9e-51c0-4f59-9b4c-74436402ed14 ", "email": "bob@example.com"}'
Started Workflow Id: customer:bob, run Id: b1515c67-2a65-43e3-8c5f-fe67207fa961
Updating the workflow when Bob does a transaction
$ docker run --network=host --rm temporalio/tctl:0.21.1 workflow signal --workflow_id "customer:bob" --name "LoyaltyWorkflow_postTransaction" --input "99"
Signal workflow succeeded.
Checking points balance after the transaction
$ docker run --network=host --rm temporalio/tctl:0.21.1 workflow query --workflow_id "customer:bob" --query_type "LoyaltyWorkflow_redeemablePoints"
Query result as JSON:
99
Next time
In the next part in this series we’ll implement the “loyalty loop” - a loop that will iterate each time the “tier” or “points validity” of the customer should be re-evaluated.