The Valuable Dev

A Detailed Explanation of The KISS Principle in Software

the KISS principle is not about kissing

When I search about the KISS principle on The Internet, I stumble across many websites defining it in a couple of lines: simplicity is important, let’s be simple, the end. They often fail to explain what is simplicity, why simplicity is important, and how to achieve it.

Simplicity is one of the driving idea we should keep in mind at all time when designing a system. The problem: it’s really hard to achieve.

You guessed it: we’ll dive into simplicity (and complexity) in this article. I won’t write about all the different ways complexity can creep in your codebase but, instead, I’ll try to give you a quick overview of the different masks complexity can wear, with many examples. We’ll go from the business domain itself, through the nitty-gritty (the implementation), to end up in the complexity of software architecture.

More precisely, we’ll try to answer these questions:

  • What’s the difference between simple and easy?
  • Why do we need to keep things simple?
  • Can we avoid the
  • Why should we thrive to delete code?
  • What is cyclomatic complexity?
  • What means simplicity for an architecture?
  • How to manage your dependencies?
  • Why we shouldn’t try to outsmart everyone with our code?

Simple… Or Easy?

There are important difference between the concepts of easy and simple we need to understand first. Let’s begin with plain definitions. What the dictionary has to say about simple?

Plain, basic, or uncomplicated in form, nature, or design; without much decoration or ornamentation.

Composed of a single element; not compound.

Easy enough: if your system has only a few parts, you have a simple system.

We could also try to define “simple” by looking at the definition of its contrary, complex:

Consisting of many different and connected parts.

Oho! That’s interesting. A simple system is not a system with one and only one element necessarily. It’s a system which has not too many different elements connected to each other.

If you’re still not convinced, that’s great: it’s really important to have a critical mind. Let’s look at a synonym of complex, complicated:

Consisting of many interconnecting parts or elements; intricate.

I can feel your adrenaline going through your entire body, your neurons doing the dance of understanding, and your whole soul projecting the light of knowledge in the universe. Again the same idea!

Now, what’s the difference with easy? Let’s open the dictionary again:

Achieved without great effort; presenting few difficulties.

Let’s say that you have to do an addition in binary, and you have no idea how to do it: it’s not easy for you. You lack some knowledge. But your addition is not a system with too many parts. It’s not a system with connected parts. As a result, it’s hard for you, but it’s also simple.

Another example: if you have a codebase with 18237 classes all coupled to each other, you have a complex system: there are too many elements (the classes) entwined with each others. Even if you can understand every single algorithm in your system (they’re easy for you), the system itself stays complex.

To summarize, here are the two sins of complexity:

  1. Too many parts in the system.
  2. Too many interconnected part in the system.

Now that we really know what complexity (and, by extension, simplicity) means, you might wonder:

  • Why is it important for a software developer to put his nose in some boring dictionary?
  • Why simplicity should be considered as a God who will save us all?

Why Do We Need Simplicity?

Developer Having a Mental Model

Why being a software developer can be hard? One word: change.

If the application you’re building will never change, stop reading this article immediately! Actually, don’t read anything about good principles in software development, it’s useless for you. Just do some procedural code in any language you want, don’t follow any principle, and you’ll succeed. Nobody will put an eye on your codebase anyway.

A codebase which doesn’t change is a myth. The context around your application will change (libraries, market…) and, as a result, your application will need to change, too. To modify your application, you’ll have to understand how the different parts of your system work together. This representation sitting in your brain is the mental model of your system.

Depending on the system, your mental model will be more or less accurate. What about complex systems? I’m sure you’ve guessed it: it’s difficult and tiring to create an accurate mental model of a complex application.

Controlling complexity is the essence of computer programming. We will always be limited by the sheer number of details that we can keep straight in our heads.

Let’s look at the codebase of Dave, your colleague developer. It’s full of hidden dependencies and global mutable state. When you change something, you have no idea of the rippling effects propagating in the whole codebase. Your energy level goes down as you’re trying to put everything in your head and, eventually, your anger goes up. At that point, you might surprise yourself cursing Dave’s family on multiple generations.

