Accelerating App Development at Scale: Intuit’s Micro Frontend Architecture

Muzaffar Malik
Intuit Engineering
Published in
10 min readJul 16, 2021

A micro frontend-based application development architecture accelerates developer productivity and provides seamless customer experiences across a portfolio of products.

After many years of exponential application growth at Intuit, we embarked on an initiative to scale app development through a unified AppFabric (Application Fabric) platform. In a recent Intuit Medium Engineering Blog, we described how this strategy has enabled us to shorten release cycles, stitch seams between products, and easily share experiences across products.

Altogether, this has meant delivering new capabilities faster to consumer, small business, and self-employed customers who use Intuit’s TurboTax, QuickBooks, and Mint products and services. In just one year, we’ve seen a 4X growth in web applications created and hosted on the platform and a 5X increase in inner source contributions from our ecosystem of Intuit frontend developers. In a second blog in our series on Intuit’s AppFabric, we delved into Intuit’s content delivery network, with an example of managing failovers in peak tax season for our TurboTax business.

In this blog, we take a deep dive into the micro frontend architecture of the AppFabric platform. Our hope is that sharing our approach will prove insightful to companies exploring a micro frontend-based architecture as a way to tackle scalability challenges that arise with large frontend code bases, large teams, multiple developers contributing to common user experiences and/or as a way to incrementally update their tech stacks.

But first…!

A micro frontend primer

The micro frontend architecture introduces microservice development principles to frontend applications. It’s an architectural pattern where the frontend of a web application is decomposed into many smaller, more manageable pieces that can be developed, tested, and deployed independently. These are then recomposed at runtime to create a cohesive end user experience. With this approach, today’s companies are successfully overcoming the scalability challenges that come with a monolithic frontend.

To learn more about the origins of this increasingly popular micro frontend architecture trend, I encourage you to read ThoughtWorks’ full-stack web developer and consultant, Cam Jackson’s excellent write-up.

Intuit’s flavor of micro frontends

While the concept of micro frontends has gained considerable momentum, today there are no industry standards and many strategies exist “in the wild” for implementing the core intent behind the concept: to manage the complexity and scale by breaking frontend monoliths into smaller and more manageable pieces.

So, we embarked on our own flavor of micro frontends here at Intuit.

Intuit’s AppFabric platform enables the easy creation of single-page applications (SPAs) in a micro frontend style, designed to optimize the reuse of experiences across applications.

Our architecture revolves around the following core concepts and components at a high level:

Plugin: plugin is a term used to describe a micro frontend, an artifact that is delivered independently and can be ‘plugged’ into different applications to contribute a set of user experiences.

Widget: The user experiences that are packaged in a plugin are called widgets.

Application: An application represents a full experience for a particular product (TurboTax, Quickbooks, Mint).

Plugin Registry: Plugins are registered in a plugin registry, which stores versioned entries for releases of each plugin in the system.

Application manifest: A way to define an application declaratively via a config file.

Application Registry: A Registry that stores application manifests.

Multi-tenant Application Service: A service that is able to serve first page HTML for multiple intuit applications based on manifests registered in the Application Registry.

CoreJS: A client-side (browser) component that provides the environment and capabilities for plugins to function within an application.

Anatomy of a plugin

A plugin specifies its contributions to an application using a manifest file. For example, the manifest file for a plugin can specify:

Widgets exposed for reuse

Widgets are composable. Exposed widgets can be pulled into widgets residing in other plugins. For example, a credit card widget can be pulled into a transactions widget to provide payment functionality.

The relationship between a plugin and its widgets in our architecture is analogous to the one between remotes and exposed modules as recently introduced in the Webpack 5 Module federation concept.

Most of our UI stack is based on React, hence widgets today are also implemented as React components.

The SPA (single-page application) routes handled in the application

Typically these are also mapped to widgets in the plugin. For example, when the user navigates to a specific route, the corresponding widget mapped to that route in the plugin manifest is rendered.

