Improving Resiliency and Failovers with Module Federation

Module Federation is a feature of Webpack that allows you to dynamically load modules from different bundles at runtime. This can help you improve the resiliency and failover capabilities of your web applications by reducing the impact of network failures, server errors, or code bugs.

Resiliency is the ability of a system to recover from failures and continue to function. Failovers are the mechanisms that allow a system to switch to a backup or alternative mode of operation when a failure occurs.

In this guide, you will learn how to use Module Federation to:

  • Load modules from remote sources with fallbacks

  • Handle errors and retries when loading modules

  • Implement a circuit breaker pattern to avoid cascading failures

  • Use service workers to cache modules and serve them offline

Prerequisites

To follow this guide, you need to have:

  • A basic understanding of Webpack and Module Federation

  • Node.js and npm installed on your machine

  • A code editor of your choice

Setting up the project

To demonstrate how Module Federation works, we will create a simple web application that consists of two parts: a host app and a remote app. The host app will load a module from the remote app and display its content on the page. The remote app will expose a module that returns a greeting message.

To set up the project, follow these steps:

  1. Create a new folder called module-federation-demo and navigate to it in your terminal.

  2. Run npm init -y to create a package.json file with default values.

  3. Run npm install webpack webpack-cli webpack-dev-server html-webpack-plugin to install the dependencies.

  4. Create two subfolders called host and remote inside the module-federation-demo folder. These will contain the source code for the host app and the remote app respectively.

  5. Create a webpack.config.js file in each subfolder with the following content:

  • host webpack.config

  • remote webpack.config

// webpack.config.js for host app
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { ModuleFederationPlugin } = require("webpack").container;

module.exports = {
  mode: "development",
  devServer: {
    port: 3000,
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./src/index.html",
    }),
    new ModuleFederationPlugin({
      name: "host",
      remotes: {
        remote: "remote@http://localhost:3001/remoteEntry.js",
      },
    }),
  ],
};
// webpack.config.js for remote app
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { ModuleFederationPlugin } = require("webpack").container;

module.exports = {
  mode: "development",
  devServer: {
    port: 3001,
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./src/index.html",
    }),
    new ModuleFederationPlugin({
      name: "remote",
      filename: "remoteEntry.js",
      exposes: {
        "./greeting": "./src/greeting.js",
      },
    }),
  ],
};

The ModuleFederationPlugin is the main plugin that enables Module Federation. It takes some options that define how the modules are exposed and consumed.

In the host app, we specify a remotes option that tells Webpack where to find the remote app’s entry point. The syntax is <name>@<url>, where <name> is an alias that we can use to import modules from the remote app, and <url> is the URL of the remote app’s entry point.

In the remote app, we specify a filename option that tells Webpack what name to use for the entry point file. We also specify an exposes option that tells Webpack what modules we want to expose to other apps. The syntax is <name>:<path>, where <name> is an alias that other apps can use to import our module, and <path> is the relative path to our module file.

  1. Create an index.html file in each subfolder with the following content:

  • host index.html

  • remote index.html

<!-- index.html for host app -->
<html>
  <head>
    <title>Host App</title>
  </head>
  <body>
    <h1>Host App</h1>
    <div id="container"></div>
    <script src="main.js"></script>
  </body>
</html>
<!-- index.html for remote app -->
<html>
  <head>
    <title>Remote App</title>
  </head>
  <body>
    <h1>Remote App</h1>
    <script src="remoteEntry.js"></script>
  </body>
</html>

The index.html files are the entry points for the web applications. They load the respective JavaScript bundles generated by Webpack.

  1. Create a src folder in each subfolder and add the following files:

Host:

index.js
// src/index.js for host app
import("./bootstrap");
bootstrap.js
// src/bootstrap.js for host app
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

ReactDOM.render(<App />, document.getElementById("container"));
App.js
// src/App.js for host app
import React, { useEffect, useState } from "react";

