Simpler, not easier

Alessandro Berardi
The Startup
Published in
13 min readJan 4, 2018

The journey of an artist begins with uncertain attempts at the easiest tasks. The first strokes copied from masters, the first lines inspired by mentors. Step by step, as confidence increases with experience, achievements grow in complexity. Eventually a peak is reached, beyond which complexity vanishes. True mastery is expressed in simplicity. Only a handful of strokes, only a few lines in a piece, but each one in its form and place for a precise reason. Each one carrying deep meaning, each one an expression of a vast experience, each one a product of deep understanding. The message distilled in its essence, the artist’s vision captured in a single gesture. Simplicity becomes the vessel and the goal, the ultimate demonstration of beauty. The journey of an artist goes from ease to complexity. The journey of a master goes from complexity to simplicity.

Essential simplicity is not easy. Expressing the fullness of deep understanding in the pure absence of the superfluous is a skill that’s hard to grasp and much harder to master.

Simplicity in software development

The journey of a software engineer is analogous to that of an artist. The first few uncertain lines, often copied and pasted without full understanding. The excitement of the first working results. The challenge of understanding and being autonomous. The satisfaction of implementing complex functionality. The drive to learn more. The definition of a skill-set paired with a self-defining style. The identification of repeatable patterns, their interweaving in complex structures. Code becoming more succinct, flexible and powerful. Less repetition by packing variable functionality into accommodating and ever-growing interfaces. The intuition of being clever. Code that eventually becomes hard to understand even for the author.

The realisation that cleverness is not a synonym of quality. Going beyond the phase of having to prove oneself to others and leading to the phase of wanting to follow beauty and deliver value. Deepening the understanding that less is more. Writing code so simple that it is obviously free of deficiencies, where every responsibility is defined and recognisable at a glance, where levels of detail are clearly separated, so that it is possible to observe the big picture and dig deeper into the details with a perception of intuitive flow. Where dependencies are minimised and coupled elements have the same change rate. Where contracts are clear, solid and dependable, driven by consistency and common sense. Code that is stripped of the superfluous and, yet, is founded on principles of maintainability and extensibility.

A master’s code should be easy to understand and reason about. It should be essential and expressive. It should embody solutions based on the deep understanding of the problems it addresses. It should be holistic in scope and time. It should fit the big picture and welcome future changes based on their probability. It should be maintainable and reusable in its generic responsibilities. It should represent the essence and reason of good design. It should be evolutionary. It should be working beautifully at a glance.

A master never stops learning. A master’s code never stops evolving.

Divide et impera

How can simplicity be achieved? How can complexities be acknowledged, understood, and yet be addressed with simplicity?

The saying “divide and conquer” suggests breaking bigger problems into smaller ones which are easier to solve. In coding, this equates to identifying single responsibilities and implementing solutions for each one. But when does one stop breaking down bigger pieces into smaller ones? I would suggest that the boundary is defined by the possibility of contextually understanding the elements of a solution.

A multi-cellular biological organism can be an extremely complex system. It is nevertheless possible to identify the responsibilities of each organ. Organs are made of tissues, which are in turn made of specialised cells. In many cases, it is possible to infer which tissue a cell belongs to, even when it’s isolated from the system. This is because, while most cells share common characteristics, the specialised functions of a cell are clues of the environment where its interactions take place. A cell is not indivisible, it is made of molecules, which are in turn made of atoms. These components, however, do not offer sufficient clues to identifying to which kind of cell they belong when observed individually. At this level, the specific characteristics of cell specialisation are lost. This is because such specialisations are emergent from the structure and interactions of the components. It is not a simple sum of molecules that defines a cell, but a specific set of behaviours, an order in space and time.

Similarly, I would suggest that in code, a unit should be the parallel of a biological cell. Within the scope of a particular solution, a unit should be a collection of components whose structure and interactions serve a single purpose. Units may be organised hierarchically, so that units with higher level responsibilities depend on units with more detailed responsibilities. It is possible to identify generic responsibilities that do not belong specifically to the solution. These responsibilities can be implemented in reusable components which can be leveraged by units of the solution to perform generic tasks.

Unit testing should map to the same level of separation. Each test should verify a single responsibility within the solution, therefore unit tests should treat each unit as a closed box, testing the contracts and interfaces, but not the implementation details. Tests that follow these principles and level of detail are more meaningful, as they describe expected behaviours, and less brittle, for they don’t fail if only the implementation details change and they continue to be a valuable means of guaranteeing that contracts are honoured.

These principles, in Object-oriented programming, recall the Single responsibility principle, which is also the first principle of the SOLID quintuple of principles.