Conceptual View of a Plugin

The contributions in the manifest have pointers to plugin assets, which are dynamically loaded at runtime, as needed. For example, when a particular widget provided by a plugin is required or when a plugin navigates to a SPA route.

Composing applications from plugins

Applications are composed of plugins, as shown in this Intuit’s Quickbooks Online micro frontend example:

As individual pieces of the full experience are packaged as plugins, we can represent an application simply as a set of plugins coming together at runtime. The full experience for the product then is a composition of the experiences contributed by all plugins included in the application.

For example, the QuickBooks application can be represented by the following set of plugins:

Specifying applications as a set of plugins allows us to define new applications easily by adding a relevant set of plugins, and allows us to easily reuse experiences across applications.

For example, the banking plugin that provides the “connect your bank account” experience is applicable to use cases for both QuickBooks Online small business and Mint personal finance applications.

So, how do we get from a set of plugins to a single page application at runtime delivering a cohesive end user experience in a customer’s browser?

Serving the first page for the SPA

As illustrated above, we can see that all applications in this architecture are structurally very similar. When serving a request for any application, an application service must first look at the set of plugins relevant for the application, fetch the latest manifests for these plugins from the plugin registry, construct the first page HTML for the SPA using that information, and serve it back to the client. The returned HTML must also include CoreJS (i.e., the client side component that can provide the environment and capabilities needed by the plugins to function and use plugin contributions to create a full product experience for the end user).

Application manifests and a multi-tenant application service

Given the structural similarities, instead of having every application setting up the infrastructure to serve requests and manually integrate components like the plugin registry and CoreJS, we introduced the concept of an application manifest and a multi-tenant application service.

The application manifest is how we define an SPA application in this architecture. The application manifest provides essential information to the multi-tenant application service to enable it to serve the SPA first page for a particular application. It also provides information to the CoreJS component at the client, enabling it to configure the system capabilities according to the needs of an application.

Following are some key things developers can configure in the application manifest:

  • List of plugins for the application
  • How the SPA first page should be served: Authentication requirements, headers (e.g. content security policy), to a default first page template (e.g. injecting app-specific data), etc.
  • The application shell

The application shell above refers to common elements or central layout for the application (header, footer, navigation bars, etc.). For the application shell and other central application-specific customizations (themes, shared datastores, etc), we introduced an app-specific plugin that is loaded up-front with special hooks that allow it to become part of the SPA bootstrap process and to handle application-specific central concerns.

Following is an example of what a manifest would look like for an application like Mint, with some of these configurations specified:

Application manifests are stored in the application registry and are used by the application service and the CoreJS(at the client).

With these concepts, we are able to serve requests for multiple applications from the same service. The following figure illustrates how the SPA first page HTML is served for an application by this multi-tenant application service.

This technique for defining and centrally serving applications allows us to:

  • Spin up new production-ready applications very quickly
  • Free up application developers to solve core business problems versus infrastructure management, monitoring and scaling concerns.
  • Guarantee a consistent, centrally managed environment for plugins for easy sharing and reuse of experiences across applications.

On the client side…

The following diagram illustrates a browser runtime view of the important components in play “under the hood” in a web application.

Bootstrapping the SPA

On the client, the CoreJS injected in the index.html bootstraps the SPA, which involves:

Registering plugin and application manifests

  • All plugin manifests for an application (as returned in the first page HTML) are registered with CoreJS, in addition to the application manifest. Thus, CoreJS knows about contributions by all plugins within a particular application context.

Establishing a base capabilities layer

  • CoreJS establishes this layer, which includes a long list of cross-cutting capabilities (experimentation, feature flags, logging, real user monitoring, analytics, etc.) The application manifest and application plugin are used to provide application-specific customizations for this layer.
  • This is foundational for the reuse of experiences and capabilities, allowing plugins to be embedded in multiple applications by providing a consistent environment, cross-cutting capabilities, and API guarantees across all applications. This also enables the standardization of operational tools (monitoring, performance metrics) as well as insights (analytics).

