3. Modifiability is a central system quality

Why it matters

When we work in an Agile environment, we are learning two things continuously:

We have learned that NFRs guide architectural decisions. NFRs do change over time. Let’s look at a small example:

NFRs change over time, for example, the architecture needed for Proof of Concept, Minimum Viable Product (MVP) and Big Launch release will vary.

As NFRs change, this means that the architecture must also change.

The second important factor is learning how to build it, and doing so using iterations. This means that we will go back to old solutions and revise them. So the iterative process gives us the chance to improve our architecture over time.

In summary, we have two drivers to change the architecture:

  1. Our understanding of what the right thing to build is and the requirements for our architecture change
  2. Our insight of “the right way to build it” will change

While we can’t foresee the “final” architecture that will be fit for purpose, since not all facts are known from the start, we can foresee that the architecture needs to be modifiable.

How it works

Let’s start with an analogy: If you as a software developer write unit tests for your code, then you will automatically make your code test friendly. You will for example learn that “dependency injection” is a good pattern to make your code testable.

In the same way, if you want the architecture to be modifiable, then force yourself to modify it using iterations from the start. The hidden benefit this process will bring is a drive towards a simple and modifiable architecture early on, which enables you to learn about your fit for purpose early using fast feedback rather than Big Design Up Front (which produces late or no fit for purpose feedback).

This means you should build your architecture iteratively, and make it scenario-driven:

A simplified picture of the architecture in a self-driving car: The car senses what is happening in its surrounding through sensors, then feeds this to a Perception layer that interprets what these objects are doing. Will they cross the road? The car decides its course of action and actuates this decision, it steers, accelerates and brakes.

  1. Intentionally, just as when you use TDD, do “the simplest possible solution that will possibly work” that will solve one scenario or use case. For example, for a self-driving car, an early scenario could be “follow a straight and curved road inside a constrained area”.
  2. Strengthen the parts that need to be strengthened to support the NFRs that need to be supported. Extending our self-driving car analogy: “follow the road and make sure that the accuracy, safety and speed is good enough”.
  3. Regularly refactor your architecture, reduce technical debt and simplify design.
  4. Evaluate your architecture’s fit for purpose continuously using fast feedback.
  5. Actively avoid the “pancake architecture” approach - think about supporting scenarios and adding more of them over time, rather than building platform layers.

The danger of pancake architecture is how it influences the design of the code: Every layer provides a service for the layer on top, and the trap is that you want to create a complete service layer, you try to predict “what features need to be there” even if you don’t need them right now. The second trap with the pancake architecture: Surprisingly, features and services are often added for symmetry reasons in the pancake architecture. For example, when you add a “create method” to a service you also add a “delete method” (for symmetry). Likewise, if I can create and delete one type of object, I should be able to create and delete all types of objects.

What is hidden from view is what drove us here in the first place: the assumption of the pancake architecture that we need to complete all layers.

The cost of this is technical debt, overengineering, loss of early feedback and missed market opportunities. 

Modularization is not the answer to everything. If you modularize too hard, and too early, you create many dependencies across the architecture. Dependencies slow you down.

A good test for your architecture is: “How many modules must I touch to change one feature?”  The fewer the better!