Architectural Fitness Functions: An intro to building evolutionary architectures

Dragos-Cornel Serban
Yonder TechBlog
Published in
8 min readMar 4, 2024

--

“Tell your story…”, Medium says

And so I will… Nothing better than a short, romanticized foreword about the origins of a newly-found passion.

It started during my time as an Associate Teacher. I’d just held a seminar on System Design, a topic that I’d been willing to tackle for a long time, but I was never brave enough to do so.

And may I say, it was one of my personal favourites, not only the performance was good, but the questions that I received were the factual proof that I raised a tiny bit of interest. What more could a teacher wish for?

I stand corrected, there is something… A request to tackle this subject in depth “if time allows”, was the much-needed incentive to read more about it in the following weeks. Before I knew it, the books and bookmarks started piling up, the stash of diagrams just got bigger, and I started using a lot of funny words ending in “-ility”.

Fast-forward to the present day, what is the one thing that I got from this entire learning experience? Contrarily to my expectations, I came to find out that there is never only one correct answer to a problem. There is not one perfect architecture, yet there are architectural patterns that tick more boxes than others. It all comes down to the requirements that are the most relevant to the problem at hand.

In an ideal world, a system would satisfy all non-functional characteristics listed in a Software Quality standard, but most of the time, the “best” architecture, turns out to be the one that has the best choice of tradeoffs.

All these considered, how do you make sure that once you’ve decided upon a fitting architecture, it will not degrade over time?

Developers come and go, each of them gets to add a sprinkle of their own magic into the code. But code is also volatile, there is always a need to come back later to “undo some deeds”. This still raises the question of whether a fix is an actual fix, or just some more magic sprinkled on top.

If we think about domain changes, we should have a large safety net under the form of unit, functional and even acceptance testing to make sure that newly integrated features didn’t break other parts of the system. But… do we have a counter-part on the architectural side?

What happens when someone starts “re-wiring” things in the process?

In Building Evolutionary Architectures, the concept of an evolutionary architecture is introduced for those architectures that stand through the test of time and can be submitted to fundamental changes. How is that achieved? Through “guided, incremental” changes. At the very base of this concept, lies the notion of an architectural fitness function, which is defined as:

“Any mechanism that performs an objective integrity assessment of some architecture characteristic or combination of architecture characteristics.”

Should we elaborate on this?

You might’ve heard about this term already, but probably not in this context. Most likely, you’ve encountered this in the same phrase as genetic algorithms. Fitness functions were defined to determine how “fit” a candidate solution is for a particular problem. In this case, we assess how close our architecture is to fulfilling a non-functional requirement.

In very simple terms, it is the equivalent of an unit test for architectural characteristics.

I will avoid using this analogy for the rest of the article, considering that the range of tools and techniques used to design a fitness function is much broader, unit tests will turn out to be just a subset of these.

Fitness Functions as presented in the Building Evolutionary Architectures book

Let’s give an example of a fitness function. We mentioned earlier that it is an assessment of a non-functional characteristic, correct? Then maintainability will do just great for us. The maintainability index is calculated based on several code metrics, including cyclomatic complexity. Setting an upper threshold for the cyclomatic complexity of a method and having this as part of a custom quality gate for your application is no less than a fitness function. It doesn’t sound like I wrote anything of novelty here. Running a code analysis task in a CI pipeline is a common practice, but it translates into a fitness function that preserves the coding standards of your architecture. We’ve had these since forever, we just never called them like this.

For many techniques, as many categories

The fitness function that we just described addressed only one architectural characteristic. From a scope perspective, this is an atomic fitness function. Still, anticipating real-world situations we’d want to assess how combinations of different characteristics work in a shared context. For this purpose, holistic functions are defined.

Considering the number of architectural characteristics that can be evaluated, both in isolation, and diverse combinations, I received a very good question at one point:

Then this means we should design fitness functions for all the possible combinations?

As enchanting as it sounds, we can acknowledge that the time invested into designing so many intentional fitness functions will stretch for more than we’d like to. Therefore, we should determine the precise characteristics that require attention at the inception of the project and design fitness functions specifically for these. Surely, the “unknown unknowns” of our architecture will make their presence known, and as our system grows, so will the need for some emergent fitness functions.

