Designing Simple Systems

Simplicity against Complexity

Many amazing products outshining their competition because of how simple they are. Even tech giants are, at their core, relentlessly focusing on designing “simple” and streamlined experiences for every user.

The Value of Simplicity

It is important to understand that “simplicity” is often a key success factor of a company or product.

Take Trello, the product-management app that was so flexible that many startups decided to use it at the core of all their processes. Even though they were competing with well-established project-management giants, Trello was brilliantly providing a simpler alternative to many other overcomplicated software.

Even Slack, or Discord, revolutionised team communications only a couple years ago. Instant messaging and voice chat were nothing new, but the value these companies brought to the table was the ease of use, the almost zero-configuration setup, and an intuitive, accessible UX.

Simple Apps

Credit: (Eric Burke)

Simple is Hard (not easy)

The first and most important idea is that achieving simplicity is not easy. Quite the contrary: building a simple product or service is hard.

  • ✅ “Easy” usually means easy to make, easy to code, easy to implement, solving an “easy” problem.
  • 🚀 “Simple” usually means easy to understand and easy to use by others.

Voice assistants like Google Assistant or Siri are meant to be pretty simple products: users can request almost anything in natural language, and the software executes accordingly.

Though, the difficulty of writing the algorithms required to actually build such features are already reaching a level a complexity beyond imagination (and they still have room for improvements).

We have seen even more advanced examples of voice assistants in sci-fi movies. They have always been self-explanatory: everyone understands the idea, the concept of a voice assistant and how to use it.

The hard part about simplicity is exactly that: making it easy for others. The difficult part is usually hiding all the complexity from the end user.

My own personal mental modal is to conceptualise it in 3 different areas:

  • 1️⃣ Generalisation
  • 2️⃣ Separation of concerns
  • 3️⃣ Levels of abstractions


Generalisation is finding a unique solution that applies to multiple cases.

This requires abstraction and is a first source of difficulty. It requires our brains to build links between abstract topics and extract a solution.

God I wish there was an easier way to do this

Generalisation can apply at many different levels. An organisation can benefit from generalised processes.

How complicated would it be if every company rule and policy would vary for each employee.

Similarly, a big engineering team using 30 different programming languages will require many experts and specialists. On the other hand, a team with a unified tech stack (for example, one or two programming language) is easier to “understand”, easier to use, more flexible and more agile.

Code can obviously be generalised too: 👩🏻‍💻 👨‍💻 shared reusable code is easier to maintain and update in the long run. Also, as seen in the previous ironic Twitter thread, finding the “general” solution would mean way less lines of codes.

⚠️ But there is always a balance to keep in mind: forcing a general solution is not good either. Forcing a unified language inappropriately , or forcing the a tool in an unsuitable context is not making the system any simpler. It is, arguably, not about generalisation.

Generalisation is ultimately finding not about “making a solution unique”, but more about correctly identifying the use cases that can be solved by a unique solution.

Separation of Concerns

My second rule of thumb, and probably the most important one when designing simple systems, is called “separation of concerns”.

A system can become simple when its parts are correctly isolated into sub-parts.

To some extent, a good system “architecture” requires good separation of concerns.

If you are a programmer, you have probably encountered multiple “messy” codebases with very long files, intricate logic, unclear scopes and namespaces… all of that makes it a really bad experience. 🤮

The ideal situation for any system is to have multiple parts, each with a clear purpose, and most importantly with clear interfaces to communicate with its other parts.

If we take a step back, the good approach to any complex problems is thinking how to split it into digestible chunks. Each digestible chunk should be isolated in order to be maintained independently of the other chunks, limiting the scope and the cognitive load required to… “digest” it.

Separation of concerns is also at the core of any human organisation: we can also think of the different business departments in a big company as its different components like the marketing department, the engineering department, the sales department, the finance department… They all exist to fulfil a certain role, and they still have to collaborate with each other in order to provide the necessary resources to other members. The way this division is operated is a key success factor.

Levels of Abstraction

The last part of the puzzle is to understand the hierarchy that helps organising the different parts of the system.

So far, we have seen that 1️⃣ “generalisation” is a process of striving for “less”, and improving the “efficiency” of a solution, of each line of code.

Generalisation is about optimising the effort required to understand a proposed solution. Therefore, each second spent understanding the solution (for example a pice of code written by a teammate) will cover a larger range of the problems it is addressing.

After the issues have been sort of grouped together, the 2️⃣ “separation of concerns” is the process of keeping the solution “digestible” for other humans. As the complexity of our suggested solution adds up, it reaches a point when it becomes necessary to split it in multiple parts.

Now, the reality is not as binary as having the issues on one side and the solutions on the other side.

Instead, a system is a solution made of multiple sub-parts that are also solutions. Each sub-part face their own challenges and implementation details. They are all dependent on other solutions to work properly.

The right way to conceptualise this issue/solution inception is by imagining layers.

Layers work because there is an inherent hierarchy in the solutions. Ideally, differentiating the levels allow to ignore the underlying assumptions we make. For example, when programmers write an application for computers, they rarely care about how electricity work. To some extent, they don’t always care about the specificities of network protocols or CPU cores implementation… Sometimes they have to, when working on these layers, but most of time it is an abstract layer that they take for granted and building upon it.


There is nothing easy in designing simple systems.

My own mental model is currently a 3-step process but I hope to keep re-thinking this framework and continuously improve upon it.

  • 1️⃣ Generalisation
  • 2️⃣ Separation of concerns
  • 3️⃣ Levels of abstractions

If bad code is spaghetti, good code is lasagna:

  • ✅ Robust architecture
  • ✅ Clearly defined layers of abstraction
  • ✅ Well-designed separation of concerns with purposeful distinguishable components
← All Articles