The crux of our job as web engineers is to deliver business value. To start creating business value, we need to know at least two things:

What do we actually need to deliver?

Be it a fully realized product feature or a minimum viable product (MVP), we need to scope out the requirements and convert these requirements into a technical specification.

How do we know when this project will be done?

We don’t, so we have to estimate the effort involved to approximate when it will be finished. This is usually done by assigning time or points to a collection of stories and aggregating these points against the team’s velocity.

Estimating is fallible

Chances are you’ve had this exchange with a stakeholder at some point:

“How long do you think it will take to build?”

“Oh, I don’t know. Maybe a day or two.”

Next thing you know, a week has passed, and as you’re applying the finishing touches, it occurs to you that your estimation was about as accurate as a drunk guy playing darts.

When someone asks you to build a new webpage in, let’s say, a React application, what you are estimating is likely the effort involved in building the components (adding the event listeners, configuring the routing, etc.).

But oftentimes, you forget everything else that eats time and saps brain power — things like missing requirements, edge cases, endless iterations, unit testing, integration testing, end-to-end testing with Cypress and QA, code review feedback, and, you know, the deployment process.

Poor estimation and you

Pressure mounts on the deadline. You have two days to deliver a new feature, and you are halfway through your initial estimation.

So you start… taking some shortcuts.

You copy and paste some snippets from StackOverflow to get some animation semi-working.

You duplicate a couple of components that already exist in the project and change them slightly to meet the requirements.

You create a function that formats a number into currency — also taken from StackOverflow.

The feature was a rush job, but you got it done.

You get your paycheck and a pat on the back.

Over the course of the next few months, the same thing happens again and again. Each requirement resulting in a new function that has been slightly tweaked, and a component that is decorated with extra functionality that probably shouldn’t be there. Your component files are growing as fast as summer grass. “It may not be perfect, but we deliver,” you say in your defense.

Short-term thinking leads to long-term debt

Yes, you have delivered, but you have also accumulated a lot of technical debt that is going to be painful in the long term. Your codebase will get so complicated and unmaintainable, you’ll spend less time working on new features and more time looking at the existing code thinking: “What was this person thinking? Oh… it was me.”

Technical debt is unavoidable

I’d be lying if I said that you can avoid technical debt altogether. It’s simply not possible, especially in the front-end world.

As you may know, front end moves very fast with shiny new libraries and frameworks. Consider every startup using the first version of Angular, aka AngularJS. It was very much the choice of many to build single-page applications from around 2010 to 2016.

Then the bombshell dropped.

“Since Angular 2 is not backward compatible, all existing apps built in Angular 1.x are required to be migrated to version 2 because the Angular team will now be focusing more support on latest versions and previous ones will have less and less support in the future.”

That’s an expensive statement to make for a lot of companies. Ouch!

Now, the problem here is not support for version 1. Stable versions are being released until this day, and applications built on this framework can continue to function as before.

The problem lies with engineers.

We want to stay relevant in the industry and don’t want to be working on an old framework like AngularJS. This means that finding engineers who do is very difficult. So what usually happens is that a migration process has to occur to take all of the code — which is now considered debt — and translate it into the new ideal.

On the plus side, it keeps us all employable.

How to reduce technical debt

You may not have control over the latest trends in the industry, but you can do things to keep debt down to make sure your applications are easier to work with.

I’m going to tell you a success story I had, and it wasn’t an easy feat. I joined a company to contribute to the migration from a legacy Ruby on Rails application to a microservice architecture with multiple React applications dealing with different aspects of our investment product.

Two React applications existed already and contained a collection of React components that essentially mirrored each other but were implemented in a different way. A button, header, and many more UI components existed twice but were completely different.

Design vs. development

The designs handed to the engineers for each project were bespoke. Although the designs looked great, there was no consistency across them, as every designer had their own approach.

The engineers would get frustrated because they had to do copies of existing UI components and make slight changes to match the design. So already during the migration, the technical debt was accumulating.

Here, you can see that the designers were also contributing to the debt.

How to fix this?

I and a colleague on my team had discussions with one of the designers to see how we could try to unify the process from design to development. We agreed on some new standards for spacing, colors, sizing, and typography at first. Then, using Atomic Design, we started drafting atomic components such as buttons, panels, icons, and more.

A design system?

We decided the time was right to start building a design system. We did some research to see what was out there and to find a design system that would fit our vision. We needed one that matched our technology — one being React — and we found one. We looked at how it was built and used it for inspiration for our own. We pitched the idea to the front-end team, and pretty much everyone was on board with the idea.

Let’s build this

Luckily, with the nod from the tech lead, we were allocated time during our sprints, and the project that followed was to be built entirely from the components of our design system. Other teams were planning to use it as well because they could import each component as a package via npm. This would avoid the duplication of components — a problem that existed already.

It’s a blocker

Because a lot of time was spent implementing the foundations agreed upon with design, we got a bit of resistance from other teams who had deadlines to meet. They were spending time fixing issues with the design system, and we received complaints. A retrospective meeting was arranged to decide if the design system was a good idea and if it should be scrapped.

Tough times

This was a tough and frustrating time because we had applied a lot of effort to get it all set up and to think about other teams and how we could help them. The foundations of the design system were always going to be a difficult thing to work out.

But, once they were defined, they would never have to be revisited again.

Eventually we finished and started our own project, importing the components we needed and using Storybook to visualize how our components looked in their various states.

A lot of questions were being raised in the business about why we had invested time in building this design system when we should have been focusing on the features of the applications so we could deliver business value. We also missed our deadline for the project.

But then something magical happened.

We did it!

We were getting pull requests from many teams for new and existing components on a daily basis. It was being talked about in meetings weekly. It was being mentioned in mission updates and presentations.

A few months ago, we were whiteboarding the idea of a design system, and at one point we had over 25 solid components built on our foundations that were used across the company. The CTO, design, and product fully embraced the design system, and it was now part of the core development process.

The eagle, my friends, had landed.

We finally finished the project, and all the components we built were already being used by another team in a different project. We probably saved them two months of work, so they were able to meet their deadlines, unlike us. But it was worth it!

What did we learn?

Implementing the design system in the short term was considered a blocker because it was a lot of work to define the foundations of our brand and visuals. But in the long term, it has unified our workflow, reduced a lot of technical debt, and allowed the design and development teams to sing from the same hymnal.

It is now delivering business value because it has sped up our front-end development, meaning we can now finish work much more quickly.

Communication and collaboration between design and development improved significantly. Rather than designers pushing designs down to us, we started sitting down, discussing the designs together to find a consensus, and pushing back on things that didn’t seem right.

It is a two-way process after all.

Full visibility into production React apps Debugging React applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, Debugging React applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more. The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores. Modernize how you debug your React apps — start monitoring for free.