Be Software Architecture Wise
Be Wise Apps

Leer en español · Lire en français · Auf Deutsch lesen

Section 2.6 of 2.8 · Intermediate · 22 min read

Modifiability, testability and deployability: designing for change

Video lesson

5 min · narrated, with captions
Listen only (audio)
Read the transcript

Most of the cost of a successful system comes after its first release, as it's changed again and again. This lesson is about designing for that change.

Think of a kitchen. If the cooker is built into the wall, with the pipes buried in concrete, replacing it means demolishing the wall. If it plugs into a standard socket, you swap it in an afternoon. Software is the same. Parts that will change get standard connections. Then you make it easy to check that everything still works after a swap, and make delivering the new version routine rather than risky.

Those three steps are three qualities. Modifiability is about the cost and risk of making a change. Testability is about how easily you can show, by testing, that the system behaves as intended. And deployability is about how quickly, safely and predictably a change reaches production, and how quickly it can be taken out again. They form a loop: a modular system with no fast tests still changes slowly.

Modifiability means nothing until you say what's expected to change. Plan for every possible change and you over-engineer; plan for none and every change is a rewrite. So ask four questions. First, what can change: functions, platforms, partners or load? Second, how likely is it? Payment providers and tax rules change often. The idea of an order rarely does. Spend design effort where change is probable. Third, who makes the change, and when? A developer in code, an engineer through configuration, or a product manager flipping a feature flag. The moment a decision is fixed is called its binding time. And fourth, what does it cost, in effort, time and risk? The answers give you a testable scenario.

Here's a key tactic: reduce coupling with an intermediary. Before, checkout, refunds and subscriptions each talk directly to every payment provider. That's six dependencies, and a new provider means changing three services. After, all three depend on one payment interface, which hides the providers behind it. Each provider gets its own adapter. Adding a provider now means adding one adapter, and the callers don't change at all.

A food-delivery company connects to restaurants' point of sale systems. Each new vendor takes twenty person-days of work and testing. At six hundred pounds a day, that's twelve thousand pounds. Sales expects eight new vendors this year: ninety six thousand pounds. The architect proposes an adapter interface. Building it costs eighteen thousand pounds, once. After that, each vendor needs only four person-days: two thousand four hundred pounds. So eight vendors cost thirty seven thousand two hundred pounds in total, saving fifty eight thousand eight hundred pounds in the first year. The investment pays for itself by the second vendor. But only if the vendors really arrive. That's why a modifiability requirement must say what's likely to change.

Now testability. A component is testable when a test can control its inputs and state, and observe what it did. Code that calls a real payment service or reads the system clock directly is hard to control. The fix is dependency injection. In production, the order service depends only on a payment gateway interface, implemented by a gateway that makes real network calls. In a unit test, the same order service is given a fake gateway instead. It gives scripted replies and records every call, so the test has full control, with no network and no waiting.

Finally, deployability. The central idea is small batches. A release with one change is easy to reason about and easy to roll back. A release of three months' work contains hundreds of interacting changes. Small batches need automation: a pipeline that builds, tests and deploys every change the same way, with no manual steps someone can forget. Architecture helps with independently deployable parts, backward-compatible changes, and gradual rollouts to a small share of traffic first. Research by the DevOps Research and Assessment programme, known as Dora, found five measures of delivery performance, and that the best teams are fast and stable at the same time. Loosely coupled architecture goes with higher scores.

The full section covers binding time, the kinds of test double, a costly deployment failure and the Dora metrics in detail. Next, in Section two point seven, we look at security, usability and more. See you there.

What you need to know

Three qualities about change

Most of the cost of a successful system is incurred after its first release, as it is changed again and again. Three related qualities describe how well an architecture supports that. Modifiability is about the cost and risk of making a change. Testability is about how easily you can show, by testing, that the system (or a part of it) behaves as intended, and how readily faults reveal themselves. Deployability is about how quickly, safely and predictably a change can be put into production, and taken back out again.

They are linked in a loop. A change is only cheap if you can make it in one place (modifiability), know quickly that you have not broken anything (testability), and ship it without ceremony (deployability). A system that is modular but has no fast tests will still change slowly, because every change needs weeks of manual regression testing. A system with good tests but a quarterly release window will still deliver value slowly.

Modifiability: four questions about any change

Bass, Clements and Kazman suggest that modifiability is meaningless until you say what is expected to change. Planning for every possible change produces an over-engineered system full of unused extension points; planning for none produces a system where every change is a rewrite. Four questions sharpen the requirement.

