Managing complexity, as we saw in Managing Complexity, is the discipline of keeping a system understandable today. It is about drawing boundaries so that the cost of reasoning remains within our budget. But a system that is easy to understand today may still be prohibitively expensive to change tomorrow. Requirements evolve and business models pivot. If every such change requires reaching across boundaries and rewriting stable logic, the system has failed a fundamental engineering test.
Designing for change is the practice of building “extension points” into a system — locations where behavior can vary without breaking the contracts that surround them. The goal is not to predict the future, which is impossible, but to build a system that can absorb it.
An extension point is a place where we can alter behavior without editing the code that uses that behavior. In a well-designed system, extension points are placed at the points of highest likely volatility.
Consider a notification system. Today, it sends emails. Tomorrow, it might need to send SMS messages or push notifications. If the logic for “how to send an email” is hardcoded into the heart of the dispatch workflow, then adding SMS support requires modifying the dispatch workflow. The dispatch workflow is now coupled to the notification transport.
An extension point decouples them. By defining a stable contract — Notification_Transport.send(message) — the dispatch workflow can remain entirely ignorant of how the message is delivered. It depends on the interface of the transport, not its implementation. When a new transport is added, we don’t change the dispatch logic; we simply provide a different implementation of the extension point.
Good extension points isolate volatility. They allow the stable parts of the system (the “what”) to remain unchanged while the volatile parts (the “how”) evolve independently.
The backbone of a change-safe system is the stable contract. A contract is an agreement about behavior, inputs, and outputs. When a contract is stable, the code on either side of it can change freely as long as the agreement is honored.
The most dangerous kind of change is the “breaking” change — an alteration to a contract that forces every consumer of that contract to change as well. In a large system, a single breaking change at a low level can trigger a cascade of updates that consumes weeks of engineering time.
To minimize this cost, we adopt a discipline of additive evolution: - Prefer additive fields: When a data structure needs more information, add a new field rather than repurposing an old one. - Maintain deprecation windows: Give consumers time to migrate to new interfaces before removing old ones. - Provide compatibility adapters: If a contract must change, provide a layer that allows old clients to talk to the new implementation.
Without versioning discipline and contract stability, every “improvement” to the system becomes a migration crisis for the rest of the team.
Consider the requirement:
“We need to experiment with a new ranking algorithm for search results, but we must be able to switch back to the old one instantly if the metrics drop.”
Without an extension point, the ranking logic is likely embedded in the search service. Switching algorithms means a code change and a risky deployment.
With an extension point, we design for variation:
Ranking_Strategy, with a single operation: rank(results: List[Document]).Legacy_Ranking and Experimental_Ranking, both adhering to the Ranking_Strategy contract.Ranking_Strategy at runtime. Which implementation it gets is decided by a configuration setting or a feature flag.The search service remains unchanged. Its contract is satisfied by any object that knows how to rank. We can now swap algorithms — to run an A/B test, or to roll back a failure — without touching the core search orchestration.
In Nex, we use classes and features to define these extension points. The require and ensure clauses make the contract explicit, ensuring that any new implementation of the extension point honors the same behavioral guarantees as the old one.
-- The Extension Point Definition
deferred class Ranking_Strategy
feature
rank(query: String, candidates: Array[String]): Array[String]
require
query_present: query /= ""
has_candidates: candidates.size > 0
do
ensure
results_match_input_size: result.size = candidates.size
end
end
-- Variant 1: Legacy
class Legacy_Ranking
inherit Ranking_Strategy
feature
rank(query: String, candidates: Array[String]): Array[String]
do
result := candidates
end
end
-- Variant 2: Modern (ML-based)
class Modern_Ranking
inherit Ranking_Strategy
feature
rank(query: String, candidates: Array[String]): Array[String]
do
-- Complex ranking logic...
result := candidates -- placeholder
end
end
-- The Consumer: Unchanged by variation
class Search_Service
create
make(strategy: Ranking_Strategy) do
this.strategy := strategy
end
feature
strategy: Ranking_Strategy
fetch_from_index(q: String): Array[String]
require
query_present: q /= ""
do
result := ["DOC:A", "DOC:B"]
ensure
has_candidates: result.size > 0
end
execute_search(q: String): Array[String]
require
query_present: q /= ""
do
let initial_docs: Array[String] := fetch_from_index(q)
result := strategy.rank(q, initial_docs)
ensure
has_candidates: result.size > 0
end
end
The deferred class Ranking_Strategy acts as the port. The Search_Service depends only on this abstraction. Whether the strategy is an instance of Legacy_Ranking or Modern_Ranking is a configuration detail. The search logic is protected from the volatility of the ranking algorithm.
Each running system has an obvious place for one.
| System | Extension point | What varies behind it | What stays unchanged |
|---|---|---|---|
| Delivery network | Routing_Policy |
The routing objective, such as shortest distance or minimum fuel. | The dispatch engine that executes the routes. |
| Knowledge engine | Document_Parser |
One parser per supported file format, such as PDF or Markdown. | The indexing pipeline, which depends only on the Parsed_Content contract. |
| Virtual world | Entity_Behavior |
Decision-making logic, which differs between, say, a “Player” and an “NPC”. | The physics engine and the simulation rules all entities share. |
The pattern is to find the part most likely to vary and put a stable contract in front of it, so that the rest of the system depends on the contract rather than on the variation.
Premature Abstraction. The most common mistake is building extension points for variations that never happen. Every extension point adds a layer of indirection and a small cost to reasoning. If you build five different “strategy” ports for logic that hasn’t changed in three years, you have wasted complexity budget. The remedy is to add extension points only when volatility is evidenced or highly probable.
Leaky Abstractions. An extension point is useless if the contract requires the caller to know about the implementation. If Ranking_Strategy requires the caller to pass database-specific credentials, the extension point has leaked infrastructure details into the domain. The remedy is to keep contracts focused on the intent of the operation, not the mechanics of the implementation.
The “Big Bang” Migration. Designing for change implies that the system will evolve. If a team introduces a new version of a service but provides no compatibility for old clients, they haven’t designed for change — they’ve designed for disruption. The remedy is to make compatibility a first-class architectural requirement, using adapters and versioned interfaces to bridge the gap.
Quick Exercise
Pick one part of your system that you expect to change in the next six months. Define the “Port” (the stable contract) that would allow that change to happen without affecting the surrounding code.
require and ensure conditions?Takeaways
The next chapter, Refactoring Without Fear, examines the practical discipline of refactoring — how to move a system from its current structure to a better one while proving that its behavior remains unchanged.