All writing

Freshworks engineering · Tech debt

How we migrated Freshworks’ critical monorepo tackling tech debts

One codebase, two very different experiences, and a quarter to move both onto Vue 3 — while shipping a revamp on top of it. Here is the plan we made, the proof of concept that de-risked it, and the numbers that came out the other side.

Published
Originally
Freshworks engineering blog
Stack
Vue 3 · Nuxt 3 · Vite · Pinia · Vitest
Read
~10 minutes

At Freshworks Marketplace, we have leveraged a single codebase to power two distinct experiences: a unified in-product gallery and a public website. Despite their differing natures, we have designed them in a manner that ensures a consistent UI across both experiences.

Our gallery experience is developed as a single-page application using Vue.js powered with Vue CLI, Vue Router, Vuex, and deployed to AWS S3. On the other hand, our website is a server-side rendered application powered by Nuxt.js and deployed to S3.

You can read the full story behind our decision to use a single codebase here.

While we have enjoyed the benefits of using a monorepo to help with this unique problem, it has also presented its own set of tradeoffs. One such challenge we encountered was migrating our entire codebase to the latest versions of the components in our tech stack. To make things more interesting, around the same time we recognized the need to improve the user experience through a major revamp. Our latest revamp features prominent filters, upgraded search capabilities, eye-catching colors, better use of available real estate, and a streamlined user interface that’s intuitive and easy to use. With these enhancements, our platform now provides an even more user-friendly experience than ever before. This post summarizes our learnings and the approaches that worked for us during this journey.

Getting started

On February 7, 2022, Vue 3 was announced as the new major version for Vue.js. Having heavily invested in Vue.js since 2018, we were eager to learn about the new upgrades and advancements from this community-led project.

Prior to this launch, back in February 2021, the first stable release of Vite JS caused a stir on Twitter. On one of the regular knowledge-sharing sessions we hold internally, we covered Vite and studied its potential very closely. This session prompted a proposal from one of our engineering managers to explore its use for our platform.

Since then, we have actively sought opportunities to integrate Vite into our workflow. It was a pleasant surprise to discover that Vite has been adopted as the official build toolchain for Vue 3. Meanwhile, we noticed that other important tools like Pinia for state management and Vitest for testing paired up with Vue 3 after gaining popularity as the defaults in many developer toolchains. This momentum presents a promising opportunity for us to become early adopters of Vue 3 and grow together with the community.

We wasted no time and began our experimentation immediately.

What made this upgrade critical to us?

Vue 3 is faster, smaller, more maintainable and it’s easier to target native.— Evan You

As stated earlier, Vue 3 was set to become the new default version, and it is expected that support for Vue 2 will end by December 31, 2023. Vue 3 introduces several unforeseen modifications to its fundamental principles and APIs, diverging from previous updates. This necessitates progress in all other areas of the framework, which would soon push our current toolset into maintenance mode. This, in turn, could result in a growing number of security issues that are challenging to handle. Additionally, the latest upgrade pledges significant performance enhancements and a complete shift in API methodology, further emphasizing its significance.

Additionally, Vue 3 offers the following advantages that we particularly cared about:

You can read more about what’s new in Vue 3 here.

All the above factors provided enough persuasion to take on the challenge of being an early adopter for Vue 3. As an early adopter, with few proven examples to benchmark against, we also recognized the responsibility with which we must undertake this exercise.

It is important to keep in mind that there are likely to be some compatibility issues with existing code and third-party libraries during any major upgrade. It is therefore essential to conduct thorough research and take gradual steps. Also, staying current with the latest framework updates and changes is vital, as they are subject to frequent modifications from early feedback.

Planning the migration

By failing to prepare, you are preparing to fail.

Before embarking on this large-scale migration, it was important to have a plan. We needed to get down to the level of tasks and their estimated timelines because we had planned marketing activity around the revamped design. The plan is particularly crucial in our case, as our repository is a monorepo that supports two distinct user experiences and therefore requires due consideration of how different components interplay across the stack.

We eventually came up with the following list of high-level items as part of the migration plan:

As many of the above-listed items are relatively new to the community, the learning curve will undoubtedly be steep. Therefore, meticulous planning and prior experimentation were deemed to be necessary.

Consequently, we opted to begin with a comprehensive analysis of the migration and its impact on the existing codebase. Through this process, we were able to provide an approximate estimation of the required efforts and timelines.