The result of complex systems? Bugs, unhappy developers, risk of burnout, and frustrated customers at the end of the chain. Nobody wants that.

Business Complexity

The complexity of a system will also be a good indicator of the amount of time developers need to add a new functionality. This is really important, business wise. Nowadays, an organization needs to have a good amount of adaptability and velocity (speed with a direction) to succeed.

Yet, we can’t always avoid complexity. It’s first and foremost the mirror of the business complexity we build an application for. The complexity of the requirements will decide of a good part of the complexity of our systems!

For example, if you develop an e-commerce, you will have difficulty to avoid having products, orders, shipments, stock management, or returns. There a lot of moving parts in there.

That’s why, as a developer, you need to keep the following in mind:

  1. If you can avoid complexity, avoid it, as much as you can.
  2. If you can’t, you need to make sure that you manage it properly.

How the hell do we do that?

Simpler Requirements

To avoid as much business complexity as possible, I suggest attacking the complexity of the business requirements directly. How?

  1. If you don’t have any planning meeting where developers and business people (project managers and whatnot) discuss the requirements, please ask for it. Insist on that point, till you get it. Don’t follow blindly what the top management is throwing at you. They might not understand it, but a developer has a lot of valuable knowledge which can help to reduce the complexity of the requirements.

  2. During these meetings, when you see a potential feature which will bring a lot of complexity, ask questions which begin by “why”. Why do we need this feature? Why is it useful for our customer?

  3. If you really need this feature (which will be often the case), you might have a better idea to make it simpler. For example, what about having 10 fields instead of 20 for a user register form? Additionally, a simpler feature to implement will often be simpler for the customer too. This is a good argument to sell your idea to your stakeholders.

That said, be aware that reducing complexity directly in the requirements is difficult. Why?

First, many stakeholders seem married to their ideas. That’s normal: when we have ideas, we often think it’s THE idea which will change the world. We often have close relationships with them, associating our ideas with who we are. This is a mistake: these ideas are external entities. They are not an extension of our personalities.

Additionally, the more we work on our ideas, the more we will feel the need to get something out of it. Even if the idea is a very bad one, we can stay stuck on it for a long time.

If you see somebody too attached to their ideas, knowing how to argue properly can help here.

Second, it’s sometimes not possible to simplify a feature. In that case, try to reduce the complexity has much as you can while implementing it. More on that later.

Keep in mind though: if you try to understand the business domain you’re working for, you will find simpler solutions. Try to speak with domain experts (people having the knowledge of the business itself) as much as you can, always refresh your knowledge, and avoid being overconfident in what you know.

The customers, at the end of the chain, should be the ones discarding ideas and features. Listening to them is utterly important.

A Good Implementation is a Deleted One

Less code will often make your system simpler. Big is often synonym with complexity.

Indeed, changing your code can have an impact on the surrounding code, and create undesired results. When you think about it, lines of code are potential technical debts, sources of bugs, triggers for headaches, screams, and tears. Show no mercy: remove every useless piece of code you can find.

As Antoine de Saint-Exupéry famously said:

Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.

Enough said.

Dead Code

Destroy All the Dead Code

Do you want a cemetery full of old functionalities rotting in your shiny system? Me neither.

But first, what’s dead code? Everything which is not used in the application at runtime: variables initialized but never called, method never used, classes never instantiated.

Letting your dead code around will have the following consequences:

  • Developers will be confused while working on the system. Questions and doubts will cripple their mind: what’s the purpose of the code? Should I keep it? Is it useful?
  • Your colleagues will possibly maintain it, spending time refactoring it for no benefits.
  • Dead code hide the real implementation. Did you ask yourself already: “why, when I modify this method, nothing happens”? I did. It gets old, after a while.

Simplifying our codebases by removing dead code will simplify our mental models. It means fewer headaches and less energy drained into these useless zombies.

The YAGNI Principle

