Module Federation: Component Level Ownership

In this guide, we will delve into the concept of Component Level Ownership in the context of Module Federation, a feature introduced in Webpack 5. You will learn how this ownership model works, its benefits, and how to implement it within your projects.

Introduction to Component Level Ownership

Component Level Ownership is a design pattern that allows individual components to be owned, managed, and versioned independently within a federated architecture. This approach empowers developers to create more maintainable and scalable applications by encapsulating the logic and dependencies of each component, reducing the risk of conflicts and promoting better collaboration.

By leveraging Component Level Ownership, teams can work on different parts of the application simultaneously, enabling faster development cycles and increased agility.

A good example of this would be the checkout team, they own everything around cart, payment, and quantities. A different team owns the shop experience, but that app needs an "add to cart" modal. Before federation, this meant old-school npm packages for the shared part or literally having to PR some component you own in someone else’s app.

With Module Federation this is achievable at runtime in a very convenient way:

import CartModal from "checkoutTeam/components/cart-modal"

Implementing Component Level Ownership

To achieve Component Level Ownership, each application needs to configure its Webpack Module Federation plugin with two options: name and exposes. The name option defines the unique identifier of the application, which will be used by other applications to reference it. The exposes option defines a mapping of keys to local files that contain the components to be exposed. For example:

new ModuleFederationPlugin({
  name: "app1",
  exposes: {
    "./Heroes": "./src/app/heroes/heroes.component.ts",
    "./Villains": "./src/app/villains/villains.component.ts",
  },
});

This configuration tells Webpack that app1 exposes two components: Heroes and Villains, which are located in the specified files. These components can be imported by other applications using the syntax app1/ComponentName, where app1 is the name of the application and ComponentName is the key of the component in the exposes option.

For example, another application called app2 can import and use the Heroes component from app1 like this:

import { Heroes } from "app1/Heroes";

// use Heroes component in app2

To make this work, app2 also needs to configure its Webpack module federation plugin with two options: remotes and shared. The remotes option defines a mapping of names to URLs that point to the remote entry files of other applications. The remote entry file is a special file generated by Webpack that contains information about the exposed modules and how to load them. The shared option defines which modules are shared between the applications, such as vendor libraries or common dependencies.

For example:

new ModuleFederationPlugin({
  remotes: {
    app1: "app1@http://localhost:3000/remoteEntry.js",
  },
  shared: ["@angular/core", "@angular/common", "@angular/router"],
});

This configuration tells Webpack that app2 can import modules from app1, which has a remote entry file at the specified URL. It also tells Webpack that both applications share some Angular modules, so they don’t need to load them twice.

Benefits of Component Level Ownership

  1. Improved Scalability: As your application grows, Component Level Ownership allows you to maintain a modular architecture, preventing tightly-coupled dependencies and reducing complexity by breaking down an application into smaller components, so you can better allocate resources and scale your application as needed.

  2. Maintainability: By encapsulating the logic and dependencies of each component, you can improve code readability and maintainability, making it easier to update and refactor components as needed. Additionally, assigning ownership to individual components simplifies the process of maintaining and updating the codebase too, as it is clear who is responsible for each component.

  3. Team Collaboration: Component Level Ownership enables better collaboration between teams by allowing them to work independently on their respective components, streamlining the development process and reducing the risk of conflicts.

  4. Performance: With dynamic imports and lazy loading, you can optimize your application’s performance by only loading the required components and their dependencies as needed.

What are the challenges of Component Level Ownership?

Component Level Ownership also comes with some challenges that need to be addressed:

  • It requires careful design and documentation of the exposed components, as they need to have a clear interface and contract with other applications.

  • It introduces some complexity and overhead in the configuration and orchestration of the applications, as they need to know where and how to find and load each other’s components.

  • It may cause some compatibility issues or conflicts between different versions or implementations of the same component, especially if they are not properly isolated or scoped.

How can we overcome these challenges?

There are some best practices and tools that can help us overcome these challenges and leverage Component Level Ownership effectively:

  • Use standalone components that have minimal dependencies and side effects, and follow a single responsibility principle. This will make them easier to expose

  • Use SCAM (Single Component Angular Module) pattern for shared components, which means creating a dedicated NgModule for each component that declares and exports it. This will make them easier to import and reuse by other applications.

  • Use Dynamic Module Federation to load remote components on demand, instead of statically importing them. This will reduce the initial bundle size and improve performance.

  • Use Angular’s built-in mechanisms to isolate and scope component styles, such as ViewEncapsulation and :host selector. This will prevent style conflicts and leakage between components.

  • Use Angular’s dependency injection system to provide services and configuration to components, instead of relying on global variables or constants. This will make them more testable and adaptable to different environments³.

  • Use custom elements or web components to wrap standalone components and expose them as standard HTML elements. This will make them interoperable with other frameworks or vanilla JavaScript.

Conclusion

Component Level Ownership is a powerful concept that enables a federated architecture for Angular applications. It allows each application to expose and consume individual components from other applications, without requiring coordination or synchronization between teams or domains. It also reduces coupling and increases cohesion between applications, improves scalability and performance, and enhances user experience.

However, Component Level Ownership also comes with some challenges that need to be addressed, such as design and documentation of the exposed components, configuration and orchestration of the applications, and compatibility and conflict resolution between different versions or implementations of the same component.

To overcome these challenges, there are some best practices and tools that can help us leverage Component Level Ownership effectively, such as using standalone components that have minimal dependencies and side effects, using SCAM pattern for shared components, using Dynamic Module Federation to load remote components on demand, using Angular’s built-in mechanisms to isolate and scope component styles, using Angular’s dependency injection system to provide services and configuration to components, and using custom elements or web components to wrap standalone components and expose them as standard HTML elements.

By following these best practices and tools, we can take full advantage of Module Federation and Component Level Ownership in Angular applications.