Building a multi-step customer journey using finite state machines and Django REST Framework
Bungalow recently launched a new roommate matching experience to strengthen our foundations in community and personalizing the home-matching experience. In a collaborative effort across our engineering and product teams, we’ve recently shipped a novel experience for incoming Bungalow residents that encompasses the steps from submitting an application to signing the lease for their new home:
- submitting an application
- getting pre-approved
- completing their Bungalow profile
- shopping for a home
- doing a meet & greet with their potential roommates
- signing a lease
This multi-step customer journey acts as the single source of truth of where the applicant is in their pre-lease process — their status indicates what they’ve already accomplished, and what’s up next to secure their Bungalow home.
In this article, we’ll discuss how we built this new application flow from a backend perspective, and how we implemented control-flow and traceability for an applicant’s experience.
Technical requirements
From the customer journey outlined above, each step is dependent on the successful completion of the last.
For example, Bungalow can’t pre-approve an application if it hasn’t been submitted, and applicants aren’t going to be sent a lease without passed a meet and greet with the roommates in the home.
To engineering, following this sequence is important because certain screens/views can’t be successfully rendered without valid data that was captured in the previous stage.
Knowing exactly where leads are in the demand funnel also allows us to provide context for our sales teams to follow up with them, and analyze drop-off points.
Basic implementation
We want to keep track of an application’s status
, and update it whenever we know they've completed a stage in our process.
We implemented a status
field on our application model, which is a choice field for each of the possible stages to be completed in our customer journey.
A naive solution for making status updates would be to explicitly update the status when a certain button is clicked or API endpoint is hit. This could live in-line in several points in the code. However, this can quickly get messy if we add new statuses and transitions later on.
We wanted to implement a system that allows for some abstraction, code reuse, and extensibility down the road — a device commonly used in computing for such use cases is a finite state machine.
Finite state machines
A finite state machine (FSM) is a device that stores the status of something at any given time and can operate on an input to change it. When the status is changed, it can cause an action to take place.
An FSM lets us break down a larger flow into independent, smaller flows that are linked by transition events. There are several key benefits:
- Abstracting state/transition logic allows for better code reuse and maintainability
Abstracting the transitions to the calling code makes it easy to make changes to our FSM down the line. We’re able to make updates to inputs & outputs in a single place, rather than checking several different classes and functions. - Defining a finite number of states and transitions brings ease to debugging and extensibility
Explicitly defining valid status transitions in one place gives us a single source of truth for state flows (if an incorrect transition happens, we know it’s because the state machine is wrong). - Side effects from transitions are more easily implemented
With a state machine, we can easily trigger actions for a given status change in a couple of lines of code, rather than in-line for every single transition.
In an FSM, we have a set of inputs that each cause a certain change in state. These inputs are a five-element tuple:
- a set of states (Q)
- an input alphabet (Σ)
- a set of transition functions (δ)
- the starting state (q0)
- a set of accepting states (F)
Implementing an FSM for our application flow
Designing the state machine
Q: a finite set of states
For us, this is each of the possible application statuses (initialized earlier in our models.py
file): open
, submitted
, preapproved
, profile_completed
, room_selected
and approved
Σ: an input alphabet
In a loose way, our inputs are a series of requests from our frontend. As such, our pseudo-alphabet could look something like a set of:
POST
to thesubmit-application
endpointPOST
to theapprove-application
endpointPUT
to theupdate-profile
endpoint
δ: the set of transition functions
From certain requests in our input alphabet, we’ll update the application’s status and trigger some side effects.
q₀: the starting state
An application always needs to be submitted first, so we’ll start from open
.
F: the set of accepting states
This isn’t applicable for us, since not all leads might complete the whole flow. Any state will be a valid ending state, but we’re hoping that it’ll be approved
! 🤞
Configuring the state machine in a reusable function
To maximize reusability and readability, we registered our transitions in a single source of truth that is abstracted from the calling code. So, when an element in our input alphabet is triggered, we can call a function that will determine and execute the transition that needs to happen based on the current status.
Hooking in our FSM transitions to our inputs
Our input alphabet is a request to our API endpoints. Let’s illustrate this example with an applicant who is selecting a home to enter a meet & greet with the roommates. This is the transition from profile_completed
to room_selected
.
We wanted to store a record of when an applicant selects a room, so we created a model that looks a little something like this:
To make this transition happen from an API endpoint, we implemented a serializer for the RoomSelection
where we overrode the behaviour for the create
method to call the FSM when a POST
request is made.
Finally, we exposed a POST
endpoint to the frontend as an interface to our state machine.
Now, a request to our room selection endpoint:
- Validates that we’re in a good state (the status is what we expect)
- Creates a record that a room was selected (a
RoomSelection
object) - Triggers a transition function: given our status is
profile_completed
, callingtransition(application)
references the FSM to update the status toroom_selected
And there we have it! In this example, we’re processing an element in our input alphabet and triggering a pre-defined transition, which switches to a different state.
Extensibility of an FSM in our Django app
A finite state machine is a great pattern to implement in a Django app given its ease of extensibility for future use cases. We can incorporate a new stage in the customer journey simply by registering new states and transitions.
Here are a few more things an FSM enables us to do with optimal code reuse:
- Adding features or A/B tests that re-order the customer journey (we’d just need to change some values in the
transitions
dictionary) - Deciding to transition between two statuses based on the value of a peripheral piece of data (ex. moving an applicant forward or backward in the flow, based on their meet and greet feedback)
- Performing a side effect on each transition (i.e. writing log messages or sending notifications)
Conclusions
Using a finite state machine and Django Rest Framework, our engineering team was able to build a web experience that makes the state of an application exceptionally clear to both our applicants and our internal staff.
Most importantly, the extensibility of this design pattern will allow developers to easily build on the existing experience down the line.