Metablocks: Our Journey to an Agglomerated Navigation Logic

Myung Kim
Nextdoor Engineering
9 min readOct 19, 2021

--

In this piece, we will explain our journey through solving fragmented navigation logic across different platforms by developing a backend-driven navigation system that helps users build flows.

By: Hirokazu Tei, Myung Kim, and Blake Robinson

Background

As engineers on Nextdoor’s Business Experience team, we were tasked with remaking our business onboarding feature. This flow allows businesses to register and onboard to Nextdoor as a business, a core part of our ecosystem. The registration and verification of businesses, however, is a complex task with many opportunities where the user could drop off. Our redesign simplified the onboarding flow and abstracted away much of the business logic from the user.

When we started developing, flows were mostly linear with little forking happening due to user options or user attributes.

  • User Options: Navigate to “A” if the user clicks “X”, navigate to “B” when the user clicks “Y”.
  • User Attributes: Navigate to “A” if the user has attribute “X”, navigate to “B” if the user has attribute “Y”.

However, we began to notice that as different features were being introduced, and as we added bespoke flows for third-party partners, the management of the navigation logic became hard to maintain, debug, and develop.

Our team decided to upgrade this onboarding flow and also implement natively on iOS and Android and we saw the need to abstract the logic. Implementing this complex logic three times over (once per platform) was going to be long and bug-prone.

Our Approach

While a page-embedded navigation logic is easy to understand on a page-by-page basis, it becomes difficult to comprehend on a holistic level since one must traverse page-to-page like a graph to get the whole picture. To understand a specific user’s flow in code, we had to visit multiple files to verify their intended path. The effort for development, testing, and debugging becomes more complicated as more paths get added.

Therefore, the first remedy was to decouple navigation logic from individual pages and agglomerate them in one place. By using a finite-state machine, we are able to have it take in consideration of the user’s current step, the action that was taken, and the context state of the session to determine where to navigate the user next. This is a relatively easy task for a singular codebase, but how do we enforce consistent navigation logic across three platforms?

Are we left to implement navigation logic in three different codebases?

Conception of Metablocks

Our team was not satisfied with the option to maintain a consistent navigation logic in three different codebases, so we came up with Metablocks. Metablocks was developed as a backend-driven navigation system, meaning that we give the responsibility of determining the next step of a navigation procedure to a backend state machine.

“Metablocks is a backend-driven navigation system…”

While the main goal was to solve this particular logic fragmentation problem, we also noticed that there were certain features and redundancies which every flow implementation had to repeat and ranged from the flow implementation itself (the set-up one had to do to create a navigation flow), flow analysis tracking, features like pause-resume (the ability for users to drop off from a flow and come back to the app landing on where they left off), plus more.

“… that helps users build flows.”

Here is where the “that helps users build flows” portion of the description comes in. We realized that Metablocks could be more than a backend navigation agglomerator and can be a whole ecosystem where we can provide libraries on each of the clients to interface with Metablocks seamlessly, create a flow in an opinionated consistent manner, provide free tracking, and support auxiliary features.

Our team has these libraries built to support React, iOS, and Android. The collection of these libraries and the backend system can be understood as Metablocks.

How it Works

There are five things that the Metablocks finite-state machine requires to determine a user’s next step:

  • `flowName` — simply distinguishes different flows an organization may have.
  • `flowVersion` — gives us the ability to version mobile client updates.
  • `currentStepId` — denotes the current step that the user is on.
  • `action` — describes the user or client event that has occurred.
  • `context` — is an object that contains the auxiliary information that the finite-state machine needs to take into account when returning the next step.

Whenever an event occurs that begets navigation, the client calls a method provided by the library to inform the library which action occurred. The library itself implicitly knows the `flowName` and `flowVersion` during instantiation, and keeps track of the `currentStepId` as well as the `context`. A GQL query is made to the Metablocks backend to determine the next step.

Web (React)

The library offers a `flowBuilder` that allows the developer to specify configurations related to their flow, and types used for strict type checking with the flow. The resulting architecture that the builder produces is a series of higher-order components wrapping a navigation manager that maps the current step of the flow to a specific page component. Above the navigation manager, a flow state manager exposes various methods to interface with Metablocks through React’s `ContextAPI` and manages the state of the flow. This layer is responsible for making the GQL query to the Metablocks backend to obtain the next step. Finally, the outermost higher order component is responsible for context state management as well as exposing the API to access and update the state using React’s `ContextAPI`.

While `flowBuilder` helps the developer build this flow holistically, we also offer factories that create each of these layers in case any of these layers require special modification.

Android

