BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles Fitness Functions for Your Architecture

Fitness Functions for Your Architecture

Log in to listen to this article

Key Takeaways

  • Software architecture must evolve to keep up with changing requirements.
  • We need protection against erratic and unwanted changes, guardrails that ensure architecture evolution stays within the desired direction.
  • Fitness functions provide such guardrails with objective measures; they can be thought of as automated (unit) tests for your architecture.
  • With libraries like ArchUnit it becomes feasible to write fitness functions for the structural aspects of architectural fitness.
  • Using fitness functions fosters discussions and collaboration between architects and developers.

Software, its size, its requirements, and its infrastructure environment evolve over time. Software architecture should evolve accordingly. Otherwise, we risk an architecture that no longer meets current and future operational and developmental requirements. We even risk the ability to implement feature changes and additions. Fitness functions are guardrails that enable continuous evolution of your system's architecture, within a range and a direction, that you desire and define.

What are fitness functions?

Fitness functions come in various flavors and are applicable in different domains. Let's focus on fitness functions for software here, more precisely software architecture. I like to use the definition from the book, Building Evolutionary Architectures, by Neal Ford, Rebecca Parsons, Patrick Kua, and Pramod Sadalage: "An architectural fitness function is any mechanism that provides an objective integrity assessment of some architectural characteristic(s)".

In case that reads too vaguely: fitness functions offer us self-defined guardrails for certain aspects of our architecture. If we stay within certain (self-chosen) ranges, we're safe (our architecture is "good"). If we're outside those ranges we get fast feedback in our build pipeline, for example, or even faster if we run the fitness functions locally on our development machine. Developers and architects can react quickly, fix the violations, and make the code "fit" again.

An objective measure of each fitness function is important! Measurements and measures should not be based on personal opinion. For instance, "readability" is probably not a good fitness function: "Your code does not look readable to me. I'd write it differently".

Objective numbers are a better choice: "We have 17 violations which is within our tolerable range of up to 20 violations. So we're good for now. Let's talk about changing the tolerable range to allow only 10 violations for the next iteration". Or "We found two unwanted dependencies between the modules. We must remove these dependencies". These measurements are objective, based on numbers. Possible actions are clear.

The purpose of fitness functions in evolutionary architectures

Fitness functions are the essential foundation for evolutionary architectures: They enable evolution in an advantageous direction. We can define what "advantageous" means in our context.

There will always be deviations in systems, and changes to the code, the architecture, and the product. Good changes (the ones that we need for the future, for the "survival" of our software) as well as bad changes that might endanger the viability of our system.

Fitness functions help us decide which deviations or changes are acceptable, which can be tolerated for the time being, and which deviations or changes we should revert. In that respect, architectural fitness functions are similar to unit tests. While the latter are used to assert certain characteristics of our domain code, the former are tests for our architecture. Of interest, there are tools that allow us to write architectural fitness functions in a unit test style.

Some examples of using fitness functions

Many projects already use some kinds of fitness functions, although they might not use the term. For example, metrics from static code checkers, linters, and verification tools (such as PMD, FindBugs/SpotBugs, ESLint, SonarQube, and many more). Collecting the metrics alone doesn't make it a fitness function, though. You'll need fast feedback for your developers, and you need to define clear measures: limits or ranges for tolerated violations and actions to take if a metric indicates a violation.

In software architecture, we have certain architectural styles and patterns to structure our code in order to improve understandability, maintainability, replaceability, and so on. Maybe the most well-known pattern is a layered architecture with, quite often, a front-end layer above a back-end layer. To take advantage of such layering, we'll allow and disallow certain dependencies between the layers. Usually, dependencies are allowed from top to down, i.e. from the front end to the back end, but not the other way around. A fitness function for a layered architecture will analyze the code to find all dependencies between the front end and the back end. This function will issue a warning or maybe even "break" (stop) the build if it finds a dependency from the back end to the front end.

For example, a simple fitness function to assert correct layering might look like this:

noClasses()
  .that().resideInAPackage("..backend.".)
  .should().dependOnClassesThat().resideInAPackage("..frontend.".);

I’ve used ArchUnit for Java for most of the examples shown here. ArchUnit is a small library that you include as usual as a dependency on your project. The ArchUnit fitness functions, or "rules", can be executed by any unit testing framework; there is a convenient integration into JUnit.

If you have more layers, this simple approach will become cumbersome. For such cases, ArchUnit offers predefined rule sets like layeredArchitecture(), where defining your structures becomes more declarative, less tedious, and less error-prone. JMolecules, a library that builds upon ArchUnit (and other tools), goes even further, allowing you to put tags (markers) onto your structures (package Annotations in Java). Executing the fitness function becomes an expression as simple as

JMoleculesArchitectureRules.ensureLayering();

JMolecules is the Java implementation of the xMolecules project, which is available for .NET and PHP as well.

There are other styles and patterns, like vertical "slices", which better support domain-based architectures. There are also inside-outside patterns like "hexagonal" and "clean" as well as "onion" architectural styles, where the idea is to separate the (domain) core of your application, which is under your control, from the outside world (external systems) that often you cannot control as much. We can easily write fitness functions for the allowed and disallowed dependencies that enforce the respective patterns.

For example, to assess that our vertically sliced domains are free of cyclic dependencies, we could write an ArchUnit fitness function as follows:

slices()
  .matching("com.myapp.(*).".)
  .should().beFreeOfCycles();

We can even combine these patterns and implement multiple fitness functions. Quite often that makes sense, depending on your context.

Not only can we use fitness functions for the dependencies between structures, we can also use them to improve the code design within one structure. If, for example, the architects intend to make the API of existing modules independent from implementation details, we could write an ArchUnit fitness function like

