Reading Notes: Building Evolutionary Architectures
( Notes & Quotes from the book “Building Evolutionary Architectures”, ISBN: 978–1491986363 )
- Software Architecture is defined as the “parts hard to change later”
- An Evolutionary Architecture supports guided, incremental change across multiple dimensions
The terms “continual”, “agile”, and “emergent” all capture the notion of change over time, which is clearly a characteristic of evolutionary architecture, but none of these terms capture ‘how’ the architecture changes, or how the desired or end state ‘should be’
Prefer “Evolutionary” over “Adaptable”:
- Adapting implies finding ways to make something work, regardless of longevity of solution
- Evolutionary is about the process of having a system that is fit for purpose, and can survive the ever-changing environment
Evolutionary Architecture consists of three main components (1) incremental change, (2) Fitness Function, and (3) Appropriate coupling
Chapter 2: Fitness Functions
“An architectural fitness function provides an objective integrity assessment of some architectural characteristics”
- Atomic versus Holistic
- Triggered versus Continual
- Static versus Dynamic
- Automated versus Manual
- Temporal ( e.g. ‘break upon upgrade’ test)
- Intentional over Emergent (tackling ‘unknown unknowns’)
- Domain Specific (e.g. security or regulatory requirements tests)
Chapter 3: Incremental Change
Two aspects of incremental change
- Development: How developers build software
- Operational: How teams deploy software
- One of the bedevilling problems in component-based systems is component cycles i.e. when component A depends on B, while B depends on C, which requires A again
- Any tool that helps assess some architectural characteristic qualifies as a fitness function
- Automation is the key to continual evaluation
3.1 Continuous Integration versus Deployment Pipelines
- Continuous Integration provides an “official” build location, and developers enjoy the concept of a single mechanism to ensure working code
- Deployment Pipelines encourage developers to split individual tasks into stages
- Thus a deployment pipeline commonly includes application testing at multiple levels, automated environment provisioning, and a host of other verification responsibilities (e.g. https://www.gocd.org/ )
- Deployment Pipeline also offers an ideal way to execute the fitness function defined for an architecture
- When a team makes a change to production, they must ensure that:
a. They have not negatively affected the current production state, and
b. Their changes were successful
- A common way to accommodate both “Continuous Deployment” and staged releases is to use “Feature Toggles”
3.2 Hypothesis Driven Development
- Hypothesis driven development requires the coordination of many moving parts:
a. Evolutionary Architecture
b. Modern DevOps
c. Modified Requirements Gathering, and
d. Ability to run multiple versions of an application simultaneously
( Example: Re-releasing a legacy application ‘only’ with logging enabled, just to understand how the application works in production )
Chapter 4: Architectural Coupling
- Business concepts semantically bind parts of the system together, creating “functional cohesion”
- An “architectural quantum” is an independently deployable component with high functional cohesion
- In micro-service architecture, the “bounded context” serves as quantum boundary, and includes dependent components e.g. DB servers
- Quantum Size determines the lower bound of the incremental change possible within and architecture
4.1 Evolvability of Architectural Styles
A. Big Ball of Mud
Incremental Change: Difficult, as related code is scattered. Changes to one component might cause unexpected breaking elsewhere
Guided Change with Fitness Functions: Difficult, since no partitioning exists
Appropriate Coupling: Difficult, rather an example of inappropriate coupling
B. Monoliths
Incremental Change: Difficult, because high coupling requires deploying large chunks of application
Guided Change with Fitness Functions: Difficult, but not impossible. Because this has existed for long time, many tools and testing practices are available
Appropriate Coupling: As bad as big ball of mud
C. Layered Architecture
Incremental Change: Cross-layer changes can cause coordination challenges
Guided Change with Fitness Functions: Layers allow developers to test more parts in isolation making it easier to create fitness functions
Appropriate Coupling: Layered architecture makes it easy to swap out the database, business rules, or any other layer with minimal side effects
D. Modular Monoliths
Incremental Change: Degree of deployability of components determines the rate of incremental change
Guided Change with Fitness Functions: Easier mocking & other testing techniques that rely on isolation layers
Appropriate Coupling: Each component is functionally cohesive, with good interfaces between them and low coupling
E. Microkernel
Incremental Change: Most behaviour comes from plugins. If plugins are independent, incremental change becomes easier
Guided Change with Fitness Functions: If you design a system with interacting plugins, you should also build fitness functions (two types viz. core & plug-ins) to protect the integration points
Appropriate Coupling: Primary challenge is revolving around contracts, a form of semantic coupling
F. Event-Driven Architectures
F1: Broker Type EDA
(Typically consists of Message Queue, Initiating Event, Intra-process events, and Event Processors )
Incremental Change: Building a deployment pipeline for broker EDAs can be challenging, because the essence of the architecture is asynchronous communication which is notoriously difficult to test.
Guided Change with Fitness Functions: Synthetic transactions allow developers to test coordination logic without actual (for example) order
Appropriate Coupling: Brokers EDAs exhibit a low degree of coupling, enhancing the ability to make evolutionary change
F2: Mediator Type EDA
(with additional component — The Event Hub, acting as coordinator)
Incremental Change: Services in mediator EDA are typically small & self-contained. Thus many operational advantages
Guided Change with Fitness Functions: Developers find it easier to build fitness functions for mediator EDA than broker EDA. Tests for event processors do not differ, however they are easier to build because developers can rely on mediator to handle the coordination
Appropriate Coupling: While testing becomes easier with mediators, coupling increases. The mediator includes domain logic, thus increasing the size of architectural quantum to encompass it
G. Service-Oriented Architectures
Organization of services, which is based on strictly defined service taxonomy. Segregation os services is based on reusability, shared concepts, and scope
Incremental Change: well established technical service taxonomy allows reuse & segregation of resources, while it hampers making the most common type of changes to business domains. The architectural quantum for ESB driven SOA is massive. It basically encompasses entire system, much like monolith, but more complex since it is distributed architecture
Guided Change with Fitness Functions: difficult, no one piece is complete as every piece is part of larger workflow and isn’t designed for isolated testing
Appropriate Coupling: From potential enterprise reuse standpoint, extravagant taxonomy makes sense; however ESB driven SOA isn’t built to allow independent evolvable parts. Design for categorized reuse harms the ability to make evolutionary change at architecture level
H. Microservices
Incremental Change: Easy to make changes as each service forms a bounded context, plus with continuous delivery verification comes easy
Guided Change with Fitness Functions: Easy for both atomic & holistic fitness functions since each service has well defined boundary
Appropriate Coupling: Microservice architectures exhibit two types of coupling, viz. Integration Coupling & Service Template Coupling. Integration coupling is obvious, while ST coupling prevents some harmful duplication (e.g. each service is left to correctly use logging, authorization, and other boilerplates which can upgrade anytime separately)
I. Serverless Architectures
Incremental Change: consists of redeploying code — natural fits for deployment pipelines
Guided Changes with Fitness Functions: Using a specialized “Anti-corruption layer” wherever any 3rd party integration or other service complexity need abstraction. However, be careful to avoid “Vendor-King Anti-pattern”
Appropriate Coupling: Offloads much of complexity to the invoker, i.e. caller must handle transaction behaviours & other complex coordination
Chapter 5: Evolutionary Data
- By incorporating database changes into the development pipeline feedback loop, developers have more opportunities to incorporate automation and earlier verification into the project’s build cadence
- Evolve capabilities via a schema change — use Expand/Contract pattern
- Features that are common for developers do not exist for DBAs: refactoring support, out-of-container testing, unit testing, mocking & stubbing
- Micro-services architecture aren’t particularly well suited for heavily transactional systems
- Each service doesn’t have to be small, but rather capture a useful bounded context
- While it is true that database schema do not change as often as application code, database schemas still represent an abstraction of the real world
- While data abstractions resist change better than behaviour, they must still evolve
Chapter 6: Building Evolutionary Architectures
6.1 Mechanics
a. Identify Dimensions affected by Evolution ( the “-ilities” specifically )
b. Define Fitness Function(s) for Each Dimension
c. Use Deployment Pipelines to Automate Fitness Functions
6.2 Retrofitting Existing Architectures
Depends on three factors, viz. :
a. Component Coupling ( Functional cohesion determines ultimate granularity of restructured components )
b. Engineering Practice Maturity (Absence of Continuous Delivery makes it impossible otherwise)
c. Developer ease in crafting Fitness Functions
6.3 Guidelines for Building Evolutionary Architectures
a. Remove Needless Variability — Modern DevOps solved this dynamic equilibrium locally be replacing snowflakes with immutable infrastructure. Snowflake computers are ones that have been manually crafted. “Immutable infrastructure” refers to system entirely defined programatically
b. Make Decisions Reversible — Many DevOps practices allow decisions that can be reversible e.g. “Blue/Green Deployments” i.e. if current production system is running on the blue, green is the staging area for the next release. When the green release is ready, it becomes production system, and blue temporarily shifts to backup status. “Feature Toggles” is another common way developers can make decisions reversible
c. Prefer Evolvable over Predictable — All architectures become iterative because of unknown unknowns, agile just recognizes it and does it sooner
d. Build Anticorruption Layers — As soon as you smell “Stuff that is part of your project that isn’t suppose to be there and is in the way of stuff that is suppose to be there”
e. Build Sacrificial Architectures — Twitter was written in Ruby-on-Rails for faster time-to-market but had to be rewritten to be scalable. Cloud environments make sacrificial architectures more attractive. Business people hate to throw away functioning code, so architecture tend toward always adding, never removing, or decommissioning
f. Mitigating External Change — Stability is one of the foundation of both Continuous Delivery and evolutionary architecture. A good start on dependency management models external dependencies using a pull request
g. Updating Libraries versus Frameworks — “A developer’s code calls a library, whereas the framework calls a developer’s code”. Prefer libraries as they bring less coupling to your application, making them easier to swap out when architecture needs to evolve. Frameworks include capabilities such as UI, ORM, scaffolding like MVC/MVP, and so on. Libraries generally are less brittle coupling points allowing teams to be more casual about upgrades
h. Prefer Continuous Delivery to Snapshots — Once a component is “blessed” with a version number, the -SNAPSHOT moniker drops away. Developers use snapshots because of historical assumption that testing is difficult and time consuming, leading developers to try to segregate things changing from things not changing. Two types of external dependencies: fluid (auto-update) & guarded (version-restricted)
i. Version Services Internally — Common patterns to version endpoints: version endpoints, or internal resolution via logic around context of call
Chapter 7: Evolutionary Architecture Pitfalls & Antipatterns
An antipattern is a practice that initially looks like a good idea, but turns out to be a mistake.
A pitfall looks superficially like a good idea but immediately reveals itself to be a bad path
7.1 AntiPattern: Vendor King
An architecture built entirely around a vendor product that pathologically couples the organization to a tool. And once it is done, because they required huge investments of both time & money, companies are reluctant to admit when they doesn’t work. Rather than fall victim to VK antipattern, treat vendor products as just another integration point
7.2 Pitfall: Leaky Abstractions
All non-trivial abstractions leak, because we come to trust that abstractions are always accurate. A typical modern architecture has tonnes of moving parts. “Primordial abstraction ooze”, where breaking abstraction at low-level causes unexpected havoc.
7.3 Antipattern: Last 10% Trap
80–90% project following the technical framework & guidelines, but that last 10% filled with hacks and violations to patch things up without which project wont meet performance or budgetary goals
7.4 Code Reuse Abuse
Developers open spend a lot of time trying to build reusable modules that turn out to have little practical reuse. Microservices eschew code reuse, adopting the philosophy of prefer duplication to coupling, and microservices architectures are extremely decouples. Overall, the benefits of reuse are illusory and the coupling it introduces comes with its disadvantages — break the coupling by forking or duplication
7.5 Resume-Driven Development
Utilizing every framework and library possible to tout that knowledge on resume.
7.6 Antipattern: Inappropriate Governance
If an airline company’s iPad application is terrible, it will eventually impact the company bottomline. Microservices architecture embraces polyglot environments where each service team can choose a suitable technology stack to implement their service rather than try to homogenize. In modern environments, it is inappropriate governance to homogenize on a single technology stack. From a practical standpoint in large organization, “Goldilocks Governance Model” works well, pick three technology stacks -simple, intermediate, and complex for standardization
7.7 Pitfall: Lack of Speed to Release
Continuous Delivery strives for data-driven results by tracking cycle time: the elapsed time between the initiation and completion of a unit of work, which in this case is software development. Faster cycle time implies a faster ability to evolve
7.8 Product Customizations
Common include i. Unique build for each customer, ii. Permanent Feature Toggles, and iii. Product-driven Customizations. Customization also impede evolvability, but this should not discourage companies from building, but rather to realistically assess the associated costs
7.9 Antipattern: Reporting
Organizations struggle to provide all possible perspectives. Reporting is good example of inadvertant coupling in monolithic architectures. Architects and DBAs want to use same databse schema for both systems of record and reporting but encounter problems because a design to support both is optimized for neither
7.10 Planning Horizons
Larger the planning horizon without an opportunity to revisit decisions, means many decisions are made with least amount of information. More and more effort put into the assumptions, even if they turn out to be false in six months, leads to a strong attachment to them. Beware of long planning horizons that force architects to irreversible decisions
Chapter 8: Putting Evolutionary Architectures into Practice
Cross Functional Team — Domain-centric teams tend to be cross-functional, meaning every project role is covered by someone on the project and operational friction has been eliminated
Finding new Resources via Automating DevOps — Much of the manual work performed by operations is accidental complexity
Product over Project — following Amazon’s two pizza team
Dealing with External Change — Create fitness functions, i.e. in microservices common practice is the use of consumer-driven contracts, or otherwise an “engineering safety net”
Culture — Well fucntioning architects take on leadership roles, creating the technical culture and designing approaches for how developers build systems. Culture of asking questions, especially when any new framework or library is suggested
Culture of Experimentation — This could include
i. Bringing ideas from outside
ii. Encouraging explicit improvement
iii. Spike & Stabalize
iv. Creating innovation time
v. Following set-based development
vi. Connecting engineers with end-users
Generative Testing — Generative tests check every possible value and report on edge cases that break
Convincing Others — Demonstrate often how these ideas improve their practices
Moving Fast without Breaking Things — Business people fear breaking change. If developers build an architecture that allows incremental change with better confidence than older architectures, both business & engineering win
(PS: These notes from the book were made for my own reference, as the book was extremely appealing. I did not add my interpretations here. If anything does not make complete sense, perhaps I did not pay attention enough or wasn’t of interest to me & got skipped. In any case, I suggest all to read the original book itself)