A specific form of complexity (or dead code), which is often ferociously defended by developers, is the famous code which will be useful one day, in the future, so we should keep it.

This is where the YAGNI principle comes from: it means “You Aren’t Gonna Need It”. And it’s right, 99% of the time: you never know what will happen in the future, and the code trying to do some divination will never be used and forgotten.

If you stumble across some code written for no actual present purposes, delete it. If you need to implement it in the future, it will be in a different context, and your old code would have become obsolete anyway. You can still come back to your first implementation you’ve deleted with your version control system (git, anyone?).

I don’t count how many debates I had about this specific topic during my career. Here’s an example:

  • Me: Why the functionality to put a hat on users is implemented in the system? I don’t see the option available on the frontend… but it’s everywhere in the backend!
  • Colleague: It’s not used for now. But we will use it one day, for sure.
  • Me: But… it was implemented in 2016, we are in 2019. If nobody needed it for three years, maybe we won’t need it for two years more. We could delete it.
  • Him: No. It’s too complicated. We will use it, for sure.
  • Me: … but developers will maintain that. I spent hours myself to understand how to manage it, since I’m implementing something directly related which could break it.
  • Colleague: We will need that for sure.
  • Me: … and what about this functionality which qualify the user as “wizard”?
  • Colleague: Oh. It’s not used. But it will be used one day, for sure.
  • Me: …

Nobody knows the future. Functionalities can be canceled or staying in the backlog forever. Context change. Meanwhile, useless features will be maintained, sometimes with great efforts, for nothing.

Yet, I saw developers spending a crazy amount of time to be as flexible as possible for the hypothetical next features which never come.

Only add the complexity you need to answer the present business needs, no more.

We’re developers, not soothsayers.

A Wizard Developer Trying to Foresee the Future

Global States and Behaviors

Using global states is not a good idea 99% of the time. I wrote a detailed article about it here, also explaining the dangers of states with growing scopes.

Simple Architecture

Complexity kills. It sucks the life out of developers, it makes products difficult to plan, build and test, it introduces security challenges, and it causes end-user and administrator frustration.

Ray Ozzie

Lasagna Architecture

Complex Architecture in Lego

Lasagna is a delicious dish (when it’s well-made), but a nightmare in a codebase. The problem of a lasagna architecture? Too many layers of indirection.

Let’s imagine that you have an application which simply takes some inputs from APIs, apply some business logic to transform the data and store it somewhere. Sounds reasonable, does it?

Here’s what you can do. Each point is a layer:

  1. APIs receiving some request.
  2. Wrappers of the APIs.
  3. Server factories which create the different controller and inject the APIs wrapper.
  4. A pool of server factories in case you want to have multiple of them (?).
  5. Controllers where the API wrappers are called via interface constructs.
  6. A validation layer to validate the inputs.
  7. Another layer to share code between every controller (in an attempt to make everything as DRY as possible).
  8. A model layer which is as dumb as my feet (anemic models).
  9. A persistent layer called via interface constructs from the controllers.
  10. Another layer which only get the Ids of entities to fetch the whole entity itself.
  11. A layer which does the actual CRUD and implement the real business logic. In case of selects, this layer does some SQL request, get only the IDs and pass it to the layer 10 which will fetch the entity.

You didn’t get the whole picture? That’s normal.

Now, imagine yourself trying to modify some functionality. You might have to:

  • Go through most of the layers.
  • Try to find out if one layer will crash because of your change.
  • Try to remember how everything works in your painful brain.
  • Make your change.
  • Wonder what the layer was doing.
  • Verify everything again.
  • Fix three tests failing on 4 layers. Two days of work only for that.
  • Call your boyfriend, your girlfriend, or your favorite cat, screaming that you’re such a fraud.
  • Dream about a little house in the mountain where you can spend your time making some goat cheese instead of wasting it with some nonsense.

You’ll need to have a mental model spreading on 11 layers to correctly manage the changes of your codebase. Good luck!