const App = () => {
  const [greeting, setGreeting] = useState("");

  useEffect(() => {
    // Load the greeting module from the remote app
    import("remote/greeting")
      .then((module) => {
        // Call the module's default export function and set the greeting state
        setGreeting(module.default());
      })
      .catch((error) => {
        // Handle any errors while loading the module
        console.error(error);
        setGreeting("Oops, something went wrong!");
      });
  }, []);

  return (
    <div>
      <p>The greeting from the remote app is:</p>
      <p>{greeting}</p>
    </div>
  );
};

export default App;

Remote

index.js
// src/index.js for remote app
import("./bootstrap");
bootstrap.js
// src/bootstrap.js for remote app
import React from "react";
import ReactDOM from "react-dom";
import Greeting from "./Greeting";

ReactDOM.render(<Greeting />, document.getElementById("root"));
greeting.js
// src/Greeting.js for remote app
import React from "react";

const Greeting = () => {
  return <h2>Hello from the remote app!</h2>;
};

export default Greeting;

The src/index.js files are the entry points for the JavaScript bundles. They import a bootstrap.js file that contains the actual logic of the apps. This is a common pattern to enable asynchronous loading of modules with Module Federation.

The src/bootstrap.js files for the host app and the remote app use React to render some components on the page. The host app imports a App.js file that contains a component that loads the greeting module from the remote app and displays it on the page. The remote app imports a Greeting.js file that contains a component that renders a greeting message on the page.

The src/greeting.js file for the remote app is the module that we expose to other apps. It exports a function that returns a greeting message.

  1. Run npm run dev in both subfolders to start the development servers. You should see something like this in your browser:

You have successfully set up a basic Module Federation project. Next, we will see how to improve its resiliency and failover capabilities.

== Loading modules with fallbacks

One of the benefits of Module Federation is that it allows you to load modules from remote sources without having to bundle them with your application. This can reduce your bundle size and improve your performance.

However, this also introduces some risks. What if the remote source is unavailable or slow? What if the module fails to load or execute? How can you ensure that your application still works in these scenarios?

One way to handle these situations is to provide fallbacks for your modules. A fallback is an alternative module that you can load in case the original module fails. For example, you can provide a local copy of the module, or a mock module that returns some dummy data.

To use fallbacks with Module Federation, you can use the fallback option of the ModuleFederationPlugin. This option allows you to specify an object that maps remote names to fallback modules. For example, you can modify your host app’s Webpack configuration like this:

// webpack.config.js for host app
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { ModuleFederationPlugin } = require("webpack").container;

module.exports = {
  mode: "development",
  devServer: {
    port: 3000,
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./src/index.html",
    }),
    new ModuleFederationPlugin({
      name: "host
      remotes: {
        remote: "remote@http://localhost:3001/remoteEntry.js",
      },
      // Specify the fallback modules for the remote app
      fallback: {
        remote: "./src/fallback.js",
      },
    }),
  ],
};

The fallback option tells Webpack to load the src/fallback.js file as a fallback for the remote app. This file should export the same modules as the remote app, but with different implementations. For example, you can create a src/fallback.js file like this:

// src/fallback.js for host app
// Export a mock greeting module that returns a static message
export const greeting = () => {
  return "Hello from the fallback module!";
};

Now, if the remote app fails to load or expose the greeting module, the host app will use the fallback module instead. You can test this by stopping the remote app’s server and refreshing the host app’s page. You should see something like this:

You have successfully implemented a fallback mechanism for your modules. Next, we will see how to handle errors and retries when loading modules.

## Handling errors and retries when loading modules

Another way to improve the resiliency of your web application is to handle errors and retries when loading modules from remote sources. This can help you recover from temporary failures or network issues.

To handle errors and retries with Module Federation, you can use the import() function that Webpack provides. This function returns a promise that resolves to the module object if the module is loaded successfully, or rejects with an error if the module fails to load. You can use the catch() method of the promise to handle any errors and retry loading the module if needed.

For example, you can modify your host app’s App.js file like this:

// src/App.js for host app
import React, { useEffect, useState } from "react";

