MCMatthew CoatneySoftware Engineer · Systems Builder
← Back to blog

Reducing Complexity

Sometimes the architecture with more parts creates the simpler system. Complexity lives in the interactions.

Reducing ComplexityNxAngularIonicScrumAgileSoftware Architecture

Before we talk about reducing complexity, first we have to differentiate between complicated and complex. A complicated problem may have many parts that are hard to understand, but it follows clear rules and can be solved. A complex problem may have many parts that change and react to each other, making the final outcome unpredictable.

A complicated system might have many parts, but if we understand their relationships and those relationships behave predictably, increased knowledge allows our predictions to become increasingly accurate over time. In a complex system, however, the interactions themselves introduce uncertainty. As the number of independently changing parts and relationships increases, the number of possible system states can grow much faster than the number of parts themselves.

Reducing complexity is not necessarily reducing complication.
Diagram showing how the number of possible interactions grows as more components are added to a system
Adding components increases what we build. Adding interactions increases what we must understand, coordinate, and keep in sync.

Simplicity is not necessarily fewer parts

With Scrum, collective understanding and expertise contribute to a result greater than the sum of its parts. Simplicity is the art of maximizing the amount of work not done. A more complicated system, with understanding and expertise, can actually reduce the amount of work done while also reducing complexity.

And this is the definition of complexity we will discuss here.

A tale of two applications

Let's look at an example of a project to illustrate this.

A Scrum team is given a set of requirements they must now build. The client wants a mobile app that will be used by customers, with a dedicated admin backend that will be used on desktops only. These will share many of the same resources, but those resources are used—and may appear—differently, though not always. One requirement is that these must inherently be disconnected pieces for security purposes.

A team member suggests implementing a monorepo that will allow libraries to be shared. He is met with opposition suggesting that the monorepo creates complexity and that the two apps should simply be created as separate projects, each with its own Scrum team.

This team member argues that he wants a simpler system.

While the monorepo is complicated, it is not necessarily complex, despite the opposition's reasoning. It might be simpler on the surface to have two teams develop two simpler systems, but what happens when these two teams must interact in order to share resources, while their Scrum teams have prioritized those resources differently?

What exactly are we measuring when we say "simpler"?

We deliberately introduce complication in order to constrain complexity.

When a technical dependency becomes an organizational dependency

Suppose both applications display a Product. The customer app needs Product updated this sprint because customers need a new availability state. The admin team hasn't prioritized that feature.

Team A cannot simply wait.

So Team A changes its Product representation.

Now perhaps the API contract needs changing. Team B's application depends on the old contract. Maybe Team A adds compatibility logic. Maybe Team B duplicates the model. Maybe someone adds another endpoint. None of these decisions is necessarily unreasonable.

But something important just happened:

A technical dependency became an organizational dependency.

And because the teams have independent backlogs, priorities, and release cycles, the state of one application can no longer be predicted solely by understanding its code.

That's complexity.

Introducing Nx

Now consider the monorepo proposal:

Nx Workspace

apps/
├── customer
└── admin

libs/
├── domain
├── data-access
├── auth
└── ui

Nx hasn't magically made the system simple.

In fact, it's made the repository more complicated. Developers have to understand project boundaries, libraries, dependency constraints, Nx tooling, Angular architecture, etc.

But Product can now have an authoritative home.

Instead of asking:

How do Team A's Product and Team B's Product remain synchronized?

We can potentially ask:

How do both applications consume the Product domain library?

We've deliberately removed a problem that otherwise had to be continuously coordinated.

The work not done

"Work not done" isn't laziness or fewer files.

It's the second Product model you didn't write.

The second bug fix you didn't make.

The synchronization meeting you didn't need.

The compatibility layer you didn't create.

The discrepancy you didn't debug.

The documentation explaining two implementations you didn't maintain.

Sometimes maximizing the amount of work not done requires introducing deliberate complication.