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 captionsListen 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
- Modifiability is the cost and risk of change; specify it by saying what is likely to change, who makes the change, when it binds and what the change may cost.
- Modifiability tactics increase cohesion, reduce coupling (encapsulate, use an intermediary, restrict dependencies) and defer binding, but each later binding time adds machinery.
- Testability depends on controllability and observability; isolate dependencies behind interfaces and inject them so tests can use test doubles.
- Deployability rests on small batches, automation, independently deployable units, backward-compatible changes, gradual rollouts and fast rollback.
- DORA's current software delivery metrics are change lead time, deployment frequency, failed deployment recovery time, change fail rate and deployment rework rate.
- ISO/IEC 25010:2023 maintainability has five sub-characteristics: modularity, reusability, analysability, modifiability and testability.
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 changes | What changes | When it binds | Response measure |
|---|---|---|---|
| Developer | Add a new delivery partner to a food-delivery platform | Coding time | Only the partner adapter changes; done, tested and deployed within 5 days |
| Product manager | Turn a new checkout flow on for 10% of users | Run time (feature flag) | Takes effect within 1 minute, without a deployment |
| Operations engineer | Point the system at a new database host | Deployment (configuration) | No code change; one pipeline run of under 15 minutes |
| Customer administrator | Add a custom field to an invoice | Run time (settings) | No supplier involvement; available immediately |
| Platform team | Upgrade the web framework to a new major version | Build time | Fewer 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
Binding time
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.
- 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.
- 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.
- 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.
- 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 today | 15 + 5 = 20 person-days, so 20 x £600 = £12,000 |
|---|---|
| Cost for 8 vendors today | 8 x £12,000 = £96,000 |
| One-off cost of the refactoring | Design and build the interface and migrate the 3 existing vendors: 30 person-days = £18,000 |
| Cost per vendor afterwards | Write 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-even | Each 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
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"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)
| Metric | Group | What it measures | Architectural levers |
|---|---|---|---|
| Change lead time | Throughput | Time from a change being committed to version control to it running in production | Fast automated tests, small independently deployable units, no hand-offs |
| Deployment frequency | Throughput | How often changes are deployed to production (or the time between deployments) | Independent deployability, automated pipeline, feature flags |
| Failed deployment recovery time | Throughput | Time to recover from a deployment that fails and needs immediate intervention | Automated rollback, canary releases, good monitoring, small changes |
| Change fail rate | Instability | Proportion of deployments that need immediate intervention, such as a rollback or hotfix | Testability, contract tests, backward-compatible changes |
| Deployment rework rate | Instability | Proportion of deployments that are unplanned and happen because of an incident in production | Quality 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.
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?”