Using Adapter Design Patterns To Migrate Legacy API Calls in FE
In a fast-growing startup, its common to make tradeoffs between rapid growth, and future-proof architecture. While the early days of Hippo saw much of the former, the monolithic structure and lack of strong types in our main backend repository left much to be desired. Instead of fueling growth, it started to stunt it. As the company continues to scale and mature, we have shifted towards a micro-service architecture, making proper modeling of our insurance policy data a necessity. However, incorporating these new services into our existing applications comes with its own set of challenges.
Customer Activation
Our team manages the consumer-facing insurance signup flow: a multi-step single-page frontend application with variations on each step depending on the insurance product and the state the policy application is in. We’ll refer to each step in the flow as a “page.” This seemingly simple application is riddled with complexity at every layer.
Early in 2022, our team began migrating this React application to consume insurance policy data from a new Policy micro-service. Essentially, this changes the way we work with policy data from the backend. In the original implementation, the frontend called multiple legacy RESTful APIs directly to retrieve and update the data required to complete an insurance policy application. After our changes, the frontend will only call a public gateway — a backend-for-frontend (BFF) via GraphQL — to retrieve pre-composed and unified data.
Before: The frontend calls multiple RESTful APIs and stitches the responses together
After: The frontend calls a BFF once to get composed and unified data, orchestrated across our various micro-services
Additonal Context
Before this change, we had already established a BFF to serve non-policy data. Examples include, data describing which specific UI elements are shown on individual pages, how discounts should be described, and when modals are shown for a given insurance flow.
The goal of this project was to retrieve policy data from the BFF, already transformed and unified, alongside the UI data. Further, this allows us to retire the legacy RESTFul API calls in frontend.
If at First you don’t Succeed…
At first, we thought this was easy, as we could just start calling the new policy service from the BFF, and retire legacy API calls from the frontend at the same time. However, this turned out to be significantly more difficult than we thought.
Problem 1: it was difficult to manage two different sets of responses in the frontend.
At any given migration point, the frontend needed to manage two totally different sets of responses: legacy RESTFul policy data and new UI config with policy data mixed in. As you can probably guess, there is very little overlap between the format of these two data sets.
Since we were migrating page by page, there was a crucial point where a page being migrated needed to support and manage both responses. This makes our global application state extremely hard to manage. Because we were dealing with two incompatible data sets, it would fail when navigating between any migrated and non-migrated page. But, this was not the hardest part…
Problem 2: it was almost impossible to do our desired canary-release to a subset of user sessions in an arbitrary page order for the migrations.
I think it would be good to call out exactly how we want to do the canary-releases, i.e., rollout this work for a specific state or product at a time.
We have hundreds of products to support, and in order to mitigate risk we wanted to release a subset of these products on a page-by-page basis. This would mean thousands of release combinations to manually coordinate!
We also needed the flexibility to migrate arbitrary pages regardless of their order in the flow due to different priorities of the new policy micro-service. This became impossible as jumping between migrated and non-migrated pages was a nightmare (see problem 1).
So, what should we do?
I eventually found some inspiration in the adapter design pattern. Before I plot out my idea, here is a brief introduction of the pattern.
Adapter pattern works as a bridge between two incompatible interfaces. This type of design pattern comes under structural pattern as this pattern combines the capability of two independent interfaces.
A Policy Adapter in the BFF
What if I put those two different systems in the BFF and normalize them so the upstream and downstream will not be affected. Here is my design:
Have only one response for migrated pages in the frontend. We adapt the difference between legacy RESTFul API and the new policy micro-service in the BFF.
In this case, the BFF is a standalone service, but has no database of its own. It simply acts as a sort of proxy to other services while normalizing and orchestrating their responses into a format the frontend understands. This way, the frontend doesn’t need to worry about different response types and making multiple RESTful API calls.
This gives us quite a lot of benefits.
- Pro 1: No more dramatically different response sets in the frontend. This means the migration work in the frontend could be much simpler! No need to handle legacy API calls, we can do a straight forward one-time migration per page.
- Pro 2: Easy canary-releasing and arbitrary page order migration. We can migrate any page in coordination with other teams working on that page, regardless of the page order in the flow. This gives the service team more flexibility to do their work in their own sequencing.
This plan worked out. For our first major rollout, there were only one or two minor issues regarding data analytics and no issues with the actual policy application data interactions. Overall it worked as expected.
A bit of Gratitude
Here I’d like to take this opportunity to thank my manager Adi and my teammates David, Chris, Kyle and Maciek for the full support. Without you guys, this idea couldn’t be so successful.