Micro-Frontend Composition Patterns
Design patterns to help you build micro frontends effectively
When building micro frontends (MFEs), there are a, number of different techniques/patterns you can use. Here’s a list of them, including the pros and cons of using each.
MFEs split up a website into small separate services for pages and also components. An overview of MFEs can be found at micro-frontends.org.
If a component changes a lot and is used by multiple pages, you may want to use this technique. Page services make HTTP requests out to component services, which return HTML. The component HTML response can then be concatenated into the rest of the page. This enables component teams and page teams to deploy changes at their own rate.
Resiliency considerations need to be made for this architecture as the component request could fail or become slow. Is the component mandatory? Can the page show a placeholder if the component fails? Component teams wanting to own the resilience of their service could provide a library that returns the fallback in failure scenarios.
Pages with multiple independent components are able to make component requests in parallel. Response times for components are still important as the page is only as fast as the slowest response.
Pages with a high load may want to consider caching components, especially if the components have low-input cardinality. To allow component teams control over the cache, TTLs pages can use the
Cache-Control header to know how long the response can be cached for.
Pages and components don’t need to return a full response — they could return a response which contains placeholders similar to templates. The placeholders could be for content that needs to be localised or other components that need to be embedded. This allows for the MFEs to be simple, potentially just a static response built up in the routing service. This allows for cross-cutting concerns to be in one place.
One downside to this architecture is it makes testing individual applications more challenging (as you need to test through the service that fills out the template response). Parsing the templates may also be computationally expensive, especially when dealing with large responses. It may be more scalable and easier to test having the MFEs handle localisation and composition, keeping any proxy/routing services simple.
Using libraries to distribute your components may be the most obvious and simplest composition architecture, but it’s worth calling it out.
As with any piece of software, if a module is used by many applications, it’s extracted out into a separate library. This can be done with front-end components, too. React and other front-end frameworks are great tools for extracting common components into libraries. If you’re not using a framework that supports this or you want to be framework agnostic, your component library can just return a string containing HTML.
Keeping applications running on the latest version can become a challenge when using libraries — as it is up to the application owner to update it as a dependency. There could be situations where different applications/pages are using different versions of a component giving the customer a confusing experience. For this reason, it’s best to use libraries when you know the component isn’t going to change often or you can update the consumers easily.
Headers are best used for data rather than UI components. If there’s a common set of data all front ends want access to, headers are a great way to propagate it. Generally, these headers are set by a gateway, but they could be set by a common library in all MFEs.
Keeping a common naming convention — e.g., a company or application prefix — allows applications to automatically proxy these headers to downstream services passing on the common data. A common example of this is customer tracking data that’s needed throughout the application stack.
The data in the header is best stored in URL-encoded JSON. This allows for the data to be sent safely but also allows humans to read it without decoding. Using JSON allows consumers to parse the data and also allow for the schema to change whilst maintaining backwards compatibility.
If the data required is from a complex source, it may be advisable to extract it from the gateway out into a separate service. This adds extra complexity, especially around resilience. What happens when the service is down? Some data/headers may be considered mandatory, in which case you may want to terminate the request with a non2xx status code. Others may be optional; however, it’s still advisable to set the header with a default value as to protect applications from null errors.
You should now have an understanding of some of the different ways to build MFEs and when it’s best to use them. An example application demonstrating some of these patterns can be found here.