The Android library consists of three extendable enums that represent the steps, actions, and points of entry, along with an API class. To implement Metablocks, a developer needs to subclass each of the enums (to encompass their domain-specific values for steps, actions, and points of entry). Then write a repository layer (that slightly extends and already-written Metablocks API class) that can take the domain-specific enum values, make the backend request for the next step, then expose an observable for the result of that call that ViewModels can observe to know what the next step for the flow is.

We are investigating using Jetpack Navigation in the future to have more guard rails in place, and guarantee behavior parity between the FSM and navigation behavior on Android.

iOS

The functionality in the iOS Library is handled primarily by the FlowCoordinator, which is assisted by the FlowInstaller class, the FlowInteractor class and the analytics class.

To use the library, a developer needs to create entities that conform to two protocols: FlowInitializing and Flow. After FlowInitializing initializes the flow, the Metablocks library fetches the initial step from the backend and passes it to the entity that conforms to Flow. There, the initial step is mapped to a view controller, which is then presented to the user using a UINavigationController. The process of “fetch a step, map it to a view controller” repeats until the flow is terminated.

Obtaining the Next Step from the Backend

For every flow there is a `flowName` associated with it, which maps to a finite state machine that takes `currentStepId` and `action` as an input, and the next step as the output. However, a combination of `currentStepId` and `action` does not always map to a single next step. For navigation events that require the consideration of the context state, developers can add a controller layer to embed custom navigation logic that takes account of the context state to return the next step.

Once the GQL query returns the next step to the client, the flow state manager updates its `currentStepId`, which updates the navigation manager that maps the step to a page to be rendered, completing the navigation.

Impressions So Far

The Business Experience Team has rewritten our onboarding flow on Web leveraging Metablocks, and we have written our flow natively in iOS & Android, using Metablocks as well. It has been about a month since we have released our new flow as well as Metablocks. Here are some of our impressions….

Leaner Frontends & Strict Responsibility

Before Metablocks, we often referenced individual pages in our frontend to implement new navigation business logic or to debug a navigation-related issue. However, after leveraging Metablocks, we noticed that part of the code is not referenced anymore. Our team has successfully decoupled individual pages’ dependency on navigation. A page’s presentation layer is concerned with rendering; the page’s logical layer is concerned with logic that only applies to itself and to update flow-persistent state. Moreover, pages no longer contain any information regarding what to consider and where to go next. It simply notifies the Metablocks interface that a certain button was clicked or a certain event was fired that could result in a navigation transition.

This strict responsibility sets a clear boundary for engineers to understand where to change or develop when we need to make modifications or improvements.

Easy Maintenance and Debugging

Since navigational behaviors are deterministic given an input, we can guarantee that if the right input was given but the wrong navigation step was returned, the culprit of a bug lies on the backend navigation logic. On a similar note, if the flow exhibits a bug, and we test our Metablocks GQL query to confirm that given a particular input, our service is returning the anticipated next step, the issue would lie on the platform side. Since GQL queries are type-checked, the likely culprit often is an error in updating the context.

This hard-line we’ve drawn allows us to quickly identify bugs in our flow and reduce the frequency of introducing bugs. This confidence gave us the ability to iterate quickly and to fix bugs speedily.

Easier and Explicit Tests

When navigation logic is coupled with individual pages and fragmented across different pages, testing also becomes tied with the logic of the individual page and is inevitably spread across multiple files. However, by defining a singular state machine entity to be responsible for navigation, it becomes not only easy to test but also makes the test much more explicit and deterministic.

Free Tracking

Each Metablocks library implementation on three platforms agree to a common tracking standard and Metablocks allowed us to implement free flow analysis tracking. When a team leverages Metablocks, they do not have to write a single line of code related to tracking to get instrumentation data on flow initiation, flow transition, and flow termination.

Not a SINGLE line of code.

This is incredibly powerful since this enforces tracking practices across different flows with almost no downside. This means that once we write one analytical query for one flow, we can simply change the flow name on the events, and have it tell us about the completion rate, for example, of other flows.

Standardizing Common Practices

At Nextdoor, we have dozens of teams responsible for a flow. Traditionally, individual teams implemented their own flavor of flow since there were no strong incentives to keep flow implementations consistent. However, as more common features are required by different flows like pause-resume, and as our organization grows larger, standardization becomes an important benefit. There will always be flows with requirements that lie beyond what is possible for Metablocks. However, in most cases, there is now a canonical way to implement flows which is an important step forward as Nextdoor grows.

What Does the Future Hold?

We plan to keep improving Metablocks to make it more easily adaptable, more pluggable components to enable different features that we have not yet considered, and open-sourcing the project to garner feedback and contribution.

Let us know if you have any questions or comments about our server-side navigation system. And, if you’re interested in solving challenging problems like this, come join us! We are hiring iOS, Android and Full Stack engineers on all our teams. Visit our careers page to learn more!

--

--