Transcript
Luca Mezzalira: Let me start with a quick survey. How many of you have heard about micro-frontends? How many of you are implementing micro-frontends right now? In the last more or less 10 years, I spent a lot of time implementing micro-frontends first. Then in the last 5 when I joined AWS, helping internal teams from Amazon, AWS, plus many of the customers that I work with from New Zealand to Silicon Valley, moving whatever they had to micro-frontends. The reason being is because they have seen that the complexity that they were dealing with day-to-day with the monolith codebase couldn't help them to achieve what really people want when they move to distributed system. We will see later on what this is. Let's start from here. Vast majority of the time when I am called to review an architecture that either is already micro-frontends or they are thinking about moving towards micro-frontends, the situation is more or less this one.
They take whatever system they have, let's assume this nice e-commerce. Think that this is an e-commerce. Then they said, ok, so we take what we have today and we just move and load everything at runtime. All the red spots are loaded at runtime. This is more or less how many companies that I work with, they start thinking about micro-frontends. Unfortunately, it doesn't work in that way. Because here we start to go back to the roots of how everything started in frontend, backend or whatever. Even in frontend, back in the days, it was Flash developer, work with Flex, AIR, Flash Lite, all the suites of Flash. You had to design architecture. It's not just taking a bunch of libraries, stitch them together, thinking that you are designing a system. You're just implementing decisions made by others on specific solutions. That doesn't work in a distributed system because there is way more that you have to think about, and that's what we're going to talk about today.
What Is a Micro-Frontend?
Let's start with the basics first because this is probably the most misunderstood part of micro-frontends. What is a micro-frontend in reality? How micro is a micro-frontend, or how micro is a microservice? A vast majority of the time there is the confusion that micro-frontends are just components. Let's try to analyze this nice button. This is a button inside a larger application. What this button does a vast majority of the time is that you want to have a button inside your design system that has the possibility to set a specific label, to set a specific action, to have an ID, maybe also have some capabilities that you set a larger label because your website is translating multiple languages, it will adapt itself based on that. You have a bunch of capabilities and you want to design this capability in a way that the container of this button will take care of setting the button inside a specific context.
If this button is used for a form and you want to activate this button or enable this button only when the mandatory fields are filled, then you need to set specific capabilities of this button. Similarly, if you want to use exactly the same button in another part of your system, you will be able to do that and instruct how the button should behave. As you have seen in this case, the context is not owned by the button. The button is there trying to abstract capabilities and UI, in this case, also UX. In micro-frontends, we have something different. It's not anymore the container that is instructing someone else how the thing should work. In micro-frontends, when you load that, first of all, usually they are more coarse-grained. They are bigger. It's not just a single button. There is a reason for that. We will see that.
In reality, the micro-frontend itself is aware of how the things are working. That enables us to have a micro-frontend that is completely independent. Now, as you have seen, I've used some keywords here to describe one and the other. When we think about the button, we think about reusability. We want to use a design system or a shared component library that is available everywhere. We want to design that and have that for reusability. We want to have the same look and feel inside the application. The problem is, when you have that, you have a leak of the domain, because it has to be available in multiple domains, multiple areas of your system, to the container. The container has a direct knowledge of how to instruct the component to behave in a certain way. We want to do that because we want to reduce duplication. That's the main goal of a component.
We want also to do that because we want to have UI consistency. Micro-frontend cannot be like that. It cannot be a component loaded at runtime, because what we want instead is optimized for independence. We want to reduce the external dependencies of our teams, and we want to enable fast flow. When we think about that, we need to reduce external dependencies. Therefore, we want not to have the reduction of duplication at any cost, or reusability at any cost. Therefore, the way we should think about micro-frontends must be different. Micro-frontends, at the end, are way different from components. They diverge to a certain extent. Because when you optimize for reusability, you create a form of potentially coupling between the container and the components. Now, everyone knows about each other, the components less than the container. Micro-frontends is that, in order to be independently deployable, it has to be self-contained. Therefore, we need to act and design things differently.
The Migration Journey - Learnings from Hundreds of Teams
As I said, this talk will cover the migration part of micro-frontends. I have spent countless days and years looking at different implementation, and I collected, basically, in this talk, what works, what doesn't work, and why. A vast majority of the time, after seeing that diagram of the nice e-commerce, that everything will load at runtime, the second step in the conversation is, ok, so Luca, we decided to go with Single-SPA, or Next.js multi-zone, or we are using Web Components, or we are using Module Federation, a bunch of libraries/technical implementation, that at this stage of the process, doesn't make any sense. This part is relevant, but not at this stage of the conversation. At this stage, we don't even know how we are going to split this system. Because if you think about micro-frontends, what we want to achieve is independent teams that are deploying at their own pace, and they want to have their own part of the system, just a part of the system.
The real question we need to ask ourselves is, what do we want to achieve? You think that you know that, but trust me, not everyone does. Because distributed systems are different. Here it's not anymore, we have all hands on deck, and start to write code, and create this monster of 25,000 views, that is everything coupled and difficult to deploy, we need to deploy everything together, and so on. Recently, for instance, I was in Italy with a customer that has something like 900 views of a web application that has to be migrated from Vaadin, that is an old Java and HTML framework, but a Java framework, that didn't have any API, anything. They just need to migrate away from Vaadin, I think it was version 3, now they are version 22. As you can imagine, there is a lot of divergence in reality. The real question is, they never ask themselves, what do we want to achieve?
They just continue to add features on this system that became quite complicated very quickly. The first thing is, when we think about distributed systems, what people really want from that, the C-suite especially, is teams' autonomy, because they are tired that there are countless external dependencies, teams that are saying, no, we cannot deliver this feature because we need to wait for the other team to finish a specific part of the system and stuff like that. They want to have teams' autonomy. They want to have independent teams that are shipping things together. They want to have scalability. Don't think about scalability on, we need to move to the cloud, and therefore we are going to have scalability because there we can scale horizontally, vertically, whatever is needed at some point. We need to have organizational scalability. That is by far more difficult. There are people involved here.
It's not machine, it's people. We need to think about scalability in terms that I can plug a new team like this inside my organization for developing a specific area of the system without affecting the throughput of the others. Then we need to think about agility. Probably everyone here remembers what happened during the pandemic. Overnight there were businesses that in a week or less had to change their business model. That is not one thing that happened once in a lifetime. We have seen now with many other things that are external to our business how these things are affecting the advent of AI, or the democratization of AI. We need to change and revisit how we thought about how the things are happening. Therefore, if you don't apply modularity inside your system, then it's becoming difficult because you have a very rigid system while outside the world is moving very fast in different directions.
Then we want to reach the fast flow, because the reality we discovered is that if we are capable of shipping more often in production, we can gain more feedback from the users. We can design things better. We can have a better system that complies with all the other things that we have seen over there. Do you notice anything here? I'm talking about, use React for doing certain things, or use Angular or stuff like that. There is nothing that is technical because in reality what we do day-to-day is enabling the business and our customers and users to succeed in their tasks. Therefore, these characteristics have to be taken into account when you are designing anything, frontend, backend, you name it. Because if you don't, then don't blame that in 6-months' time now instead of 3 years with GenAI you can reach spaghetti code. It's not a problem with GenAI. It's a problem that you don't know what you are trying to solve.
Because there are these three areas of a socio-technical system that are always linked: architecture, organization, and engineering culture. There are certain situations where we think that we are just taking a design decision. The reality is we are touching all the three. I'll give you an example. At the beginning when I started to talk about micro-frontends in 2017, I counted roughly every other conference that I talk about them, the question lands into, should we use monorepo or polyrepo? That was always the question. My answer is it depends. Every architect would say that. There is a strong reason why I'm saying it depends. Because the moment that you select one of the two strategies, you are basically designing your engineering culture as well as potentially your organization. Imagine that you are going in an environment or in a context where you don't have many senior engineers, maybe you have a few of them that are taking the shot.
Let's say we go with monorepo. With monorepo, a vast majority of the time you are going to use trunk-based development. The branches should be very short-living, and you should have a fast pace. You should have master always up and running without any crashes, because otherwise you are going to affect everyone. You also rely on the discipline of developers. Therefore, if you say, in this shared folder we are going to share only the code and components that are really needed to be shared, you try to rely on the teams to do the right things. Then reality strikes. Then you have maybe a feature that has to be delivered by yesterday. Therefore, it's easier if we shove this code inside of the shared library. The problem is that when you are going to solve this stuff, it probably will be six months, if you are lucky, or a couple of years later.
Because then you start to grow. You open up some doors that becomes very complicated. Monorepo could be extremely helpful, in particular, when you are working with developers that have a certain seniority. They are capable of handling this kind of complexity. It is inherited inside their engineering practices. A simple decision like polyrepo and monorepo that seems apparently only on the technical side is affecting all three of those dimensions. The way you all structure the teams to a certain extent. It's very important that you bear in mind when you take a decision that these three things are linked together. A decision that is an architectural decision could affect the other two. The way you say, we go with distributed systems, but then the organization cannot remain as it is if you have a monolith for 20 years. The architecture should enable certain decision that you make up front for the organization, therefore, if you decide to go distributed system, you decide to decentralize. Therefore, no more ivory tower. You need to decentralize, slowly but steadily, multiple things. There will be a more federated approach on decisions. Still, these things matter. Don't think that it's just a technical decision.
In reality, context is king. How can you take this decision? Then, very often, I remember back in the days, there was this rush towards the Spotify model. Have you ever heard about Spotify model? We need to have guilds. We need to have every team that is independently decentralized and taking their own decision, and all the practices that were described there. A few years later, surprise, there was an article that came out saying, we didn't even implement this Spotify model in Spotify. Are you Spotify? Are you working for Spotify? Maybe you do. There, there is specific engineering culture. There is specific goal. There is specific aim. Very often, we forget that certain technical decisions that were made in a talk like this might not be useful for your context. You need to understand your context first. Even when we talk now about AI, we talk about context engineering.
Context engineering, not just engineering, not just best practice. Because if you don't provide the context to generative AI, to your code assistant, to whatever it is, then don't be surprised if it doesn't generate what you are looking for. Another thing that I want to highlight is that micro-frontends are definitely not a silver bullet. What I mean by that is, it should be used only when you need distributed system. Even if you have distributed system on the backend, you might not need micro-frontends. It's very important, this one. Don't think that everything we have done so far with our old, good monolith should be trashed. This is not hype. This is architecture. Architecture is expressing certain characteristics. Either you have the characteristics nailed up front, but otherwise, it's difficult to choose a specific architecture.
Step 1: Micro-Frontends Decisions Framework
Let's start in this migration journey. Back in 2019, I was in Norway. I was delivering a workshop on micro-frontends. The term micro-frontends came out in 2016. 2019 was still early days. You have to figure out how you can create some mental models for people in order to approach the design properly of an architecture. I created the vocabulary around micro-frontends architecture. Then I started to create patterns or better mental models to help people to grasp it better. In 2019, during this workshop, I had this light bulb in a moment. I remember I wrote the idea in a napkin during a break because it was groundbreaking. That was the decision framework. Because I was delivering a few talks on micro-frontends. There are a few things that are happening over and over again that I tested on server-side rendering, edge-side, on client-side. It's always the same thing. Usually, in micro-frontends, you have a few things.
First of all, you have either an application shell or a UI composer. What's the difference? An application shell usually is how you describe the container micro-frontends on a client-side rendering solution. While a UI composer might or might not be available in a server-side rendering solution. Because the composer is just composing server-side rendering fragments that will be then composed and shipped to the browser. An application shell has way more logic and stuff that it has to handle. The vast majority of the time, at the application shell level, you handle configuration, authorization, composition, and routing. Those are the four aspects that you care about. A good application shell is the one that could be reused outside your system, the current system that you're having. It has to be domain agnostic. It doesn't have to be shipped every time that a micro-frontend is changing. Because otherwise, you leak the domain outside the system.
You usually have two flavors of micro-frontends. You have a vertical and horizontal. You need to identify them. How micro is a micro-frontend? We go back to the same question. The reality is, those are not mutually exclusive. At the beginning of a system, you might find yourself that it's fine to have more coarse-grained micro-frontends. While when you move towards more maturity and the domain and things are changing, you might go more granular. Those are definitely two approaches. Vertical means that you have one view or group of views that are representing a micro-frontend. Horizontal means that you have a micro-frontend that is living alongside other micro-frontends, like in this case that I'm highlighting here, in the same view. This is the difference between the two. One is more coarse-grained. The other one is more fine-grained, but not too fine-grained like a component.
The other decision you need to make is how I'm going to render this stuff, on the server or on the client. That's very important because that changes everything. I will show you a few common architectures that I have seen. Obviously, there are edge cases on top of them. Those are quite key. The vast majority of the time, you will choose server-side rendering if you do an e-commerce. You need to have the best performance ever because every millisecond matters. Client-side rendering is everything that, maybe you have an application that is running behind authentication, you don't care about an organic SKU. Therefore, server-side rendering could be perfect. Or you have a solution that runs inside specific devices. I worked on embedded devices for a long time, therefore, client-side rendering for me was bread and butter. There are different frameworks. At this stage, we can start to discuss about frameworks.
If you decide to go with client-side rendering, the most common one, in my experience, is Module Federation or Single-SPA nowadays. Those are the two main frameworks that I have seen more often used. Then there are also Web Components. Native Federation, if you're using Angular. Piral, that is a fantastic solution that works either client or server-side rendering. It provides a lot of the things that I'm describing here as a platform. It's completely open source. Definitely check it out. On the server-side rendering, you can still use Module Federation with certain limitations. Nowadays, modern frameworks like Next.js and Astro.js, and also Web Fragments came out recently from Cloudflare, those are providing a more built-in approach. Next.js, for instance, has an idea of multi-zone that you can use in order to create micro-frontends, so it is splitting up your application in multiple chunks. We will see later how. This is an approach.
Astro.js has Astro Server Islands, for instance, that enables you to have a specific part of your UI that is completely rendered autonomously. In the same view, you could have one part that is fully cached and can be used and rendered and cached at the CDN level, and another part that can be refreshed multiple times. You basically rely on this server island that, on the server, can update the UI without refreshing the entire page. That is pretty handy. Then you need to think about, when you select this stuff, on how we route between micro-frontends. How I can move from home to catalog. Majority of the time, the routing part goes alongside the strategy you want to render stuff. If it's client-side rendering, you will have a routing that is living inside the application shell. If you are server-side rendering, you have a route that can happen at the edge or an application load balancer, API gateway, wherever you decide to have on your server-side.
Then you need to think about how to communicate. It's easy when you have a vertical split. You have a group of views that are living all together. The vast majority of time, the communication is pretty easy because it's living inside the same domain. On the other side, when you have multiple micro-frontends in the same view, then you need to think about how to communicate. Usually, the three usual suspects are custom events, event emitter, and reactive streams, with a strong preference around event emitter. The main reason is because it's the simplest one. It provides loose coupling. Also, it enables you to define events in and out of specific micro-frontends. Everyone can plug into them. You don't have to have a complicated global state like many teams have done. Then they blame the architecture when they took a wrong decision.
Step 2: Common Architectures
Common architectures that I've seen nowadays. First one is client-side rendering. Client-side rendering is very simple. In whatever, on-prem or cloud provider you're using, usually you have a CDN that is caching your static artifacts, because at the end of the day, if you think about a single-page application, it's a bunch of JavaScript files, CSS, and HTML. Nothing more. That is highly cacheable, and therefore you can store in your object storage, blob storage, or whatever you want to call it, your static files. Please don't use a container to serve static files unless it's strictly necessary. Because very often I've seen developers telling me, we are using Kubernetes for serving static files. Don't do that. You need to maintain a security posture and complexity that is not needed. Nowadays, a CDN, in the case of AWS, for instance, CloudFront with S3, is an object storage. You can scale immensely.
In my previous company we were scaling from 10,000 users to 2 million in one minute inside the platform. We were serving everything very nicely. We never had an issue on serving the frontend, and very rarely an issue on the backend. Trust me, these things scale very nicely. Your deployment basically is pushing a specific JavaScript chunk into the blob storage or object storage. That is your deployment. Super easy to handle that, and highly cacheable. On the server-side rendering, we still have a CDN because many people, for instance, think that the CDN is just useful for static files. Not really. CDN nowadays offers you way more. You have compute on the edge. You have security, so you can do way more. Vast majority of the time, if you think about e-commerce, I follow several customers on that space, they usually divide in first-level URL. You can have a home micro-frontend.
You can have a products micro-frontend that includes also the product ID. You can have myaccount. You can have checkout, and so on. This is quite common. Then how you want to serve that, it could be API gateway with a container, it could be an application load balancer with container, completely up to you. This is how you split. The cool thing is, if, for instance, products is down, it doesn't affect anything else. If I'm in the product team, I can release how many times I want per day, no one will be affected. I can test my stuff and everything will work very nicely. This is like classic server-side rendering approach that you will see, but instead of having one thing, you will have multiple containers. If you go to formula1.com, one of my customers that I worked with a couple of years ago, is using this approach here. We did a talk at re:Invent in Vegas where the senior architect came with me on the stage talking about exactly this. You can find it on YouTube if you want.
The other approach that I've seen for more advanced customers is this one. Same approach as before, but they are loading pages at runtime. If you think about Next.js, Astro.js, any server-side rendering framework nowadays, what it does is literally dividing the system in pages. You can load the page at runtime, so I don't have to deploy my infrastructure every time. It's literally, I drop my JavaScript file and my micro-frontend inside object storage, like before, and that is my deployment. The rest of the system will be always up and running. I don't touch that. That is quite common, I've seen with more advanced customers. Then you have a more sophisticated way to do that if you're now using the more recent frameworks. This one is, for instance, a very well-known name in the industry called BBC. It's a broadcaster in the UK, and very well-known in general.
What they use is different level of caches. This is a simplification, obviously, but you can find online the talk about that. What they do is basically have a composer that is retrieving from different services, HTML fragments, composing all together. Some of them are cached in order to not create strain towards the upstream dependencies, and therefore they're retrieving from the cache. It could be as simple as a Redis cache, and then serve to the browser. That is slightly more complicated, but served very well. The implementation of this is basically BBC News, so probably similar to what you have here in CNN. If you think about how much traffic they handle, this is how they are handling that. What about edge? I didn't talk about edge, because edge, as of today, is not very well used by enterprises worldwide. I think I found one only in the insurance space that did that because they have data replicated in multi-region approach.
Their services are available in multiple regions, in multiple parts of the world. All the others, if you are running server-side rendering on the edge, and your data is staying in a single region or a single data center, you're not gaining much. You have a worse developer experience. You can do everything definitely inside the region and leveraging the CDN, not for rendering stuff, but for caching stuff, and definitely is something that will work very well.
Step 3: Identify Micro-Frontends
Step three, identify micro-frontends. We have understood how everything works. My suggestion to every single team is start simple. Start with a vertical split. You have one view or group of views that represent a micro-frontend. There is less complexity. It's easier to split, because if you think about that, you take a website, you look at the first-level URL. You look at how your users are using your website, and you take a decision. It's faster to build and deploy because in reality, if you are used to working in a server-side rendering approach or in a client-side rendering, it's basically a portion of what you are doing already, and it's just split it and divide it from the others. Nothing more. Very easy. It's easier to roll back as well. That is another important aspect. We will discuss more about that. One thing that you need to think about how to design, how micro is a micro-frontend, is this one: integrators versus disintegrators.
There are some metrics that will help you, or heuristics, that will help you to understand if you want to co-locate code or not. For instance, code volatility. How often do you change? If you have, for instance, a system that you know upfront that there are 10 payment methods inside your system, but 9 of them don't change very often. You just set up this SDK. The SDK is changing rarely every year or whatever it is, but there is a payment method that is changing very often that probably you can split in two micro-frontends, not in 10, like I've seen very often. Because the reality is, at the beginning, it's easier to work more coarse-grained, and then you decide to split it up if and when it's needed. It's now like, I was a developer for a long time. I love doing abstraction. Developers love doing abstraction because it's cool. I can start to create a library, and maybe I will reuse, and my teams will reuse everywhere. Vast majority of the time, your library is used twice in the whole lifecycle of a system. Sorry about that, but that's the reality. I learned when I became older, I cannot say wiser, that this stuff doesn't pay off.
The other thing you need to think about, and I want to give you an example. This is how I was thinking about my system. I was working in a company called DAZN that is a live sport streaming platform like Netflix, but doing live sport. These are mainly the main parts that our single-page application was composed with. When I started to look into traffic and how our users were using our system, I discovered that the landing page had millions of requests. Then there was a drop because people weren't convinced about the offering, whatever it is. Then I started to think, so landing page could be a micro-frontend. Then I started to say, so then the user goes in, sign in, sign up, because if he goes to the landing page, it means that the user is not signed in, or is not signed up to the service.
Therefore, I can take the onboarding experience together, another micro-frontend. Then I have the catalog and video player that those together can be another micro-frontend. Then I have my account that is an authenticated area, and therefore I want to have an independent one also because we were mixing and matching different domains. Finally, we have Help and FAQ, another micro-frontend. This was our system. Very simple. We started very simple. Then we realized that there was some complexity on the onboarding micro-frontend, sign in, sign up, so we split it up. That was after we started to have friction inside the teams. That's the important thing. Don't do premature abstractions here. Try to be very simple in what you do. In this case, then we started to divide into team A, team B, team C, team D. That was how we're splitting and assigning micro-frontends.
Step 4: Go End-to-End with Your First Micro-Frontend
Now we arrive at the cool part that is going end-to-end with the first micro-frontend. That's a suggestion and recommendation that I do, not only for micro-frontends, also following the serverless customers worldwide. Also, with serverless, if you are migrating, whatever it is, go end-to-end with one chunk of the system. It's a single API, it's a single micro-frontend, but go end-to-end. Why that? Because you are going to touch a lot of things, a lot of work that you will reuse later on. First of all, you start with application shell. A few heuristics. It has to be technology agnostic. Very often I've seen customers say, we decided to go with Angular, also the application shell is Angular. The problem is that you immediately start to have some challenges six months later because Angular is changing the version and now you need to maintain a different version of Angular inside your micro-frontend system.
Doable, but at the beginning start simple. You have to think about how you route between the old system, legacy system, and micro-frontends. How you are going to compose micro-frontends is horizontal, vertical, is client-side rendering, is server-side rendering. Usually, the application shell should be stable very quickly. My rule of thumb here, or heuristic, is if you are capable of creating an application shell that doesn't change very often after one year, you are in a good way to design the application shell. Because this doesn't do much. It's very agnostic. It doesn't have any domain-specific things. It's just literally loading and routing micro-frontends in the best way. Obviously, sometimes you need to expose an initial configuration to everyone so the application shell is the best place to do these kinds of things.
Now, you start with the first micro-frontend. You identify, in the previous example, the onboarding experience, for instance, or the catalog. Then you start to go end-to-end. You design. You develop. You test. You deploy. You observe. You go end-to-end. Because when you do this, you touch everything. Think about this. It's a tiny portion, but you are touching basically the design part. You start to think about how micro-frontends should be tested against others in the future, how to deploy the automation pipeline that at the beginning will be simple. You don't have to have all the bells and whistles that you think about, you just need to have something that works and ship it faster. Then you need also to observe. That is one part that usually frontend developers are not very good at, because they didn't have the need before to observe how a system is deployed in production, if it fails, how it looks like.
If you have a horizontal split, this stuff becomes very complicated. My recommendation in this case is start to develop all the different horizontal micro-frontends that you need to build and ship them all together inside the same view. Because otherwise, doing a rollback on a system that has a mix of the old world and a mix of the new world is going to be a nightmare. The amount of code that you are writing and complexity in understanding how the things are working inside the system will be magnified tremendously, because one thing is, I have two micro-frontends and that's it. Fast forward one year from now, you probably have 20, 30, 50, 100 micro-frontends alongside the old system, and removing that complexity is very difficult. Therefore, try to think about how to route these things nicely. Because your problem is this one. You have a legacy monolith that has to move into the new catalog.
One-hundred percent of the traffic here, so how can we move? We want to have an iterative migration. I want first classic Strangler Fig pattern. You want to have a legacy monolith that's living alongside my other micro-frontends. The best way I've found so far is leveraging edge compute. We know, as we have seen before, there's always a CDN in every design that we have experienced. Therefore, what you can do is leverage edge compute and say, everything that is not new catalog, in this case, will be in the legacy monolith. If I found the request for a catalog, I will redirect to the new catalog. Because at the end of the day, either server-side render or client-side render, you need to pass through there. You don't add any additional specific latency. It will be pretty straightforward. Therefore, you can start to do quite cool things. Because you can also say, let me add a bit of canary releases on the system.
I start to say, now I have a new version of the catalog but I don't want to impact all the users. I want that only 10% of the users, maybe only in a specific country, maybe in a specific browser, will have that specific thing, this new version. All the others can go in the legacy monolith or can go in a previous version of micro-frontend that is more stable. These things are something that you can do very easily. The other thing is rollback strategy. If you design properly the edge compute part, that is basically having a JSON file that is externalized or a service that provides the information on how to move from the new system to the old system, the rollback is changing one configuration. You don't have to deploy anything. You don't have to write code that will be thrown away six months later.
You just literally change a configuration. It's extremely powerful. With this mechanism, my teams moved from 3 deployments per month of every single micro-frontend to 25 per day per team because now they have safe net. They have the possibility to deploy, test, and see what works, what doesn't, without affecting real traffic. The only challenge you have is how we are going to communicate between one and the other, and the answer is absolute URL. If the only change you do in the legacy monolith is just saying every time that the user clicks the button on the header of the catalog, we have to have the absolute URL, and that will basically land in a hard navigation, so full refresh of the page to the catalog. That's the only drawback that you're going to have. The rest will be sleek and very easy.
Step 5: Let's Move to the Second Micro-Frontend in Your Platform
Now, step five. We have deployed the first micro-frontend. We have the mechanism for routing. We have a lot of stuff already built. Now we have to build a few other things. We build the second micro-frontend, so it means that we need to have design consistency. You need to think about how you handle shared libraries. Ideally, not many at the beginning, or shared components, not many at the beginning. You will need to be very intentional with this stuff, but definitely you will need to have a design system because that will enable you to have UI consistency inside your system. That is the first thing that you need to think about. Then, secondly, we need to change our way to think. Duplication is not evil. As Sandi Metz says, duplication is far cheaper than the wrong abstraction. I'll give you an example. My previous company, we have four or five micro-frontends, whatever it was, and there we decided intentionally to not abstract the header because I did the research inside our GitHub.
We changed the header once in three years. Why do we need to abstract that? If the effort for changing a font for my developers, even if there are five teams, will be probably matters of one hour max, the effort will be creating five tickets. If instead I need to abstract something, I need to create governance around that. I need to understand if something has a specific edge case, they need to understand the edge case of that specific domain. There is way more associated to a simple, let's abstract that because it's available five times. I trust my developers to create a pixel-perfect implementation of the header, because if we are not capable, probably we hired the wrong developers.
Also, this one is something that happens very often. There are some teams that started to create optimization on a multi-framework approach. It's called micro-frontends anarchy. It came from the Thoughtworks Tech Radar from 2017. The idea is, yes, there is a period of time during a migration that is fine that you have same framework, older version and newer version living together, or multiple frameworks, but that is a temporary state. You don't optimize for that because you are bringing philosophies inside your system. The way Vue.js is rendering or Solid.js is rendering Angular, there are different stacks, there are different CI/CD, there are way more than a simple framework that you're bringing inside the company. There are different ways to evolve that are independent from each other, so you don't want to handle that because it's very complicated. At some point, you need to do sharing of data between views.
Therefore, here comes a few techniques. Usually, when you have something that is persistent, you want to use cookies, you want to use local session storage, or ideally, some backend API. Imagine that you have a personalization for a user. You can potentially, instead of squeezing everything in a cookie that is 100 kilobytes, you're going to have a backend API that stores that thing, understand who the user is and provide the information that's stored. If you have ephemeral data, you can use query strings. Query string is available in JavaScript from anywhere. Every micro-frontend by convention can say, ok, so I need the product ID. I pass inside the query string and they will retrieve from the query string this product ID. You don't have to store it anywhere because it's ephemeral. The user can move and go in another product in a second and everything will work very nice. Plus, it's helpful when you have a deep link.
Then you can think about horizontal split communication. We said before that we have custom events, event emitter, reactive streams. Why not a global state? Because the complexity is that if you change an object and transform it into a number, then you need to retest everything. There is a lot of complexity inherited when you have a global state. That is something that you don't want to do. I have a customer trying to do that and they spent four months moving away from this approach. Four months without shipping anything, just refactoring this stuff. We need to remember that what we want is encapsulation instead. This is not related to micro-frontends, but what we want is methods and data that are syncing together. We have events that are coming in and out from a specific micro-frontend because we want to remove unnecessary complexity. If you split it, the state management data with methods, then it becomes very complex.
The other thing is you can solve the problem of having multiple micro-frontends, not only having multiple micro-frontends, but delegating to a team that's generating the BFF. Example, my account that I described before. You have payment information. You have user information. You have settings. Those three are three domains that are living in different parts of the company and in different subdomains of my system, therefore, I can create a BFF. Those are fetching the information that is needed, and yet there is a single team handling the view of this complex system instead of having three teams that need to coordinate and create governance and complexity around for doing this. You can also think about this approach. AuthN or AuthZ, it's very easy to think about. Majority of the time, you can have it centralized in the application shell. The application shell takes care of the authentication, authorization part.
Micro-frontends shouldn't. There are certain cases, especially in finance, that I've seen that they decentralized that and they wanted to have every micro-frontend to check the authorization easily. That's quite common. The last thing is to think about global versus local routing. Usually, when you move between first-level URL, this is a global routing. This is basically, you are loading a new micro-frontend. Inside that micro-frontend, you can have local routing. In catalog, I have furnitures, and furnitures can move into the product details, for instance. This is local routing, that should live inside the same micro-frontend. What does it mean? It means that you delegate, basically, this approach to the micro-frontend itself, when it's loaded inside the future, it will take care of that. It's soft navigation. It's not hard navigation anymore like before. It doesn't have to reload. It's living inside the same system if you work with a vertical split.
There is an implementation I have done and I share it here. You can find a GitHub repository with an e-commerce that contains the code of all the things that I describe here. It's updated regularly. I use that because I just finished my second edition of the book on micro-frontends. For this one, if you are interested, check it out because it's an implementation with Module Federation. Remember, build and iterate. That is the most important part.
Step 6: Can AI Help Us?
Can AI help us? That's the real question. Yes, but not on everything. Defining boundaries, difficult. There is some context that is inherited inside your company. It's not written in Jira or it's not written inside any other internal tool that you're using. Therefore, you need to be very explicit on how to think about that. Usually, boundaries is better as a human activity. You define through event storming or domain story mapping or other techniques, to define what are your boundaries. Dependency management definitely is something that you can do with agentic AI. There are plenty of tools that currently can help you to write even a Dependabot script or better configuration for updating your dependencies. That is one of the problems with micro-frontends. Adopt fitness functions. It is a way for checking the integrity of your architecture. It's something that you can do very easily with GenAI. I've created quite a few of them.
Let me show you a few examples. Budget size. I want to make sure that a micro-frontend is not bigger than 30 kilobytes because probably someone forgets about configuring tree shaking or having too large bundles to ship. These kinds of things can easily be handled by GenAI. Second thing is testing your architecture. There is a library called ts-arch that is a porting of ArchUnit in Java. Basically, what it does is say, I want that this folder doesn't have any other dependency, or I want that this micro-frontend doesn't take or import this super useful monorepo, other libraries from outside the shared folder. Stuff like that can be enforced without you discovering six months later when everything is done.
Step 5.1: AI Can't Do Everything
AI can't do everything. There is some stuff that I saw that AI cannot do properly, that is libraries incompatibility. Sometimes you find yourself in a situation that the specific library does clash with another version of another library. For me, it was React Router and Module Federation. If you're using a specific version of React Router and Module Federation, it doesn't work. It triggers an error. AI started to create abstraction on top of abstraction. What was the solution in the end? Just downgrading the version of React Router and everything worked. No problem at all. In the example I shared before, you can see that implementation. This is something that AI wasn't able to handle. I've noticed that it's great on co-located code and less so on modularization. You can provide some instructions, but if you have a session of six hours like I usually do when I write code with GenAI, at some point we will lose context.
It becomes very complicated. Therefore, usually what I do is just co-locate the code first and then I modularize in the way that I want, explicitly saying what I want to achieve. Be mindful with your cognitive load because AI is going not only to create a lot of code but also to delete a lot of code. The things that you were optimizing before if you're not careful might harm your cognitive load, and you start to think, yes, implementing that way, no, sorry, AI removed that. A model is only as smart as the context we give it. Clarity of intention changes everything. Be explicit when you use GenAI on what you want to achieve, and try to do it iteratively. Start small. Plan first. You can ask to your code assistant, I need to do this. How would you do that? That's the real question. Then you iterate on the plan, and finally you start to implement the code.
What's Next?
What's next? There are plenty of other topics that have to be taken into account when you are designing micro-frontends, but I hope that this was a nice glance into how you migrate in multiple steps. I've seen this approach very successful with multiple companies. The other thing is, remember that in a distributed system you need to grow your software, not building it. That's very important. Because when you grow your software, your mindset is that you need to nurture the software. You need to check and recheck why certain decisions were made six months in the past. While if you're building your brain starts to switch off, I've already done this part let's walk away, but your business in the meantime evolved. Your needs from the users evolved, and therefore it's important that you think about that.
Resources
If you're interested in knowing more about micro-frontends, I have a free newsletter. You can subscribe if you want. It's something that I do every two weeks. I have a couple of newsletters, this one is one, the other one is called, Dear Architects. It's weekly. It's my Saturday night, usually. I have a lot of fun sharing what's happening in the community. I have the privilege to know many of the people that are building these frameworks for micro-frontends, so I usually share a few stuff there.
See more presentations with transcripts