const App = () => {
  const [greeting, setGreeting] = useState("");
  const [retryCount, setRetryCount] = useState(0);

  useEffect(() => {
    // Load the greeting module from the remote app
    import("remote/greeting")
      .then((module) => {
        // Call the module's default export function and set the greeting state
        setGreeting(module.default());
      })
      .catch((error) => {
        // Handle any errors while loading the module
        console.error(error);
        // Check if we have reached the maximum number of retries
        if (retryCount < 3) {
          // Increment the retry count
          setRetryCount(retryCount + 1);
          // Retry loading the module after 1 second
          setTimeout(() => {
            import("remote/greeting").then((module) => {
              setGreeting(module.default());
            });
          }, 1000);
        } else {
          // Give up and show an error message
          setGreeting("Sorry, we could not load the greeting module.");
        }
      });
  }, [retryCount]);

  return (
    <div>
      <p>The greeting from the remote app is:</p>
      <p>{greeting}</p>
    </div>
  );
};

export default App;

The App.js file now uses a retryCount state to keep track of how many times it has tried to load the greeting module. If the module fails to load, it checks if the retry count is less than 3. If so, it increments the retry count and tries to load the module again after 1 second. If not, it gives up and shows an error message.

You can test this by simulating a network failure in your browser’s developer tools. You should see something like this:

You have successfully implemented an error handling and retry mechanism for your modules. Next, we will see how to implement a circuit breaker pattern to avoid cascading failures.

## Implementing a circuit breaker pattern to avoid cascading failures

Another way to improve the resiliency of your web application is to implement a circuit breaker pattern to avoid cascading failures. A circuit breaker is a design pattern that monitors the health of a remote service and prevents excessive requests when the service is unhealthy. This can help you avoid overloading the service or wasting resources when the service is unlikely to respond.

To implement a circuit breaker pattern with Module Federation, you can use a third-party library called opossum. This library provides a circuitBreaker function that wraps a promise-based function and monitors its success and failure rates. It also provides some options to configure the circuit breaker’s behavior, such as the failure threshold, the timeout duration, and the reset timeout.

For example, you can modify your host app’s App.js file like this:

// src/App.js for host app
import React, { useEffect, useState } from "react";
import { circuitBreaker } from "opossum";

const App = () => {
  const [greeting, setGreeting] = useState("");

  useEffect(() => {
    // Create a circuit breaker for loading the greeting module
    const breaker = circuitBreaker(() => import("remote/greeting"), {
      // Set the failure threshold to 50%
      errorThresholdPercentage: 50,
      // Set the timeout duration to 3 seconds
      timeout: 3000,
      // Set the reset timeout to 10 seconds
      resetTimeout: 10000,
    });

    // Load the greeting module using the circuit breaker
    breaker
      .fire()
      .then((module) => {
        // Call the module's default export function and set the greeting state
        setGreeting(module.default());
      })
      .catch((error) => {
        // Handle any errors while loading the module
        console.error(error);
        // Check if the circuit breaker is open
        if (breaker.opened) {
          // Show a message that the service is unavailable
          setGreeting("The remote service is unavailable. Please try again later.");
        } else {
          // Show a message that something went wrong
          setGreeting("Oops, something went wrong!");
        }
      });
  }, []);

  return (
    <div>
      <p>The greeting from the remote app is:</p>
      <p>{greeting}</p>
    </div>
  );
};

export default App;

The App.js file now uses a circuit breaker to load the greeting module. The circuit breaker will monitor the success and failure rates of loading the module and open or close accordingly. If the circuit breaker is open, it will reject any requests immediately and show a message that the service is unavailable. If the circuit breaker is closed, it will try to load the module normally and show a message that something went wrong if it fails.

You can test this by simulating a network failure in your browser’s developer tools. You should see something like this:

You have successfully implemented a circuit breaker pattern for your modules. Next, we will see how to use service workers to cache modules and serve them offline.

## Using service workers to cache modules and serve them offline

