Che significa fitness function-driven development
Learn

Fitness function-driven development

15 May 2023 - 6 minutes reading

One of the main responsibilities of a software architect is to design an architecture that can evolve with the client’s needs. Once released, a software product is meant to evolve continuously and adapt not only to customer requirements but also to the constant demand for new features that were not initially planned. At the same time, the software must also ensure a set of non-functional requirements such as scalability, reliability, observability, and other “-abilities” (in English, “-ilities”).

How can we ensure the operability and resilience of functionalities when applications are deployed to production?

In this article, i will introduce the concept of Fitness function and the Fitness function-driven development approach, complementing the explanation with example code.

Introduction – Evolutionary Architecture and Software Evolution

Evolutionary Architecture

The idea that architecture can support change is described in the book “Building Evolutionary Architectures“, now in its second edition.

Evolutionary Architecture” is essentially divided into two areas: mechanics and structure.

  • Mechanics: the set of engineering practices and validations that allow an architecture to evolve. This includes well-known practices such as testing, the adoption of metrics, and hosting.
  • Structure: the other aspect of an Evolutionary Architecture concerns the structure, the topological aspects of a software project. It examines which architectural styles are best suited to building a system capable of evolving.

The Evolution of Software

How can we measure the deterioration of a system over time? Is there an index, or a simple indicator, that can deterministically prove that the developed software is degrading in terms of performance, security, and scalability?

The terms “bit rot” or “software rot” refer to a gradual deterioration in software quality over time, or a decline in its responsiveness that eventually renders the software defective, unusable, and/or in need of updates. This is not strictly a physical phenomenon, meaning the software does not actually decay, but users perceive a loss of responsiveness or simply a lack of updates to keep pace with its ever-changing environment. In practice, “the software feels old!”

This is where the concept of the fitness function comes in.

Fitness function

Once the problem has been identified, how can the evolution of a system be managed?
Architectural goals and constraints can change independently of functional expectations. For example, you might decide to move deployment from an on-premise setup to the cloud, knowing that the software was designed for this transition. Functionally, no one will notice the change, but structurally, the release pipelines will certainly be updated.

If well-established techniques are used in software development—such as TDD or BDD—to “protect” the code during refactoring phases, how can we ensure the same level of reliability at the architectural level?

Fitness functions describe how close an architecture is to achieving a specific architectural goal. They provide an objective assessment of the integrity of certain architectural characteristics.

Regardless of the application architecture (monolithic, microservices, etc.), fitness function-driven development can introduce continuous feedback for architectural compliance and inform the development process as it happens, rather than fixing issues after the fact.

For example, in a monolithic application, even if the initial goal is to keep each module’s responsibilities independent, an IDE’s automatic suggestion to reference an existing function from another module might unintentionally break this constraint. A fitness function ensures that this cannot happen. A great tool for this, available for both Java and .NET, is ArchUnit, as shown in the following code.

using ArchUnitNET.Domain;
using ArchUnitNET.Fluent;
using ArchUnitNET.Loader;
using ArchUnitNET.xUnit;
using Brewup.Modules.Sales.Abstracts;

using static ArchUnitNET.Fluent.ArchRuleDefinition;

namespace Brewup.Modules.Sales.Fitness.Tests
{
   public class SalesFitnessTest
   {
      private static readonly Architecture Architecture = new ArchLoader().LoadAssemblies(typeof(ISalesOrchestrator).Assembly)
          .Build();

      private readonly IObjectProvider _forbiddenLayer = Types().
          That().
          ResideInNamespace("Brewup.Modules.Warehouse").
          As("Forbidden Layer");

      private readonly IObjectProvider _forbiddenInterfaces = Interfaces().
          That().
          HaveFullNameContaining("Warehouse").
          As("Forbidden Interfaces");      

      [Fact]
      public void SalesTypesShouldBeInCorrectLayer()
      { 
          IArchRule forbiddenInterfacesShouldBeInForbiddenLayer =
              Interfaces().That().Are(_forbiddenInterfaces).Should().Be(_forbiddenLayer);

          forbiddenInterfacesShouldBeInForbiddenLayer.Check(Architecture); 
      }
   } 
}