Establishing a shared runtime layer

  • CoreJS establishes a runtime layer that includes libraries shared between plugins (redux, React, etc.) These are not bundled in individual plugins.
  • This is critical to operating micro frontends at scale as it avoids the performance penalty of downloading and parsing common assets multiple times as plugins are loaded in the browser.

Rendering the application shell

  • CoreJS invokes the code provided in the app plugin to render the application shell.
  • Performing the initial navigation
  • Based on the browser route, CoreJS performs the SPA navigation which renders a widget in the application shell.

Plugin/Widget lifecycles

As the user interacts with the application, CoreJS dynamically loads and initializes plugins and relevant widgets within them, based on the contributions needed for the interaction.

Accessing cross-cutting capabilities in plugins

CoreJS injects a sandbox into all widgets and controllers for a plugin as it initializes them.

The sandbox is the interface of the plugin with the rest of the system, which is how the plugin accesses the base capabilities layer to consume cross-cutting capabilities e.g., the widget can access the logging capability as below:

sandbox.logger.log(“Log This!”)

  • The sandbox is contextual, so all APIs are executed in the context of the originating widget and owner plugin. For instrumentation APIs, this means that the widget and plugin context is automatically added as metadata to the payload.
  • Consequently, we’re able to provide observability tools for plugin teams to track metrics/logs using predefined dashboards and alerts.
  • We’re also able to create data scopes for query APIs on the sandbox configured for a requesting plugin.
  • And, each plugin gets its instance of the sandbox with the flexibility to add methods for sharing among widgets/controllers in the same plugin. For example, this could be beneficial for hosting a global redux store for the plugin shared among the different widgets in the plugin.

The concept of a sandbox is inspired by Scalable Javascript Application Architecture Talk by Nicolas Zakas.

Inter-plugin communication

Plugins can communicate with one another in two ways: global messaging or embedding widgets from other plugins.

Global messaging — a global messaging system based on a publish/subscribe design pattern allows for decoupled communication between plugins. This system is exposed via the pubsub namespace on the sandbox. With this, plugins can publish and subscribe to interesting events even across plugin boundaries.

Embedding widgets from other plugins — widgets in plugins can embed widgets from other plugins. The sandbox has a widgets namespace to facilitate this.

Widgets define and publish their interfaces for consumers using the concept of widget descriptors. These interfaces also follow a version scheme, which allows the developers to introduce breaking changes to a Widget’s contract without needing to update all consumers at once.

Developing and releasing a plugin

Plugins are developed and released using a standard CLI (commandline interface) module: plugin-cli. This is a command line tool similar to create-react-app that is provided to plugin developers. This ensures that:

  • Developers don’t spend countless hours trying to get frontend tooling right.
  • Best practices/enforcements for performance, testing, linting are codified and managed/updated centrally.
  • Developers have consistent experience as they move between projects at Intuit.

Plugin developers are also provided standard CI/CD (continuous integration/continuous delivery) pipelines that build, test, and release plugins based on pushes to GitHub.

The release step generates a new plugin version (based on types of changes being used using semantic release conventions). This version is entered in a plugin registry, which holds data for each plugin release. The corresponding assets are also pushed to the CDN.

Marking a plugin release live

Once a new version is released to the content delivery network (CDN) and plugin registry, the developer still has to make it go live. In a portal, the plugin developer updates the active version of a plugin to the latest released version. At this point, the central application service can start picking up this version for any applications that include that particular plugin.

Accelerating app development at scale

We hope this deep dive into our micro frontend architecture journey proves helpful for engineering organizations embarking on their own journeys — especially given today’s lack of industry standards. Ultimately, at Intuit we’ve achieved our goal to accelerate app development at scale to deliver seamless, reusable customer experiences across products. While it wasn’t easy, it was well worth it!

--

--