Micro-frontends help break up monolithic front-end applications into independently deployable loosely-coupled apps. This architecture helps us scale projects as an organisation scales, however it's also an increase in complexity. How can we leverage this architecture without having to face its upfront cost? In this talk, I'll show some of the risks associated with micro-frontends, and what patterns and libraries we can use to benefit from this architectural style without going full-on micro-frontends.
Adopting Micro-Frontends Without Micro-Frontends
AI Generated Video Summary
Today's Talk explores the adoption of micro frontends without actually implementing them. The main benefits of micro frontends are business scalability and the ability to independently deploy and compose frontend applications. The process of breaking up a monolith into smaller parts can be done using LEAN principles and composable apps. State management and data sharing in micro frontends are complex topics that require careful consideration to avoid coupling and maintain loose coupling.
1. Introduction to Micro Frontends
Today I'm going to talk about adopting micro frontends without micro frontends. What is a microfrontend? It's an architectural style where independently deliverable frontend applications are composed into a greater whole. The main benefit of microfrontends is business scalability. However, there are cons such as performance and consistency issues.
Today I'm going to talk about adopting micro frontends without micro frontends. Ruben, the previous speaker, he gave a great talk and I was just there looking at what he was saying. I was like, wow, he actually explained everything. So now I'm going to try to convince you how to maybe not go towards the end of that path.
Who am I? Alex. I was introduced. I currently work for Miro. If you don't know Miro, it's a very complex frontend. Very challenging. It's an infinite realtime collaboration ward. Really challenging. If you want to join the challenge, you can join the company. And today, you know, Ruben didn't give you the definition, so here I am. I'm going to give it to you. What is a microfrontend? And this is the, I would say the canonical definition you will find in any blog post and conference talk which was written a few years ago and basically says that it's an architectural style where independently deliverable frontend applications are composed into a greater whole.
2. Reliability and Micro Frontends
You can create a design system with different versions. Reliability is crucial for code execution. Let's use Twitter as an example. Breaking it into micro-frontends, we have a feed and a reply micro-frontend. Different versions can be deployed, but if they are independent, they should work.
You can create a design system, but you can also have different versions of your design system. And then reliability. And by reliability I mean, when you execute some code, it basically should not fail, right? Once the code, it should produce the expected outcome. Those are, you know, you have ways to work around that and to improve those things and mitigate quite a lot of those things. Performance and consistency. But it's not the case with reliability.
Let me give you an example. I'm going to use Twitter as an example here. Let's say we want to break up Twitter into micro-frontends. And so, we have this page contains, this is the home page. It's a feed page and this is, we'll create a feed micro-frontend. When you click on that button, it shows a model and we could create another micro-frontend for that. It's a different team, engaging team, whatever. It's micro-frontend, we'll call it reply micro-frontend.
3. Building Composable Micro Front-ends
We need to focus on composing micro-frontends and ensuring they are loosely coupled. Strong boundaries, well-defined interfaces, and minimal dependencies are key. Executing micro-frontends independently allows for autonomy and reduces coordination. Narrowing the focus to the business domain is crucial for loose coupling and avoiding difficulties.
We just make them completely independent. If you think that this is like a weird case, that it wouldn't happen that you deploy, navigate to a page, and then there are many versions deployed until you execute action. I don't know about you, but all these services, I open a tab and I keep them for ages. Right? So Spotify, Mirror as well. So it's a common case in many applications.
So there is a tension between these two things. You have your micro-frontends. You want to make them super-independent so you are super scalable, but then you are going to hurt performance and UI consistency because then you will need to share things. And so oftentimes what I see is that we focus a lot on micro-frontends in the end goal. Oh, we want to be independent and autonomous. And so we think of that part of the definition. But I think we should focus more on this part, right, and think of how we are going to compose the applications because if you fail in that part then everything after that will be wrong.
But so here are some principles that I came up with when I tried to build composable or decompose micro front-ends. One is that they should be loosely coupled. And here the idea is they should have strong boundaries, good interfaces, well-defined interfaces, your smaller apps, micro front-ends, should have no dependents and dependencies should be ideally build time. That's gonna give you like loose couple micro front-ends. They should also be executed independently. Because if you actually execute, you're able to execute your micro front-end independently without a lot of mocking and ceremony around it, it means that it's actually loosely coupled. Because otherwise you have to do a lot of work. Then you can work autonomously. You don't need to coordinate with other teams. You can reduce the community load of executing all that big thing. Right? You only execute a small subset of your application in a smaller context. And so, you can develop that autonomously. And then it needs to be narrowed to a domain, a business domain. Right? Because I've also seen examples where people deploy independently components, but a component is a developer construct. We use that to solve business problems. So you need to think of business domain. Because otherwise, if your business domain is too small, it's going to be actually very difficult to make it loosely coupled. And you're going to suffer.
4. Breaking Up a Monolith with LEAN Principles
The LEAN principles can be applied to break up a monolith into small parts. The first step is to create packages, encapsulating code and building them independently. Parallelizing the building of multiple packages allows for a faster feedback loop. Executing these packages enables autonomous development. The final step is to deploy and compose them at runtime. To further enhance the process, a new construct called a composable app can be used, allowing for build time and runtime compositions. This approach provides flexibility and performance optimization.
The other principles are going to suffer if you have very, very small things. So, I turned this into an acronym that I call a LEAN. So let's apply these LEAN principles to break up a monolith. And so here's an example. We have this monolith and we want to break it into small parts.
The first step we could do is we could create packages. You know, maybe you deploy them independently, or you have a monorepo, it doesn't matter, you have a package. It's a way to encapsulate code. And you can also build that package independently. So instead of building it all together with all the code in your monolith, you could have a build for that. You can also, if you are building multiple packages, you can parallelize so you get a basically faster feedback loop. You can also execute those packages. So you are developing autonomously. And then the last step, what you would do is, OK, I'm going to deploy this somewhere and I will compose that into something at runtime.
So what I propose is, why don't we stop there? Let's focus on the other parts, let's do a good job in building those. And maybe you don't have to deploy independently, and maybe if you do, then you actually have a good foundation to do that. So what I propose is to create some new construct, which is a composable app. A primitive, not the component. You will not think of a component when you're building these microfrontends, but you have some composable app, right? That works with the principles that I just described. And then with this composable app, it's like with components, right? You just put them in different places. And it shouldn't matter if you want to do build time compositions, run time composition, because that's a decision that sometimes we need to make up front. Like, oh, I'm going to decompose this monolith. What are we going to do? We're going to do build time or run time and then you commit for that. With that all the way down. And also, you can say, well, maybe some part of my monolith is build time. Because build time will give me more performance. I can do more tree shaking, I can share some state and so on. And this other part is going to be run time, right? You can combine all of this. One other thing you can do, which is what I'm going to do today, is I'm going to have run time on development. So I'm going to show a demo now of a small monolith.
5. Breaking Up Monolithic Deployment
I will break it up into smaller apps and composable apps. I will run the composition on development. So you can already experiment with micro front ends. And you can continue doing your monolithic deployment, which doesn't sound very sexy, but if you think about all your QA is the same, all your infra is the same.
I will break it up into smaller apps. And composable apps. And I will run them also runtime. I will run the composition on development. So you can already experiment with micro front ends. And you can continue doing your monolithic deployment, which doesn't sound very sexy, but if you think about all your QA is the same, all your infra is the same. You're not going to have to convince anyone in your company to make those changes. So it's just an architectural change.
So for the demo, I'm going to use three packages. If you like what you're going to see, you can go and explore this later. So basically it's an implementation of these ideas that I'm saying of building composable apps. So let me see if I'm running it. I'm actually going to run it again, just in case.
6. Introduction to the Demo
I'm going to create a fake application, a platform for creative developers. With code, you'll create art. I'll show you my artistic piece of work, called Zima Blue. It's a planet with a moving rectangle that can be interacted with and made bigger. Back to engineering.
So for the demo I thought of, okay, I'm going to create a fake application, and this application is a platform for creative developers. We are all creative, so you can join this platform. And so the idea is, with code, you are going to create art, right? And I'm the only artist at the moment in this community. I'll show you my artistic piece of work. So it's called, this is a planet. It has this rectangle that is moving. I can interact with it and make it bigger and bigger, and bigger and bigger. And I call it Zima Blue. OK, you don't need to understand my art for me to feel I'm an artist. But if you watch Love, Death, and Robots on Netflix, maybe, I don't know, I just found it was very funny. Anyway, back to engineering.
7. Exploring Monolith Breakdown
Let me show you what we've got. We have a monolith, a classic monorepo. I broke it up into smaller parts, like the chat. I extracted the chat into a package. I switched it to a composable app, creating stronger boundaries and more decoupling. The same idea applies to dynamic imports. I have an artistic piece of work.
Let me show you what we've got. So we have this monolith. Is it big enough? Yes, it is. So we have this monolith. It's a monorepo, a very classic monorepo with packages and so on.
What I want to show you is everything is running in one component tree, right? Because this is one monolithic application. So one thing I could do, I broke it up into smaller parts, like the chat, for instance. This chat is in a package, so I extracted that into a package. Great.
Nothing stops me from reading Redux that is used by other part of the monolith. And so I can use it in my chat. And now, OK, I have this runtime dependency. I can maybe use some state from Redux. I'm going to say, oh, I will use this value that other team maintains. And then they change it, and then my chat is not working anymore because I was using this. So what I'm going to do is now I'm going to switch that to not a component, but a composable lab, right? So I can go and navigate to this thing. So I have the chat here. The code is the same, but I just put it into a different package. This composable labs is just another workspace, and it's a package JSON.
And what I do is, instead of exporting my component, I export a composable app. So I can come here and say, I want to host this application here. So if I go now and run my code, I have this other chat app. So now if I try to read from the React, from the Redux, the store on the context, it will fail, because they are different apps. So you are creating more strong boundaries here. So more decoupled. I can do the same in this case. Just for the example I was using suspense and fallback is the same idea. So this is dynamic import. I can navigate to this thing as well. Same thing, I have my artistic piece of work, which half of it is I copied from the internet.
8. Switching to Runtime Composition
Now I can see another application here. If you realize this approach doesn't work for you, it's easy to make changes. Just export and use the component. However, in a monolithic build, breaking one part affects the others. Switching to runtime composition allows for independent builds. Running the applications separately ensures they work correctly.
So let's see. Now I can see I have another application here. If I didn't like this, you need to also have your contingency plan if you're trying to convince or to make changes in your architecture. What if you take this path and you realize that that's not a good approach for you? You could come here and say, I'm going to export this guy and then with the proper syntax. It's just a very easy change. You have a component, you're using a component. Obviously, in your app, you would not use the host. You would just do it like we did here. We could just put your component here. So it's very easy to change from one to the other one.
The thing is, this is a monolithic build. It's all built in the same webpack configuration. So if I go to, let's go to the chart, and I break it, obviously breaks, obviously it breaks because it's the same build. So now what I'm going to do is I'm going to switch to runtime composition. I'm going to try this runtime composition. And what I'm going to do is in my webpack I have this plugin from the linjs packages. And I'm going to say, these two are going to be, if I'm not on production, because otherwise we would be doing micro frontends on production. And today's talk is about we don't do micro frontends. But I still can do it in development. I want to experiment with this runtime. I will have to stop and start again. Is this still broken? No. So it should be happy. So I got the same result, the different component trees. But if I reload, it fails. The reason is, well, now they are independent builds. They're independent applications. We need to go and run them. I'm going to run this guy and this guy. If I reload the page, it's working.
9. Module Federation and CAI
I can look at the Network tab and see the packages. Module Federation is under the hood, but it's not necessary. If you don't want to use it, fall back to build time composition. In the CAI, micro-frontends are registered in a proxy and can be executed independently. Breaking the app doesn't affect the shell. You can continue navigating. We're not doing micro-frontends on production, but implementing them without micro-frontends. Thank you.
I can look at the Network tab, and I will see that some of these. Now there are also more packages. I'm not going to get into the details. But there's Module Federation under the hood doing this thing.
You also don't need to use Module Federation. You could use this architecture, and later on, it's like, oh, Module Federation, we don't want to use it anymore because we want to use, I don't know, ROLAP or ES build, whatever. And it's not implemented there. OK, just don't do runtime composition. You fall back to build time composition. And maybe in the future, there will be such plug-in in those build tools.
And in the CAI, every time you start a micro-frontend, you register it into some proxy so I can see the micro-frontends I'm running here. I can execute them independently. They work. And if I, I don't know if it's a good way to finish my demo. But if I break my app, I'm going to break it again. So you got this overlay because the other build is sending a message to this app, the shell. And it's saying, hey, that other one failed. But in reality, the build of the monolith didn't fail. Or not the monolith. Let's call it the shell app. I can close this and continue navigating. You know, I can use the chat. I can go here because the shell actually, it works because the build didn't fail. And if you wanted, you could go all the way and say, you know what, I'm going to do micro-frontends on production. But we are not going to do that today. Because otherwise, my talk will not be my talk. So we are not doing micro-frontends. We're going to implement micro-frontends without micro-frontends on production. And finally, that was my talk. Thank you very much.
Conclusion and Q&A
This idea of composable apps works only on the client side. You can break up your application into micro-apps or composable apps, regardless of whether it's runtime or build time, server or client. Thank you very much. We are going to have a live Q&A session. I'm feeling good and excited to see the questions. When narrowing micro frontends to a business, it's important to have a well-organized team structure.
I hope you enjoyed. And you can try any of these tools if you want. Just also to mention, this idea of composable apps works only on client side. And we are experimenting with server side. So the idea would be, you break up your application into micro-apps or composable apps. You can do runtime or build time, server or client. It doesn't really matter for your app. So it's like a simple construct.
Thank you very much. Thank you very much, Alex. Really appreciate the talk. Feel free to get comfortable. Come grab a seat with me. We are going to have a live Q&A. So just in case you haven't, feel free to jump into the Slido and go over to the URL track, and your questions will come in. And I'll be able to ask questions, so don't worry. Even if the question's really hard, really easy. And we will go to them.
How are you feeling, Alex? I'm feeling good. Yeah, I feel good. Very excited to see what the questions are. Yeah, me too, me too, me too. I did have a question about sort of the lean when you did the acronym. And you talk about narrowing it to a business, to like a part of the business. Just out of curiosity, because sometimes there's thinking of your application from a business perspective, and then in terms of like, what are the parts of the business this application is serving, versus thinking of it as a developer. And maybe what are the parts of this application and the way you want to build. Where do you draw the line as to where that border should be, in terms of narrowing each of those different micro frontings? That's a really good question. And it's actually not easy to implement that properly unless you already have a good organization of your teams. So it's not like as an engineer, you would go and say, oh, I need to come up with how we split this from nowhere. It's like you will look at your teams.
Team Structure and Composable Apps
One way to start is one team, one app. Another option is one team, multiple apps. Composable apps allow you to embed components anywhere in your app. They can be hosted by other composable apps and can be moved around freely. This approach provides flexibility and exciting possibilities, especially when exploring multiple routes in DevTools.
And probably one way to start is one team is one app. Or you could have one team, multiple apps. Having one app, multiple teams, like if you slice some part of your app into some domain, and that is owned by many teams, that's maybe an indication that you could go deeper to a smaller subdomain.
Now, that makes sense. I love the people-oriented approach to that as well.
We do have some questions coming in on the Slido. One of the first ones was, can you further explain what a composable app is actually doing? So under the hood, the idea is when you compose things, you have like an input and an output, right? So it's some application that you can embed a component anywhere in your app. You can actually have a composable app hosted by another composable app. So you can change them. So it's basically apps that you can throw in your application, one inside the other one. And you can move them around in your application. And it just works. They don't care about where they're actually being displayed or hosted. I'm actually really excited, especially when you go into the DevTools and see the multiple routes. So I definitely want to go and check that and try it out.
State Management in Microfrontends
State management in Microfrontends is a complex topic. While completely independent Microfrontends may not share any state management, in reality, some state sharing may be necessary. However, it's important to avoid sharing the logic that manipulates the state. For example, Redux may not be suitable in this paradigm as it includes the logic for state manipulation. The key is to keep the behavior within the boundaries of the business domains.
And another person has asked a question. What about state management in Microfrontends? I think that's part of what your talk was about, knowing when and when things should share state, maybe when things shouldn't. But what just about state management? Do you have any things you can speak to on that?
So yeah, I mean, if you want to have completely independent Microfrontends and so very reliable that they won't fail, probably, you don't share any state management. You don't use any state management because you're not going to share any state. The reality is that you will probably have to share some state. Because the example I always say is the locale, if you have different languages. So what I recommend there is that when you share a state, you don't share the logic that manipulates that state. Like for instance, Redux could not be a good choice in this paradigm because Redux has also the logic, the behavior that manipulates the state. Like, fetching the data, initializing the data. And you want behavior to be in the boundaries of those business domains. And so, yeah, maybe share some state. But don't share the logic that changes the state. I think that the classic one is, it depends, right? And I feel like that's the answer.
Data Sharing and Avoiding Coupling
Data sharing between applications is crucial, and it's important to avoid accidental coupling. The concept of loosely coupling is essential. To share information without becoming too coupled, keep the dependencies minimal and avoid code dependence on your application. Focus on maintaining a low level of shared state and revise the code if necessary. When sharing things like locale or a GraphQL cache between independent runtime apps, there is a tradeoff in making them less independent. One approach is to create a runtime that is shared at runtime, although this increases dependency and carries the risk of failure.
And this just kind of bounces onto the next one, which is talking about data sharing between applications and how that's really important. And they spoke about accidental coupling. And we heard about that in the talk before. I really like the L in lean, which was the loosely coupling. So is there any advice, and this kind of builds on the same thing, on how to share that information to avoid becoming too coupled?
Yeah, so in the library, like also dancing the questions we said before, I got before, your composable apps as a construct. It knows very little from the outside, like only maybe the routing. So you can change things. So you can put your application somewhere. And you don't need to reload the page. So there are certain things that your composable app knows about the outside. And you cannot change. You cannot add more props and things like that. So you want to keep the dependencies that go in very, very minimal. Right? But you can have some. Like you want to know what is the state that maybe we are sharing. You need to try to keep that very low. And what you should also do is you should have no dependence. Like no code should depend on your application. So that's something easier to do than not having dependency. So some dependencies like state, very few dependence, and then you will be able to have a loosely couple. And as I said in the talk, if you're able to run your micro app, composable app independently without having to actually add a lot of things, actually it's quite loosely coupled. If you are not able, then revise the code.
Now that definitely makes sense. Another question's come in. How can you share things like locale or a GraphQL cache between runtime apps if they're independent? So basically, you make a tradeoff in that thing that you're sharing. So you are making them less independent. So what I do normally, actually, in ling.js, the libraries, we have in the core some function called to create a runtime. And so basically you create a runtime that you share at runtime. Now, that's not a good thing, because you're making your apps more dependent and it could fail. But at least you do it in a control manner.
Shared Runtime and Q&A
You have restricted access to the shared runtime. TypeScript can be used for validations during development to ensure understanding of shared components. Runtime validation prevents the use of shared runtime by those who ignore TypeScript warnings. Thank you for your questions. Feel free to contact Alex for further inquiries.
And you have restricted access to who can actually add things to that shared state. Not shared state, shared runtime. And you can have also TypeScript to do validations during development to make sure that people understand what is shared and what is not. And you can also have runtime validation. So people who use ts-ignore, they cannot also use things in the shared runtime. ts-ignore. I love that. I love that.
Folks, thank you so much for your questions. Now, I know there are a few other questions that I didn't get to. But you can feel free to grab Alex. Alex is heading over to the speaker room. If you are joining in virtually, you can go over to the Discord. And Alex will also catch your questions in the speaker room. And feel free to grab him. But we will continue.