Where to begin?

Just as with the phase of gathering functional specifications, the best way to start working with fitness functions is to bring together all the stakeholders (stakeholders, users, developers) in a room for gathering structural specifications. Of course, in this case, the objectives are the architectural attributes that are considered most important for the product’s success, which often, if not always, align with the architectural “-ilities.” At the end of this gathering, the results are grouped into common themes such as resilience, operability, and stability.

Once the objectives are grouped, it will be discovered that the famous “Linus blanket” is always too short because flexibility goals can conflict with those of stability and resilience. To promote agility, barriers to change need to be reduced, if not eliminated, which is the opposite practice of maintaining stability, requiring the raising of barriers to reduce change and thus keep the system stable. Therefore, a balancing exercise and prioritization is started with the goal of producing the desired fitness functions.

After collecting the fitness functions, it is necessary to draft them into a testing framework, and there are many of these, each dedicated to a specific objective. ArchUnit, which I mentioned, is useful in development, but for monitoring activities, it is necessary to rely on an orchestration tool that can automatically assess whether the system is healthy or under pressure. The same automatic release pipelines must be able to assess the metrics exposed by the tools to determine whether or not to allow the release of the new version. The key is to maintain the good habit of periodically reviewing the fitness functions and checking if they are still adequate, or if the priority scale has changed and they need to be updated accordingly.

Categories of Architectural Fitness Functions

Fitness functions exist in a variety of categories related to their scope, frequency, result, invocation, and coverage.

Scope: Atomic vs Holistic

Atomic fitness functions are executed in a single context and check a specific aspect of the architecture. An example is a unit test that verifies modular coupling (as seen earlier) or cyclomatic complexity.
Holistic fitness functions work in a shared context and exercise a combination of architectural aspects, such as security and scalability.

Frequency: Triggered vs Continual vs Temporal

The “triggered” fitness functions are executed based on a specific event, such as the execution of a unit test or exploratory tests conducted by a Quality Assurance lead.
The “continual” fitness functions perform constant checks on an architectural aspect, such as transaction speed. The MDD (Monitoring Driven Development) technique is growing in popularity as a tool for assessing, in production, both the technical and commercial health of a system, rather than relying solely on tests.
Finally, the “temporal” fitness functions are those that are executed at regular intervals. An example is monitoring the libraries used for encryption. The goal of the fitness function is to notify the team that it is time to verify the validity of the library, either automatically or manually.

Result: Static vs Dynamic

When we talk about static fitness functions, we refer to functions with a binary result, such as passing or failing a unit test.
On the other hand, dynamic fitness functions are based on a changing definition depending on an additional context, such as a test verifying scalability and response time for a certain number of users.

Invocation: Automated vs Manual

As the name suggests, automated fitness functions are executed in an automated context, just the way software architects like it.
As for manual fitness functions, they require the verification of processes that are person-based.

Conclusions

In this introduction to fitness functions, I have shown that the architecture of a software system, just like its infrastructure and code, can be monitored and tested.

What benefits do fitness functions bring?

First and foremost, they allow us to objectively measure technical debt and promote code quality. In the example I provided, we saw how modern IDEs too easily resolve sometimes uncomfortable dependencies and how we developers, sometimes out of laziness, unconditionally accept them. Specific tests help monitor this unfortunate coupling. Other functions support teams dealing with security, for example.

In general, it can be said that fitness function-driven development communicates architectural standards as code and enables development teams to deliver features aligned with them. Just as users of an application request changes to its functionality, those responsible for architecture can request changes to it, such as transforming a monolith into a microservices architecture. The inclusion of these functions within build and deployment pipelines enables teams to create secure and compliant operational services, respecting all the “-ability” properties required by modern, scalable software.

For those who wish to dive deeper into the topic, I recommend the book “Building Evolutionary Architectures (2nd Edition)“.

Article written by