Another way to improve the resiliency of your web application is to use service workers to cache modules and serve them offline. A service worker is a script that runs in the background and intercepts network requests. It can cache the responses and serve them from the cache when the network is unavailable or slow. This can help you improve the performance and reliability of your web application.

To use service workers with Module Federation, you can use a third-party library called workbox. This library provides some tools and modules to simplify the creation and management of service workers. It also provides some strategies to control how the service worker handles network requests and cache responses.

For example, you can modify your host app’s Webpack configuration like this:

// webpack.config.js for host app
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { ModuleFederationPlugin } = require("webpack").container;
const { InjectManifest } = require("workbox-webpack-plugin");

module.exports = {
  mode: "development",
  devServer: {
    port: 3000,
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./src/index.html",
    }),
    new ModuleFederationPlugin({
      name: "host",
      remotes: {
        remote: "remote@http://localhost:3001/remoteEntry.js",
      },
      fallback: {
        remote: "./src/fallback.js",
      },
    }),
    // Use the InjectManifest plugin to generate a service worker
    new InjectManifest({
      swSrc: "./src/sw.js",
      swDest: "sw.js",
    }),
  ],
};

The InjectManifest plugin is a plugin that generates a service worker based on a source file. It takes some options that define the source and destination of the service worker file.

In this case, we specify a swSrc option that tells Webpack to use the src/sw.js file as the source of the service worker. We also specify a swDest option that tells Webpack what name to use for the generated service worker file.

Next, we need to create a src/sw.js file that contains the logic of the service worker. We can use the workbox modules to implement some caching strategies for our modules. For example, we can create a src/sw.js file like this:

// src/sw.js for host app
import { precacheAndRoute } from "workbox-precaching";
import { registerRoute } from "workbox-routing";
import { StaleWhileRevalidate } from "workbox-strategies";

// Precache and route the files generated by Webpack
precacheAndRoute(self.__WB_MANIFEST);

// Register a route for remote modules using a stale-while-revalidate strategy
registerRoute(
  ({ url }) => url.origin === "http://localhost:3001",
  new StaleWhileRevalidate()
);

The sw.js file imports some modules from workbox and uses them to implement some caching strategies for our modules.

The precacheAndRoute function takes an array of files to precache and route. In this case, we pass it the self.__WB_MANIFEST variable, which is an array of files generated by Webpack. This will ensure that our host app’s files are cached and served from the cache when offline.

The registerRoute function takes a matching function and a caching strategy. In this case, we pass it a function that matches any requests to the remote app’s origin, and a StaleWhileRevalidate strategy. This will ensure that any remote modules are cached and served from the cache if available, while also updating the cache in the background if possible.

Finally, we need to register the service worker in our host app’s index.html file. We can add a script tag like this:

<!-- index.html for host app -->
<html>
  <head>
    <title>Host App</title>
  </head>
  <body>
    <h1>Host App</h1>
    <div id="container"></div>
    <script src="main.js"></script>
    <!-- Register the service worker -->
    <script>
      if ("serviceWorker" in navigator) {
        window.addEventListener("load", () => {
          navigator.serviceWorker.register("/sw.js");
        });
      }
    </script>
  </body>
</html>

The script tag checks if the browser supports service workers and registers the sw.js file as a service worker.

Now, if you reload your host app’s page, you should see something like this in your browser’s developer tools:

You have successfully registered a service worker that caches your modules and serves them offline. You can test this by simulating an offline mode in your browser’s developer tools. You should see something like this:

You have successfully implemented a service worker to cache modules and serve them offline.

## Conclusion

In this guide, you learned how to use Module Federation to improve the resiliency and failover capabilities of your web applications. You learned how to:

  • Load modules from remote sources with fallbacks

  • Handle errors and retries when loading modules

  • Implement a circuit breaker pattern to avoid cascading failures

  • Use service workers to cache modules and serve them offline

You can find the source code for this guide on GitHub.

We hope you enjoyed this guide and learned something new. If you have any feedback or questions, please let us know in the comments below. Thank you for reading! 😊