Thirty Madison Quiz Library

Akonwi Ngoh
Thirty Madison Engineering
7 min readNov 11, 2020

At Thirty Madison, we have multiple brands that each have a unique customer experience built around a specific chronic condition. While the experiences themselves are different, they share many common key milestones in delivering care. One of these milestones is completing a medical questionnaire, formatted as a multi-step form to either gather more information about a condition or guide a customer through their experience. We call these questionnaires, quizzes. Most customers of a Thirty Madison brand take a consultation quiz in which they answer questions about their medical conditions to inform their doctor, just like an in-person doctor visit.

A gif of the current Cove consultation quiz
The current Cove consultation quiz

Until recently, we implemented quizzes across the different brand experiences by creating a table in the database for every quiz and manually creating a UI for users to answer the quiz. This process is slow. I’ll show you why:

Step 1: You can’t build a quiz without content

While business and product teams may decide they want a quiz and ask the engineering team to build it. We can’t get started until they decide which questions to ask. At that point, we create a migration for a new table representing the answers, add new APIs for the table, and finally build a UI to take the quiz and save answers using the new API. Oh, I forgot to mention that these quizzes can be up to 50 questions long 😳 , and some questions are conditionally hidden or dependent on answers to other questions.

Step 2: You can’t ship a quiz without doctor oversight

Still there? So imagine you’ve spent over a week building this quiz, migration and all. Think you’re done? Nope! By the time you finish, product managers, in consultation with our consulting doctors, have probably changed some questions and the logic between them. So you need to go back and change those in the code for this large multi-page form or even update the table if there are new questions.

Step 3: You still can’t ship without doing QA

Once that’s all set, it’s time for QA. I don’t have to tell you that there are also bound to be bugs found while QAing this quiz because of a code mistake or a miscommunication with the product team. Fixing them could require changes all across the stack again.

Step 4: You can’t update the quiz easily

But even if there were no bugs, when we inevitably want to make changes to this quiz in the future this painful process would start again.

I haven’t spoken to someone yet who likes this process. No engineer is happy about the various quiz tables or the long and confusing logic of building a form to answer a quiz. So we started working on a solution that reduces the friction of building and maintaining our quizzes.

How I used to feel about building our quizzes

Some of the requirements for this system were:

  • The ability to add or remove questions in a quiz without touching the database or redeploying our API
  • The ability to make logic changes in a quiz without touching frontend code and redeploying a brand’s frontend
  • Support for multiple versions of a quiz
  • Support for rendering and customizing a form UI for a quiz in our brand’s frontends

With these requirements in mind, we’ve built a new framework composed of four parts:

  • A single model and API to create and update quiz responses
  • A system of configuration files to describe the data we want to gather from a quiz and how to display the quiz
  • Storage of the configuration files in AWS s3 buckets
  • A shared UI component any brand frontend can use to render a quiz

Describing Quiz Data and Answers

We needed to figure out how to encapsulate two things into configuration files; the logic of which data to capture and the questions’ presentation. Using configuration files meant we would need a way to validate the files against some set of rules about how they’re formatted and what they contain. JSON was an easy choice for the file type because it’s structured, familiar, and easy to read and navigate. For validation, we used JSON Schema, which provides a vocabulary for describing the format we want JSON data in and, subsequently, a way to validate that inputs match that format.

With the help of JSON Schema and some libraries, we’ve built a DSL for configuration files. We have two main types of configuration files. The first we call a “data config,” and it describes the data structures to use for answers to questions and the logic of which questions to ask and their order. Below is an example of a very basic data config for a two-question quiz. In this case, we want to ask for a favorite color, which will be a string, and whether they like ice cream, stored as a boolean, and then the quiz is complete.

an example of a data config
There is support for more complex quiz logic with branching and conditionally displayed fields with a syntax similar to that of JSON schema for conditional validations

The other type of configuration is a “ui config,” which describes each quiz step’s UI. Here is what a corresponding ui config for the sample quiz looks like:

A couple of notes: `$type` in this case, is `write-ui` because this config is describing the form to create or edit a quiz response. The other option is `read-ui`, which would be a config about how to render the answers to a quiz. We haven’t yet finished support for rendering the responses, so I’ll skip that for now.

Hopefully, the ui config is pretty self-explanatory. We’ve abstracted a lot of the code specific to each form we’ve built already into a common standard where we can declare what the user should see and use shared rendering code that understands this language to provide a UI for us.

So, how does JSON schema come into play? We have meta-schemas for each type of config file that describes the expected format of that config. For example, the data config meta schema describes a data config as a JSON object that looks roughly like the sample above and has a property “$type” with a value of “data” (There’s more to it than that, but I won’t go into the details because that’s just JSON Schema). With the help of a JSON Schema validator library, we’ve built a validator for our config files that validates structure and field references between ui and data config files. This means as a developer is creating or editing a quiz config, they can get feedback about wrong syntax, invalid field names in the ui config, and more.

Now that we have configuration files for quizzes, we store them in S3 buckets. The primary reason for using S3 is that it’s straightforward to use as a secure cloud file storage. Since it mimics a directory structure, we can store configs intuitively and retrieve them directly from the buckets easily without figuring out how to store, version, and retrieve them within our database. Our config files start in a GitHub repo, which has a build pipeline that runs validations on pull requests and pushes new versions of quiz configs to S3. Storing the configs in the cloud allows us to create, test, and deploy quiz changes without changing any other app code.

Model and API

Our new quiz response model is a dedicated table for quiz responses with columns for quiz metadata (like config_name, config_version, etc.), user_id, and the actual answers as JSON in a JSONB column. We decided to use JSON for the answers because it enables us to support various answer data structures without modifying the table for every change.

The API allows creating/updating quiz responses and retrieving quiz responses per user. The create and update graphql mutations do JSON schema validation on the provided answers. Upon receiving parameters for a quiz response, the server will find the corresponding data config and do schema validation on the answers according to the schema specified in the config (See the sample data config above) before saving. And that’s it! We no longer need new quiz specific graphql mutations because we have a standard model and API for this type of data.

Rendering

The last part of this new system is the renderer. Because all of our frontends use React, we wanted a component that does the magic of retrieving the configs and rendering everything necessary to take the quiz. The component supports theming because it is built with a system-ui theme, which means any consumer of this component can customize the look of individual elements and overall layout.

This component is available to our frontend teams through an NPM package that houses the logic of fetching and parsing config files, translating the configs to UI, making API calls to create/update quiz responses as the user answers questions, and validating and displaying errors. Our frontend teams can set up a new quiz UI in a fraction of the time it used to take by just doing something like this:

What’s Next?

The initial priority was creating a framework to enable rapid development and release of quizzes, and we’ve accomplished that. We are currently using this new framework for quizzes in three out of four of our brand frontend teams. Now, we’re working to extend the shared library code to support rendering quiz responses in different contexts (i.e., answers are presented differently to the users and doctors). We will also provide APIs that enable teams to build their own renderers for more complex or custom user experiences.

If other teams have an interest in using a system like this, we’re considering making this an open-source project.

We’re also hiring! If working on projects like this sounds exciting, please come join us.

--

--