Solving spaghetti code with a state machine

Dinesh Appavoo
Affirm Tech Blog
Published in
5 min readFeb 22, 2021

--

Introduction

Affirm processes hundreds of millions of requests every day. We use a server-driven UI approach to change the experience of our customers dynamically. Server-driven UI helps to improve performance, user engagement, and user experience.

Duplicated Spaghetti Code

At Affirm, we have a number of complex flows to guide a user through their browsing and purchasing experience — signing in or up, prequalifying, checking out, repaying their loan, and more.

Building these experiences led to a few problems:

  • Complex client-side business logic: Clients held a large portion of the business logic, including complex orchestration, state, and API management.
  • Duplication: In addition to being complex, the logic was duplicated across our three platforms — Web, iOS, and Android — leading to increased development effort to release simultaneously across platforms. Further, certain logic was fragmented between the frontend and backend.

Server Driven UI

To solve these problems, we began investigating server-driven UI, the concept of presenting user interfaces based on the response from the backend, with UI clients — Web, iOS, and Android — having minimal business logic.

There are different approaches to implement server-driven UI. The main approaches include:

  • Component Driven UI — Server indicates which components to render and with what dynamic content.
  • Prebuilt Screens — The server indicates which screen to render and the dynamic data to go with it.

There are pros and cons for both, with component-driven generally being a bit more heavyweight and requiring a more robust DSL, though with the benefit of enabling more dynamic configuration without a deploy.

As a lightweight starting point, we chose prebuilt screens to get started in the server-driven UI world and align with our presentation-oriented APIs philosophy. When a user starts a flow or submits a page in the flow, we return to the frontend with the screen to render and data to hydrate the screen.

code-block(1): Sample API response

Flow Framework

Now that we’ve pushed out business logic to the backend, we need to handle user state and side effects in a sane way. To do this, we’ve built a state machine and related ecosystem that we call the Flow Framework.

State Machines Explained

A state machine is a device for storing the status or “state” of an entity at a given time and managing the changes between those states as inputs come in. While used in various applications, we’ve opted the state machine to model our user experiences — the state machine maps states to user-facing screens.

Our flow state machine framework is comprised of a few main components

  • Steps, which model the user-facing screens of a particular flow.
  • Events, which map to user actions on those screens their corresponding APIs
  • Transitions, which determine the destination step to go to based on the current step & event
  • Hooks, which allow for adding additional business logic at various stages of a transition’s lifecycle.

In addition to the basic mechanics of a state machine, we also provide a robust data layer that allows engineers to store additional data about the flow across API requests, freeing the frontend from that responsibility and enabling restart ability.

diagram (1): Flow state machine

In the example flow state machine shown in the diagram(1),

  1. The flow initially starts with step1
  2. HTTP /api1 event transition the state from step1 to step2
  3. Once the flow state reaches step2, hook1 is triggered
  4. HTTP /api2 event triggers the hook2, and also transition the state from step2 to step3

For simplicity, a sequence of step transitions are shown, but there could be many possible step transitions from one step.

Example: Checkout Flow

Let’s consider the checkout flow example, and see how the screens are dynamically changed. The flow has four screens and four associated steps.

diagram (2): Client presentation components for identity & checkout steps/screens

When the user checks out using Affirm, here is how the flow sequence looks,

diagram (3): user identity/checkout sequence diagram

As you can see in the checkout sequence diagram(3) above, the user did not see the split pay & interest bearing loan components[screen-3] but just the interest bearing loan components, although clients built the screen-2. Based on the user credit checks, the backend will decide to show split pay & interest bearing or an interest-bearing loan alone to the user with the info collected in identity.

Benefits of Server Driven UI & Flow Framework

  • UI clients hold minimal business logic and focus more exclusively on presentational concerns. A single API powers the entire page.
  • The consolidation of business logic and separation of concern has drastically increased our development velocity.
  • The server-driven approach allows a better dynamic configuration of the checkout experience based on user, merchant, geography, and more.
  • Increased standardization allows us to lean in heavily on further devX improvements, including scaffolding.
  • Any product or experience built on the framework gets observability & analytics for free.

Future of Server Driven UI at Affirm

We’re beginning to look more towards fuller component-driven UI, beginning with incremental improvements like copy and theming. Component-driven UI is, in many ways, just a more granular version of prebuilt screens.

We are looking into building load shedding capability using server-driven UI when the traffic is high and show only essential user interface screens. And dynamic application failovers — for example, if the third party SMS service is down to send the phone pin to users, the backend can switch the user experience to show the email pin screen directly by default and avoid the phone pin screen until the services are back up.

On the flow state machine framework side, there are numerous new initiatives like composable flows to stitch one flow to another flow — for example, stitch authentication flow before checkout flow — sharing steps across multiple flows, improving the availability and performance of the state machine in the distributed environment, and more.

Conclusion

The server-driven UI and flow framework majorly helped to improve the user experience, code orchestration, and development velocity, leading us to invest in server-driven UI heavily.

If developing and scaling solutions to initiatives like these excite you, check out our careers page.

--

--