We talked about the conceptual model for signup and onboarding in the last post. Today we will dive deeper into the solution aspects: define the UX models and talk about architecture for one of them.
Here are the all the posts in this series:
- Understanding signup flows
- Fixing product signups (this post)
- Language models in signup
- Lessons in customer validation
Let’s get started with the UX.
UX mental model
We prototyped three flows. Each augments the previous with one or two interesting bits.
Model 1 streamlines a fragmented experience into a coherent set of signup steps. It removes the unnecessary clutter we discussed in the last post. Makes an attempt to educate the customer along the way and encourage using the product immediately after signup.
Model 2 builds on top of Model 1 and provides an always-on guidance agent. Customer can ask questions, or a product recommendation. Agent is step-aware and responds keeping in mind the Customer’s choices so far.
Model 3 radically inverses the wizard-like signup into a natural language conversation. It includes the guidance agent and brings along an advisor agent who can guide the customer through signup process.
Both Model 2 & 3 use large language models.
Architecture
Several interesting patterns emerge on analyzing the conceptual flow.
- Signup is a collection of forms. Each form collects some information, validates it locally, sends it to server, we check for consistency there and the commit.
- Tasks handled by the forms belong to varied domains. E.g., a set of forms deal with creating an Account (identity), and another set deal with Payment. Both of these are deep technical problems on their own right.
- Is there an ordering of forms? Yes, it seems sequential at first. You’d need an Account and Product to do Payment. Can we navigate back? Or, can we fast-forward? Yes, say you had to pause the signup in middle, we’ll let you continue from where you left with a login screen (Account). If your login token expires, we must show you the login again (back to Account). We’re dealing with a cyclic graph of forms.
- Can we model this with functional paradigm? We can imagine each form to be pure: have a set of invariants, inputs and outputs. Orchestrator moves to next form if previous form indicates completion and invariants for the next form in the graph are met. Part of the state can be stored on client (this instance of signup, e.g., current active step) and the rest may be in the service (specific to the Account, and relating to both current & future signups; e.g., payment cards of the Customer).
Axes of evolution
How does a typical signup flow architecture evolve?
- New steps: business requirements can introduce additional steps, or merge existing steps to simplify the flow. E.g., use voice or video based human proof. Steps should be self contained owning their presentation and business logic.
- New horizontal capabilities: we could introduce features that apply to all steps. E.g., LLM based support and guidance for the entire flow. Or, we could introduce new themes based on the product. These could be part of the core layer and separate from the evolution of steps.
- Platform concerns: payments, provisioning etc. are sub problems in this flow. A change upstream can drive updates to this flow. E.g., add support for PayPal based payments in signup. We’ll use SDKs for the platforms wherever available. Additionally, use a port-adapter mental model to invert the dependencies.
- Experimentation: A small change in the signup process can yield a huge effect 1 on the user experience. Additionally, if the Company supports M markets and N payment modes, every change must go through A/B testing. E.g., introducing or deprecating a payment mode. Our architecture must allow for multiple flows to coexist while minimally impacting each other. Ensure the abstractions allow refactoring and reusability across experiments.
Let’s talk briefly about one possible architecture and what it takes to implement Model 1.
Model 1: Step surgery
Architecture below has a single page client (SPA) talking to a set of purpose-built backend services.
Here’s the flow.
- Customer visits a webpage. This hits the back-end front door. Downloaded webpage includes the bundled SPA code. The front door service could pass additional configuration to the SPA during the bootstrap.
- SPA app starts the signup flow with a graph of steps. Steps are the smallest unit of work. Each consists of a form, validation logic and related service integration with backend.
- Orchestrator in the SPA app loads the theme related settings, and the step.
- Current active step (form) is rendered. Customer fills the form.
- Form inputs are validated and stored in the backend if necessary. This uses the Network Services layer in the SPA. Some forms that are platform specific (e.g., Payment or Identity) may use an SDK to connect with respective backends.
- Orchestrator decides to navigate and render next step if current form inputs are valid and invariants for next step are met.
The Backend for Frontend (Provision service here) is a critical component. It must evolve hand-in-hand with the SPA. It encapsulates the complexity of multiple service and state handshakes.
Our Model 1 prototype involved consolidating a bunch of steps to streamline the flow. We also introduced new steps. In the core layer, we updated the Orchestrator and Theme for modern presentation and include the education components, i.e., show informative videos while the Customer is waiting on long-running provision operations.
Now think of the experimentation challenge. Imagine we had to ship the prototype flow for a particular region (A/B testing for one market). How do we ensure the steps are reused across both A and B flows?
We refactored the steps. Extract a component to abstract the presentation and business logic. Use this component with different presentation settings in A and B.
What if the step cannot be refactored cleanly? Say if we’re consolidating 3
forms into one and each form comes with hardcoded Submit button from a SDK. We
hacked these by hiding the hardcoded Submit button with CSS and then
programmatically navigating using React ref
. Not at all clean, but we had to
make progress.
We used the same monkey patching for dealing with platform SDKs where we had
little control. My lesson: one of the SDKs actually promoted monkey patching as
the official extensibility model. E.g., the SDK documented various CSS
primitives and asked the consumer to use them to hide elements, or use ref
to
programmatically control form submission. After losing tons of hair in
debugging, I am not quite sure if this is the approach.
For Model 1, we were operating under the constraint of keeping all changes in the SPA only. It worked pretty well since the architecture had sound abstractions and most of our changes were related to UX.
Thank you for reading this far.
In the next post, we’ll talk a thing or two about large language models and using them for Model 2 & 3. See you around!
Footnotes
-
I observed Customers struggle with the incorrect placement of default action first hand during the experiment. E.g., confusion if we place it to the left instead of right. Minor things like this do matter, a lot. ↩