Evolutionary Architecture

Evolutionary Architecture is an approach to building architecture that aims to move forward in small steps while maintaining the pace of development.

At its core, this architecture embodies the property of "evolvability," meaning the ability of a software product to evolve over time without requiring a complete restructuring of the application.

Typically, evolvability is defined concerning specific requirements but can also be a characteristic of the entire application.

Evolvability coexists well with other "healthy" requirements. The primary issue in software development is "anti-patterns," and evolutionary architecture is one way to address this problem. Anti-patterns generate unhealthy requirements.

Evolvability directly depends on the organization of the development process and is impossible without a well-structured development process.

At the code and application level, i.e., at the level of regular developers, problems arise from architectural, methodological, and organizational anti-patterns:

  • Big Ball of Mud: A system with an unrecognizable structure.
  • God Object: A solution to coupling issues by concentrating functionality in one place.
  • Interface Bloat: The development of an interface that is very powerful and complex to implement.
  • Re-Coupling: The process of introducing unnecessary dependencies.
  • Race Hazard (Race Condition): Unforeseen possibilities of events occurring in an order different from what was expected.
  • Mouse Treadmill: Unjustified creation of many small and abstract classes to solve one specific higher-level task.

Methodology:

  • Copy-Paste Programming: Copying (and lightly modifying) existing code instead of creating generic solutions.
  • De-factoring: The process of destroying functionality and replacing it with documentation or organizational actions (prescriptions).
  • Golden Hammer: A strong belief that a favorite solution is universally applicable. The name comes from the saying, "When you have a hammer, everything looks like a nail."
  • Premature Optimization: Optimization at the design stage of a code segment that leads to its complexity or distortion.
  • Programming by Permutation: An approach to software development with small random changes.
  • Two Chairs: Moving new functionality into a separate application instead of extending the existing one.

Configuration:

  • Dependency Hell: The growth of a graph of mutual dependencies among software products and libraries, leading to the difficulty of installing new products and removing old ones.
  • Vendor King: When architecture is built around the capabilities of a vendor.

The main problem that needs to be solved to create good software varies by approach.

How to divide an application so that it can progress with small, "manageable" changes without falling into anti-patterns (dependency hell, copy-paste, etc.)?

Granularity: Optimal division into modules or components. Granularity addresses the question of atomicity (identifying functionally indivisible components that generate new functionality by combining atoms with each other). It should not be confused with monolithic.

  • Modules: Logical separation.
  • Components: Physical (a component is typically a class in OOP or a composition of classes). One variant of a component is a service or library.

What Works Poorly, Especially Without Experience:

  • Single Responsibility Principle: This principle helps combat the desire to consolidate and create God components, but it does not prevent excessive reduction of components. Granularity is not about grinding down but about finding the golden mean.
  • Liskov Substitution Principle: Often, this principle is not fully understood, so it is used only in its simplest form—if you can substitute the highest-level parent, then everything is fine.
  • Open/Closed Principle: Often violated, it is very difficult to implement in practice, leading to "copy/paste" or "two chairs."

What Works Well:

  • Coupling and Cohesion: Two principles that clearly indicate the optimal level of application granularity (initially, 2-3 connections per module; otherwise, combinatorial complexity will kill the application and prevent making "small" changes).
  • Programming to Interfaces: Maintaining minimalist interfaces that initially contain 2-3 methods; again, combinatorial complexity. It is precisely the limitation of connections and public methods that allows significant advancement in evolvability. However, it is important to understand that the level of granularity of a project changes over time and with the project's development. In later stages, interfaces with 5-7 methods can still be considered minimalist, but it is important to reduce combinatorial complexity.

The architecture of any application is about examining the task from different perspectives and determining the capabilities of the software. Additional areas for analyzing evolvability (besides the code level discussed above) include:

  • Technology/Infrastructure
  • Data
  • Security
  • Integrations/Systems

Conway's Law: "Organizations that design systems are constrained to produce designs which are copies of the communication structures of these organizations."

Monitoring

If you cannot measure something, you cannot control it. The importance of monitoring is as crucial as in microservices architecture.

The foundation is fitness functions, which can be either automated or organizational.

Design objectives can include any requirements received from the client or those resulting from the design. We can create a fitness function in the form of an automated test or as an instruction for a person who will manually check for the necessary properties in the system.

Fitness functions need to be as automated as possible. For example, if we define a performance requirement for executing a set of operations, say 100 ms per operation, we can write a small function or program to monitor this indicator, thus creating a fitness function.

Fitness functions can be of the following types:

  • Atomic/Whole
  • Continuous/Discontinuous
  • Static/Dynamic
  • Automated/Manual - Time-Based

By purpose, fitness functions are categorized as:

  • Key
  • Relevant/Irrelevant

This is the primary mechanism of evolutionary architecture. The idea is to move forward in small steps by replacing outdated components with newer counterparts. At some point, both old and new components may be available. A small change can remain only as long as it is self-sufficient, has no unnecessary connections, and does not solve global problems. Each change should be followed by a deployment stage.

The foundation of evolutionary architecture is continuous deployment.

Process:

  • Incremental changes
  • Building and maintaining fitness functions
  • Maintaining modularity and managing connections

Algorithm for Building Evolutionary Architecture:

  1. Identify requirements that are protected by evolution.
  2. Define fitness functions (quality assessments) for features and requirements.
  3. Set up monitoring.
  4. Establish continuous deployment, considering automation of fitness functions (deployment pipeline).
  5. Move forward with the process described above, i.e., for each new feature, determine changes, new or existing fitness functions, and manage connections during the implementation.

Unstructured Monolith:

  • Big Ball of Mud
  • Layered Architecture (Incremental changes depend on proper granulation of components within)
  • Modular Monolith
  • Microkernel (Plugin-based)
  • Event-Driven
  • Microservices
  • Serverless Architectures (Services, API Gateway)

Large Features, Small Changes

Solution:

  • Decomposition into constituent tasks
  • Transparency until the last task is implemented (the idea of a "final point").

Sequential Chains (problem of sequential processing): We must wait for changes in dependent modules to be implemented.

Solution:

  • In rare cases, wait if it involves core or library functions.
  • Reactivity
  • Isolate APIs and maintain backward compatibility.
  • Conditional compilation/building.

Conflict of Interest or Similar Functionality:

Different teams want similar functionality but with slight differences.

Solution:

  • Additional granulation and review of atomicity.

The Problem of Legacy Monoliths:

An application is already a tightly coupled, unstructured monolith; small steps are essentially impossible.

Solution:

  • Apply the "Two Chairs" pattern: implement new functionality separately, iteratively removing the old one. Important! In the long term, this is an anti-pattern, so this state cannot be maintained for long; otherwise, it will only get worse.

Excessive Variability:

Bloated interfaces and classes generate a lot of different chains.

Solution:

  • Reduce variability.

Comments

Popular posts from this blog

Books Every Developer Should Read (In My Opinion)

TypeScript: Why It's Needed and Why It's So Popular

Reactive Architecture at the Code Level