Case study: Adopting Microfrontends for Scalable Frontend Architecture

Case study: Adopting Microfrontends for Scalable Frontend Architecture

7 min read
Krzysztof Radomski

Krzysztof Radomski

Frontend Tech Lead

In this article
  • Client Overview
  • Experience management solutions and scaling issues.
  • Refactoring the monolith with microfrontends and Kubernetes.
  • Proof of concept, deployment, and overcoming challenges.
  • Key tools: React, TypeScript, Kubernetes, and Azure.
  • Improved scalability, developer experience, and next steps.

Client

The client is a Swedish leader in experience management solutions, specializing in customer and employee feedback. Their platform empowers businesses to collect, analyze, and act on feedback, enabling data-driven decisions that improve both customer satisfaction and employee engagement. With a strong foothold in Europe and a growing global presence, the client is known for its user-friendly tools, innovative solutions, and commitment to delivering actionable insights for businesses of all sizes.

Challenge

This is the story of how we transformed our architecture to provide a unified user experience while empowering our developers.

    Solution

    This article focuses on the frontend aspect of the modern app platform, but it applies to the legacy too.

    Microfrontends in our understanding are small pieces of UI, mostly delivering a particular page or a set of pages. We mostly have vertical microfrontends – top-level routes are standalone React applications that are served by the top-level route container app we call “the shell.”
    The shell orchestrates top-level routing, provides navigation bars and layout containers, and most importantly, handles login. And that’s it. No state sharing, except basic local storage UI details. Each page is a separate being, handling separate issues, like survey orchestration, user management, etc. It has its own separate set of microservices too. All of these are containerized, and we use Kubernetes to scale them both horizontally and vertically as needed. No more limitations of the old monolithic app.

    The monolith grew until it became unmaintainable and is being refactored into smaller pieces as we speak. Its scale and size offered poor developer experience, long build times, and problems with static analysis. But this burden does not apply to the new apps.

    They have individual build pipelines, their own containers, and are highly independent. This means faster deployments and better developer experience. All the React apps follow the same template, have nearly identical pipelines and tooling, which means that if you are familiar with one of them, you can easily find your way around others too!

    No more problems with scaling the machines or the teams. Apps are contained in their own repositories, meaning parallel development is no longer an issue.

      From proof of concept…

      There was a short distance between the idea itself and a proof of concept being made. This PoC consisted of several React apps and web components dockerized in a local setup, then deployed to Azure. The module federation plugin was used, as it played nicely with Vite. We explored state sharing, but soon ditched that plan, and the apps only really share server-side cookies. We found only several issues; at that time, the plugin ecosystem was not mature enough to support our ideas of shared state – we explored this concept heavily, from cookies to window.postMessage. Eventually, we decided against using any of these, aiming to create nearly stateless apps. Once this was solved, a new set of issues popped up – our complex cloud orchestrations meant we had to really pay attention to urls and paths. Especially since we need to specify them in Vite config, Dockerized Ngingx, and few other places too. With that out of the way too, our Azure cloud was soon hosting a new set of apps and the way forward was opened.


      …to implementation

      This new, shiny setup came at a cost – instead of one frontend repository, we would eventually have around a dozen.

      Same for the cloud pipelines and other configurations. Local development was a bit of an issue too, especially bundling and authentication, but we solved those too. The one frontend app and repo powering the platform was soon split into a few smaller ones and the road had begun. We quickly iterated on newly discovered issues, yet again related to resource sharing, and soon two separate teams worked on their own features in their own repositories, all contributing to the same codebase.

      As we continued to work and add new features to the platform, we created more services and more repos. A new page with new functionality usually meant a new frontend repository, a new BFF (backend-for-frontend), and sometimes a new backend services repo too.

      All new code was independent of the already existing ones.

      We followed the already mentioned segmentation – by feature – and the split was vertical, meaning a new page meant something new. Still, everything was served under one domain.

      Our stack

      All apps followed the same ever-refined templates – React/TypeScript/Vite for frontend, .NET with Kafka for backend. This sped up the whole process tremendously.

      In the case of the frontend, we also rely on battle-tested libraries like Redux Toolkit, and we hardly store any state except for local UI state. This means no issues synchronizing local and server state, mostly. Using PrimeReact components, proposed by our chief designer, for the UI meant we could iterate fast on Figma designs. We had cohesive and visually pleasing apps. Not bothered by writing components by hand, we could focus on static analysis and almost totally ditch unit tests in the frontend. We rely on TypeScript for interfaces and contracts between all pieces of the system, further enforced by ESLint and Prettier, and incorporated into our build pipelines.

      Already mentioned technologies like Azure, Docker, Nginx, and Helm charts are other essential elements of the system.

      Overcoming Obstacles

      We learned that we sometimes needed to pass some state between pages, so we used URL params for that. On several occasions, we also needed to pass params to child modules, and for this, we simply relied on props – as our microfrontend modules are essentially treated as lazy-loaded child components. By the way, we lazy load all our pages and share third-party libraries between them – courtesy of the module federation system.

      Lazy loading pages has the added benefit of users not downloading any piece of UI code that they don’t have access rights to.

      We quickly learned to overcome other challenges with this new approach, and there were some. Setting up a new service or frontend meant a lot of logistical work in Azure and in the cloud in general. We created documents and some colleagues became really good at doing it without manuals. The codebase still grew, but it was a managed growth with all the apps being familiar to everyone. Until they were not. We soon had around 15 frontend repos and far fewer developers. Then we learned about Nx, and our road to a monorepo was laid out before us. But this is a different story.

      Returning to the microfrontends, they proved to be an investment that paid off plentifully. The architecture was a success; it was easy to both contribute to existing projects and create a new app in the same fashion. All other React apps were done in essentially the same way, minus the microfrontend part, proving the strength of our templates and general setups.

      What’s next?

      The shift to modern architecture and embracing the community’s most popular solutions lifted our React apps. However, we must navigate carefully and avoid jumping to the latest, shiniest thing. We still believe in the power of Redux and Toolkit, even though we have enormous amounts of custom hooks. This combination of the best new things and battle-tested classes, powered by Azure DevOps, was the basis of our success.

      With great developer experience come products that are easy to work with and work on. This is the way.

      As we bring this new approach to the old monolith, slowly chipping away at the core features, we look forward to further expanding the client’s web platform.

      Author

      Krzysztof Radomski

      Krzysztof Radomski

      Frontend Tech Lead

      I am a software engineer with over seven years of experience specializing in React.js. Currently Tech Lead for the frontend team at a SaaS company, I have a strong background in web development, focusing on building scalable, maintainable, and high-performance applications. With a proven track record of enhancing user experiences and boosting team productivity throughout the software development lifecycle, I have released multiple successful projects across various industries ranging from home automation to e-commerce.

      Let us tailor our services to your needs

      Related articles

      2024-11-29
      Case Study: Custom Loyalty Program for UCC
        Client   UCC Coffee, a prominent coffee brand known for its premium offerings, sought to strengthen customer engagement and retention...
      Read More
      2024-04-04
      Case Study: Comprehensive Data Integration Solution for a Global Market Leader in Consumer Goods 
        Client   A leading multinational corporation specializing in a wide range of consumer goods, known for its extensive portfolio of...
      Read More
      2024-04-04
      Case Study: Transforming Real-Data Accessibility in Supply Chain Management
        Client   A global leader in the supply chain industry, our client manages a complex network of information flow and...
      Read More