Skip to content

Instantly share code, notes, and snippets.

@scottwd9
Created August 13, 2018 12:28
Show Gist options
  • Save scottwd9/ada88f963aac95893e1eba10d4ad8f6d to your computer and use it in GitHub Desktop.
Save scottwd9/ada88f963aac95893e1eba10d4ad8f6d to your computer and use it in GitHub Desktop.
Building Evolutionary Architectures

Building Evolutionary Architectures

Chapter 1 - Software Architecture

Architecture is 'the important stuff, whatever that is' or 'the parts that are hard to change later'. An architect analyzes business, domain, and other requirements to develop solutions that satisfy a list of prioritized architectural characteristics (-ilities). We should consider time and change with respect to architecture, or evolvability.

Software ecosystems are in a state of dynamic equilibrium. New languages, tools, methods constant force new equilibriums to emerge (free OS, linux, + free operations, puppet, led to the shift to containers). The pace of change in technology is constantly and rapidly changing in unexpected ways. We should architect systems knowing the landscape will change. Make ease of change a principal of architecture, remove the 'hard to change' definition of architecture.

An evolutionary architecture supports guided, incremental change across multiple dimensions. Evolvability is a meta characteristic that protects other -ilities. It is continual, having no end state and designed to change with the ecosystem.

An architecture that allows incremental change, during both development and deployment, is easier to evolve. Fitness functions drive decision making and guide change to protect important -ilities.

Architectural dimensions include broad topics, which are a superset of -ilities, such as technical (frameworks, libraries), data (schemas, query optimization), security (policies, tools), operations (deployment, servers). Architects must guide change (using fitness functions) across an increasing number of dimensions as software grows.

Architecture documentation views:

  • 4 + 1 architecture view model
  • C4

Architectural scope consists of requirements (business, domain) and the prioritized dimensions. Architects should consider coupling of classes, packages, libraries/frameworks, schemas, and transactions to ensure quanta are as small as possible.

Conway's Law

Organizations which design systems ... are constrained to produce designs which are copies of the communication structures of these organizations.

Teams should not align around technical boundaries, but around service or business boundaries.

Summary

Evolutionary architecture consists of incremental change, fitness functions, and appropriate coupling. (no mention of coupling in the chapter?)

Chapter 2 - Fitness Functions

Architectural fitness functions provide an objective assessment of the integrity of one or more architectural characteristics (-ilities). Evolutionary architecture relies on guiding incremental change, or the notion of something to move towards. Fitness functions provide the guidance. A systemwide fitness function may consist of several independent functions (unit tests, contract tests, process metrics, monitoring, architecture metrics), but may not be able to be evaluated itself. It can instead guide decisions comparing apples to oranges (performance to security).

Fitness functions may not be automatable or evaluable, but are still useful. Examples include performance (call must return with 100ms), coding standards (cyclomatic complexity, linting), reliability, scalability, security, operability, failover, disaster recovery, etc. The systemwide fitness function may not be evaluable to an explicit score, but it drives decision making when considering tradeoffs that affect the function.

Atomic/Holistic - Fitness functions can be run against a singular context (unit test) or a shared context (the effect of caching on security and scalability).

Triggered/Continual - The evaluation cadence of fitness functions can be triggered (unit test, build pipeline) or continual (monitoring). A continual function can be simulated and run in production with real-world load.

Static/Dynamic - Fitness functions can static, resulting in a success/fail or dynamic, relying on context. A dynamic example could be relaxed transaction speed at scale.

Automated/Manual - Fitness functions should be automated when possible, but manual tests are valuable as well. A manual example could be meeting legal requirements, manual QA testing.

Temporal - A time component can be useful to serve as a reminder, for example to fail on version change.

Fitness functions should be intentional, identified as early as possible, as opposed to emergent and categorized as Key (critical to architecture decisions - performance), Relevant (consider at feature level - code metrics), and Not Relevant (no affect on architecture - cycle time). Review fitness functions with key business/technical stakeholders regularly, checking relevancy, determining change in scale of each function, consider new approaches to measuring, and considering new functions.

Chapter 3 - Engineering Incremental Change

