Designing “Simple” Systems
Amazing products outshine the competition thanks to 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
Understanding “simplicity” is a key success factor for any company or product.
Take Trello, the product-management app that was so simple and flexible that many organizations decided to switch from well-established project-management giants and use it at the core of all their processes.
Trello was brilliantly providing a simpler solution to many overcomplicated alternatives.
Even Slack, or Discord, revolutionized team communications only a couple of years ago (even though it feels way longer than that). Instant messaging and voice chat were nothing new at the time, 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 is Hard
The first and most important concept is that achieving simplicity is not easy.
Quite the contrary: building a simple product or service is hard and often requires many iterations.
- ✅ “Easy” commonly refers to something easy to make, easy to code, easy to implement… basically solving a problem without requiring too much effort, thinking, or energy.
- 🚀 “Simple” — on the other hand — would refer to a solution that is easy to understand, easy to use by others, and easy to maintain for others. Building a “simple” solution to a difficult problem is not an easy task.
Voice assistants like Google Assistant or Siri are meant to be simple products: users interact with the assistant using natural language, and the software executes the command accordingly.
Though, the difficulty of writing the algorithms required to build such features is already today reaching a level a high level of complexity (and they still have room for improvements in the next upcoming years).
Voice assistants have always been present in sci-fi movies, and they never required an introduction or a manual for the viewer to understand the concept. They have always been self-explanatory: everyone grasps the idea, the concept of a voice assistant, and how to use it.
This is the hard part about simplicity: making it easy for others. How to hide the complexity from the end user.
My own personal mental modal is to conceptualize it in 3 different areas:
- 1️⃣ Generalization
- 2️⃣ Separation of concerns
- 3️⃣ Levels of abstractions
Generalization 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.
Generalization can apply at many different levels. An organization can benefit from generalized 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 languages) is easier to “understand”, easier to use, more flexible, and more agile.
Code can obviously be generalized 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 fewer lines of code.
⚠️ 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 tool in an unsuitable context is not making the system any simpler. It is, arguably, not about generalization.
Generalization 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 a 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 problem is thinking about 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 organization: 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 fulfill 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 organize the different parts of the system.
So far, we have seen that 1️⃣ “generalization” is a process of striving for “less”, and improving the “efficiency” of a solution, of each line of code.
Generalization is about optimizing the effort required to understand a proposed solution. Therefore, each second spent understanding the solution (for example a piece 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 into 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 faces its own challenges and implementation details. They are all dependent on other solutions to work properly.
The right way to conceptualize this issue/solution inception is by imagining layers.
Layers work because there is an inherent hierarchy in the solutions. Ideally, differentiating the levels allow ignoring 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 the time it is an abstract layer that they take for granted and build 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 rethinking 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