noClasses()
  .that().resideInAPackage("..api.".)
  .should().dependOnClassesThat().resideInAPackage("..impl.".);

You’ll find more examples on the projects’ web pages. I’ve collected some examples in my GitHub repo as well.

How fitness functions and agility relate to each other

Agility is the ability to quickly adapt to change. This ability requires a certain fitness, in both nature (imagine a gazelle) and sports, as well as in software architecture. If your software products lack the necessary fitness, the need to move quickly (i.e. the need to implement changing requirements) leads to pain, stumbling, and deterioration.

Fitness functions help us gain and maintain fitness in certain aspects that we consider critical for the evolvability of our systems. This way, they enable agility on an architectural level. And that's interesting, because changing (evolving) the architecture of a currently successful software system often clashes with how governance is implemented in many larger companies. Governance is seen as an obstacle to change, an impediment to agility.

But governance is probably not the problem, long and slow feedback loops are. If your developers get feedback on violations in their code, in their implementation of the desired architecture, weeks or even months after they've worked on a feature, you will earn their frustration.

Fitness functions "shift left" governance to the development teams to give them fast feedback, while they're working on the code. That shift shortens the feedback loop dramatically. Governance might even be seen as an "enabler" for agility, because suddenly it becomes a safety net that springs into action.

We don't need late inspections; instead, teams can ensure compliance with our architectural guidelines right from the start, in an agile way. Playing with a famous quote: You can't inspect (architectural) fitness into a frail system, it must be built into it.

Bridging the gap between architects and software developers

Large enterprises tend to prescribe architectural rules from somewhere "above". That approach often does not work well and yields unnecessarily complex systems, because developers will not understand the intent of the rules and the big picture of the enterprise architecture.They will unintentionally write code that undermines the objective of the rules.

Small startups or "agile shops" tend to ignore architectural rules or leave architecture decentralized to hopefully capable developers in their respective teams. This lack of awareness can easily lead to architectural chaos. Again, a big picture of an overall architecture is easily missed.

The best architects I've met offer guidance, not prescription. They offer a direction in which to think and to design the code. With fitness functions, architects can write their intentions into executable code. Even better, architects and developers can discuss their architectural rules together and the developers can write the fitness functions themselves. This collaboration fosters communication about architecture and the intent (and meaningfulness) of certain rules.

By "shifting left" the assessment of architectural characteristics into the development teams, architecture becomes a concern of all the people involved in building the software, rather than a few in certain positions or job descriptions. By implementing this approach, architects can focus on explaining and teaching and can keep an eye on the overall architecture. Architecture becomes a role that many people can take: developers as well as architects. This approach closes the gap between architects and software developers step by step, fitness function by fitness function.

Architecture as a team sport – not limited to architecture teams

Fitness functions take architecture out of its silo, out of the heads of fixed job positions. Now, all the people involved in software development are concerned with a viable architecture and its further evolution.

Fitness functions are simultaneously safety net, documentation, and architectural evolution enabler. Because many brilliant people now regularly think about architecture and its implementation in code, the architecture improves, not only within the products, but overall.

Another thing I noticed when architects and developers collaborated on fitness functions: those teams and departments usually worked in an atmosphere of creativity and innovation. I'm not sure if fitness functions are the cause or the consequence, but there probably is a correlation.

Fitness functions – a silver bullet?

It almost sounds too good to be true. Shouldn't every project and every team use fitness functions then? Well, more and more are, at least in my observation. However, their use is often confined to the more obvious structural fitness functions that we saw above.

It’s rather hard to identify and define good fitness functions apart from the structural ones, especially with regards to if a function is violated for:

  • Runtime behaviors such as network traffic between services
  • Properties of data products like adherence to data contracts
  • Output of GenAI solutions to stay within certain ranges

It’s definitely possible to define good fitness functions, but it requires some experience, some experimentation, and sometimes some unconventional ideas. So, starting with the easier structural fitness functions is a good choice. There might be areas where having fitness functions in place is an obstacle rather than a useful tool. Consider the three stages of product innovation and development:

  • Explore the domain
  • Build the right thing
  • Build the thing right

If you’re in the first stage and you don’t know the direction yet, fitness functions might impose too much unwanted direction. But in the second stage, using fitness functions might make sense already, although you might want to use rather broad guardrails. These guardrails may be narrowed in the third stage, where the desired direction should be clearer.

Alternatives to using fitness functions

Of course, we managed to build software systems for many years without the use of fitness functions. So we can always fall back to the previous way to assert architectural compliance: conducting architecture reviews and assessments. This might still be a valid choice, for example, if someone requires such explicit events. You can always do this in addition to using fitness functions, which might yield even better architecture implementations and understanding.

As these manual reviews and assessments are usually not (completely) automated, they come at the cost of speed and scalability. Companies that prefer architecture reviews and assessments usually have a centralized architecture, defined (and controlled) by an architecture team with a limited number of people.

If you see your architecture as static or you deliberately need to be slow in changing the architecture, manual reviews might be your first choice. Using fitness functions will still protect you from unwanted deviations from your target architecture.

Bottom line

If you haven’t heard of fitness functions yet or haven’t used them so far, it might be worth looking into them. They might not be suitable for each and every project, but most projects will benefit from using fitness functions in one or the other way, especially if maintenance is required over many years.

Fitness functions keep our software and its architecture "soft", and continuously evolvable. This matters because change is not the exception, but the rule. Having some kind of guardrails gives us safety to implement changes in existing, successful software systems, which makes evolutionary architecture possible.

Even if you cannot use the libraries that I presented in this article, the concept of fitness functions is independent of programming languages and technologies.

About the Author

BT