Reading fromTech Explained Simply
Volume 3 · Chapter 10ch 3.10 · 0/10 · lesson 6
Designing for Change: Stable Boundaries, Reversible Decisions, and Safe Migrations
📑 On this page
- A concrete example: payment provider
- Identify likely change
- Cohesion
- Coupling
- Contracts
- Encapsulation
- Adapters
- Data schemas
- Expand and contract
- Version compatibility
- Feature flags
- Configuration
- Reversible decisions
- Tests
- Observability
- Decision records
- Avoid premature abstraction
- Deprecation
- Create a change budget
- Knowledge check
- The one idea to remember
Software changes because users, business rules, scale, vendors, regulation, and understanding change.
Trying to predict every future requirement produces abstractions nobody needs. Ignoring change makes ordinary evolution dangerous.
Design for probable change to be inexpensive and improbable change to remain possible through clear boundaries, stable contracts, reversible decisions, migrations, tests, and observable behaviour.
Adaptability is selective, not universal.
A concrete example: payment provider
An application defines an internal payment operation:
- create authorization,
- capture,
- refund,
- and inspect status.
One adapter translates those operations to a provider. Provider-specific webhooks, IDs, and errors remain behind the boundary.
Replacing the provider still requires work, but the rest of the product does not speak its API everywhere.
Identify likely change
Use evidence from:
- roadmap,
- support requests,
- regulation,
- vendor history,
- scale forecasts,
- incidents,
- and domain knowledge.
Do not generalize because “anything might change.” Rank change by probability and impact.
Cohesion
Keep related behaviour and data together.
A module responsible for pricing should contain pricing rules and expose intentional operations. Scattered rules across UI, jobs, and database triggers make change inconsistent.
High cohesion gives one place to understand and modify a concept.
Coupling
Coupling is dependence between components.
Some coupling is necessary. The goal is visible, intentional coupling through stable interfaces rather than hidden dependence on internal tables, timing, or undocumented side effects.
Measure how many owners and deployments one change requires.
Contracts
A contract defines:
- inputs,
- outputs,
- errors,
- timing,
- ordering,
- compatibility,
- and ownership.
Contracts can be APIs, events, schemas, files, or user workflows.
Test contracts from the consumer's perspective.
Encapsulation
Hide implementation details likely to change behind a meaningful boundary.
Do not expose raw vendor objects or database rows throughout the product. Return domain concepts with documented semantics.
Avoid hiding everything behind generic maps that erase useful types.
Adapters
An adapter translates between an internal contract and an external system.
Adapters are useful for vendors, storage, messaging, and legacy systems when a stable internal meaning exists.
Do not force fundamentally different services into a false identical abstraction. Preserve capabilities or choose a deliberate common subset.
Data schemas
Data outlives code versions.
Design schema changes with:
- additive fields,
- defaults,
- nullable transitions,
- backfills,
- dual reads or writes,
- validation,
- and retirement.
Never assume every service and record changes simultaneously.
Expand and contract
A safe migration often:
- expands the system to accept old and new forms,
- migrates producers and data,
- verifies adoption,
- stops old production,
- and contracts by removing old support.
This reduces coordinated downtime.
Version compatibility
Clients, services, devices, and jobs can run different versions.
Define backward and forward compatibility, negotiation, and support windows. Mobile and IoT clients may remain old for months or years.
Reject unsafe downgrade.
Feature flags
Feature flags separate deployment from exposure.
They support:
- canaries,
- experiments,
- tenant rollout,
- emergency disablement,
- and gradual migration.
Flags need owners, expiry, testing of both paths, and removal. Old flags create combinatorial complexity.
Configuration
Configuration changes behaviour without code deployment.
Validate types and ranges, version changes, require review for high-risk settings, stage rollout, and retain rollback. Configuration is production code in operational effect.
Avoid one global switch with unlimited blast radius.
Reversible decisions
Early decisions under uncertainty should preserve exit:
- bounded contracts,
- exportable data,
- modular rollout,
- and temporary architecture.
Reversibility has cost, so invest where uncertainty and consequence justify it.
Irreversible decisions need stronger evidence and review.
Tests
Tests create confidence to change:
- unit tests for rules,
- contract tests for boundaries,
- integration tests for workflows,
- migration tests for data,
- and regression tests for incidents.
Test old and new versions together during compatibility changes.
Observability
You cannot change safely if you cannot see effect.
Measure:
- adoption,
- errors,
- latency,
- data quality,
- fallback,
- user outcomes,
- and version distribution.
Compare before, during, and after rollout.
Decision records
Record important decisions:
- context,
- options,
- chosen path,
- assumptions,
- consequences,
- and review triggers.
Future teams can distinguish intentional constraints from accidents and revisit assumptions without repeating history.
Avoid premature abstraction
Wait for at least a credible pattern before extracting a general framework.
Two pieces of code that look similar may change for different reasons. Duplication can be cheaper than coupling unrelated concepts.
Abstract when it removes repeated meaningful complexity and the common contract is understood.
Deprecation
Removing old behaviour requires:
- usage inventory,
- owner communication,
- migration guide,
- telemetry,
- deadlines,
- support,
- and final enforcement.
Do not maintain every version forever, but do not break unknown consumers without evidence.
Create a change budget
Track the recurring cost of changing each important area:
- teams involved,
- lead time,
- migrations,
- compatibility window,
- test effort,
- rollout time,
- and incident risk.
High change cost in a frequently changing domain is evidence that the boundary needs improvement.
Do not optimize only code-edit time. A one-line schema change that requires six teams and a month of coordination is expensive. Conversely, a larger internal refactor may be worthwhile if it makes repeated product changes safer and independently deployable.
Knowledge check
- How should teams identify likely change?
- What does an expand-and-contract migration do?
- Why do feature flags need expiry?
- How do tests and observability support change differently?
- When is duplication preferable to abstraction?
The one idea to remember
Adaptable software does not predict everything. It isolates likely variation behind meaningful contracts, evolves data and versions gradually, separates rollout from deployment, records decisions, observes effects, and removes obsolete paths once migration evidence is complete.