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:

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:

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.

Comments

comments powered by Disqus