---
title: Structuring Conditionals in a Wizard
teaser: Branching early helps structure multi-step forms.
tags: good code,elm,ruby,rails
author: Joël Quenneville
published_on: 2023-08-08
---

**Multi-step forms** ("wizards") often grow organically, adding a conditional
here, a new step there. This can quickly evolve into a scary tangle of
conditional code that no one dares to modify.

There is a better way! I've worked on several projects, both Rails and Elm,
where the wizard code had gotten so convoluted that the team were scared to work
with it. Applying the refactors shown below had a major impact and allowed
everyone to feel confident when dealing with this part of the system.

## Single flow of control

As developers we love to put everything into a single linear flow. Sure, there
may be diversions off the main path but as quickly as possible we try to rejoin.

Consider a wizard consists of 3 steps, each of which might take several actions
like saving to the database or sending an email. Both the bio and address steps
have must be completed within a limited time window, so that's a **shared**
action between them.

- **Bio** (`fetch_alma_mater`, `reset_timer`, and `create_user`)
- **Address** (`confirm_email`, `reset_timer`, `geo_locate`, and `create_address`)
- **Socials** (`verify_number`)

In a Rails app, a common implementation might be wrapping each of these actions
in an independent condition that checks for the presence of a param or a boolean
flag. While this may _look_ clean and composable, it scales very poorly.

```ruby
if params[:user][:school].present?
  fetch_alma_mater
end

if params[:address][:email].present?
  confirm_email
end

if params[:address][:phone].present? && @user.active?
  verify_number
end

# 4 more of these...
```

We might represent these visually using a control flow diagram.

![control flow diagram showing a wizard implemented as a single linear flow](https://images.thoughtbot.com/pnl1pwibf8wr1eibov3uly3phfr3_linear-flow-small.png)

This style is hard to **read**. It is challenging to figure out what code will
get executed for a given step. Are you chasing down a bug in the bio step? Good
luck finding which of these lines is relevant to your search.

This style is also hard to **change**. If you want to add extra behavior to the
step that gathers biographical info (and only that step), is it safe to do so
inside the `if params[:user][:name].present?` branch? It's hard to say. Maybe
you should wrap the new behavior in its own conditional? Pretty soon this file
will be a mess of [defensive code].

[defensive code]: https://practicingruby.com/articles/confident-ruby

## Combinatorial explosion

Part of the problem is that all these independent conditions create a
**combinatorial explosion** of paths through the code. Even though our problem
only describes 3 separate steps for our wizard, the way our code is structured
allows for 19 different paths! That's 4 branches that may or may not be
triggered (2^4 = 16 combinations) plus 3 possible endings from the full diagram.

Most of these "should never happen". For example, know that the **socials** step
should only execute the `verify_number` action but you'd never know that by
looking at the diagram or the code! The reality is that it's completely possible
for the sequence `confirm_email + fetch_alma_mater + reset_timer +
verify_number` to execute.

No wonder the code is a nightmare to read and modify!

## Branching early

Let's give up on the idea of a single linear flow. We know this is a problem
that inherently has 3 distinct paths so let's not fight that. Instead, we'll do
an early multi-way branch using a `case` expression.

```ruby
case step
when :bio
  fetch_alma_mater
  reset_timer
  create_user
when :address
  confirm_email
  reset_timer
  geo_locate
  create_address
when :socials
  verify_number
end
```

Visually, we might represent this using the following control flow diagram:

![control flow diagram showing a wizard implemented by branching 3 ways early](https://images.thoughtbot.com/bg436dadhvnk5fla80s6tk91t5zl_early-branching-small.png)

It's now much easier to answer questions like "what actions get triggered in the
bio step?" and "which steps use the shared action?"? Because you only need to
hold 3 paths in your head (instead of 19), it's much easier to read the code and
get a high-level understanding of what it does.

## Branching even earlier with routes

When the body of a Rails controller action is a big `case` expression, it's
often a sign that this could better be modeled as multiple resources. Let the
router handle the branching instead of doing it manually.

Wizards are no exception. In general, I find the best structure is to have each
step be its own controller. Use your favorite code-sharing mechanism for work
that is in common to several steps.

```ruby
# config/routes.rb

resource :bio, only: [:edit, :update]
resource :address, only: [:edit, :update]
resource :socials, only: [:edit, :update]
```

## Modeling a wizard on the front end

Wizards are also a common experience to implement in a front-end app. Consider
an implementation in Elm. Of course, we start with the data structure:

```elm
type alias User =
  { name : Maybe String
  , email : Maybe String
  , phone : Maybe String
  , active : Maybe Bool
  }
```

This data structure has [24 possible states], many of [which are invalid]! For
example, is it possible to have a phone number (captured in step 3) but no name
(captured in step 1)?

Instead, we could refactor to a custom type that clearly represents each of our
steps. This type makes it clear that at each step, we are guaranteed to have the
data from the previous steps.

```elm
type Wizard
  = Bio (Maybe String)
  | Address { name : String} (Maybe String)
  | Socials { name : String, address : String } (Maybe String)
  | Complete { name : String, address : String, phone : String }
```

You may wonder why we're looking at data structures here rather than conditional
code like in the Ruby examples above. This is because **your choice of data
structure impacts how you will branch on it!**

The record pushes you towards nested conditions checking for the presence of
various fields, kind of like the original Ruby example. The custom type
on the other hand encourages you to write one flat case expression.

## Triangle of separation

![diagrams representing each of the 3 principles overlayed on the vertices of a triangle labeled "triangle of separation"](https://images.thoughtbot.com/6400cj233tculk1epho0zbu0r00h_triangle-of-separation.png)

The refactors shown in this article are a specific application of a more general
set of principles I call the [triangle of separation](https://thoughtbot.com/blog/triangle-of-separation):


1. Write code at a single level of abstraction
2. Separate branching from doing code
3. Push conditionals up the decision tree

The three principles are really just the same idea viewed from different
perspectives.

[24 possible states]: https://guide.elm-lang.org/appendix/types_as_sets.html
[which are invalid]: https://www.youtube.com/watch?v=IcgmSRJHu_8
