Shared Context in a Micro Frontends Architecture

Shahar Galukman
Pecan Tech Blog
Published in
6 min readAug 1, 2023
Shared Context in a Micro Frontend Architecture

Derived conclusions from a real case implementation

A cornerstone in almost every medium to large company’s web application would be a micro frontends architecture. This is especially true for a distributed R&D team structure, where each team has its own domain, concern, or a piece from the whole web application.

While many client-end developers agree on the architectural principle, the implementation and solution found by each tend to differ, this is true whether it is the infrastructure scaffold (e.g., bit.dev, Gatsby, or an in-house solution) or the services supporting advanced use cases existing within the different application fragments.

The latest effort I took part in at Pecan.ai took an in-house approach, and I’d like to share with you a slice of that effort, whose goal was to support a shared context between the different application fragments (a.k.a. micro frontend applications).

Before we dive in, let’s ensure we are aware of what we expected to achieve with the effort’s solution:

  • A single place to share the common application’s dependencies, services, and utilities.
  • Allow mocking of said context during local development of a single micro frontend application.
  • Supporting local, dev, and production environments.
  • Supporting different frameworks/libraries (e.g., both Angular and React).
  • The possibility to change the implementation of the common solution in a transparent manner, as in no direct impact on active consumers.

With those goals in mind, we can now delve deeper.

Defining Context

Web applications have certain needs in order to function properly. Examples of such needs could be configuration settings, authentication mechanisms, or even maintaining a specific state during usage. We decided on naming as the context of our application. Since micro frontends architecture introduces multiple applications, requiring an orchestration to be composed to a single whole, those needs are usually shared for most (if not all) of them.

We needed to decide on an approach to take:

  1. Having each micro frontend application managing its own context.
  2. Sharing the common context between all the micro frontend applications.

Examining approach #1:

Naturally, as part of a healthy development approach, each application fetches, and manages, its own application context. This ensured the separation we aimed for when structuring our development team in a distributed fashion.

The cons of this approach became evident quickly:
— Overload on our servers, due to duplication in common data fetching (e.g., fetching runtime configuration, polling for feature flags).
— Repetitive updates in each and every application when updating a common dependency.

We looked for a way to mitigate this to ensure both a healthy development cycle and a healthier runtime for our application.

Examining approach #2:

We concluded after a lot of challenges that sharing the common context between the different applications was the best approach. For example, the duplication of the polling mechanism showcased the issues far better than we could theoretically anticipate.

With the decision to share the common context of our applications, we took advantage of the way micro frontends are built.

Micro frontend architecture involves two main entities:

  • Host (a.k.a Shell) — Orchestrating and serving multiple Remote applications
  • Remote (a.k.a micro frontend application) — a small scope application served by the Host

This architecture-derived decision set us on a path where our Host application defined and managed the common context, while our Remote application consumed it.

Provider / Consumer pattern

During the solution design, we aimed for a small remote application (a micro frontend application) that would take ownership on the shared context. We then aptly named it Host Context Service. While the Host Context Service remote is responsible for creating, managing, and serving the context data and tooling, it should be accessed by an SDK defined by two layers:

1. Provider layer — would be utilized by our Host application, taking the responsibility of setting and updating the context, whether it’d be directly or indirectly by triggering runtime actions (e.g., login). Therefore our solution would need to expose a “write” functionality to the managed shared context.

2. Consumer layer — will have the “read” role of the managed context, which will then be used by our Remote applications as well by our Host.

After mapping out the desired concept, we created an SDK to serve as a facade for our mentioned Host Context Service. This SDK would be wrapped as a library that both initiates the Host Context Service as well provides access to the context created by it. Our different applications then, both Host and Remotes, would have the SDK as their dependency, the host using the provider SDK layer while remotes use the consumer SDK layer.

This SDK facade allowed us to have our different applications, whether they are Host type or Remote, to not really “know” what the Host Context Service is or how it works. The applications will rely on the SDK interface presented which limits the access to the artifacts of the Host Context Service. Therefore updating and enriching, with backward compatibility in mind, the Host Context Service could be built without direct impact on the using applications.

Glancing under the hood

In fact, the methods allowing sharing data between web applications are somewhat limited. Either they’d be event-based or served via the global scope. The more suitable for us was utilizing the global scope, as in the browser’s window object, to be the context aggregator. This meant that our Host Context Service would set its artifacts on the topmost window while the SDK provides means to access and manipulate it using a strict interface.

Supporting the asynchronous nature of fetching data or manipulating it on runtime was made using Promises. Therefore the SDK ensured any access to the context consumption would be Promise based, allowing consumers to get the desired data in a reliable fashion.

In fact, the asynchronous nature of the solution comes from the base need of the SDK loading the Host Context Service. As our service was created as a Remote application, loading it dynamically was not an easy feat, as importing dynamically is not an out-of-the-box feature in the JavaScript ecosystem when you are Webpack dependent.

function importHostContextService() {
return import(/* webpackIgnore: true */ '/_host-context/remoteEntry.js')
.then((m) => m.get('./HostContextService'))
.then((m) => m().HostContextService)
.catch((_) => Promise.reject(new Error('Failed retrieving HostContextService')));
}

Luckily, Webpack provides a means to override its behaviour. Using the webpackIgnore comment “hack”, we were able to build our SDK library, which loads our Host Context Service Remote where its actual location would be determined by our different proxy servers.

Going forward

Pecan.ai embraced the domain-driven approach of application development. The client-end was not left behind and was considered to be part of the same approach. While even though this distributed approach tends to create its own problems, we the developers are encouraged to find solutions that aid in facing those problems.

The Host Context is just one example of such a solution, and we are confident it will serve as a solid foundation for us to solve further similar issues relating common data and tools in the micro frontend world. We strive for a solution with major impact on the infrastructure behaviour as well with minimal impact on the development cycle.

While different methodologies can vary, especially with more recent technologies (e.g., micro frontends), the problems tend to resemble each other, and solutions can be slightly adjusted to accommodate them. I suggest considering what fits every solution you encounter and ensuring that the concepts are aligned correctly to address your specific application issues.

--

--