An evolutionary architecture supports guided, incremental change across multiple dimensions. Two aspects of incremental change involve development and operations. Continuous Delivery (the book by Humble and Farley) describe many of the engineering practices required. Architecture can not be judged until design, implementation, upgrade, and change are successful.

Consider the star rating example. Suppose an ecosystem of microservices uses a star rating microservice. The star rating team releases a new version of their service that supports half ratings. Instead of forcing other services to upgrade, they run both versions in parallel, using service discovery. Once other services have upgraded and there are no more requests to the old service, it can safely be taken down.

Feature toggles are invaluable in this type of environment. They allow deliberate rollout of features and QA in production.

Testability is a difficult part of architecture. Module coupling, test coverage, code complexity are all -ilities that can be tested using automated deployment pipelines. Apart from building an artifact/container, the pipeline should run in stages resulting in unit tested code, functional tested code, architecturally tested code, and holistically tested code after deployment.

When to test different types of fitness functions:

  • atomic/triggered - unit/functional tests (circular dependencies, cyclomatic complexity, code coverage)
  • holistic/triggered - integration tests (effect of higher security on scalability)
  • atomic/continual - architectural tests (REST endpoints support all HTTP verbs, conformity monkey)
  • holistic/continual - performance monitoring, failover (netflix chaos monkey, github scientist)

Hypothesis- and Data-Driven Development - GitHub Scientist is an example of data-driven development. Hypothesis-driven development is more akin to A/B testing, or using an in-app behavioral question to validate theories. Figure out what experiments to run. For example: changing the color of the Checkout button will result in a 5% increase in sales - this is easily A/B testable. Another example is rewriting a UI, how does the team decide which feature to port and in what order? Answer: user tracking. This requires evolutionary architecture, modern devops, different methods of gathering requirements, and the ability to run multiple versions of an app in parallel.

Chapter 4 - Architectural Coupling

Building software requires relying on and coupling with other components.

Modules group related code. Modularity is a logical grouping of related code. Components are the physical packaging of modules. Libraries are compile-time dependencies, components that run in the same memory space as the calling code, and communicate via function calls. Services are runtime dependencies, run in their own address space, and communicate over networking protocols.