While there are many design practices that can improve software development and make coding a more joyful experience, following are a few that support simplicity:

  • Readable naming. Don’t over abbreviate unless the abbreviation is perfectly clear in its context. Name functional objects and methods after what they do, not after how they do it. Name domain model objects, modules and packages after what they represent. Code should be expressive and self explanatory. You should be able to write code that acts as its own documentation, where comments are unnecessary.
  • Responsibility separation. Each method should do exactly one thing. If you think the method should contain an ‘and’ or an ‘or’ it its name to describe its purpose, then you should split it. The implementation of a method can rely on other method calls, each one contributes to the ‘how’ of the method’s ‘what’. The responsibility of a method can be at a higher or lower level, but it should be clearly identifiable. Similarly, classes and modules should either represent a domain concept or a domain behaviour, not both. For instance, while a ‘User’ class should represent a user with its attributes, it should not implement the behaviour to find or modify a user record. Those behaviours should be implemented in separate classes or module methods. Keeping responsibilities separate makes it easier to reason about and maintain code.
  • Behaviour based testing. Tests should have the dual purpose of describing functionality and verifying its expected behaviour. They should therefore cover the ‘what’, not the ‘how’, and they should target interfaces and behaviour, not implementation details. They should break if the behaviour changes, not if the way the behaviour is implemented does. Tests that focus on implementation details ultimately hinder refactoring, while tests that focus on behaviour support ruthless refactoring by detecting alignment to expectations during changes to implementation.

Writing simpler code based on these principles is usually harder than writing complex, less understandable and maintainable code, as it requires more design effort, more attention and dedication, a broader understanding of the impact of changes, and usually also greater experience. The main difference between easy and simple code is that the latter is easy to reason about, maintain and modify by design.

Simplicity in project management

Many people often think they know what they want, oblivious of the fact that their understanding is superficial. As humans, we tend to identify the object of our desires based on our instincts, even though our instincts have evolved to react and help us obtain what we really want without necessarily helping us understand what that may really be.

When you’re trying to get some sleep and the neighbours are having a loud party, your first instinct may tell you that what you want is for them to stop. If you realise that what you really want is to be able to sleep, your solution space expands. With this realisation, asking your neighbours to turn the volume down, or finding some other way for them to reduce the noise, are no longer your only options. While asking them to reduce the volume may seem like your easiest option, it may not be cost effective in certain cases, and therefore not the simplest. You may not be on good terms with them, or it may be particularly early, so earplugs,or other ways to find sleep despite the noise, might be simpler solutions. Whatever the case, understanding the problem you’re actually trying to solve ensures better use of your time and resources.

The same pattern often applies when managing a project. A client or the business might request a solution to be implemented, but that solution may be symptomatic of the problem, not an actual solution to the problem itself. For instance, a client may request a complex navigation menu on their website in order to make the diverse options visible to their customers. The problem they are really trying to solve, though, might be that of offering a variety of products and options to a heterogeneous clientele. A complex menu may introduce resistance for customers and ultimately lead to potential revenue loss. A more effective solution could be to identify the main groups of customer types and provide targeted experiences, maybe by layering the navigation experience via simple filtering options, or possibly even by splitting the website into multiple websites. Also, trade-offs should be considered. Do all possible targets of the clientele need to be catered for? Can some classifications be simplified and most of the efforts be directed toward the targets representing the largest portions of customers or the ones more likely to convert visits into sales?

Another aspect to consider is intervention bias. The cost of implementing the wrong solution can be greater than the cost of not doing anything, therefore it is important to understand the root problem and identity the ramifications and consequences of any solution prior to implementation. In certain cases, it is better to abstain from intervention until more information is obtained. In other cases, when sufficient information is unavailable upfront, shadow testing, prototyping, A/B testing and other probing techniques can be very useful in identifying any issues with a solution before fully investing in it.

The principles of dividing complex problems into simpler ones can be applied to project management as well. In particular, iterative value delivery and prototyping rely on trade-off analysis to identify steps toward minimum viable products and subsequent improvements. These iterative approaches allow for minimising costs by adjusting the course of action on the base of continuous feedback and focus on instant gratification and improvement.

Simplifying solutions can help minimise costs and focus efforts on the aspects of greater value. This can be harder than trying to implement solutions based on information available upfront or simply doing what the client or the business ask, as it requires more involvement, deeper analysis, better understanding of context, lateral thinking and continuous adaptation. Solutions based on these approaches are more likely to actually solve problems and tend to deliver value more rapidly, as they can be implemented and adapted incrementally.

Simplicity in leadership

Managers may be tempted to implement complex and detailed processes aimed at minimising risks. The theory being that by ensuring that every step is detailed and fully documented, people have less room for error. This theory falls short when we realise that, in general, people are not particularly good at adhering to complex instructions. Humans can only focus on a limited amount of information at the time. Following check-lists and documentation can help, but the denser the information, the more likely people are to get distracted and make mistakes. This kind of approach is better suited to machines than humans. In fact, risk adverse tasks should be automated as much as possible.

