Paying technical debt in the front-end

Gonzalo
Miro Engineering
Published in
8 min readSep 16, 2021
https://unsplash.com/photos/Olki5QpHxts

We all love to hate technical debt. It’s one of the main reasons projects don’t reach their milestones on time. Tasks take longer than expected, and what should be a simple change ends up being a huge endeavor that nobody can predict, nor estimate. What can we do when technical debt bogs us down, sprint after sprint? Pay the debt.

As with finance, the bigger your debt, the longer it takes to pay it back. At some point in the past, building technical debt allowed the company to move forward faster: move fast now, implement technical improvements later. It’s only after some time that this debt starts to collect interest in the form of delayed tasks. We need to take action and come up with a plan.

In our case, we had a huge debt to pay in terms of the front-end libraries we used. A year and a half ago, most of the code you stumbled upon in Miro’s front-end client was written in an outdated version of Angular. Although React was on our guild’s roadmap for new features, there were still big chunks of Angular floating around in the codebase.

Fortunately, our client was already modular, with the application being split into 3 distinct parts: the board, the dashboard, and the settings. It was natural for us to divide and conquer, tackling each of these concerns one at a time.

Security settings for a company

I’ll focus on the settings part of the migration from Angular to React.

If you have never used Miro before, or if you’ve never been an admin, this is how our top-level company settings look like. Navigation is on the left, and the main content is displayed on the right, along with some extra navigation controls such as profile and going back to the dashboard. The pages on the left can include tabs and other navigation levels to provide admins with a set of controls to change settings, and to manage their users.

Our teams were growing fast, and that meant 2 things for us:

  • We could leverage the power of collaboration and move faster in this migration. The more, the merrier.
  • Different teams also have their own objectives, so we couldn’t stop all development and just migrate. This meant that some functionality would evolve while migrating; this increased complexity.

Fortunately, all the teams wanted to move away from the legacy version of Angular, and although we were not fully dedicated to this project, we had the buy-in to improve things as a whole front-end stream.

We started our journey by analyzing the current status of the codebase, visualizing and color-coding the different technologies that were being used to build the whole solution.

The partial status of the migration is shown using Miro. Pink means Angular code, Black means React code.

This very simple representation of our codebase allowed us to further split the problem into smaller chunks that we could tackle in sprints: divide and conquer.

As a side note, we love to use Miro to build Miro. We created a huge board just for this migration, which also contained presentations to show progress to stakeholders, and to recruit others to join our quest during front-end guild meetings.

Enter the migration

Our team was only a few months old when we started this migration; the codebase had been there for years before us. Nobody in our team had a complete understanding of how the small moving parts made up the whole solution. But who doesn’t love layered cakes?

https://unsplash.com/photos/PPfAUR-jAis

Layered architectures have been around for a long time. It’s a simple, yet powerful way to separate concerns. In general, you see this approach involving the front-end, the back-end, the database, and all the other parts you need to build a product. We wanted to use the same approach, but zooming in for the front-end only. This allowed us to split our big problem even further!

The 3 main layers in the front-end were:

  • State management layer: it used a legacy library that we were also planning to deprecate. We decided to not touch it at this point, and to abstract the implementation details as much as possible from our codebase, making it easy to change in the future.
  • Business logic layer: making HTTP calls to our server and implementing business rules. A mix of fat Angular components and some JavaScript services and functions. These components also implemented responsibilities from the next layer.
  • Presentation layer: rendering our UI. Multiple Angular components built this layer. Many of them were reused in other parts of the application, outside the scope of our settings module.

The bottom layer

To maximize our impact during the migration, we opted for a bottom-up approach. We wanted to start from the visual layer, extracting some of our reusable components into the work-in-progress design system that we were also building. We were also shaping design system contracts, and starting discussions on how to interact with them. Our migration helped other teams with their migrations: teamwork is key in big migrations.

Partial extension of the middle layer to add consistency to the bottom layer.

During this part, we had to extract some pieces of logic that weren’t entirely presentational upwards in the chain of responsibility, sometimes extending our already fat Angular components to do some extra heavy lifting. This was temporary, as the business layer was next on our list.

The middle layer

The business layer was one of the trickiest to migrate, mainly because many of us were still learning some domain rules and objects that were at play in these fat Angular components. We already had a working product, so we did not want to break it during our migration. It would have been amazing if we’d had 100% complete documentation on how our components were supposed to work, but asking for that is science fiction. To solve this, teamwork played a key part again: we knew who to ask questions to, and we were not shy to do so. We were all in this together, and we wanted to complete the puzzle.

Using custom hooks to encapsulate form logic.

Since we were migrating from Angular to React, we made heavy use of custom hooks, built-in hooks, and context to split the existing fat components into slimmer ones, with fewer responsibilities. We composed and reused small components as much as possible, and we completely abstracted state management from these components. We wanted to have a single connection between our state management layer and the tree of components we were building. Our abstractions aimed toward this goal, which would make change easier in the upcoming migration.

Separating a feature into layers, using Miro.

The top layer

Although changing the state management layer wasn’t in our scope, we knew that the next migration would involve replacing it as a whole. Working through Layer 1 and Layer 2 gave us enough knowledge about how the system worked; this was key in understanding what steps we needed to take for the migration, and how to minimize its risks.

The quality assurance

At each phase of the migration, we had a working product; we had a working product even before starting migrating. We didn’t want to introduce regressions as we migrated; quality is of paramount importance. Unfortunately, most Angular components lacked unit tests. Writing unit tests for each component we rewrote made us confident that we could ship regularly.

It’s extremely important to write unit tests as you go. There are so many components, and telling yourself you’ll write them all after completing the migration is wishful thinking. Writing tests is key to understanding what role your components play in the whole solution; this helps improve your component APIs, and also test against regressions you might introduce (we’re not perfect, bugs do happen).

We also did a lot of manual testing to increase the existing coverage of our end-to-end tests.

Finishing your quest

Starting a big migration is a huge step forward toward improving your day-to-day operations and developer experience. Finishing it is crucial. Nobody likes technical migrations that last forever. We’re software engineers delivering product increments — this is our key metric.

We stated a clear scope to finish the migration. Our scope was the settings module. As it can take a lot of time, showing progress and intermediate milestones to stakeholders and other front-end engineers helped us reach the finish line. We ran presentations at guild meetings to share progress and learnings, and to inspire other engineers to join our crusade against this huge technical debt. We organized our migration into a big epic in JIRA, and allowed others to join us whenever they had free time in their sprint. By harnessing collaboration and parallelization of tasks, you get to finish earlier, and knowledge spreads organically across the collaborators.

What did we unblock?

Since the beginning, our goal was to unblock future initiatives. We just could not make them happen in time without paying some of our technical debt.

After finishing the first iteration of the React migration, we unlocked progress in these areas:

  • Accessibility features: with Angular out of the way, we were now ready to start implementing them.
  • State management migration to Redux: all the knowledge we gained, as well as abstracting details away from components, meant we were ready for the next step.
  • Speed of development: better developer experience and unit tests sped up the development of all features.
  • Taking care of our codebase: as we’re not dealing with “legacy code” anymore, there are more initiatives to improve our codebase, little by little.
  • Faster onboarding of new engineers: nobody wants to deal with an unsupported version of a front-end library.
Using Miro to build Miro, harnessing sync & async collaboration 😍

Join our team!

Would you like to be an Engineer at Miro? Check out opportunities to join the Engineering team.

--

--

Gonzalo
Miro Engineering

Software Engineer. Food enthusiast. Amsterdam, NL 🇳🇱