System Design when I was an associate teacher
There is 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 I’d wanted to tackle for a long time but was never brave enough to do so.
And may I say, it was one of my personal favorites; not only was the performance 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.”
There is not one perfect architecture
Fast-forward to the present day, what is the one thing that I got from this entire learning experience? Contrary 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 with the best choice of tradeoffs.
How to stop it from degrading over time
All these considered, how do you ensure that a fitting architecture will not degrade over time once you’ve decided upon it?
Developers come and go, and each gets to add a sprinkle of their own magic onto the code. However, code is also volatile, and 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 in the form of unit, functional, and even acceptance testing to make sure that newly integrated features don’t break other parts of the system. But… do we have a counterpart on the architectural side?
What happens when someone starts “re-wiring” things in the process?
In Building Evolutionary Architectures, the concept of evolutionary architecture is introduced for those architectures that withstand 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 have heard about this term already, but probably not in this context. Most likely, you’ve encountered it 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.
Photo by Gary Butterfield on Unsplash
Unit test for architectural characteristics
In very simple terms, it is the equivalent of a 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.
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 forever; we just never called them like this.
For many techniques, there are as many categories
Atomic vs holistic
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:
Does this mean we should design fitness functions for all the possible combinations?
Intentional vs emergent
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.
Triggered vs continual
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.
Static vs dynamic
This 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 a unit test that would fail each time the CC of a method would be over the upper bound, we would have a binary result. The same goes 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.
Let’s get some practice
I couldn’t help but foreshadow throughout the article. We are about to see how we can apply actual unit testing to 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 a 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.
ArchUnit
This means that we need to set some constraints regarding how classes can interact with each other. For this purpose, we use 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 by defining an ArchRule, for 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:
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 and 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.
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 interface names to start with an “I.” This is not the convention for naming Java interfaces, so… have fun using the dentifiable interface.
How much should you invest?
The question remains: How much should be invested in designing these fitness functions when a project has yet to achieve a breakthrough?
If the architecture has yet to mature, then we should tackle only the characteristics we deem relevant. For instance, performance-related issues can be addressed early, in conjunction 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 in 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!
By Dragos-Cornel Serban
Java Developer at Yonder
STAY TUNED
Subscribe to our newsletter today and get regular updates on customer cases, blog posts, best practices and events.