Skip to content

Designing evolutionary architectures

Practice · Chapter 16

  • Architecture consists of the earliest, hardest-to-change design decisions (see What is software architecture?). Evolutionary architecture aims to make even those decisions easy to change, plus support non-architectural modifications.
  • Parsons, Ford, and Kua (Building Evolutionary Architectures) define it as one that supports “guided incremental change across multiple dimensions” — three ideas worth unpacking: guided, incremental, and multiple dimensions.
  • Changes must be guided so the architecture’s characteristics and quality attributes remain intact.
  • Systems are dynamic; ad-hoc changes can have unintended consequences and, without oversight, quality erodes and the architecture stops meeting its purpose.
  • The architect’s job is to steer changes so requirements and architectural goals keep being met. Fitness functions are one tool for measuring the impact of a change.
  • Borrowed from evolutionary computing, where a fitness function is an objective function measuring how close a solution is to the desired result (used in genetic algorithms).
  • Applied to architecture, they objectively assess how close the design is to the desired architectural characteristics.
  • They should be clearly defined and quantitative, so you can compare the architecture before vs. after a change, and compare competing solutions.

Categories of fitness function (not mutually exclusive)

Section titled “Categories of fitness function (not mutually exclusive)”
CategoryMeaning
Atomic vs. holisticAtomic tests one characteristic in one context (e.g. a unit test); holistic tests several characteristics together. Both matter because features that pass in isolation may fail in combination.
Triggered vs. continuousTriggered runs on an event (build, unit test); continuous runs constantly (e.g. a monitoring alert).
Static vs. dynamicStatic checks against a constant value; dynamic acceptable values shift with context (e.g. lower performance tolerated at higher scale).
Automated vs. manualAutomated (in CI/build) is ideal when possible; some functions must or should be run manually.
TemporalTriggered by elapsed time rather than a system change (e.g. apply a framework patch within N days of release).
Intentional vs. emergentIntentional are defined early once characteristics are known; emergent appear as the system develops.
Domain-specificBased on business-domain concerns — regulatory, security, PII — ensuring the architecture keeps conforming.
  • Fitness functions can be tests (automated or manual), monitoring, or metric collection. Not every test is a fitness function — only those that assess an architectural characteristic.
  • Metric-based: track cyclomatic complexity, LOC, and depth of inheritance tree (DIT) to alert when maintainability thresholds are exceeded, prompting analysis and possible refactoring.
  • Performance and security tests confirm recent changes have not degraded those characteristics.
  • Chaos/resilience engineering: Netflix’s Chaos Monkey deliberately disables a machine to verify fault tolerance — a holistic, continuous fitness function scheduled during business hours so engineers can respond.
  • Incremental change covers how changes are introduced, built, and deployed.
  • Smaller-scope changes are easier to understand, lower the risk of defects, and are easier to code-review and test.
  • Ties back to Lehman’s Law VIII (feedback): agile methods and DevOps practices like CI and CD speed up the feedback loop (see DevOps practices).
    • CI: developers integrate frequently; frequent commits reduce merge conflicts, and automated builds with automated testing give fast feedback on a small batch of changes.
    • CD: releasing changes to production safely, repeatably, and sustainably — low-risk, high-quality releases with faster time-to-market makes the system highly evolvable.

Architectural changes across multiple dimensions

Section titled “Architectural changes across multiple dimensions”
  • A system has many dimensions beyond code: the database and its data, security, performance, and the deployment environment.
  • Architects tend to focus on technical aspects (language, frameworks, libraries) but must consider all dimensions to keep the system evolvable as changes are made.
  • Coupling is the degree of dependency between components (see Designing orthogonal software systems).
  • Tight coupling makes change harder: a change to one component is more likely to ripple into others, increasing total effort for development and testing.
  • Minimize dependencies and make components as independent as possible. Loose coupling reduces complexity, tends to increase cohesion, and improves maintainability.
  • Example: decouple cross-cutting concerns from application logic — ideally as their own services depended on via interfaces — so concerns like caching and logging can evolve independently, reducing duplication.
  • You cannot anticipate future API changes. Message contracts may change: amount of data sent/received, field names or data types, or new data representations for new client types.
  • APIs — especially once published — should be designed to evolve without breaking existing clients.
  • The Robustness Principle (Postel’s Law): be conservative in what you send, liberal in what you accept. Originally from TCP design, applicable to message contracts.
  • Conservative in what you send: expose only the minimum data necessary, lessening the chance a breaking contract change is needed.
  • Liberal in what you accept: consumers should extract only what they need and ignore the rest, so future changes to unused parts do not break them.
  • Choosing standards (language, framework, library, protocol, database, data-interchange format, tools) increases maintainability and eases evolution.
  • Standards make integration with other systems easier and make it easier to find people familiar with the technologies over a long maintenance life.
  • A standard is not always the best choice, but it is a factor architects should weigh given the benefits.
  • LRM: delay a decision until the cost of not deciding exceeds the cost of deciding.
  • Traditional architectures decided very early; evolutionary architectures defer important, hard-to-reverse decisions to the LRM to avoid premature, costly-to-rework choices and to gather maximum information.
  • The challenge: it is hard to know when the LRM actually is, since costs and benefits shift. Deferring unimportant decisions has little benefit — reserve LRM for decisions important to the architecture.
  • Software Architect’s Handbook (Packt, 2018), Ch.16 “Designing evolutionary architectures”, pp. 1144-1158.