Now, you might think that I’m crazy. You might think that this kind of architecture was created by my sick brain. You might think that such codebases don’t exist. But this application is real: I’ve worked on it enough to still feel the pain when I think about it.

You know what? This codebase was full of good intentions. The developers responsible for this monster didn’t want to create a complex system on purpose. They wanted to help everybody by creating something extremely flexible, elegant, DRY, the perfect software.

As my good old friend Saint Bernard of Clairvaux was saying the other day:

The road to hell is paved with good intentions.

The previous example will be useful to explain why unnecessary abstractions can make your system more complex.

Abstractions and Complexity

What’s an abstraction? It’s a way to deal with complexity by hiding some useless details. I wrote about it in depth in another article.

For example, a function is an abstraction: when you call a function in your code, you won’t necessarily be aware of its implementation, and, as a result, its complexity. Instead, you’ll:

  1. Look at the signature of the function (its name, input, and output).
  2. Decide to use it or to find something better.

It sounds great, right? It doesn’t matter how much complexity a system has anymore! Let’s develop a big Ball of Mud and put layers of beautiful, flowery abstraction on top, to hide all the mess.

Well, not really.

As we saw, functions can be considered as an abstraction. Objects in OOP too. Even some primitive of your favorite programming language, like arrays for example, can be seen as an abstraction of a more general concept (a mathematical sequence, for example).

For your abstractions to work, you need to create cohesive implementations where things which belong together are together. Otherwise, the complexity will just jump back on your face like a mental health leech. Consider this example:

<?php 

$this->createOrderAndMoveStockAndDeliverToUser();

What does this abstraction tell us?

  1. The implementation looks like it deals with many things: orders, stocks and shipping. Three concepts, all together.
  2. The method contains concepts which don’t belong together. You can create an order without moving stocks, and you can move stocks without creating an order.
  3. Even if the complexity is abstracted and hidden, it still exists. It’s just hidden, not removed.
  4. If you change the creation of an order, the stock movement and the delivery process can be affected, even if they are independent.

This abstraction hide complexity but doesn’t offer simplicity either. In that case, Divide and Conquer should be your motto:

<?php 

$this->createOrder();
$this->moveStocks();
$this->deliverToUser();

The method are now more focused. Self-containing logic is a good way to deal with complexity. It’s called encapsulation, and it’s one of the most fundamental principle we should all have in mind when we’re coding.

Let see another one of these fundamental principles.

The Cost of Polymorphism

An ugly polymorph Lego character

Polymorphism is a core concept in programming: it’s the ability of a module (class, package, and such) to take different forms. You can use the interface construct to do so, for example:

<?php

namespace App;

interface Checkout
{
    public function addProduct();
}

Interface constructs are great, right? They allow you to swap part of your system without affecting everything around. Unfortunately, polymorphism can also bring a lot of complexity. You remember the lasagna architecture I described some paragraphs above? Many layers have interfaces for the sake of flexibility.

You want to store your entities into a file instead of a database? Polymorphism via interfaces can allow you to do that. That’s great, but everything, in the magic development world, has a price:

  • It’s difficult to know what implementation is really used at runtime. Indirection can bring a lot of confusion.
  • You might need to update these interface constructs each time you add or modify the implementation. They might get bigger and bigger overtime, a clear sign that you shouldn’t abstract it.

Let’s go back to my application with 11 layers: it has 68 interfaces for 26759 lines of code. It means that, in average, there is an interface for every block of 393 lines of code. Who wants to change the implementation of so many parts? Will it ever happen? The answer is likely “no”.

In general, ask yourself: does your application need this kind of flexibility?.

Don’t get me wrong: interfaces are very useful in some cases. If you need, now, in the present, to swap an existing implementation with another existing one at runtime, please go ahead. If you think you’ll need it, in the future, for future implementations, wait to be there to do it.

Now, you might have heard about the following, from the famous Gang of Four:

Program to an ‘interface’, not an ‘implementation’.

This sounds contradictory! Should we use interfaces for everything?