What can change? Functions, platforms (a new database version, a move to another cloud), the environment (a new partner API, new regulation), qualities (twice the load), or capacity. How likely is the change? Spend design effort where change is probable: payment providers, tax rules and user interfaces change often; the concept of an order rarely does. Who makes the change, and when? A developer at coding time, a release engineer at deployment, an administrator through configuration, or an end user through settings. The moment at which a decision is fixed is called its binding time. What does the change cost? The effort, elapsed time and money to make, test and deploy it, plus the number of other elements affected and the risk of introducing defects.

Answering those questions gives a scenario of the kind introduced in Section 2.3: A developer adds support for a new payment provider, changing no existing modules other than configuration, within 3 person-days including tests, with no increase in checkout errors.

Modifiability scenarios with different sources and binding times

Who changesWhat changesWhen it bindsResponse measure
DeveloperAdd a new delivery partner to a food-delivery platformCoding timeOnly the partner adapter changes; done, tested and deployed within 5 days
Product managerTurn a new checkout flow on for 10% of usersRun time (feature flag)Takes effect within 1 minute, without a deployment
Operations engineerPoint the system at a new database hostDeployment (configuration)No code change; one pipeline run of under 15 minutes
Customer administratorAdd a custom field to an invoiceRun time (settings)No supplier involvement; available immediately
Platform teamUpgrade the web framework to a new major versionBuild timeFewer than 20 files change outside an anti-corruption layer; under 2 weeks

Tactics for modifiability

The tactics in Software Architecture in Practice reduce the cost of change by limiting how far a change ripples. They rest on two old ideas, treated in depth in Chapter 3. Cohesion measures how strongly the responsibilities inside one module belong together; coupling measures how much one module depends on another. A change that is likely should touch one cohesive module, and that module should be loosely coupled to the rest.

Increase cohesion. Split a module that does two unrelated jobs (a CustomerService that handles both login and loyalty points), and move responsibilities to where they belong, so that a likely change lands in one place.

Reduce coupling. Encapsulate: hide a module's internals behind an explicit interface, following David Parnas's principle of information hiding (1972), so callers cannot depend on what may change. Use an intermediary: put something between two parties so neither knows the other directly: an adapter, a message broker, a publish-subscribe channel, a repository in front of a database, or an API gateway. Restrict dependencies: decide which modules may call which (layering rules, a module visible only to its own package) and enforce it with tooling. Abstract common services: provide shared behaviour once, behind an interface, rather than copying it.

Defer binding. Fix a decision as late as is useful, so that the change can be made later and by someone further from the code: configuration files, environment variables, plug-ins discovered at start-up, service discovery and feature flags. Each later binding time adds machinery (configuration to validate, flags to remove, versioned plug-in interfaces), so defer only decisions that genuinely vary.

Using an intermediary to contain change

Without an intermediary, three services each call two payment providers directly; with an adapter interface, they call one interface and only the adapters know the providersBefore: every caller knows every providerCheckoutRefundsSubscriptionsProvider AAPI v3Provider BREST API6 dependencies; a new providermeans changing 3 servicesAfter: callers depend on one interfaceCheckoutRefundsSubscriptionsPaymentinterfaceAdapterAAdapterBAdapterC (new)a new provider means addingone adapter; callers unchangedDashed lines: adapters implement the interface
An interface with one adapter per provider turns a change that touched three services into the addition of one adapter.

Binding time

Binding times from coding to run time: later binding gives more flexibility at the cost of more machineryEarlier binding: simpler, cheaper, saferLater binding: more flexible, more machineryCodinghard-codedconstantBuildcompile flag,build profileDeployenvironmentvariableStart-upconfig file,plug-in scanRun timefeature flag,service lookupEach step right lets a change be made later, by someone further from the code, without a new release
Moving a decision to the right lets it change later and without a release, but every step adds machinery to manage.

Decision guide

Decision guide: how late should this decision bind?

Use it when a stakeholder says "this might change". Start at the top and stop at the first yes.

  1. Is the change unlikely, or would it need new code and new tests anyway?
    YesBind at coding time. Keep it simple and well encapsulated so the change stays local when it comes.
    NoGo to the next question.
  2. Does the value differ only between environments (test, staging, production) or between customers' installations?
    YesBind at deployment: environment variables or configuration injected by the pipeline, kept in version control.
    NoGo to the next question.
  3. Do third parties or other teams need to add behaviour without changing your code?
    YesBind at start-up or run time with a plug-in or extension interface, and version that interface carefully.
    NoGo to the next question.
  4. Must non-developers switch behaviour on or off quickly, or must you release to a fraction of users first?
    YesBind at run time with a feature flag, and give every flag an owner and a removal date.
    NoBind at build or deployment time; later binding would add machinery nobody needs.

