An Article

Overengineering a Web Application is Easier Than Ever

Nick Walsh By Nick Walsh // 5.19.2020

Innovation in software happens fastest where scale demands it.

Development teams are larger than ever — as is the impact, value, and reliance on what they create. Technology companies top the global market cap list, with several crossing the trillion-dollar mark since 2018.

Whether you look at the rising importance of software as an opportunity or burden, it’s driving the discourse. The libraries, services, and frameworks that spawn conferences, articles, and communities are often a response to that primary problem: Scale. Like Google processing an estimated 70,000 searches per second. Or hundreds of hours of video being uploaded to YouTube each minute.

It’s easy to point at the tools and patterns behind the big companies as the right way. If React could handle that kind of throughput for Facebook, why wouldn’t everyone use it?

If you make too many assumptions about the future early in development, you may end up overengineering. And overengineering carries the same time and budget impact as scope creep. It’s like building a 747 to handle the duties of a helicopter and later figuring out that a boat would’ve been the best option.

The Promise

Examples of overengineering are everywhere. Pulling in React to manage the front end, letting webpack deal with bundles, breaking a platform down into microservices, setting up load balancing, incorporating machine learning into recommendations; the list goes on. With the flip of a switch, popular services, frameworks, and libraries come with the promise to:

  • Handle traffic for an exponentially growing user base
  • Abstract away the painful parts of vanilla languages for long-term development
  • Adopt the newest technology and have access to a better talent pool for future hires
  • Modularize code and the teams working on it
  • Give the sales team license to use buzzwords to describe a platform

If we can make an architecture decision now to cover our bases in the future, why not take a bit of extra time?

The Problem

https://xkcd.com/974/ (There’s always a related XKCD)

By the time scale is really needed, or a methodology comes in handy for a large team, or a framework is used to its true potential, the landscape has changed. Itamar Turner-Trauring said it best in his article, “Let’s use Kubernetes!” Now you have 8 problems:

People tend to forget that “when we have massive scale!” they also have massive customers. So, solving that problem later is easier with more revenue, more engineers, and a clear picture of the problem (rather than a guess at what might be a problem later).

Creating Code School

When we founded Code School, every single course was a separate Rails application. New course? Just copy the last one, and maybe try to iterate and improve a bit before release. It was a nightmare to maintain, especially when each needed a security update for Ruby or Rails. After a stint with Backbone and several overhauls of the functionality, a shared course engine was finally built — years after launch, and with millions of annual recurring revenue.

Looking back, I still believe that was the right call. Several truths had changed by the time those architecture decisions were made:

  • We’d changed the entire business model, moving from one-off purchases to subscriptions (and the expectation of regular releases)
  • Course creation teams grew from three or four to dozens
  • AngularJS and front end frameworks had matured to the point of being reliable
  • We’d tried dozens of features and interfaces, and finally settled on how unique each course could be — both in design and content

It would be both a wasted effort and a hindrance to iteration if we were to try and lock everything in at the outset.

Abstractions

Even with a clear idea of the problem, adding engineering complexity carries side effects.

At the moment, we’re fans of styled-components, a library that helps clean up common CSS woes. For newcomers, though, it’s hard to figure out when things go wrong. Instead of just being a CSS problem, now it could be a CSS problem, a JavaScript problem, a React problem, or a styled-components problem. You have to weigh the benefits against:

  • New ways for things to go wrong
  • Additional spin up time for new developers
  • The need for long-term maintenance and upgrades on the new package
  • Support ending for the dependency

Making Architecture Decisions

The right balance, and the staff required to maintain that balance, ties directly to business objectives and planning. Architecture decisions made for the present and near future should include these factors.

Key MVP Objectives

This one’s a given. Engineering that tackles a platform’s primary value and goals makes the cut.

Features Dropped for Time or Budget

Things will be dropped during a development cycle, but it’s worth tracking why. Features dropped for time are usually priorities after launch, so keeping supportive architecture as part of the plan makes sense.

Six Month Roadmap

If it’s a year out, expect the potential for a rewrite. At six months, it falls closer to those features dropped for time or budget — if the organization has a reasonable track record of sticking to short- and medium-term plans.

Organizational Uniqueness

Company policy or unique IP tend to win out, even over key objectives. The value of organizational secrets will (and should) win out over just about anything.

Business Phase

Planning an exit or a merger? We’ve been through the process of combining three systems before: Active development leading up to major business shakeups is lost to the sands of time.

Team Size

Patterns and architecture should make sense for the number of people working on a platform regularly. You probably shouldn’t have more microservices than team members.

Avoid Planning Software For

On the other side, there are a few factors we try our best to keep out of sweeping structural decisions for an application.

New Frameworks and Libraries

No matter how flashy or how many problems it solves, resist the urge to put a new technology in something important. Internally, we have a rule to use new things in (at least) something internal of reasonable size before deeming it worthy.

Fringe Platform Features

As opposed to key MVP objectives, nice-to-haves and pet features of individuals shouldn’t drive larger platform planning decisions. One visualization in a deep menu choice doesn’t necessitate a full-fledged graphing library.

Future Scale

It’s not necessarily cost effective, but modern servers can handle a lot. Scale means different things for different applications, and preemptively tackling it could see you solving the wrong problem.

Tread Lightly Around

This category is a toss-up: Your mileage may vary, and it differs from place to place. Nonetheless, these are important considerations.

The Job Market

The key objectives are bigger than the language they’re written in, but finding developers to maintain a platform is a real concern. Fortunately, the march towards remote acceptance has eased some of this pressure… but it’s still harder to find Rails developers today than it was ten years ago.

0.X Open Source Software

Before it hits version 1 (and even after), open source software runs the risk of changing without much warning. For the first version of Cisco’s technical training platform, we worked through several data adapter rewrites from a very young Ember.js (version 0.9!) framework.

Second-System Effect

Having the opportunity to build something again is a developer’s dream come true, but the promise of getting it right often leads to bloat and overengineering.

In addition, all of the formal and undocumented features of the first system tend to creep back in as requirements of the second. So, you often end up just rebuilding the first again.

The Right Amount

Articles looking at the strategy of software continue to highlight the same thing: Technology outcomes are a series of decisions and tradeoffs. Our foes — scope creep and overengineering — are derived from that tree of decisions.

It’s easy to fall for a new framework, or mirror the technology stack a big tech company uses, or build features to cover unrealistic revenue projections. Time and again, we’ve seen those decisions lead to lost time, inflexibility, technical debt, and a host of other issues.

Overengineering places software in an adversarial relationship with iteration and hampers your ability to respond to business problems with clarity. A jet, instead of a boat.