People are good at inferring information within a context. It can be far more effective to clearly convey goals and provide general strategy than to give detailed instructions. This approach engages people’s capability of implementing approaches based on their strengths and measuring the outcome of their actions against the goals. Whenever the outcomes misalign with the goals, people should be able to adjust the course of action. Should this not be the case, good leadership can help clarify expectations.

Failure should be an option and a fail-fast and learn strategy allows people to improve incrementally by trial and error. Whenever a mistake is made, putting in place more checks and restrictions has the effect of increasing cost by introducing complexity and reducing trust. It is more effective to understand the root cause of a mistake and address it as needed rather than to blindly implement preventative or corrective measures at the surface. Was the mistake isolated or was it systematic? Was it perpetuated by one or by many individuals? What does this say about how clearly goals were conveyed? Do people need training? Is anybody requiring help or guidance?

How can people be held accountable without a full coverage of rules and regulations? How can they be held responsible within a culture that accepts failure? If somebody’s actions do not contribute to achieving a goal, it may be because the goal is not fully understood. This can be a chance for addressing a communication problem instead of applying sanctions. If a person continues to show misalignment to goals, this can be the base for the person’s performance review. If a person’s behaviour is detrimental to the team, colleagues and business, it clearly doesn’t align with the goals. Ultimately, if goals are clearly communicated and understood, if people are provided with sufficient assistance and guidance towards achieving goals and problems are addressed at the root cause, alignment with the goals should be a sufficient measure of performance and accountability. People’s actions should not be driven by the fear of breaking rules, but by the culture they are immersed in. The evolutionarily driven need to belong to a group can be leveraged to make people align with a culture of continuous learning and make them feel part of a team that is enabled and empowered to define and improve their path towards clear goals.

Ultimately, a culture of trust where people are given clear goals, actively involved in their achievement, assisted in their professional development and are valued for their contribution in a broader sense than just following directives, inspires people to give their best and make sincere efforts to deliver value and improve.

Providing people with context, clear goals and general direction instead of step by step directives and tasks which are strictly adherent to their job description, requires that they engage their skills actively. It requires people to think on their feet, to fill in the gaps, to sometimes step outside of their comfort zone, to show initiative and to grow. A simpler process, therefore, is not necessarily easier, as it requires greater individual and collective efforts, while at the same time being more likely to translate efforts into value and minimise mistakes and costs. Such a level of engagement is ultimately more rewarding, but not everybody is suited for it. Some people prefer to be told what to do and how to do it. It’s possible for such preference to derive from the fact that they are afraid to make mistakes, that they have never experienced anything different, that they’re used to doing things the way things have always been done without questioning why. In certain cases it is possible to inspire people to move away from this mentality, in other cases it would require an act of coercion, which would probably end up putting people in a situation they would struggle with. In the latter cases it is advisable to find ways to leverage the inclination toward following detailed tasks, otherwise it may be necessary to re-evaluate which people are a fit for the team.

A culture of trust allows for simplicity, but requires inspiring leadership, capable of deferring control, improving competence and communicating with clarity. Such an environment is harder to achieve than a traditional hierarchy where information is conveyed only to the degree that is considered useful for every member to strictly perform their duties, as it requires a higher level of engagement, role fluidity and shared responsibility. Leaders must be accountable for their teams and ultimately for achieving goals. At the same time they must be able to shape teams which they can rely on, trusting them to self-organise in the way that best suits individual personalities and collective interactions towards producing value and aligning with a shared vision.

Conclusion

The easy path and the simple path don’t always coincide. The easy path can be mistaken for the path of least resistance and reveal itself to be a dead end. Following the easy path is often the product of limited knowledge or laziness, whereas following the simple path may require the understanding of complexities, the analysing of trade-offs, the breaking down of bigger problems into smaller ones, the working iteratively with limited information, the communicating of goals clearly, the trusting of teams to step up to the challenge, the welcoming of change, and ultimately, the striving for excellence. Simplicity is often hard to achieve, but the effort pays off with long-term benefits. Simple code is easier to understand, maintain and change. Simple solutions are easier to test and adapt, increasing the probability of greater value delivery within smaller time frames. Simple processes based on clear goal communication and a culture of trust enable teams to express skills and potential beyond the limits of their duties and inspire people to continually improve.

Complexity emerges from the combination and interactions of simple components. While it’s not always possible to identify the elementary units at the basis of a complex system, breaking down problems into simpler ones and understanding them is often the most effective approach to envisaging solutions. Automation of risk-averse complexities and simple processes that rely on a culture of trust and continuous learning can efficiently lead to implementing valuable solutions in short time-frames.

Simplicity is achieved by applying knowledge, understanding and experience to problems. It is the consequence of the capacities of some of the most complex systems that we know of: humans.

This story is published in The Startup, Medium’s largest entrepreneurship publication followed by 281,454+ people.

Subscribe to receive our top stories here.

--

--

Alessandro Berardi
The Startup

I’m just a dream that wants to know its dreamer. On my journeys, I reason about technology, leadership, people and the universe.