Worked example

Worked example: what is the modifiability scenario worth?

A food-delivery company integrates restaurants' point-of-sale systems. Today each new point-of-sale vendor needs changes in the menu, order and refund services: about 15 person-days of work plus a 5-day regression test cycle. Sales expects 8 new vendors over the next 12 months. The architect proposes a vendor adapter interface (as in the diagram above). Assume a fully loaded cost of £600 per person-day.

Cost per vendor today15 + 5 = 20 person-days, so 20 x £600 = £12,000
Cost for 8 vendors today8 x £12,000 = £96,000
One-off cost of the refactoringDesign and build the interface and migrate the 3 existing vendors: 30 person-days = £18,000
Cost per vendor afterwardsWrite one adapter (3 days) plus contract tests against the interface (1 day): 4 person-days = £2,400
Cost for 8 vendors afterwards£18,000 + 8 x £2,400 = £37,200
Saving in year one£96,000 - £37,200 = £58,800
Break-evenEach vendor saves £12,000 - £2,400 = £9,600, so the £18,000 is repaid after £18,000 / £9,600 = 1.9, that is, by the second new vendor

The calculation is only as good as the prediction of change. If sales signs no new vendors, the refactoring is pure cost. This is why modifiability requirements must say what is likely to change.

Lead time often matters more than cost: cutting a vendor launch from about 4 weeks to under 1 week may win contracts, which no person-day sum captures.

The new adapter interface is itself a commitment. Changing it later affects every adapter, so it deserves careful design and versioning.

Testability: controllability and observability

A component is testable when a test can put it into a known state, feed it chosen inputs and see what it did. Two properties capture that. Controllability is the ability to set a component's inputs and internal state from a test. Observability (in the testing sense) is the ability to see its outputs and relevant internal state. Code that reads the system clock directly, calls a real payment service, picks random numbers or writes to a shared database is hard to control; code that hides its results in private state or only in log text is hard to observe.

Architectural tactics for testability therefore aim to give tests control and visibility, and to limit complexity. Isolate dependencies behind interfaces, then use dependency injection: the component receives its collaborators (a clock, a payment gateway, a repository) from outside rather than creating them, so a test can pass in a substitute. Those substitutes are test doubles, a term popularised by Gerard Meszaros: a stub returns canned answers, a fake is a working lightweight implementation (an in-memory repository), a mock checks that expected calls were made, and a spy records calls for later assertions. Provide test interfaces: hooks to set state, record and replay traffic, or run in a sandbox environment. Limit non-determinism: avoid hidden concurrency and global state, and make time and randomness injectable. Limit structural complexity: fewer dependencies and no cycles mean smaller test set-ups.

Architecture shapes the testing strategy at higher levels too. In a system of services, each boundary needs contract tests that check both sides agree on the API, and a few end-to-end tests that exercise the real path. A design with clean boundaries lets most tests run fast and in isolation, while a tangled design forces slow, brittle end-to-end tests for everything.

Dependency injection makes a component controllable

In production the order service receives a real payment gateway; in a test it receives a fake through the same interface, so the test controls inputs and observes outputsIn productionIn a unit testOrderServicePaymentGatewayinterfaceHttpGatewayreal network callsOrderServicePaymentGatewayinterfaceFakeGatewayscripted repliescontrolsand recordsdepends onimplements
The same OrderService runs against the real gateway in production and against a scripted fake in tests, because it depends only on the interface.
class OrderService:
    def __init__(self, gateway, clock):
        self.gateway = gateway   # anything with charge(amount, card)
        self.clock = clock       # anything with now()

    def place(self, order):
        if self.clock.now().hour >= 23:
            return "kitchen closed"
        return self.gateway.charge(order.total, order.card)

# In a test: full control, no network, no waiting until 11 pm
class FakeClock:
    def __init__(self, t): self.t = t
    def now(self): return self.t

class FakeGateway:
    def __init__(self): self.calls = []
    def charge(self, amount, card):
        self.calls.append(amount)
        return "approved"
Constructor injection: the service is given its clock and gateway, so a test can supply fakes.

Deployability

Deployability measures how predictably and cheaply a change moves from a developer's commit into production, and how quickly it can be undone. It became a first-class quality only in the last decade, as continuous delivery showed that release frequency and stability are not opposites. The fourth edition of Software Architecture in Practice (2021) added a chapter on it.

