Chapter 8: Modifiability – Designing for Change & Scalability
Welcome to Last Minute Lecture.
This free chapter overview is designed to help students review and understand key concepts.
These summaries supplement not replaced the original textbook and may not be redistributed or resold.
For complete coverage, always consult the official text.
Welcome back to the Deep Dive.
You know, before we get into the weeds of software architecture, let's maybe start with Charles Darwin.
He had this great insight.
Ah, the quote about survival.
Exactly.
It is not the strongest of the species that survive nor the most intelligent, but the one most responsive to change.
And that's not just biology, is it?
It's really the core mandate for, well, pretty much every software system out there today.
Absolutely.
So today we're tackling modifiability.
It's one of those crucial quality attributes you just can't ignore.
Right.
We're digging into the architect's playbook here, specifically the chapter focusing on the constant we all face.
Change.
Software always changes.
Yeah.
You're adding features, fixing bugs, tweaking the user experience, maybe bringing in new tech.
It's constant.
It really is.
And our goal today, our mission, if you like, is to break down how architects plan for that.
It's fundamentally about lowering the cost and importantly, the risk of making those changes over the system's life.
So for you, the listener, this is your path to understanding how to build systems that can actually adapt without getting buried under technical debt later on.
We're looking at the architect's strategy for the inevitable.
Precisely.
Okay.
So where does an architect start?
Planning.
Right.
Before we even get to specific tactics,
the source material lays out these four critical questions.
What are they?
Yeah.
These are foundational.
First, what can change?
And people often jump straight to functions, but it's much wider.
Like what?
Well, think about the platform, the hardware, the OS, or the environment, maybe new systems it needs to talk to, new protocols.
Even the quality attributes themselves might need to change,
like needing better performance, more reliability.
Oh, and capacity scaling up for more users.
That's a big one.
Okay.
That makes sense.
Yeah.
So once you know what might change, what's next?
Then comes the reality check.
What is the likelihood of the change?
Because let's face it, you can't architect for every single possibility under the sun.
Right.
You'd never ship anything.
Exactly.
You'd spend all your time and money preparing for things that might never happen.
So architects have to make some tough calls, focus on the changes they think are genuinely likely.
It's a bit of a bet, really.
A calculated risk.
Okay.
Third question.
This one's about process.
When is the change made and who makes it?
This is critical because the when dictates the kind of architectural support you need.
What do you mean by when?
Is the change happening when a developer writes source code or maybe later at compile time using switches or during the build, swapping out libraries or even later at configuration time via parameters or right at runtime, maybe using plugins.
And the who matters too.
Absolutely.
Is it a developer doing the change, an end user, a sys admin, or increasingly is it the system itself adapting?
Each of those implies different mechanisms.
Which leads perfectly into the final question, the bottom line.
What is the cost of the change?
Right.
And there are two sides to this cost coin.
First, the cost of putting the mechanism in place to allow the change, maybe buying or building some framework.
Like a UI builder tool or something?
Yeah, exactly.
And second, there's the cost of actually making the modification using that mechanism each time it happens, which for some self -modifying systems could actually be near zero per change, but the initial mechanism cost might be high.
Okay.
So how do you weigh those costs?
You mentioned a bet earlier.
There's a kind of simplified equation the source uses to think about this justification.
It goes something like none dollar times the cost without the mechanism should be less than or equal to the cost of creating the mechanism plus dollar times the cost using the mechanism.
Let me break that down.
N is the number of times you expect to make that kind of change.
Exactly.
N is your prediction.
Let's say you think you'll need to tweak some configuration 50 times over the system's life.
That's your N.
Okay.
So if doing it manually each time is super expensive, but building a config tool is a big one -time cost plus a small cost each time you use the tool.
Right.
If N is high, like 50, investing in that tool probably makes sense.
But if N turns out to be, say, only two - Then you over -engineer it.
You spend a lot upfront for very little payback.
Precisely.
That's the opportunity cost.
You
close is key.
It's about balancing that upfront investment against future pain.
So you're constantly walking this tightrope between enabling future change cheaply and not overspending on flexibility you might never need.
That's the architect's challenge in a nutshell.
Minimizing future technical debt without going broke on mechanisms for low probability changes.
Okay.
Let's dig into modifiability itself.
It's a big term.
The source breaks it down into different
flavors, right?
Starting with scalability.
Yeah.
Scalability is probably the most familiar.
It's just about handling more of something, usually by adding resources.
We talk about horizontal scalability that's scaling out.
Think adding more servers to a cluster.
Right.
And vertical scalability.
That's scaling up, making a single machine more powerful, adding more RAM, faster CPUs, that kind of thing.
And in the cloud world, we hear elasticity a lot.
Is that different?
It's essentially horizontal scalability, but with the added dimension of being dynamic and automatic in cloud environments.
The system can grab or release resources from a pool as NAID, responding to demand shifts.
Got it.
What other flavors are there?
Next up is variability.
This is about supporting a set of pre -planned variations of a system.
Think of a software product line.
Like having a base product and then slightly different versions for different customers.
Exactly.
You want to share as much engineering work as possible across those versions, but have mechanisms in configuration, maybe different modules, to handle the specific variations needed for each context.
Okay.
Then there's portability.
Right.
How easy is it to move the software from one platform, say, Windows, to another, like Linux?
The key here is usually minimizing and isolating those platform -specific dependencies.
Often using a virtual machine or some abstraction layer.
Yep.
That's a common approach.
Abstract away the platform detail so your main application code doesn't really care where it's running.
And the last one mentioned is location independence.
Crucial for distributed systems.
This means two components that need to talk to each other, don't need to know the physical or network location of the other one beforehand, or maybe those locations change dynamically at runtime.
So if a microservice moves, other services can still find it without manual reconfiguration.
That's the idea.
Makes the whole system much more resilient and, well, modifiable.
Okay.
These are all important aspects.
How do architects make sure they're thinking about them properly when designing for a specific change?
The source mentions the general scenario framework.
Ah, yes.
The general scenario.
It's basically a structured way to describe a modifiability requirement or analyze a potential change.
It forces you to be specific.
It has six parts.
Let's walk through them.
Okay.
First, the source.
Who or what is initiating the change?
Is it a developer?
An end user?
Maybe the system itself?
Then the stimulus.
What is the change?
Adding a feature, fixing a defect, changing performance needs, moving a service.
Third, artifacts.
What parts of the system are actually getting modified?
Is it source code, configuration files,
database schema,
specific components?
Fourth,
environment.
When does this change happen?
During development, design time, compile time, build time, deployment,
or while the system is actually running, runtime.
Fifth is the response.
What action needs to be taken?
Make the change, test it, deploy it, or maybe the system just modifies itself.
And finally, the crucial part.
Response measure.
How do you know if the change was successful and efficient?
Right.
This is where you quantify it.
How much effort did it take?
Person hours.
How much calendar time?
How much money?
And very importantly, did it introduce new bugs or side effects?
So using that structure, I could define a requirement really clearly.
Like, a developer, source needs to update the tax calculation logic, stimulus, affecting the billing module code artifacts at design time, environment.
This change must be developed, tested, and deployed.
Response.
Within one week, response measure.
With no new regressions, response measure.
Exactly.
It removes ambiguity and gives you concrete goals to design and measure against.
Okay.
So we know what to change, why, and how to measure it.
Now let's get into the nitty gritty, the actual engineering tactics.
The book grounds these in some pretty fundamental software design concepts from way back.
Right.
Coupling and cohesion.
Oh yeah, these are classics for a reason.
Let's start with coupling.
High coupling is, frankly, the arch nemesis of modifiability.
What is that?
Coupling measures how much one module depends on or knows about the internals of another.
If two modules are tightly coupled, a change in one almost inevitably forces a change in the other.
It creates this ripple effect.
The nightmare scenario where changing a button color somehow breaks the login process?
You joke, but I've seen things close to that.
High coupling means changes are risky, time consuming, and expensive because you have to trace and fix dependencies all over the place.
Tactics here focus on reducing that dependency, often by introducing intermediaries.
Okay, so low coupling is the goal.
What about cohesion?
Cohesion is the flip side, the positive quality we want.
It measures how well the responsibilities within a single module belong together, how focused that module is on a single purpose.
So high cohesion is good.
Very good.
If a module is highly cohesive, say, it handles all aspects of user authentication and nothing else, then a change related to authentication is likely contained entirely within that one module.
It doesn't spread out, makes changes much easier and safer.
Makes sense.
What else influences the ease of change?
The source mentioned size and binding time.
Right.
Size is pretty intuitive.
Bigger modules are just harder to understand and modify than smaller ones, but binding time is really interesting.
What do we mean by binding time here?
It's about when a decision or a value becomes fixed or bound into the system.
The key insight is the later you can successfully bind something, assuming the architecture is prepared for it, the cheaper the modification generally is.
Why later is cheaper?
Because work done later, especially by the computer at runtime, like reading a config file, discovering a service tends to be faster, cheaper and less prone to human error than work done earlier by humans, like changing source code, recompiling and redeploying.
Ah, okay.
So pushing decisions to runtime where possible increases flexibility and reduces cost.
That's the principle.
All right.
Let's tie these concepts to specific tactics.
The book groups them, right?
First, tactics to increase cohesion.
Yes.
Basically reorganizing responsibilities.
One tactic is split module.
If you have a big module doing too many unrelated things, low cohesion, you break it down into smaller, more focused, more cohesive modules.
But you have to split it intelligently, right?
Not just based on lines of code.
Absolutely.
The splur has to be based on logical responsibilities.
The other tactic is kind of the inverse.
Redistribute responsibilities.
Sometimes you find related functions scattered across multiple modules.
This tactic is about gathering them together into one place, either a new module or an existing one to increase cohesion.
Okay.
So those help create well -defined modules.
What about the connections between modules?
Tactics to reduce coupling.
The main idea here is restrict dependencies.
You actively limit which modules are allowed to talk to or depend on which other modules.
How do you enforce that?
Through things like visibility rules, making internal parts private or authorization mechanisms.
Layered architectures are a classic example.
A layer is generally only allowed to use the example,
hiding complex internals behind a simpler, restricted interface.
Got it.
And the last group of tactics focuses on that binding time idea.
Defer binding.
Yes, or late binding.
This is where the real flexibility often comes in.
The simplest mechanism is using parameters instead of hard -coded values.
So instead of coding tax rate 0 .08, you have tax rate, either read from config file?
Exactly.
That simple change shifts the binding of the tax rate value from compile time to, say, deployment time or even runtime.
And the source talks about different phases where binding can happen.
Right.
You can bind things at compile build time, maybe swapping components, or using conditional compilation.
You can push it later to deployment startup time, which is where configuration files, resource files, things an admin can change come in.
And the latest binding.
That happens at runtime.
Think service discovery, polymorphism in object -oriented languages, or dynamically interpreting parameters or scripts.
The really powerful idea here is externalizing the change, moving the control outside the source code so non -developers can make modifications.
Okay.
This is starting to build a picture.
These tactics aren't used in isolation, though.
They often come packaged together in established architectural patterns, right?
Absolutely.
Patterns are like known solutions that inherently use these tactics to achieve modifiability, among other qualities.
The classic one is client -server.
How does that help modifiability?
Well, the structure is a server offers services clients use them.
Right.
Crucially, the server usually doesn't know or care which specific clients are connecting.
There's discovery involved, then interaction.
This creates very low coupling between the server and its clients.
So the server can change or new clients can be added without breaking everything.
Exactly.
They can evolve independently as long as the service interface remains stable.
The big trade -off, of course, is often network latency and the complexity needed for security over the network.
Okay.
What's another pattern?
The plugin pattern, sometimes called microkernel.
Here you have a core system, the microkernel providing essential functions, and then specific features or variations are added via plugins.
Like extensions in a web browser.
Very similar idea.
The plugins talk to the core through fixed stable interfaces.
This gives you a controlled way to extend the system, maybe even allowing third parties to build plugins.
It keeps coupling low, provided those interfaces don't change much.
What's the downside there?
Security and privacy can be big concerns.
If you're running code from external sources as plugins, you need robust mechanisms to manage trust and permissions.
Makes sense.
How about the layers pattern?
Ah, layers.
A real workhorse.
You divide the software into distinct layers, each providing a set of cohesive services to the layer above it.
The absolute key constraint is that the allowed -to -use relationship is unidirectional.
Layers only talk downwards.
So the UI layer can talk to the business logic layer, but the business logic layer cannot talk directly to the UI layer.
Decisely.
This isolates changes.
If you need to change, say, the database, lowest layer, as long as the interface it provides to the layer above remains the same, the upper layers don't even know the change happened.
Great for portability and reuse, too.
But there are costs.
Performance can take a hit, as requests might have to pass through several layers.
And the biggest risk is developers taking shortcuts layer bridging, where they bypass intermediate layers to call deeper ones directly.
That totally undermines the modifiability benefits.
Right.
Discipline is key there.
One more pattern.
Publish, subscribe.
Yep.
Increasingly common, especially with microservices and event -driven architectures, components communicate indirectly by publishing messages, events, to some kind of central infrastructure, like an event bus.
Other components subscribe to the events they care about.
So the publisher has no idea who, if anyone, is listening.
Exactly.
It just shouts its event into the void, or rather, onto the bus.
This gives you incredibly loose coupling.
You can change system behavior just by changing who subscribes to what, or by modifying the events themselves.
Plus, it's easy to log events for auditing or replay.
Sounds great.
What are the trade -off?
Performance and latency can be harder to predict and manage, because communication is asynchronous and indirect.
Reasoning about the overall system flow can be difficult.
And things like end -to -end security become more complex when you have that intermediary bus.
Wow.
Okay.
So, wrapping this up, it seems modifiability isn't just one thing.
It's this constant challenge woven through all architectural decisions.
It really is.
It's about applying these tactics, reducing coupling, increasing cohesion, deferring binding, and using established patterns to manage that inevitable change, that technical debt, so the system can actually survive and adapt cost -effectively.
The ability to change is the ability to survive in software.
And here's something interesting to leave you, the listener, thinking about.
The source material draws a connection between client -server and the microkernel pattern.
It suggests client -server is basically microkernel, but with one key difference.
Ah, the binding time.
Exactly.
Client -server defers the binding of client -to -server all the way to runtime.
Think about how profound that shift is.
It really transforms the architecture.
Moving bindem from, say, build time, like typical plugins, to runtime fundamentally changes it from a single deployable unit into a dynamic, distributed system where components find each other on the fly.
That late binding is what enables so much of the flexibility we see on the internet today.
A fascinating perspective.
We really hope this deep dive into software modifiability helps you see systems not just as static things, but as living entities that need to be designed for continuous adaptation.
Thanks for joining us.
ⓘ This audio and summary are simplified educational interpretations and are not a substitute for the original text.
Using this chapter to study? Last Minute Lecture is free and student-run. If it helped, consider supporting the project.
Support LML ♥Related Chapters
- Cloud Computing – Architectural PrinciplesSoftware Architecture in Practice
- Reliable, Scalable & Maintainable ApplicationsDesigning Data-Intensive Applications
- Scale From Zero to Millions of UsersSystem Design Interview - An Insider's Guide (Volume 1)
- Ad Click Event Aggregation DesignSystem Design Interview - An Insider's Guide (Volume 2)
- Application LayerComputer Networking: A Top Down Approach
- Change and DevelopmentHuman Societies: A Brief Introduction