An architectural quantum is an independently deployable component with high functional cohesion (a monolith, a microservice and it's dependent parts). Smaller quanta imply faster change because developers have to coordinate with fewer lines of code/modules/etc. Developer diligence w/r coupling also determines evolvability.

(ubiquitous language - make sure all terms on the team mean the same thing)

Architecture enables certain types of change across specific dimensions. Architectural patterns are an example of this, and each have different quantum sizes.

Criteria for evaluating architectures:

  • Incremental change
  • Guided change via fitness functions
  • Appropriate coupling

Big Ball of Mud

A system with no structure and highly coupled classes. Changes ripple through the system causing, schemas leak in to the UI. This system fails all three criteria:

  • Incremental change: changes cause a cascade of never-ending ripples
  • Guided change via fitness functions - difficult because of no clearly defined partitions
  • Appropriate coupling - classes are highly coupled

Monoliths

Unstructured

Lack of coherent structure, lots of independent classes. Modules use shared classes. Very similar to Big Ball of Mud.

  • Incremental change: large quantum size with highly coupled components
  • Guided change via fitness functions: Difficult, but lots of tools to help
  • Appropriate coupling: classes are highly coupled

Layered

Separate functionality in to technical layers with distinct responsibilities (database, persistence, business rules, presentation). This gives rise to some isolation and separation of concerns. Layers communicate via well-documented interfaces, allowing implementation within layers to change without affecting others. The quantum is still large, including the database.

  • Incremental change: Changes are relatively easy within layers, but cross-layer changes are still difficult.
  • Guided change via fitness functions: Structure allows more definition around fitness functions and testing smaller parts in isolation.
  • Appropriate coupling: Limiting coupling to layers allows more change without affecting other layers.

Modular

A disciplined approach to enforcing logical modularity within a system. Quantum size is still large, but changes to one module don't affect others.

  • Incremental change: Fairly easy since functionality is separated in to logical modules, which can be modified without affecting others.
  • Guided change via fitness functions: Better separation enables testing and definition of better metrics.
  • Appropriate coupling: Components are functionally cohesive, with no cross module coupling except via interfaces.

Microkernel

A core system with an API that allows plugins. This greatly reduces the quantum size to the core system and each self-contained plugin. The platform can support cross-plugin communication, which can result in a plugin dependency graph, increasing the quantum.

  • Incremental change: A stable core results in most changes happening in self-contained plugins.
  • Guided change via fitness functions: Isolation between core/plugins allows easy definition of fitness function.
  • Appropriate coupling: The pattern itself reduces coupling between functionality.

Event-Driven Architectures

Integration of several disparate systems via message queues. Brokers advantages include evolvability, asynchronosity, scale, etc but things like transactions are easier with Mediators.

Broker

A system consisting of

  • Message queues: medium of communication
  • Initiating events: starts the business process
  • Intra-process events: messages passed between event processors
  • Event processors: perform business processing

Implementation is more difficult w/r error handling, lack of transactions, but extremely evolvable. Deployment pipelines and testing can be a challenge as well. Event processors should be stateless, decoupled, and own their data.

  • Incremental change: processors are self-contained and don't affect other processors.
  • Guided change via fitness functions: atomic functions are easy for each processor, holistic functions are complex.
  • Appropriate coupling: coupling is low, adding functionality involves new queue listeners. Cohesion is via message contracts.

Mediator

A hub coordinates the execution of a business process by posting messages to queues for processors. The mediator handles cross-processor communication, error handling, and ensuring final status notifications are sent once all processing is done.

  • Incremental change: processors are small and self-contained
  • Guided change via fitness functions: similar to EDA, mediator furthers eases testing
  • Appropriate coupling: coupling increases because of the mediator

Service-Oriented Architectures

ESB-Driven SOA

A service bus cordinates and facilitates communication between services. Similar to EDAs, but use a strictly defined service taxonomy. These capture what the company does in an abstract way (BPEL). Each business service uses enterprise services (like account management) to choreograph business implementation. The service bus is responsible for mediation, routing, process orchestration, and message transformation. The quantum is essentially the entire system, due to the service taxonomy.

  • Incremental change: both development and deployment are difficult
  • Guided change via fitness functions: testing is difficult because a workflow is spread across multiple services, must use holistic functions.
  • Appropriate coupling: doesn't allow for independent, evolvable parts

Microservices

Combines continuous delivery with bounded-context services to partition the application in to smaller quanta. Instead of layering an application along the technical dimension, layer it along the domain dimension. Each service owns the context, from database to UI, and communicates with other services via messaging. Emphasis is on understanding the problem domain. The quanta is the service itself. This is the exemplar for evolutionary architecture. Service templates provide essential, common functionality (logging, monitoring, auth) and easier upgrade paths. Microservices don't need to be small, they just need to capture a bounded business context well.

  • Incremental change: changes are isolated to business contexts
  • Guided change via fitness functions: each service is independently testable, tools available for coordination testing
  • Appropriate coupling: service integration and service templates are the coupling points.

Principles (from Building Microservices Architectures):

  • Modeled around business domain or process
  • Hide implementation details: communicate via public contracts
  • Automation: including machine provisioning
  • Decentralized: share nothing
  • Independently deployed
  • Failure isolation: reactive manifesto
  • Observable: monitoring/logging are essential with so many services

Service-based Architectures

Allow migration to evolutionary architectures by easing complexity with development. Granularity is larger than a business domain, and requires greater discipline to avoid coupling. This is a 'smaller monolith'. This architecture usually uses a monolithic database, and this style may be favored by heavily transactional systems. Integration hubs can be used to glue services together, but increases coupling. This is usually a good compromise between the complexity of microservices and the benefits they provide.

  • Incremental change: supports domain-focused changes, but not as easy as microservices
  • Guided change via fitness functions: increased coupling makes testing harder
  • Appropriate coupling: coupling is within areas slightly larger than domain, but not as small as microservices.

Serverless

Backend-as-a-Service, Function-as-a-Service. Complexity arises from having to integrate services together.

  • Incremental change: changes are limited to the deployment itself, a function?
  • Guided change via fitness functions: holistic functions must be used to test the whole system
  • Appropriate coupling: eases some area, such as operations and security at the expense of coupling between functions

Chapter 5 - Evolutionary Data

Data stores are often highly coupled to code and more difficult to evolve than code itself. Refactoring Databases (Sadalage and Ambler) is the definitive guide to evolving databases. In short, changes must be

  • Tested: Ensure ORMs stay in sync, and changes are stable.
  • Versioned: Schemas should be versioned, along with the code that uses them.
  • Incremental: Changes should be small and use automated migration tools.

Additionally, once run, migrations are immutable, meaning to undo a migration, a new migration must be added as opposed to removing the unneeded migration.

Shared Database Integration

An enterprise integration pattern of using the same relational database for several services. This effectively freezes the schemas, because each application must coordinate changes. This can be alleviated by using the Expand/Contract pattern, supporting both old and new schemas in parallel until all applications support the new schema.

Two-Phase Commit Transactions

Transactions are a special form of coupling desired by DBAs and business analysts alike. Moving to a microservice architecture requires breaking these transaction boundaries. Thus, service-based architectures are often a better solution for transaction-heavy systems since the quanta are slightly larger and align better with a business context.

Data

The real world changes, and schemas should evolve in response. Join tables fossilize many schemas on top of one another. Maintaining old data without refactoring couples architecture to the past.

Chapter 6 - Building Evolvable Architectures

Now we tie fitness functions, incremental change, and appropriate coupling together.

  1. Identify Dimensions Affected by Evolution - what architectural characteristics should we protect as the system evolves? This should happen at the beginning of projects as well as on an ongoing basis.
  2. Define Fitness Functions for Each Dimension - each dimension should be protected by one or more fitness functions, automated if possible.
  3. Use Deployment Pipelines - evaluate automated fitness functions on a regular basis.

Building evolvability in to greenfield projects is easier than untangling things in an existing architecture. With existing projects:

  • Coupling - largely determines the evolvability of architecture. Rigid data schemas limit evolvability.
  • Engineering - continuous delivery, deployment pipelines, automated operations are essential
  • Fitness Functions -

Migrating Architectures

A typical application lifecycle involves starting out with a layered monolith and migrating to microservices when that gets stressed. Coupling of data is just as hard to evolve and often forgotten about as the coupling of classes and components.

Don't get caught up in meta work, writing frameworks. Use open-source or commercial alternatives, or be ready to migrate to them when available.

Migration Steps

Understand all of the coupling points in an application, as well as cohesion across all dimensions. Understand the motivations to migrate. The current trend isn't a reason. Smaller domains with better team structure and operational isolation allow for easier incremental change.

Finding the right granularity is important. Large services don't require breaking transactional boundaries but don't facilitate incremental change, while services that are too small require too much orchestration, interdependencies, and communication overhead. Keeping the business context within a single service eases coordination problems, but can result in larger services. Ways to split a monolith:

  • Business functionality
  • Transactional boundaries
  • Deployment

After settling on service boundaries, separate the UI from the business layers. The UI often remains a monolith of sorts, since it must present a unified experience. This also allows architectural changes without affecting the UI. Build service discovery early, as it will be valuable when many services must find one another and coordinate.

When transitioning away from a monolith, build a small number of larger services, integrate and repeat. Fitness functions help ensure the integration via consumer-driven contracts.

Guidelines for Building Evolutionary Architectures

Eliminate variability, convert changeable things to constants. Infrastructure should be defined programmatically, rather than manually constructed and configured. To upgrade (operating system), the definition is updated, new infrastructure spun up and the old torn down.

Make decisions reversible. Blue/green deployments, feature toggles, etc.

Prefer evolvable over predictable. Unknown unknowns necessitate the need for architecture to change since there is no way to predict where business, technology, etc will go.

Build anticorruption layers. Defer decisions until the last possible minute, using interfaces and abstracting functionality to insulate against library changes and make switching out services/implementations easier. Don't buy tech debt, you might not need all the complexity.

Service templates are a common way to remove variability. By providing monitoring, logging, service discovery, message queues, etc, services are observable immediately and library upgrades are largely transparent to the team. This is an example of appropriate coupling.

Build sacrificial architectures. Figure out all the unknown unknowns. Plan to throw away a pilot system or proof of concept (Fred Brooks - Mythical Man Month). Build the MVP planning on restructuring the architecture once you've won the market.

Mitigate external change. Understand the costs, as well as the benefits, of third-party libraries/frameworks/software and insulate against their change.

Be aggressive in updating frameworks. Libraries can be upgraded more casually, on a 'when needed' basis.

Chapter 7 - Evolutionary Architecture Pitfalls and Antipatterns

Technical

  • Antipattern: Vendor King - An architecture built entirely around a vendor. Treat external software as an integration point. This frees developers from the technical choices of the vendor and allows business processes to be more customizable, and thus the business less like others and more competitive.

  • Pitfall: Leaky Abstractions - Abstractions allow developers to think at a higher level. However, no abstraction is perfect. Understand the abstraction layer below the work, and work to prevent leaks from propagating upwards.

  • Antipattern: Last 10% Trap - Don't fall for tools that are too narrow, allowing for fast initial development, but not robust enough to finish the application.

  • Antipattern: Code Reuse Abuse - The more reusable code is, the less reusable it is. Reusable code must support the many different ways developers will use it, which causes it to be less easy to reuse. Microservices prefer duplication to coupling.

  • Pitfall: Resume-Driven Development - Understand the problem domain before making an architecture decision. Don't build things for the sake of using a new pattern, framework, etc.

Incremental Change

  • Antipattern: Inappropriate Governance - Over-prescribing technology choices leads to unneeded complexity and overengineering. Microservices should pick their own tech stack, including database. This can actually force decoupling, since different services can't reuse code, and thus, can't inadvertently couple. Perhaps pick three blessed stacks and allow teams to choose.

  • Pitfall: Lack of Speed to Release - Continuous delivery encompasses many engineering practices required for evolutionary architecture. Continuous deployment gets developers in the mindset of change. Cycle time is the metric to minimize.

Business Concerns

  • Pitfall: Product Customization - Sales teams like to sell customizable software. This leads to several bad practices: Unique builds, permanent feature toggles, UI customization. Each of these increases testing burden. Customization impedes evolvability.

  • Antipattern: Reporting - Reporting functionality often doesn't need the complexity associated with a layered architecture, it's just pulling data from a database. Using the same database for reporting and the application leads to a rigid schema and reduces evolvability. Consider a reporting service that listens to a change stream to populate its own database.

  • Pitfall: Planning Horizons - Long planning horizons encourage lots of research and handcrafted artifacts, to which people become attached. Don't become attached, the world changes.

Chapter 8 - Putting Evolutionary Architecture into Practice

How do you sell this to your organization?

Organizational Factors

Domain-centric teams should be cross-functional, containing every role necessary to deliver value, including business analysts, architecture, testing, operations, database administration. This reduces coordination overhead.

Teams should be focused on business capabilities, rather than technical areas.

Software should be thought of not as a project, with a defined timeline, but as a product, with an infinite lifespan. This encourages ownership and quality over completion.

Consumer-driven contracts help developers maintain compatibility while evolving components.

The number of connections between people in an organization is predictive of the coordination/communication overhead. Favor small teams.

Team Coupling

Architects should take on a leadership role, influencing technical culture and design approaches. The culture of the team affects how software is built.

Experimentation drives evolution. Small experiments should be run continuously to try out new ideas, but on the technical and product fronts. Bring in ideas from the outside. Encourage explicit improvement. Spike and stabilize. Create innovation time. Connect engineers with customers.

Where to Start

Beginning with a Big Ball of Mud, how do we work towards an evolutionary architecture.

Low Hanging Fruit - Start with low hanging fruit to demonstrate validity to the organization. Start with something fairly decoupled and not in the critical path.

Highest Value - Find the most critical part of the system and build an evolutionary architecture around that. The opposite of low hanging fruit. This demonstrates commitment.

Why?

Forbes says every company is a software company. Many companies favor predictability when planning. However, evolvability is a potential solution to the Innovator's Dilemma. Inappropriate coupling limits scale. Allows for hypothesis- and data-driven design, and reduced coupling allows for easier integration of techniques like A/B testing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment