Scaling Micro-Frontends With Orchestrators
A traditional micro-frontend provides the opportunity for independence and isolation among micro-frontends. Learn why it is an excellent choice.
Join the DZone community and get the full member experience.
Join For FreeMulti-divisional organizations and team-building web applications with distributed teams are adopting micro-frontend architectures for front-end development similar to microservices. Large enterprises are seeing massive value in shifting from sequential to parallel development by architecting web experiences as independent, re-deployable, micro-frontend components.
What Are Micro-Frontends?
Micro-frontends are frontend components packaged as a mini-app to deliver a functional capability and web experience to end users. Each micro-frontend is deployable on its own and comprises user interactions and events representing a set of logical use cases for a given ecosystem. Micro-frontends share standard building blocks in the form of design system elements and dependencies in the form of self-contained packaged libraries. Micro-frontends are modular and contribute to scaling development to build parallel web experiences within an ecosystem. From a high-level skeleton code standpoint, here is a simple Micro-frontend with its package.json reflecting dependencies or scripts consumed by the micro-frontend as below.
//package.json
{
"name": "micro-frontend1",
"version": "1.0.0",
"description": "Micro frontend1 with shared dependencies",
"main": "index.js",
"scripts": {
"start": "http-server"
},
"dependencies": {
"http-server": "^0.12.3", // Library for a local server
"redux": "^4.1.1", // Library for state management
"lerna": "^4.0.0", // Library for managing multiple packages
"storybook": "^6.4.3", // Library for shared UI component library
"jest": "^27.4.0", // Library for testing
"cypress": "^9.1.0", // Library for end-to-end testing
"auth0-js": "^9.16.2", // Library for authentication
"webpack": "^5.62.3", // Library for building and code splitting
"babel-core": "^6.26.3" // Library for transpiling code
}
}
index.html
<!DOCTYPE html>
<html>
<head>
<title>Micro-Frontend 1</title>
<link rel="stylesheet" type="text/css" href="styles.css">
</head>
<body>
<div id="micro-frontend1-container">
<h1>Hello from Micro-Frontend 1!</h1>
</div>
<script src="mfe1.js"></script>
</body>
</html>
mfe1.js
// Micro-Frontend1 JavaScript
console.log('Micro-Frontend1 Loaded.');
//Micro-frontend1 functionality goes here.
styles.css
/* Micro-Frontend1 Styles */
#micro-frontend1-container {
background-color: #f0f0f0;
padding: 20px;
border: 1px solid #ddd;
}
From a high-level diagram, a simple web page with multiple micro-frontends will look like the one below.
Figure 1
Common Dependencies With Micro-Frontends
In a real-world ecosystem, most micro-frontends depend on multiple dependencies and packages. These include state management tools like Redux or Mobx communication mechanisms like web sockets or custom event buses, UI component libraries (design system elements) like Lerna, or storybooks testing frameworks like enzyme, jest, cypress, or lighthouse—additionally, authentication/authorization libraries such as Okta, Auth0, or Firebase. Build tools like Babel and Webpack also play a role alongside UI frameworks such as Semantic UI, Bootstrap, or Material UI.
Figure 2
Risks With Multiple Dependency Injections With Micro-Frontends
When an ecosystem use cases spread across multiple cross-functional teams, and each use case has logical workflows that render on a web page, micro-frontends aid in vertical development, which results in embedding multiple micro-frontends on the same page and eventually results in various dependencies injections leading to:
- Dependency conflicts
- Version challenges
- Performance overheads
- Debugging complexity and
- Maintainability issues.
These are common pitfalls leading to longer testing cycles and time to market and pose a security risk due to complicated dependency management.
Addressing the Risks With Orchestrators
A micro-frontend architecture enables a loose pattern of rules of communication, integration, and interaction between each of these micro-frontends if we treat one of the micro-frontends as a container.
An "Orchestrator" is a central micro-frontend that serves as the entry point for a web application built using the micro-frontend architecture. It's sometimes referred to as the "shell" micro-frontend. The primary role of the shell micro-frontend is to coordinate and manage the integration of other micro-frontends, resulting in a cohesive user experience but majorly eliminating the risks of multiple dependency injections. An orchestrator micro-frontend oversees and coordinates the operations of micro-frontends. This approach offers benefits:
- Consistent user experience: The orchestrator ensures that users have a consistent experience by providing a layout, navigation, and overall structure for the application. Users perceive an interface even though different micro-frontends contribute to the content.
- Seamless visual appeal: By centralizing components such as the header, footer, and navigation menus, the orchestrator maintains an appeal throughout the application. This aspect is crucial for branding purposes and enhancing user experience.
- Efficient code reusability: The orchestrator efficiently manages shared dependencies, libraries, and code to minimize duplication and promote reusability. Child micro-frontends can rely on these shared resources effectively.
- Isolated development: Child micro-frontends can be maintained independently without worrying about the application's structure and layout. Development teams can focus on features or sections of the application with ease.
- Encapsulation: The orchestrator handles the routing and integration logic, providing a structure for the child micro-frontends. This modularity significantly improves maintainability and testability.
- Parallel development: Different development teams can work simultaneously on application components. Parallel development speeds up development and release cycles since modifications in one micro-frontend typically do not affect others.
- Scalability: The orchestrator efficiently manages micro-frontends as the application expands. This scalability is crucial for handling intricate web applications.
- Managing state: The orchestrator effectively handles state management, ensuring that different application parts can access shared state or interact with each other through a system.
- Error handling and logging: Error handling and logging mechanisms can be implemented within the orchestrator, allowing for reporting and debugging throughout the application.
- Performance improvements: The orchestrator incorporates performance optimization techniques such as loading and code splitting to enhance the application's performance and provide a user experience.
- A/B testing: The orchestrator allows for the management of experimentation, feature flagging, enabling controlled feature rollouts, and A/B testing across different application sections.
- Authentication and authorization: The orchestrator handles User authentication and authorization, ensuring access control and session management throughout the application.
- Flexibility: The orchestrator is adaptable to frontend frameworks, libraries, and technologies utilized in parts of the application. It allows for flexibility in choosing the technologies.
- Isolation: By isolating child micro-frontends, the orchestrator reduces the risk of conflicts or issues arising when different application components interact directly.
- Ecosystem management: It provides an entry point to oversee the entire micro-frontend ecosystem, making it easier to understand, document, and manage effectively.
In a frontend architecture, an orchestrator micro-frontend holds a position as it brings several advantages to large-scale web applications. It effectively combines the benefits of micro-frontends and modular development while ensuring a user experience.
Applying the orchestrator method to the existing micro-frontends listed in Figure 2 will now look as below.
Figure 3
And here is how an orchestrator micro-frontend map offers a method to handle the overall structure of the application and ensure a seamless user experience. Orchestrators are particularly useful when having a cohesive and unified user interface is essential and when there is a need for shared resources and consistency across micro-frontends.
<!DOCTYPE html>
<html>
<head>
<title>Orchestrator Micro Frontend</title>
</head>
<body>
<header>
<nav>
<ul>
<li><a href="#/micro-frontend1">Home</a></li>
<li><a href="#/micro-frontend2">Account</a></li>
<li><a href="#/micro-frontend3">Products</a></li>
<li><a href="#/micro-frontend4">Dashboard</a></li>
<li><a href="#/micro-frontend5">Search</a></li>
<li><a href="#/micro-frontend6">Navigation</a></li>
</ul>
</nav>
</header>
<div id="micro-frontend-orchestrator"></div>
<script>
const mfeContainer = document.getElementById('micro-frontend-orchestrator');
// Client-side routing to load child micro-frontends
function loadMfe(route) {
const mfeMap = {
'/home': 'home-micro-frontend1.html',
'/accounts': 'accounts-micro-frontend2.html',
'/products': 'products-micro-frontend3.html',
};
const mfe = mfeMap[route];
if (mfe) {
mfeContainer.innerHTML = ''; // Clear the container
const script = document.createElement('script');
script.src = mfe;
mfeContainer.appendChild(script);
} else {
mfeContainer.innerHTML = 'Page not found';
}
}
// Initial load based on the current route
loadMicroFrontend(window.location.hash);
// Listen for route changes
window.addEventListener('hashchange', () => {
loadMicroFrontend(window.location.hash);
});
</script>
</body>
</html>
A traditional micro-frontend provides the opportunity for independence and isolation among micro-frontends, making it an excellent fit for situations where each requires autonomy and separation.
The decision on which approach to choose depends on the application's needs. Traditional micro-frontends provide modularity and independence, whereas orchestrator micro-frontends prioritize user experience and shared resources. Specific projects may combine both approaches in real-world scenarios to find ground between independence and consistency.
Opinions expressed by DZone contributors are their own.
Comments