Another aspect that needs to be considered is the cadence of a fitness function. For example, we mentioned that the CC check would be a stage of a CI pipeline. Still, it is triggered by a push in version control. What could be a continual function? Here, we could shift our attention to monitoring systems. We are using monitors in production to observe a system’s performance and availability. The usage of monitors is not a fitness function in itself, but it paves the way to create alerts in the event of deviations from a targeted threshold.

Which brings us to the last category of fitness functions. We are once more bringing up the CC check. If we were to rewrite this as an unit test that would fail each time the CC of a method would be over the upper bound, we would have a binary result. Same for the quality gate on a code analysis task. Result-wise, as long as we can predefine the range of values, it will be a static function. For a dynamic function we would have to consider a number of factors. Suppose we want to assess the scalability of the system, along with responsiveness. This means that as the number of concurrent users grows, we can allow a drop in the responsiveness of the application, but not under a point we deem troubling. The way to determine that responsiveness threshold needs to be based on how the number of users evolves.

Example: Keeping the architecture fit

I couldn’t help doing a bit of foreshadowing throughout the article. We are about to see how we can apply actual unit testing over an architecture. Remember when I mentioned something about “re-wiring”? This is also a real-life situation, as dependency violations are very common. So how can we write an unit test that can preserve the structural integrity of an architecture?

Let’s have a look at this diagram first:

The red-dotted lines mark dependency violations which either bypass, or go against the layers. Under no means should classes from the controller layer access the ones from the persistence layer directly. Also, the circular dependency between ServiceOne and PersistenceManager should be forbidden.

This means that we need to set some constraints regarding the way that classes can interact with each other. For this purpose, we make use of a cute little library called ArchUnit.

Firstly, we need to define the set of classes that should be checked for rule violations. So we’ll import the root package com.dragoss.myapp. Then, we need to set the actual constraints of package dependencies. This can be done through defining an ArchRule, of which we’ll see an example later. Still, the Library API of ArchUnit offers support for defining a layered architecture and setting the constraints in a much more fluent manner:

@Test
public void layeredArchitectureTest() {
JavaClasses jc = new ClassFileImporter().importPackages("com.dragoss.myapp");
Architectures.LayeredArchitecture layeredArchitecture =
layeredArchitecture()
.consideringAllDependencies()
.layer("Controller").definedBy("..controller..")
.layer("Service").definedBy("..service..")
.layer("Persistence").definedBy("..persistence..")
.whereLayer("Controller").mayNotBeAccessedByAnyLayer()
.whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
.whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service");

layeredArchitecture.check(jc);
}

And voilà, this is one way you can actually ensure that the integrity of the architecture is preserved.

But oh, can this framework do more than this. The API allows us to declare rules for more than just architectural concerns, but even enforce coding guidelines. Take the next example for instance, we want to make sure that any class in the controller is annotated with the Spring MVC @RestController.

@Test
public void restControllerAnnotationTest() {
JavaClasses jc = new ClassFileImporter()
.importPackages("com.dragoss.myapp");
ArchRule rule = classes().that()
.resideInAPackage("..controller..")
.should()
.beAnnotatedWith(RestController.class);
rule.check(jc);
}

The next one adds a little bit of spice to the article. One of the fitness functions that I’ve seen applied before enforced all names of interfaces to start with an “I”. This is not the convention for naming Java interfaces, so… have fun using the ‘dentifiable interface.

@Test
public void interfaceNamingGuidelinesTest() {
JavaClasses jc = new ClassFileImporter()
.importPackages("com.dragoss.myapp");
ArchRule rule = classes().that()
.areInterfaces()
.should()
.haveSimpleNameNotStartingWith("I");
rule.check(jc);
}

Conclusions

The question still remains, just how much should be invested into designing these fitness functions when a project is yet to have a breakthrough?

If the architecture has yet to mature, then we should tackle only the characteristics we deem as relevant. For instance, performance-related issues can be addressed early, in conjuction with other traits such as security and availability. As the software starts taking shape, we can also track the evolution of the code debt and decide upon taking time to “tighten the belt”.

One thing that I’ve saved for last… it is not only up to the architect to design fitness functions, but any developer is able to do it. As you’ve probably noticed, regardless of the shape, you’ve been involved into implementing a fitness function at one point, it’s just that you never thought of it this way. Data is everywhere, just think of what you want to check, and write that piece of code that “glues” everything together. Have fun with them.

I guess that’s the end of the ride, I hope you’ve found this material useful and you’ve caught yourselves giggling a couple of times.

Thank you for your time!

--

--