Photo by Markus Spiske on Unsplash
Every year more and more companies switch over to micro frontends. While organizational benefits are (or rather should be) in the center of the switch to micro frontends, quite often also technical reasons are the driving force. For instance, micro frontends allow you to compose your application on the fly, or reuse completed domain logic across different applications.
In this article I want to stress some of the most problematic points that I see most often happening in real-world projects involving micro frontends. Quite often, these anti-patterns are already known or transported in from microservices - so similarities are not uncommon.
1. Hidden Monolith
If you encounter that you cannot just publish or rollback a micro frontend you will most likely have created a hidden monolith. This might be the case when you create a feature that has parts distributed among several micro frontends. While a feature like this might exist, technically it should be possible to develop and publish the parts independently; with the whole application still running.
If not, you definitely created hidden monolith that comes with all the disadvantages of a monolith (e.g., larger releases that require a lot of alignment) and all the disadvantages of a distributed system (e.g., debugging does not work on all the code at once).
Key properties of this anti-pattern include:
- Lack of Autonomy: Teams cannot just deploy and even if they do they'll need additional alignment and coordination.
- Complex Deployment: A deployment is not within the hand of a single team, but requires coordination or a lot of CI/CD magic.
- Shared State: There are god objects and parts of the application that are somewhat used by everyone - and no one feels responsible to maintain or continue development on those.
Use DDD to properly split your application and keep loose coupling to avoid dependencies between the micro frontends
2. Chatty Frontends
Due to their decoupled nature, micro frontends still require some communication, e.g., in form of events, to talk with each other for various reasons, e.g., to process the workload of the application.
While communication itself is fine, over-communication in form of emitting events for pretty much every action results in an inefficient chatter between the different UI fragments. In general, this will make them less efficient - and less easy to develop.
Key properties of this anti-pattern include:
- Frequent Event Emission: It is totally unclear what events are necessary and which ones are not; all you see is that many events are just emitted all the time.
- Long Calling Cascades: One micro frontend calls into something else, calls into something else, calls into something else etc. - until one of the receivers just stops the chain.
- Noisy Debugging: Due to all the events and other messages it is pretty much impossible to get a clear hang on what's going on during debugging.
Only emit useful events, potentially introducing events only if there are interested parties
3. Framework Madness
Unfortunately, one of the most used reasons for advocating or introducing micro frontends historically has been the ability to render components from multiple frameworks.
While this feature is certainly "cool", it is not exclusive to micro frontends nor is it a feature that should easily be abused. Surely, if your framework renders purely on the server, or goes away at compile-time like Svelte, then the cost might be almost irrelevant. Nevertheless, every time unnecessary JavaScript is shipped we decrease performance.
Independent of the performance cost of introducing multiple frameworks the communication between the components will suffer. Furthermore, the cognitive load when trying to understand multiple areas of the application will definitely increase - presumably making it harder to impossible to understand for people who are only acquainted with a single framework.
Key properties of this anti-pattern include:
- Bad Performance: Loading the application takes a long time - most time is not spent in loading some data, but actually loading code to load the data.
- High Complexity: One needs to be an expert in multi frameworks and their own particular set of issues and philosophies to understand the code beyond a single micro frontend.
- XKCD 927: To combine the existing standards a new standard was created - finally achieving the framework independence that was aimed for. Unlikely, that just means we are hosting another framework inside.
Try to settle on a single technology and only introduce support for other frameworks - only to be introduced in justified cases.
4. Micro-Everything
One of the hardest things when going into microfrontends is to find the "right" domain decomposition. A potential observation is that the ideal domain decomposition will be impossible to find - therefore keeping it practical is key here.
You might be tempted to just open a new sub-domain with a new micro frontend when you encounter a feature that does not easily fall into the existing sub-domains. While such an approach can work, it will easily lead to tons of really little microfrontends being created.
There are several problems that originate from having too many micro frontends. Maintenance and coherence will suffer. Cognitive load will increase.
Overall, a better strategy is to place micro frontends without a clear sub-domain either into the app shell (if the relevance seems more technical / less domain-specific) or in a special micro frontend that hosts all "unassigned" parts. This way once a clear designation for the feature has established the transfer into a dedicated micro frontends makes more sense.
Key properties of this anti-pattern include:
- Excessive Fragmentation: A single feature for the end-user is brought together by more than three micro frontends.
- Reference Hell: As there are so many distributed parts essentially every function calls a function from another remote part resulting.
- Low Cohesion: Cohesion refers to the degree to which elements within a module work together to fulfill a single, well-defined purpose. In low cohesion, elements are loosely related and serve multiple purposes.
Don't split up too early, start in the closest possible domain / micro frontend and only extract once a clear sub-domain has emerged.
5. Violating Single Responsibility
Here, we see a fundamental violation of the responsibility of a module in an object-oriented design. It happens when a single micro frontend takes on multiple responsibilities or concerns that should ideally be separated. For example, when a payment processing micro frontend also handles user registration.
The Single Responsibility Principle (SRP) is one of the cornerstones of the SOLID principles for Object-Oriented Programming (OOP). It tries to reduce complexity, which consequently reduces errors and makes parts more pluggable. As a result, the implementation is probably more dedicated and lightweight - plus easier to test.
Always remember that the term "micro" does not imply a certain lines of code limit or so, but rather a focus on a single sub-domain of your application.
Key properties of this anti-pattern include:
- Unclear Ownership: When asked who is behind a certain feature or micro frontend no one feels responsible.
- No Reusability: Any time somebody comes up with an idea like "we could use this component from your micro frontend" or "can we just insert your micro frontend here" the answer is no - for technical reasons alone.
- Domain Overlap: There are multiple POs or domain specialists involved in the development of a single micro frontend.
Keep things where they belong - don't mix and match.
6. Spaghetti Architecture
Some anti-patterns are self-explanatory just like the spaghetti architecture, which is a common expression used for software architecture that lacks clear structure and organization, resulting in a tangled mess of interconnected components and modules.
A quite good model to avoid spaghetti architecture is a flat model where all micro frontends are provided by a central location - a micro frontend discovery service. Naturally, to remain flat, no micro frontend is allowed to load other micro frontends. The big question is now: How can one micro frontend then use components (or anything) from other micro frontends?
Simply put, micro frontends should be loosely coupled (see point 10). Therefore, a sound architecture puts measures in place such that there will never be the need for one micro frontend to know (or load) any other micro frontend.
Key properties of this anti-pattern include:
- Complex Control Flows: Following the reference hell and long tails we get to see the same here - potentially even with asynchronous flows contributing unknowingly.
- Lack of Separation of Concerns: Boundaries are not really defined and every micro frontend looks and feels different from each other on a technical level.
- Technical Duplication: You see the same problem solved in different ways, e.g., by having multiple state container libraries, multiple frameworks, or something else to solve one thing in different ways.
Stay at loose coupling without relying on a web of calls to provide a feature.
7. Distributed Data Inconsistency
One of the reasons why point 2 (Chatty Frontends) exists is because communication seems mandatory. How else would you want to share some data from one micro frontend to another? However, as it turns out we should not have to do it.
Once you share data that - from a domain perspective - belongs to one micro frontend, you will get in all kinds of trouble. One of the issues you might encounter is consistency problems. If the original data changes further changes need to be propagated, but this is not the only possible change. Another possibility is that the micro frontend that got a replica of the data needs the changes. Is that even allowed? Now the original and the duplicate will deviate again.
Ideally, the data is only kept on a single place (following the SRP from point 5), with other micro frontends accessing the data only in form of props or fragments in case of relevant events.
Key properties of this anti-pattern include:
- Conflicting Operations: Two micro frontends operating on the same data might result in race conditions or some unknown state.
- Question of Ownership: Who owns the data? If there is data in your (or some other) state container that has an unclear origin you clearly have an open question of ownership.
- Asynchronous Updates: While nothing is happening something is happening - presumably due to a pending task, a delayed request, or a scheduled action. In any case data should remain consistent with clear ownership.
Keep data where it belongs; let others not access the data directly, but only indirectly via attributes / props etc.
8. Dismissing Human Factors
There are certainly aspects within a project that should go beyond the project's goals and targets. However, if you feel that meeting technical objectives and deadlines is done without considering the impact on the well-being, morale, and work-life balance of the team members, then something goes wrong.
In general, you should consider micro frontends as a pattern to improve the team organization and structure - not to make the team suffer. Therefore, if there is a huge pain induced by the micro frontend solution then something has definitely gone sideways.
Key properties of this anti-pattern include:
- Overworking: Long (unpaid) working hours, work on weekends or missing time-off - there are many reasons why you should run.
- Micromanagement: Instead of empowering teams more the superiors are even more trying to control every action. If you see less power to the teams instead of more you know that it's time for a change.
- Unrealistic Expectations: Ever thought "are they even listening?" or "who will bring management the bad news?". When you are afraid to keep them leveled or if reality does not suffice than you know that failure is imminent.
Strengthen teams and avoid central management as much as possible.
9. Avoiding Observability
Debugging distributed systems is always a challenge. The whole issue only gets more severe when there is no observability implemented anywhere, e.g., if you don't know what micro frontend is the originator of an error or if you cannot debug the problematic part locally.
In general it should be possible to run a single - or multiple if necessary / wanted - micro frontend on your machine. When doing a local debugging the whole behavior, as well as its state, performance, and other metrics should be testable.
Key properties of this anti-pattern include:
- Limited Logging: In case something bad happens you want to see some logs to have at least kind of a trace where to start looking. With too little, or no logging at all - this will be more than difficult.
- Inadequate Metrics: Sometimes user stories such as "improve performance" pop-in. Things get worse when stories claim that there has been a performance regression. Using the right metrics we can at least establish baselines and go from there.
- Sparse Tracing: Especially in distributed systems we need to be able to follow trails - beyond barriers. If traces are sparse it will be quite difficult to find the root cause.
Introduce central solutions to simplify logging, collecting traces, etc.
10. Tight Coupling
Tight coupling is arguably not an anti-pattern. Nevertheless, there are various reasons to include it in this list. Most notably, tight coupling is usually the origin of many of the problems that arise when scaling micro frontends.
Tight coupling comes from the observation that a monolith has quite many aspects that are actually wanted. We do want to know what we import and use - and we want to act like we are in control. However, distributed systems are naturally distributed and therefore any control is just an illusion. Going into loose coupling is just a way to officially give up this illusion.
Why should you opt-in to loose coupling? While it may seem more difficult at first (e.g., thinking about events - you just emit some abstract event without truly knowing what micro frontend might react to it and how), it will feel more natural and powerful later. Also, it implicitly avoids tight coupling and therefore a huge fraction why hidden monoliths exist.
Key properties of this anti-pattern include:
- No Shutdown Possible: The lackmus test of micro frontends is to shut down a single (any) micro frontend. If the system keeps on working everything is fine - otherwise there is some dependency that should not exist.
- Longer Onboarding: Onboarding of a new team should be super quick. Create a micro frontend from the standard boilerplate, set up the CI/CD pipeline, push the code. If a lot more steps are involved the whole solution might be much more complex than it should be. Always remember: Micro frontend solutions are in general more complex (as a whole), but should be simpler to develop against for an individual.
- Increased Alignment Effort: Everything is about autonomy - so if you need to align more than previously with a monolith something has gone sideways. In general micro frontends should allow you to just do and have your team make the necessary decisions; alignment is an optional factor to just create wholesome user experiences - not a necessity to ship software.
Avoid direct references that require any technical knowledge (URLs, module paths, internal names, ...) of other micro frontends.
Conclusion
Hopefully you've not seen your project fitting in one or more of these categories. However, even if you did - given the right reasons this might be justified in the particular situation.
So therefore take this whole list with a grain of salt and have a solution that does not only solve the problem at hand, but also works for the resources, e.g., the team(s), available.
Top comments (7)
What is worth mentioning is that sometimes it is worth to have the governance model around MFE in place, e.g. you pointed out that observability is important and we need to have some visibility and debugging available in the Micro Frontends. That would be part of the governance as a "must have" for the MFE's owners.
The governance model might be some kind of contract between the platform owners and the MFE which allows to turn off the MFE if its not following the model.
Its not an anti pattern to not have the governance, BUT it might be extremely handy sometimes :)
I fully agree. Actually, governance is one of the things I also touch my book and I personally fully share your opinion.
Being able to (centrally) control the MFs is crucial - even though teams should be autonomous it's still ONE solution that is seen / shipped by the end-users as ONE product / application; so having also some central control over it is essential.
Absolutely!
Profound and truly engaging piece.
Nice article, one point I don't see is about treating your micro frontend no different to an API. It should be a contract and you should avoid breaking changes and design for backwards compatibility. If you can't then at least follow an expand/contract approach to add new breaking functionality.
I've been doing this in a large Vue monorepo for years now and have hit a real sweet spot with decoupling changes across the teams and contributors and not breaking the build. Albeit we don't deploy our microfrontends independently the same rules apply.
Thanks for taking the time to write.
Yes this is a good point - actually the anti-patterns for microservices do overlap vastly with the anti-patterns for MFs; so also in this regard the underlying details may be different, but overall its quite similar. Likewise, events & components etc. are your API here - treat them like that!
Contract-based testing is something that has become almost "natural" for microservices - I think this will happen with MFs, too.
Thanks for pointing this out.