The term “interface”, in that case, doesn’t refer to the interface construct you can find in many languages. It means that you need to decouple your dependencies if you need to, using whatever construct you want. A simple wrapper can be enough.

Let’s talk a bit about this dependency question of old.

The Dependency Problem

An ugly polymorph Lego character

It’s safe to say that too many dependencies in a system make it complex. After all, by their nature, dependencies couple parts of your system with one another: the definition itself of the Unforgiven Second Sin of the Complexity of the Death.

Is the Dependency Necessary?

When you create new dependencies between classes or modules, ask yourself:

  1. Is these dependencies are really necessary? Is it possible not to couple your code?
  2. If they are necessary, how can I make the dependencies obvious?

Think about the business you’re working for: does it make sense to create dependencies in the specific context you’re in? Is it necessary to couple the Shipment logic with the Order logic, for example? Not really: an order can exist without a shipment, therefore an order should not have dependencies with the shipment logic.

These questions are really important, and they can make a huge difference overtime. It might stop your coworkers to throw stones at you when you’re eating at the company’s cafeteria.

Managing the Dependency Complexity

Having every dependency grouped somewhere can really help to understand the system complexity at a glance. It will help you to have an accurate mental model of your dependencies. For example, a dependency injection container can help in that regard, but it’s not the only solution. When I code in Golang for example, I simply declare all my dependencies in the same package.

Managing your dependencies in a clear way will lower the chances to affect other part of your code when you make a change. It will serve as a guideline for everybody working on your project.

Therefore, it’s not a good idea to inject dependencies via setters or other methods in your classes. The best is to inject them in constructors: it’s then easier to find them. If they are injected and managed in random methods, how can you find them back without struggle? This is a good way to unleash the Demon of Complexity™ on your poor application, on your colleagues, and on your company.

Let’s take a very simple example:

<?php

class ProductCollection
{
}

// This is a clear implementation.
// 1. The dependency productCollection is directly visible in the constructor, at the beginning of the class.
// 2. It indicates that the class Order needs a productCollection to be  instantiated.

class Order
{
    /** @var ProductCollection */
    private $productCollection;

    public function construct(ProductCollection $productCollection)
    {
        $this->productCollection = $productCollection;
    }

    // More methods
}

// This is not obvious enough.
// 1. The setter is lost in a bunch of other methods.
// 2. It's confusing: when does the productCollection should be injected? Is it important? In what case?

class ShoppingCart
{
    // Bunch of other methods

    public function setProductCollection(ProductCollection $productCollection)
    {
        $this->productCollection = $productCollection;
    }

    // More methods
}

Don’t Try to Outsmart Your Colleagues

As an appetizer, let me quote M. A. Jackson who wrote Principles of Program Design:

Programmers… often take refuge in an understandable, but disastrous, inclination towards complexity and ingenuity in their work.

It’s a common pattern I see all the time.

Sometimes, we try to show how smart we are, without even realizing it. We bring design patterns in our codebases without solid reasons, and we invent clever ways to do things only the Best of the Best, the True Developer, The Elite of the Binary, The Code Champion of the Galaxy can understand.

As Bjarne Stroustrup was saying:

Make Simple Tasks Simple!

You might recognize that you have this problem in your company when you will see complicated CQRS architecture, factories all over the place, decorators, pools, for a CRUD application. In short, high technical complexity for simple problems.

The goal of a developer is to help a company running smoothly and satisfying their customers, not creating pile of complexity using every design pattern one can possibly know.

I’m a developer. I know how funny it is to play with new technologies and other toys. Companies should definitely allow us to experiment, in order to bring the best technologies and techniques for the tasks at hand. But if you see that a technology (or a technique) doesn’t bring any value to a company, you need to push away this useless complexity.

Let’s continue our quote feast with Rich Hickey:

Patterns mean “I have run out of language.”

If you really have no other solution, if you need more flexibility because the requirements are complex, than yes, by all mean, introduce the practice or technology you need. But Before doing so, try to find out if the programming language you’re using can offer already a descent solution without going for your complex design patterns