The central idea is small batches. A release that contains one change is easy to test, easy to reason about and easy to roll back; a release of three months' work contains hundreds of interacting changes, so failures are likely and hard to diagnose. Small batches require automation: a deployment pipeline that builds, tests and deploys every change the same way, scripted infrastructure and no manual steps that someone can forget. Architectural tactics include independently deployable units (so one team's release does not wait for another's), backward-compatible interfaces and database migrations, rollback and roll-forward mechanisms, gradual rollouts (canary releases to a small share of traffic first, blue-green switching between two environments) and feature flags, which separate deploying code from releasing a feature to users. Pipelines and strategies are covered in CI/CD and deployment strategies.

The cost of ignoring deployability can be severe. In August 2012 the trading firm Knight Capital deployed new code manually to its eight order-routing servers but missed one. A reused configuration flag then activated obsolete code on that server, which flooded the market with orders, producing about 4 million executions in roughly 45 minutes, and cost the firm about 460 million US dollars. An automated, verified deployment and the removal of dead code would each have prevented it.

Measuring delivery: the DORA metrics

The DORA research programme (DevOps Research and Assessment, described in Accelerate by Nicole Forsgren, Jez Humble and Gene Kim, 2018, and continued in annual reports now run within Google Cloud) found that a small set of measures predicts software delivery performance, and that the best performers are fast and stable at the same time. The current model has five software delivery performance metrics, grouped into throughput and instability. Their names have evolved: the old mean time to restore became failed deployment recovery time in 2023, and deployment rework rate was added in 2024.

Two cautions. First, these metrics describe a team's delivery system, not individuals; using them to rank people invites gaming. Second, they are outcomes. Architecture influences them strongly: teams that can deploy their part independently, test it in isolation and roll it back quickly score better, which is one reason the DORA research links loosely coupled architecture with higher delivery performance.

DORA software delivery performance metrics (dora.dev, current model)

MetricGroupWhat it measuresArchitectural levers
Change lead timeThroughputTime from a change being committed to version control to it running in productionFast automated tests, small independently deployable units, no hand-offs
Deployment frequencyThroughputHow often changes are deployed to production (or the time between deployments)Independent deployability, automated pipeline, feature flags
Failed deployment recovery timeThroughputTime to recover from a deployment that fails and needs immediate interventionAutomated rollback, canary releases, good monitoring, small changes
Change fail rateInstabilityProportion of deployments that need immediate intervention, such as a rollback or hotfixTestability, contract tests, backward-compatible changes
Deployment rework rateInstabilityProportion of deployments that are unplanned and happen because of an incident in productionQuality of tests and reviews; observability that catches problems early

Maintainability in ISO/IEC 25010

ISO/IEC 25010:2023 groups these concerns under the characteristic maintainability, with five sub-characteristics: modularity (the system is made of discrete components so that a change to one has minimal impact on others), reusability (an asset can be used in more than one system or in building other assets), analysability (how easily you can assess the impact of a change, diagnose a deficiency or find the parts to modify), modifiability (the system can be changed without introducing defects or degrading quality) and testability (test criteria can be established and tests performed to determine whether they are met). Deployability has no sub-characteristic of its own in the standard; installability and replaceability sit under flexibility. The model is a checklist, as Section 2.2 explains; scenarios are what make each item concrete.

Check your understanding

Why is "the system shall be easy to change" not a useful requirement?

It does not say what will change, who will change it, when, or what cost is acceptable, so nobody can design for it or test whether it has been met.

What is the difference between a stub and a mock?

A stub returns canned answers so the test can control inputs; a mock also checks that the expected calls were made, so the test verifies interactions.

Why do feature flags help deployability?

They separate deploying code from releasing a feature, so incomplete or risky features can be deployed switched off, released gradually and switched off again without a new deployment.

A team deploys monthly and each release contains about 200 changes. Which DORA metrics are likely to suffer, and why?

Change lead time (changes wait weeks for the release), deployment frequency, and probably change fail rate and recovery time, because large batches are more likely to fail and harder to diagnose and roll back.

Next · 2.7Security, usability and moreSecurity as a measurable quality, ISO/IEC 25010's security sub-characteristics and tactics, usability and WCAG 2.2 accessibility, interoperability, cost per transaction, software carbon intensity, safety, observability and regulatory qualities.Continue →

Sources and further reading

Have a question about this section? ArchitectBot, the course’s AI study assistant, answers from the course first.

Ask: “We have a monolith that takes three weeks of manual testing before each release. What architectural changes would let us deploy weekly without more risk?”