Breaking Monoliths: Exploring Micro-Frontends with Single-Spa

Sukriti Verma
Deutsche Telekom Digital Labs
6 min readJun 20, 2024

In today’s digital landscape, monolithic systems are giving way to a more modular and flexible architecture: Micro-Frontend. The rise of Micro-Frontends promises scalability, maintainability, and a streamlined developer experience. Gone are the days of unwieldy single-page applications (SPAs) bogged down by their own weight. With the rise of Micro-Frontends, we’re ushering in a newfront-endd developmenterae that promises scalability, maintainability, and a more streamlined developer experience.

The Monolithic Conundrum:

Traditionally, our applications were colossal entities, encompassing the frontend, backend, databases, APIs. While powerful, these monolithic systems faced significant challenges:

  • The frontend ballooned in size, becoming complex and cumbersome to manage.
  • Dependencies proliferated within the SPA, making upgrades and maintenance daunting.
  • Collaboration amongst developers became inefficient due to vast and intricate frontend codebase.

Enter Micro-Frontends:

Micro-Frontend architecture transforms massive monoliths into agile, self-contained modules. These modules can be developed and deployed independently and thus offer several benefits::

  • Scalability: As the application grows, so too can its individual parts. Micro-Frontends scale effortlessly, accommodating new features and updates without disrupting the entire system.
  • Flexibility: Each Micro-Frontend operates autonomously, allowing developers to work on specific modules without treading on each other’s toes. This modular approach fosters agility and adaptability.
  • Maintainability: Say goodbye to the headache of unwieldy codebases. With Micro-Frontends, maintenance becomes a breeze. Bug fixes, updates, and enhancements can be implemented swiftly and efficiently.
  • Isolation: By encapsulating functionality within distinct modules, Micro-Frontends reduce the risk of cascading failures. A hiccup in one module won’t bring down the entire application.

Unveiling Single-Spa:

Single-Spa, a framework-agnostic JavaScript library, simplifies the orchestration of multiple Micro-Frontends into a cohesive whole. It allows seamless integration of frontend modules built with different frameworks like React, Angular, and Vue.js.

Embracing the Future:

Adopting Micro-Frontends with Single-Spa signifies a shift towards modularity, agility, and scalability. This approach empowers teams to innovate, iterate, and thrive.

The Workflow

For Each Micro-Frontend (MF):

  • Owned by an Independent Team
  • Operates with its Dedicated CI/CD Pipeline
  • Deployed to its Designated Cloud Environment
  • Utilizes Shared Artifacts for Consistency
  • Maintains its Unique Repository

Responsibilities of Each Team:

  • Sets Clear Objectives and Goals
  • Ensures the Quality of Their Respective Code
  • Defines and Adheres to their Tech Stack
  • Shares Knowledge and Insights Across Teams
How the Teams are working in the same app with different MFs

Deep Dive into the Architecture

Root Config:

The root-config is the backbone, responsible for loading and initializing Single-Spa. It acts as an independent container devoid of business logic, allowing the registration, mounting, unmounting, and bundling of individual Micro-Frontends using their URLs.

Components of Root Config:

  • “Single-Spa Framework: A robust tool for constructing Micro-Frontends and integrating them into an SPA. It applies a consistent lifecycle to each application, enabling each app to react to URL routing events and manage its presence in the DOM.”
  • Shell/Container Application: Single-spa crafts a “shell” or container application, which serves as the orchestrator for various micro frontends as they are required. This architecture fosters modularity and decoupling, paving the way for a more flexible and maintainable frontend system.

Importmap.json:

This file acts as a directory, storing the names of Micro-Frontends (MFs) and their deployed URLs. It enables seamless registration of these MFs within Single-spa.

Config.json:

Within the root-config, config.json organizes the layout of MFs, defining settings for their arrangement in a grid system. It requires specific DOM ID selectors for MFs to attach.

SystemJS:

The root-config utilizes SystemJS with the import map and config.json. This technique loads all MFs via their URLs, ensuring an efficient and cohesive frontend application.

<meta content="systemjs-importmap" name="importmap-type"/>
<script id="importmap" type="systemjs-importmap"></script>

Benefits of SystemJS Integration

  1. Dynamic Module Loading: Enable lazy-loading of Micro-Frontends (MFs) based on specific route matches, optimizing the initial load of the root-config.
  2. Framework Flexibility: SystemJS is framework-agnostic, supporting MFs developed in various JavaScript frameworks or vanilla JS.
  3. Dependency Management: Control how dependencies are loaded, preventing duplicate copies of libraries across multiple MFs. This optimization enhances the application’s size and performance. Instructs webpack to exclude certain dependencies during the build, fetching them from the browser via webpack externals for each MF.
<script type= “systemjs-importmap”> 
{
"imports": {
"vue2": "./js/vue.min.js",
"vue3": "./js/vue3.3.4.min.js",
"vue-router": "./js/vue-router.min.js",
"vue-router4": "./js/vue-router4.2.2.min.js",
"react": "./js/react.production.umd.min.js",
"react-dom": "./js/react-dom.production.umd.min.js",
"microfrontendA”: “https://microfrontendA.com/js/app.js”
}
</script>

Summary:

In the setup described, SystemJS resolves the minified build of the package in the browser, delivering it to the application during runtime. Each significant shared dependency — such as React, Vue, or Angular libraries — functions as an in-browser module.

Challenges in the Architecture and Solutions

The current Micro-Frontends (MFs) setup, residing in separate repositories, poses challenges in their communication. However, there are effective resolutions available:

Process Storage:

Utilizing process-storage provides a solution to share essential details among MFs. This storage, tailored to specific instances based on business needs, is managed by a unique identifier known as the processID. Initially, one MF creates this ID and passes it within the URL, enabling subsequent MFs to access it seamlessly.

Root Store:

The root-store serves as a versatile JavaScript object, offering a centralized location to store and share information across MFs. Equipped with various methods for data storage and updates, it streamlines communication and enhances collaboration among modules.

Root-Store Features:

  1. isAppReady(): This function provides specific information on whether a particular Micro-Frontend (MF) is mounted, unmounted, or removed due to a LoadError.
// implementation in rootConfig 

window.addEventListener('single-spa:before-app-change', (evt: Event) => {
const customEvent = evt as CustomEvent; // it will give the entries in object form
const entries = Object.entries(customEvent.detail.newAppStatuses);
// looping through the entries
entries.forEach((mfName, mfStatus) => {
if(mfStatus === "mounted") // resolve mfName
else reject mfName
});

Note: mfNames are actually the promises that needs to be resolved or rejected based on the mfStatus.


// implementation in MF
mounted(){
if(props.rootStore.isAppReady(“MFA”).then(res=>{
// do something when application loaded
}).catch(err=>{
// do when not loaded
)
}

2. Event Bus:

The pub-sub based eventBus object in the root-config comprises two essential parts:

  • emit: Sends the event-name and payload.
  • on: Receives the payload in the callback based on the event-name.

Shared along with other common properties during the Single-spa registry process, it facilitates communication among registered MFs.

To know more about eventbus and how it is used in microfrontends in user journey analytics you can see

import {EventEmitter} from 'events'; 
import {registerApplication } from 'single-spa';

const eventBus: new EventEmitter(),

registerApplication(
app.name,
() => System.import(app.name).catch(err => {
return null;
}),
{
eventBus: eventBus,
}
);

And there you have it — the root-config is all set and ready to go!

“But hold on! Think it’s all smooth sailing? Well, we’ve got a few surprises in store for you.”

Implementation Guidelines for Micro-Frontends

Each MF also must abide by certain rules to be mounted on the root-config. The set of rules are as follows: -

For a Micro-Frontend (MF) to be successfully mounted on the root-config, it must adhere to specific rules. These guidelines are as follows:

  • Install Single-Spa Library: Each MF should install its corresponding single-spa library, tailored to its framework, as a dependency. This ensures seamless integration. For instance:
  • A Vue application would utilize single-spa-vue.
  • A React application would opt for single-spa-react.

Additionally, MFs are required to destructure all crucial lifecycle methods within their entry point file, aligning with the overall structure of the Micro-Frontend architecture.

import singleSpaVue from "single-spa-vue";

const vueLifecycles = singleSpaVue({
Vue,
appOptions: {
el: "#microfrontendA",
i18n,
render(h: any) {
return h(App, {
props: {
eventBus: (this as any).$data.eventBus,
// receive more props sent by root-config
},
});
},
},
});

export const bootstrap = vueLifecycles.bootstrap;
export const mount = vueLifecycles.mount;
export const unmount = vueLifecycles.unmount;

Finalizing Micro-Frontend Initialization

  1. Utilizing EventBus:
  • The EventBus has been injected into the props of this MF’s Vue instance. It must be accessed using props.eventBus.

2. “el” Property Usage:

  • The “el” property assists Single-spa in the root-config to create a placeholder for “microfrontendA” within the DOM.

With these configurations in place, “microfrontendA” is now deployed and prepared for mounting into the root-config through the importmap setup.

And there you have it — all the essential setup steps are complete to initialize and run the micro-frontends.
Get set, Code! 🚀

Conclusion

Adopting Micro-Frontends with Single-Spa heralds a paradigm shift in frontend architecture. It moves us towards modularity, agility, and scalability, allowing our applications to evolve and adapt with ease.

--

--