Software Architecture in the agile World – Part 5: Changing things without breaking them

These are the slides with some notes from my talk about software architecture when developing in an agile way. Contents

Max knows now how to add new features and how to extend the architecture in an iterative and incremental way. He knows how to split and defer decisions. But how can his team keep adding stuff without breaking what is already there? Breaking existing stuff is very time consuming and frustrating. Re-work is what costs teams a lot of their capacity. Instead of building new features, they are held back by defects.

The best thing you can do to help your team to not break things is building simple software. Software that is easy to understand and explain. If the software is easy to understand, the side-effects of a change get easier to tell. Thus it is easier to prevent the introduction of defects or unwanted design effects.

For a quick check whether your software is simple, see here.

When the source code is simple then it is easy to understand – by definition. If the code is easy to understand, it is easy to change. If it is easy to change, it is easy to refactor. And if it is easy to refactor, it can be made even simpler. This positive feedback loop helps to keep a codebase simple over a long time. Otherwise, the code will rot and it will become “legacy code” rather quickly.

I have three indicators to give you a good grasp on simplicity:

  • small things are generally simpler than big things. A 10 lines method is simpler than a 1000 lines method. Except when it is Perl 😉
  • a codebase built from a low number of concepts is generally simpler than a codebase with lots of concepts used.
  • A system with a low number of interactions is generally simpler than a system with lots of interactions. The typical problem of a badly designed microservices architecture.

Aspects of simplicity are:
Bounded – the whole is built from bounded pieces. To comprehend the whole, comprehend the parts individually and their interactions.
Conciseness – not vague, abstract or generalised.
Expressiveness – no guessing or assumptions.
Readable – the form is optimised for readability, not writability.
Understandable – it can be understood in a reasonable amount of time.
Unsurprising – it behaves as one expects.
Self-explanatory – everything needed to understand is included (you can also add links to code comments!).
Reassuring – it gives a feeling of safety to change and adapt it.

I have this list from a presentation by Kate Gregory. You can find it here.

A solution to a problem can be either specific or generic.
Specific = a solution to a single problem
Generic = a solution for several problems

A solution is also somewhere on the continuum from concrete to abstract. A chair, for example, is a bit abstract but still quite concrete – Furniture would be more abstract, “the red chair with a dragon sticker belonging to my daughter Nora” would be more concrete.

We humans can understand concepts and solutions that are on the lower right – towards specific and concrete – much easier than generic and abstract solutions. There is no hard boundary but a tendency. While we can cope with a little abstraction quite well, generic solutions quickly become hard to comprehend.

Therefore it is a really bad idea to start a new software product by developing a framework first because a framework is a rather generic and abstract solution – if it ever really solves the concrete and specific needs later anyway.

We should start with concrete, specific solutions. We should only take a step towards abstraction or genericity when it helps overall simplicity because of much less code in return. Remember that smaller things are generally simpler than big things.

Our solutions, therefore, tend to walk slowly from the concrete-specific corner towards the generic-abstract corner. Luckily, if we invest time into refactoring and reflecting our understanding of the problem and its solution, we will have so-called simplicity insights. An insight that we can achieve the same thing with a simpler solution. Then we can push our solutions a bit back to the simpler concrete-specific corner.

Of course, tests help us to keep things working, too. I like the term executable specs because we can use these tests for testing as well as for documentation of the system. Whether it’s Test-Driven Development, Acceptance Test-Driven Development or Behavior-Driven Development is not that important. Important is that we have a safety net of tests that tell us when we break existing functionality. This way, we can add or change functionality with confidence, without being slowed down by defects that we introduced in already existing functionality.

Another important concept regarding flexibility and testability is clean architecture.

Clean architecture describes some rules on how to structure the code into layers. Things in the outer layers are allowed to know things on the inside, but not vice versa. Dependencies always point to the centre or the same layer. In the image, the layers are entities, use cases, some glue code so that the outermost layer can call the use cases, and the layer containing all the code talking with the environment: database, UI, filesystem, devices, other services and so on.

The program flow can jump through the layers as needed, the dependency rule applies to the static dependencies only. Therefore, when a use case requires some data from storage, it uses an implementation that lies in the outermost layer and that implements an interface that lies in the same layer as the use case. So the use case statically only knows the interface. The implementation is injected into the use case at runtime.

The benefits of a clean architecture are that we can replace things on the outside without breaking the things on the inside – we get replaceable details. Furthermore, we can exchange the things on the outside with tests. We get 100% testability of the things in the inner layers.

In our sample matching product, the layers could look as shown in the image above.

Let’s sum up: To not break existing things, we should keep things simple use tests that support us and keep our architecture clean and flexible.

Now that we know how to change and extend things without breaking them, we need feedback so that we know where to run to. Continue reading: How to get fast feedback

About the author

Urs Enzler

1 comment

By Urs Enzler

Recent Posts