Analysis

Although we have compiled a list of the action items, it is crucial to examine and comprehend the following points:

In order to address these questions, we conducted a comprehensive analysis of each topic and documented our findings in a clear and concise manner on our internal wiki. This served as a learning center for future developers who would work on this upgraded codebase and facilitated a comprehensive peer review.

We also created a comprehensive checklist, which included a table estimating each task, and shared it with the team for initial feedback. This exercise allowed us to visualize the resources required and potential challenges and opportunities at the outset. As a result of this exercise, we made some helpful discoveries.

The experimentation

A journey of a thousand miles begins with a single step.— Chinese proverb

Now that we had completed the analysis phase, the next step was to experiment with the code. Our Marketplace boasts over a thousand apps built by developers from around the globe. Before we rocked this boat, we decided to conduct a proof of concept (POC) prior to committing to a switch.

As part of the POC, we evaluated the following factors:

While we were experimenting with the changes, the state of Nuxt was not entirely stable. Nuxt 3 was in the Release Candidate stage, and as a result, we found ourselves in a position to decide whether to adopt Nuxt 3 immediately or wait for the stable release.

The problem with not updating the Nuxt version is that we would lose the flexibility of having components in common between our in-app and website experiences. The monorepo architecture is intended to reuse components as much as possible, but if our website continues to use Nuxt 2, it will not be able to understand the Vue 3 APIs that are part of our reusable components.

We had two options:

The second option is clearly not scalable in the long term and will result in unnecessary rework in the future.

We engaged three committed engineers who would travel with us through this release journey. One of these engineers, who had previously presented a Vite experiment in our internal knowledge-sharing session, would be responsible for conducting the POC, which would be supported and reviewed by two of our leads. The POC was carried out in stages.

We created a separate clone for this POC in a private repository, and our lead and architects thoroughly examined the code. We also included it as a demo item in our weekly sprint demo and collected feedback from the entire team.

Outcomes of the POC

Phase 1

Phase 2

Stages of migration

Although we were now quite confident about the migration thanks to the learnings from the POC, it was crucial to plan the stages in which the migration would occur. As a team, we try to avoid big-bang changes and deployments unless there is no alternative available.

The idea was to gradually ship the migration in stages to ensure a smooth transition without any breakage in production. The stages we came up with are laid out below.

Phase 1: The first stage involves migrating the code to Vue 3 without affecting the UI. This can be done separately from the UI revamp, which takes longer and requires a separate release.

Phase 2: After implementing the Vue 3 code, we had the option of gradually upgrading the UI, but we ultimately decided to release the updated in-product experience all at once. We made this decision based on the quick completion of the earlier phase, which allowed us to save bandwidth and avoid inconveniencing users with inconsistent UI differences.

Phases 3 and 4: The website experience remains unchanged until this phase, where the migration from Nuxt 2 to Nuxt 3 is carried out, similar to the in-product migration in Phase 1.

Phases 3 and 4 were eventually done in parallel, building on the knowledge gained in the earlier stages to fast-track progress. The refreshed website experience can be viewed at freshworks.com/apps.

Tech debts

In addition to the aforementioned steps, we recognized the potential for improvement in other areas such as productivity, performance, and build times.

Metrics on DX improvements

From a developer productivity perspective, we noticed significant gains from this upgrade, which are shared below for reference.

Comparison: in-product
VersionDev server start timeBuild time
Old gallery (Vue 2, Vue CLI)~8609 ms~36.70 s
New gallery (Vue 3, Vite)~807 ms~9.5 s
Comparison: website
VersionDev server start timeBuild time
Old website (Nuxt 2)~40.41 s~80 s
New website (Nuxt 3)~5010 ms (Vite + nuxi)~74.42 s
Vulnerability audit: in-product + website monorepo
 Before upgradeAfter upgrade
Packages audited24451645
Total vulnerabilities905
Severity: Low20
Severity: Moderate114
Severity: High670
Severity: Critical101

Learnings and takeaways

Our team managed to accomplish all of these improvements within a quarter by planning and experimenting beforehand. Despite facing a steep learning curve, we succeeded in delivering by remaining focused on our goal, sticking to the plan, learning quickly together, and staying aligned as a team.

For anyone planning a similarly major codebase migration, we recommend considering the following major takeaways from our experience.

Good luck with your major migration!

References

Back to the shelf