It also means that you should only introduce this kind of complexity when refactoring. Try not introducing complexity when you first write a new piece of code.

KISS means: Keep It Simple Stupid. No, this doesn’t mean that you’re stupid. You’re very smart and beautiful.

It means that your code should be stupidly simple. It should be so simple, everybody should understand what the system does and grasp as quickly as possible a good mental model of it. It should be so simple that when you will quit your company, a junior developer should be able to continue your masterpiece.

To come back to my lasagna architecture with 11 layers, the design was created up front, with the only purpose of hyper-flexibility. It wasn’t a rewriting or a refactoring either.

I didn’t brush the whole complexity of this application. The author tried to follow as well some CQRS paradigms with an anemic domain model approach. Implementing the possibility for a user to copy another user took me two full days. It should have taken a couple of hours, tests included.

As a rule of thumb:

  • Code as close as possible to your immediate needs.
  • Stay close to the business problem as much as you can.
  • Be as clear as possible on the purpose of each piece of code. Don’t hide it behind abstracted generalities.
  • Extreme flexibility and future divination can introduce a lot of technical debts.

Don’t try to be the smartest guy in the room. Try to be as stupidly simple as you can.

Bottom to Top Development

I would like to describe a development method which push me to stay simple and as close to the requirements as I can, without going into crazy complex assumptions: bottom to top development.

  1. Begin by implementing the lowest layer of your application.
  2. Code only what you need for your feature to work.
  3. Test it.
  4. Refactor your code, if necessary.
  5. Move one layer up.
  6. Code only what you need for your feature to work.
  7. Test your new code if necessary (that is, if this layer has any logic, otherwise you will only test the code on the above layer twice).
  8. Go back to 4.

Unit tests can force you to see if your code is properly decoupled. In fact, it’s very difficult to test when your modules (classes, packages, and whatnot) are interconnected: you will have to mock all of them, a very good and painful indicator that you should simplify things if you can. If you can’t, don’t mock them either: write integration tests and drop your unit tests.

I won’t go into details about testing, the subject by itself deserve its own article. Just remember that automated tests help you simplify (and refactor) your codebase. It’s essential that a codebase include automated tests; they should be considered as part of your application.

The KISS Principe in a Nutshell

This article is not meant to be an exhaustive list of everything which can bring complexity in a software. I want to show how complexity can rise quickly from many different sources.

If you need to retain only one thing, it’s this: complexity needs to be considered at every stage of the life of a codebase. During design, implementation, refactoring, bug fixing, and eventual rewriting.

To summarize what we learned together:

  • A simple system doesn’t have too many parts and, more importantly, doesn’t have too many dependent, interconnected parts.
  • If you can have a clear mental model of the part of the codebase you need to act upon, the complexity is well managed in this area of the codebase.
  • If you can, try to reduce the complexity of the features your managers try to build, by proposing (almost) equivalent solutions. You will need good, business related arguments (speak about money and time).
  • Delete every bit of code for your present needs. Nobody knows what will happen in the future.
  • Avoid global mutable states and behaviors like the black plague.
  • Don’t create too many layers of indirection in your application.
  • Abstractions should be created when you need to generalize or simplify some knowledge now (not in the future).
  • Make clear where the dependencies of your application are managed.
  • Be careful not to couple your modules (classes, for examples) if you can find a better technical solution.
  • Don’t try to show off how much you know in your codebase. Keep in mind that anybody should be able to modify it; even a beginner.

What does it mean, at the end, “to keep it simple”? Should we stop speaking about the general concept of “simplicity” (or the KISS principle) and try to identify (to name) the specific problem to find an adapted solution?

Next time you catch yourself advising that “we should make the system simpler”, you could say instead “this class A is too coupled with this another class B, let’s find a solution”.

I would end this article with a warning: “simple” is good, “incomplete” is not. Your first goal is to make your code working as expected. A simple, beautiful system which doesn’t do what is supposed to do is… useless!

Share Your Knowledge