Micro frontends architecture is extremely powerful when it comes to splitting large frontend monoliths into smaller, individually deployable blocks, each is owned by an autonomous team and is focused on a business domain. But what about State? We are often told that micro frontends shouldn't share state, as this would make them coupled to each other. However, when it comes to complex UIs, it is not rare to encounter scenarios where state management between micro frontends is necessary. This talk is about finding the sweet spot — In which scenarios it is reasonable for micro frontends to share State? and how should micro frontends share State while remaining decoupled of each other? We discuss & compare different solutions in React.
Sharing is Caring: (How) Should Micro Frontends Share State?
From:

React Summit 2022
Transcription
Hi everyone, my name is Eliran Atan and I'm a software architect at Axonius. This is my second time in react Summit, but still very excited. I've been building scalable systems for the last decade now, and today I wish to focus on micro frontends and especially about micro frontends sharing state. So micro frontends is about breaking up this frontend monolith into smaller, more manageable pieces that allows development teams to work in a more efficient and effective way. This is because micro frontends are often isolated from each other, so they allow development teams to work in an autonomous way and with less friction and limitations. Now a common problem that often comes up when we talk about micro frontends is whether they should share state or communicate in some way. And if so, then how should we do it without breaking their isolation from each other? Now in this talk we will see a different approach to think about micro frontends. We're going to tackle that from a domain-driven perspective. So we would see how we can represent the micro frontend using something we call a bounded context. Bounded context is this logical creature that mirrors our implementation and can derive the implementation. And we will see how that can help us understand communication between micro frontends. So let's start from another angle. Let's talk about Agile. And in Agile we often work with user stories. So user stories are this short description of an end goal that the user would like to achieve. And it always is described from the perspective of the user and in terms of the business. So usually in many organizations resolving a user story would involve the cooperation and coordination of multi-tier teams. And that's because the affected code of resolving that story is often shared between these teams. And that's because how organizations tend to structure teams, especially when we talk about frontend. We often see this arbitrary division of responsibility between teams. Sometimes it's about owning components or pages or views. But that's not really how user stories are organized, right? What we really want is some way to organize our teams and code in a way that resolving a user story would be an end-to-end task of a single team. That means that the affected code of resolving that story would belong to a certain single team. And that will make a much nicer flow, especially at scale. So one question that we should ask ourselves is how user stories are organized. So from a domain-driven perspective, user stories are organized according to subdomains. So if we just take a standard e-commerce app, that's the most simple domain we can think of. Then a normal split to subdomains will be something like a catalog, order, delivery, support, and perhaps some other subdomains. So the catalog subdomain concerns about allowing users to browse and search products, while the order subdomains concerns users throwing products into subcarts and asking for delivery. So what we can do is to organize teams according or around business concerns, in alignment with subdomains, rather than around some technical concern, like components, pages of news, and such and so on. And together with Conway's Law, which states that organizations tend to mirror their own structure in the software they build, we are very likely to get the software that is split nicely, and that user stories are very likely to fall under the responsibility of a single team. Now, that can be taken further, and this is where we talk about micro-frontends again. By splitting up this monolith into different micro-frontends, that will enhance the segregation that we do between subdomains, and it will also give teams a further level, another level of autonomy on the technological stack level, because now they can choose their own tools, and in the design patterns, for example. Each team can optimize the design patterns, uses for the specific target, the specific goals that you want to achieve. So my point here is that a solid ground for micro-frontend is to consider the alignment with business aspects. This is why when we talk about micro-frontends, we have to think about domain-driven design, specifically about Strategic DDD. So Strategic DDD is this toolkit that allows us to properly or correctly decompose our domain into various subdomains in a way that will maximize the potential that micro-frontend has. And DDD really tells us to gather our domain experts, to take the time and brainstorm together with other stakeholders to form this unambiguous language that really captures the natural entities and processes that happen within this subdomain. And this language form is boundary that we call, it's logical boundary that we call bounded context. Bounded context has a lot to offer, a lot of benefits to work with bounded context and derive from them the actual micro-frontend implementation. So each bounded context will derive an implementation of a certain micro-frontend. We could summarize that by saying that a micro-frontend is a technical implementation of a bounded context. So having bounded context that derives micro-frontends has numerous benefits. Bounded context can drastically simplify the implementation of each micro-frontend because each term in each bounded context is described from the perspective of that bounded context. So you don't have a lot of redundant information that you need to handle. You only handle the stuff that are relevant within that context. And there is a deeper benefit to that is that now we have this, in each context we have unambiguous cohesive language that everyone understands. And that makes communication much better. That makes translating user stories much easier and so on. But I want to focus on another benefit that relates to how a micro-frontend should communicate. We have another tool that is part of the DDD toolkit is the context mapping tool. Context mapping is about understanding, brainstorming and understanding the relationships between different contexts. So for example here, although the product term usually means something else, something specific when we talk about catalog context, for example a structure that displays all the comments, reviews, product details, product images and so on. That means product will mean something else in the order context. It will probably have some ID, a price, discounts or everything that relates to ordering. But still, although they are defined differently in those different contexts, they still have this logical connection between them. Because we want to allow users to drop products that they find in the catalog into some cart that is naturally part of the ordering context. And that can be, that really derives a communication that we must have between catalog frontend and the ordering frontend. That pieces of information, a selected product, have to be communicated between these micro-frontends. So we see here a way to logically understand what are the connections that we should do between context and then derive from there the communication between the implementation, between actual micro-frontends. And that can help us avoid unnecessary communication and can also help us to understand if our model is right, whether we are not creating too much communication and so on. So I think it's very important to have some logical backup or a reference point to your implementations. Now once we have this reference point, we understand how to map these communications, we would like to talk about, okay, how should we technically implement communication between micro-frontends and especially without breaking their isolation from each other, without coupling them. So a straightforward answer to that will be, okay, let's not have this direct communication between micro-frontends. Let's use the backend. So we are basically shifting our state to the backend and a catalog micro-frontend could communicate the selected products to the backend while any other micro-frontend that is interested in that information could consume that information. And this is very good because micro-frontends are still isolated from each other. The problem is that, well, if you have a lot of micro-frontends, different micro-frontends in the same view, then you probably have some UX disadvantage because calling the backend is slower. It feels like less performant. Another problem is that we are moving a frontend complexity into the backend, where it's not really belongs. So it also makes some problems, some bottlenecks in development because now frontend developers are blocked by backend developers whenever they have to do some change. So let's talk about direct communication between micro-frontends. This is a very general problem in software engineering. We have these different units that are independent of each other. If we don't want to have them coupled with each other, but they still have to pass messages between each other because they are part of a bigger team. So if we take a look on what we are doing in backend software design, it's usually about micro-services and micro-frontend and micro-services has some correlation between them. And that's a chance to learn a lot about micro-frontends. So if we take a look on how micro-services communicate, one of the most popular and I think powerful patterns is the public subscribe mechanism, asynchronous messaging. So each micro-service can publish information, interesting stuff that happens within the micro-service to some shared space like a message bus and any other micro-service could consume this information in case it's really interesting. So this makes micro-services still decoupled from each other in a way, but still allows a very nice, very powerful communication. Yeah. So we could derive from that a solution to our micro-frontends. We could use the window object, which is a great common space that every micro-frontend can communicate with. And each micro-frontend could publish interesting events about stuff that happens, like product was selected for the cart and any other micro-frontend could consume this information. So different micro-frontends are not aware of each other, but they can still pass important information. And so that can be implemented very easily and even natively using custom events. We can create custom events and dispatch them into the window and we can add event listeners to the window to get this information back. So you could wrap that mechanism with your react or vue framework and build something more advanced that can help you automate some of this communication or make it more robust. Now that was nice, but sometimes you just have to have a state. So communicating with event is great, but there are some complex operations or some complex constructs of data that we wanted to have a shared or some sort of states, like a Redux state, to manage the process. And that can happen quite often. So what we would want is this centralized Redux store and every micro-frontend could communicate with in order to maintain this kind of complex state. And this is very problematic, especially when we talk about micro-frontends, because this completely breaks the isolations that they have from each other, especially that they are owned by different teams. So if the product micro-frontend will manipulate the state in some way, then the delivery micro-frontend and the support micro-frontend would have to adapt or understand the change and that can become very difficult. So what we really wanted is this nice aligned stores, like that we have a store per micro-frontend. Each micro-frontend can use that store like a Redux store. And these stores are decoupled from each other. But of course, here we don't have any communication between them. And that's also problematic. So another solution that I've seen to maintain a state between micro-frontends in a way that still keeps them quite decoupled is to allow micro-frontends to subscribe or view stores of other micro-frontends, but without the ability to mutate these stores. So in this example, the product micro-frontend and the delivery micro-frontends can view the store of the support micro-frontend. But still, there are some problems here because any change in how the support store is implemented would affect the delivery and product micro-frontends. So what we can do to make it even more secure is to come up with this global store. This global store manages the different stores of the different micro-frontends. Each micro-frontend can subscribe to that store. It has its own store that it can manipulate. And other micro-frontends can consume and subscribe to their own stores or other stores. And because this global store would expose some specific api, then each micro-frontend will just have to implement that specific api. And that will make everything much easier. That will align this api across those different micro-frontends. So that was an overview of three common ways to maintain communication between micro-frontends while keeping them quite isolated from each other. So I would like to summarize that talk. The first thing that we say is that to strive toward highly autonomous teams, we should structure teams in alignment with business subdomains. And that doesn't relate directly to micro-frontends. You can definitely have a monolith that is aligned with business aspects. Although, applying micro-frontend architecture could enhance this idea by segregating business concerns in a better way. And by applying strategic DTD, we can identify subdomains, model them into bounded contexts, and apply context mapping to find relationships between them. And we also say that the micro-frontend is a technical implementation of a bounded context. So context mapping can help us identify the spots, the touch points, where micro-frontends should communicate. And one important rule is that communication between micro-frontends should keep them isolated from each other. So that's the summary of the talk. I hope I gave you some inspiration about how to apply DTD in problems that we encounter in frontend and how to facilitate communication between micro-frontends in a way that will help you achieve goals like teams autonomy. Thank you very much.