From Distributed to a Monorepo
As Choco grows and tech trends evolve, we as engineers need to revisit our tech stack with a new perspective every once in a while. This iterative process enables us to improve the workflows of our developer team.

The Context
The Choco web application uses a ‘micro frontend’ architecture: in this approach, the app is built from a collection of smaller codebases, hosted and deployed independently, known as micro frontends. Each application deploys its built assets to a bucket, from which a shell app, in our case known as web
, reads them and links them together.
The shell app (web
) works as an entry point to the whole application, handling shared concerns such as authentication, dependencies, data providers, and errors. The micro frontends, which are semi-independent from one another, are responsible for specific features but cannot operate without the shell. Some internal libraries and utilities, hosted in a separate repository, support the entire thing.
This architecture, built on top of single-spa, initially allowed our teams to work independently and release quickly. However, as Choco grew, the model’s scalability and maintainability needed to be re-evaluated.

The Problem
While our previous architecture was ideal for early growth, scaling it revealed significant pain points. It became abundantly clear that it was starting to slow us down.
A discussion started to brew in the Choco frontend community: How could we ensure continued feature development while maintaining the existing platform? What pain points were we as developers facing and how could we mitigate them?
Turns out, most of our problems could be grouped into one of three categories:
1. Increased Platform Complexity
- Our codebase was split between different repositories and pipelines, making it difficult to manage. Comprehensive changes, such as updates to our authentication or major dependency upgrades, felt too risky due to a lack of centralized testing or type checking.
- Changes that impacted multiple micro frontends required a lot of manual testing and synchronization across repositories, increasing the potential for errors.
2. Slowed Development
- New features or updates to
web
required developers to coordinate across teams and repositories. This often resulted in delays, with some changes needing >15 pull requests to be fully implemented.

- The same was true for technical improvements and the adoption of new patterns. Many were not even attempted because of how cumbersome and risky it was to land them across all repositories.
- Testing and deploying updates was similarly difficult, slowing our pace of innovation and our ability to use the latest and greatest features.
3. App Maintainability and Consistency
- Many dependencies became outdated as teams had to juggle feature development and maintenance. Subsequent updates became more challenging and time-consuming.
- Isolated teams working in their separate micro frontends resulted in inconsistent implementations, code duplication, and recurring solutions to the same problems.
The Proposal
To address these challenges, we proposed consolidating our codebase into a single repository. An often polarising step between engineers, a monorepo offered several key benefits in our case:
- Automation and CI/CD could run over impacted code without the need to go back and forth between different repositories.
- Dependencies could be managed centrally, and older apps would benefit from upgrades or improvements.
- Developers could directly pull from other applications and libraries, reducing duplication and improving collaboration with other teams.
After the consolidation, we also had to prioritize maintaining team autonomy. We defined the following goals:
- Move all of our web codebases to a single repository while keeping our applications independent.
- Enable developers to seamlessly share code between teams.
- Enforce best practices to avoid unnecessary coupling.
- Reduce platform complexity to prioritize maintainability.
To achieve those goals, we decided to keep the micro frontends pattern in what we now call ‘packages’. We then needed to change the way we build, bundle, and deploy these to simplify our development experience. After evaluating different tools and frontend stacks, we decided to switch from yarn
to pnpm
for its sheer speed when installing dependencies, and nx
for its high extensibility as a workspace management tool.
We also took inspiration from the https://cookbook.marmicode.io/nx/intro to define how we wanted our packages to be defined, leading us to adopt a simpler approach for our web application. We wanted to remove a lot of the complexity associated with single-spa
, especially around sharing dependencies and the difficulty in testing changes.
Now, we faced the next challenge: migrating code to the new platform.

The Migration
There were two parallel goals during this period:
- Switch between the old micro frontends and the new packages without downtime.
- Enable developers to keep working on features during the migration.
With these goals in mind, we devised the following migration plan:
- Setting Up Libraries and Utilities: We began by moving shared libraries, configuration files, and utilities into the monorepo, while setting up the necessary tooling to let us accept and publish changes made to these components.
- Micro Frontend Migration: Each micro frontend was progressively migrated to the monorepo. This was usually done in small steps, as some refactoring of the setup and build of each application was required: library versions were unified, tooling and tests were updated and a lot of configuration files were deleted (goodbye, Webpack!).
- Release and Traffic Switch: Once the application was moved, we switched traffic incrementally to the new version. Users should not see any changes to their experience, as both the old and new version still worked exactly the same. To make sure of this, we focused on our E2E testing and relied heavily on our QA team.
This was not a simple task, as we needed to progressively migrate code while also developing new features for the Choco app. We had to create most workflows and automations from scratch, while ongoing development meant we needed to keep both apps in sync during the migration. We also faced non-technical challenges: the team needed to learn how to collaborate more effectively in a bigger, busier codebase. Until then, our frontend teams had been used to working in mostly isolated environments.
To be able to switch between the old micro frontend and the new package, we used feature flags to target application traffic, allowing us to roll back quickly in case something went wrong.

Fortunately, we had a strong foundation to build upon. Monorepos were already in use for other Choco projects, and our Platform and QA teams provided invaluable support. The migration itself provided the jumping off point for engineers to proactively fix issues and collaborate across teams.
We are now looking into pushing out widespread changes that were postponed because of platform challenges, such as updating React to its latest version, adopting Next.js and Vite, building reusable Apollo/GraphQL utilities, and decreasing our bundle size.
The Platform Today
As of May 2025, the migration has been completed. We also deprecated cypress in favour of playwright for E2E testing and developed a new way to enable engineers to test their changes in development environments via branch deployments. Refactoring and improving the existing code also became faster: an improvement to our app routing, which would have required weeks to implement and test in our old distributed setup, was developed, tested and released in a matter of days.
The gradual rollout also enabled us to improve the developer experience by getting feedback from owning teams, which in general has been positive:
- Nikita (CRM team, Sr. FE Engineer):
I can create UI Components, add translations and change a package in one PR. It improves speed, but more importantly I don’t have to create super flexible props to avoid making fewer changes. This should improve our sharable components API. It also pushes us to use unified library versions which helps with our bundle size, like with react-router. Finally, it makes it easier to share code between applications. For example, I created a reusable Google search input that is now used in two packages. In the future it should boost our speed and help us to reuse code even more.
Technical decisions of this scale are never easy to make. Time and resources need to be invested in identifying pain points, discussions and exchanging ideas between engineers. We started the investigation with the goal to improve our platform, but had to go back to the drawing board more than once. We approached things with realism: understanding what needed to be prioritized and what needed to wait, until everything was consolidated into a simpler setup.
There are always challenges in reaching a solution that aligns with engineering and product, but by prioritizing developer experience, we created a more maintainable, scalable platform that empowers our engineers to keep innovating. We still have some challenges to overcome, but this migration enables us to keep improving our frontend 1% every day.