
Moving Payoneer to micro-frontends
What are micro-frontends?
You may have heard the term microservices – perhaps in the context of backend applications – but did you know there’s also a trend in the last few years of microservices in frontend applications? Micro-frontends solve a few problems in frontend applications, especially in large-scale applications like the ones we’re developing at Payoneer.
What problems do micro-frontends help solve?
In large organizations like Payoneer, several teams often work on the same monolith application. For example, one of the main applications we’re working on is the Payoneer account. It gives our customers a large platform with many sub-modules that have different features.
Today, the Payoneer account application sits on the same monolith repository. There’s an infrastructure team (which I’m a part of) that’s responsible for the core of the application, and a lot of modules and sub-modules that are managed and developed by other development teams in Payoneer. The problem is, every time one of the teams wants to change something in their modules, their change can affect other parts of the application, and also create some conflicts of code. In addition, for every small change, we need to run all the CI/CD processes again for the whole application and it can take a lot of time.
The main goal of the micro-frontends approach is to separate this large application into smaller applications (i.e., export each module of the application to be a stand-alone module). This solution gives every team access only to the modules they’re responsible for.
General description of the solution
There’s more than one solution to implementing micro-frontends in front-end applications. A popular concept comes from one of the latest webpack 5 plugins called module federation.
Module federation refers to multiple separate builds that should form a single application. These separate builds should not have dependencies on each other, so they can be developed and deployed individually.
Press enter or click to view image in full size

In module federation, we separate the modules into hosts and remotes. Each host can contain one or more remotes, and each remote can act as a host to other remotes.
Shared modules are both overridable and provide as overrides to a nested container. They usually point to the same module in each build e.g., the same library.
What problems do micro-frontends help us solve?
When we develop a large-scale application, several teams usually work on the same application. Module federation allows us to separate the application into smaller modules and let each team manage their own modules without changing the modules of other teams. In addition, when we’re using CI/CD processes, it can take a lot of time to build and deploy those applications in large applications. By splitting those applications into smaller modules, we’re saving a lot of time on these processes.
We can also use the container app as a components library, creating common components on the container app and expose them to other modules. Changes to the components library can be separately deployed without the need to redeploy all applications. The application automatically uses the up-to-date version of the components library.
What problems arise from using micro-frontends?
There is no method that comes with no problems. The main problem with micro-frontends is the sharing of data between applications. The architecture holds that there shouldn’t be data sharing, but sometimes it’s essential. The solution is to avoid communication between modules and to build a service that shares data.
How do we implement module federation in our application?
As we look to the future of the Payoneer account application, we want to make it leaner and separate it into modules so that each team will work only on the modules they are responsible for. So, we decided to create a new module in its own repository and have the ability to use it as a stand-alone module or as part of the Payoneer account. This is where module federation comes into the picture.
I will explain all the steps we took in order to implement module federation in our system on both the host (the Payoneer account) and on the remote (the new module we want to build). Let’s dive into some code!
Step 1: Configure the webpack file on the host
Webpack 5 gives us a plugin called ModuleFederationPlugin that we need to configure. Inside the webpack.config.js file, we’ll use ModuleFederationPlugin inside the plugins of the module:
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const deps = require(paths.packageJson).dependencies; //Gives us all the dependencies from package.json file
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'Myaccount',
shared: {
...deps,
react: {
singleton: true,
requiredVersion: deps.react,
},
"react-dom": {
singleton: true,
requiredVersion: deps["react-dom"],
}
}
})
]
}Step 2: Configure the webpack file on the remote app
The remote webpack file looks almost identical to the host, with a small change. We just add the name of the file we want to create (the file that the host will import), and the files we want to expose (the app itself).
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const deps = require(paths.packageJson).dependencies; //Gives us all the dependencies from package.json file
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'MyRemoteApp',
filename: "remoteEntry.js", // The name of the file we want the host to import
exposes: {
index: path.join(process.cwd(), 'src/index.js'), // Where our app index file is located
},
shared: {
...deps,
react: {
singleton: true,
requiredVersion: deps.react,
},
"react-dom": {
singleton: true,
requiredVersion: deps["react-dom"],
}
}
})
]
}Step 3: Import the remote app into the host app
We have the option to add the list of the remotes we want to use to the webpack.config.js file as hardcoded addresses, but we wanted to import the remotes in a more dynamic way. So we created a folder in our host app called “remotes”, and created a few files to handle the import of the remote modules.
The first file is the config.js, and in this file we configure the URLs of the remotes we want to use (for the development environment and production environment).
In the second file, we create the route for the component that will hold the remote module and render it in our host application. We’ll create files for every remote we’ll want to use.
Here is how we wrote those files:
// config.js
// We can add as many remotes as we want
const remotes = {
OnBoarding: {
url: prodUrl, // The url of the remote module in production
localUrl: 'http://localhost:8082',
},
};
export const getRemoteUrl = (key) => (isDev ? remotes[key].localUrl : remotes[key].url);// onboarding.js
// Some function that return a components that holds the remote module
const Outlet = helpers.injectRemote({
remote: () => importRemote(getRemoteUrl('OnBoarding')),
path,
});
// Here we're using Route from react-router-dom
export default <Route key={path} path={path} component={Outlet} />;In the file above we use a regular react-router-dom route to render the component that contains the remote app.
And that’s it. When we navigate to the route we configure for each module it will be rendered inside our container app.
In conclusion
In this post, I wanted to show how you can use micro-frontends in your app. There are many ways to implement micro-frontends and module federation. We wanted to implement it a bit of a generic manner so we can easily add more remotes in the future. There are more aspects I didn’t talk about in this post, which maybe I will write about in the future (e.g., communication between modules).
I hope you enjoyed reading this post. Feel free to add your comments.