Balancing Coupling in Software Design Universal Design Principles For Architecting Modular Software Systems (Vlad Khononov)
Balancing Coupling in Software Design Universal Design Principles For Architecting Modular Software Systems (Vlad Khononov)
“Coupling is one of those words that is used a lot, but little understood.
Vlad propels us from simplistic slogans like ‘always decouple components’
to a nuanced discussion of coupling in the context of complexity and
software evolution. If you build modern software, read this book!”
“This book is essential for every software architect and developer, offering
an unparalleled, thorough, and directly applicable exploration of the
concept of coupling. Vlad’s work is a crucial resource that will be heavily
quoted and referenced in future discussions and publications.”
Vlad Khononov
The author and publisher have taken care in the preparation of this book,
but make no expressed or implied warranty of any kind and assume no
responsibility for errors or omissions. No liability is assumed for incidental
or consequential damages in connection with or arising out of the use of the
information or programs contained herein.
For information about buying this title in bulk quantities, or for special sales
opportunities (which may include electronic versions; custom cover
designs; and content particular to your business, training goals, marketing
focus, or branding interests), please contact our corporate sales department
at [email protected] or (800) 382-3419.
ISBN-13: 978-0-13-735348-4
ISBN-10: 0-13-735348-0
$PrintCode
Dedicated to everyone who kept asking when this book would finally be
published.
#AdoptDontShop
Contents
The first word, organic, stood out to me recently when a friend and
colleague used it to describe software architecture. I have heard and used
the word organic in connection with software development, but I didn’t
think about that word as carefully as I did then when I personally consumed
the two used together: organic architecture.
Think about the word organic, and even the word organism. For the most
part these are used when referring to living things, but are also used to
describe inanimate things that feature some characteristics that resemble life
forms. Organic originates in Greek. Its etymology is with reference to a
functioning organ of the body. If you read the etymology of organ, it has a
broader use, and in fact organic followed suit: body organs; to implement;
describes a tool for making or doing; a musical instrument.
So then, all kinds of concepts regarding software are quite organic in that
nonliving things are still “characterized” by aspects of living organisms.
When we discuss software model concepts using concrete scenarios, or
draw an architecture diagram, or write a unit test and its corresponding
domain model unit, software starts to come alive. It isn’t static, because we
continue to discuss how to make it better, subjecting it to refinement, where
one scenario leads to another, and that has an impact on the architecture and
the domain model. As we continue to iterate, the increasing value in
refinements leads to incremental growth of the organism. As time
progresses so does the software. We wrangle with and tackle complexity
through useful abstractions, and the software grows and changes shapes, all
with the explicit purpose of making work better for real living organisms at
global scales.
Sadly, software organics tend to grow poorly more often than they grow
well. Even if they start out life in good health, they tend to get diseases,
become deformed, grow unnatural appendages, atrophy, and deteriorate.
Worse still is that these symptoms are caused by efforts to refine the
software that go wrong instead of making things better. The worst part is
that with every failed refinement, everything that goes wrong with these
complexly ill bodies doesn’t cause their death. Oh, if they could just die!
Instead, we have to kill them and killing them requires nerves, skills, and
the intestinal fortitude of a dragon slayer. No, not one, but dozens of
vigorous dragon slayers. Actually, make that dozens of dragon slayers who
have really big brains.
That’s where this series comes into play. I am curating a series designed to
help you mature and reach greater success with a variety of approaches—
reactive, object, and functional architecture and programming; domain
modeling; right-sized services; patterns; and APIs. And along with that, the
series covers best uses of the associated underlying technologies. It’s not
accomplished at one fell swoop. It requires organic refinement with purpose
and skill. I and the other authors are here to help. To that end, we’ve
delivered our very best to achieve our goal.
Is balancing software coupling organic? Absolutely! Start with a sinkhole of
a repository where all goopy code has gone to collect as sludge. Triple yuk!
How can you possibly grow any new, bright life from the quagmire?
Simple. Start scooping and separating, add some good soil and nutrients,
build some modular containers around the mounds of enriched earth, and
start planting seeds in each—some common, some special, and even a few
exotics. Before you know it, poof, and there’s fresh life!
Well, sort of, but not exactly. You’ll have to learn about “software
gardening.” That includes soaking up the basic almanac of coupling: what
coupling is exactly; the bad and the good of it; how coupling relates to
system design and levels of complexity; and how modularity helps, of
course. After you are on solid ground, there’s a whole set of dimensions to
learn that will help you evaluate the environment for sustained growth:
strength, space, and time. There’s the introduction to module coupling and
connascence, which leads to Vladik’s own new model: integration strength.
This might flow like flood irrigation but keep gulping. What about distance
and how it plays into different crops being planted and nourished, and how
can cultivating and pruning one crop lead to positive and [or?] negative
impacts on another? It’s sprouting.
“Wait a minute, let me catch up,” you say? That’s a fitting response to how
time plays into planting rotations and potential volatility due to various
elements. All this requires balance to avoid the enemy of all software; that
is, growth over time. Examples of how other gardens have grown will help
your plantings to sustain life despite those harsh elements. And it works
because it’s all backed by decades of research and development by renown
software practitioners—umm, horticulturalists.
You are now ready to roll up your sleeves, open the spigot, and absorb. Get
to growing excellent software!
—Vaughn Vernon
Foreword
Successful software systems grow and evolve—to add new features and
capabilities, and to support new technologies and platforms. But is it
inevitable that over time they become unmaintainable “Big Balls of Mud?”
Well, given that complex software systems are structured out of modular
interrelated units of functionality, each with discrete responsibilities, there
will always be coupling. But the ways modules communicate and how they
share information have implications on our ability to change them.
In this book, Vlad, after informing us of the original design ideas of module
coupling and connascence, updates us with fresh ways to think about the
various dimensions of coupling: integration strength, volatility, and
distance. He then leads us on a journey to deeply understanding coupling
and offers a comprehensive model for evaluating coupling options when
designing or refactoring parts of a system. Most authors explain coupling in
a paragraph or a page. Vlad gives us an entire book.
There are various ways we can reduce coupling, and Vlad explains them.
Should we always look at coupling as something bad? Of course not. But
we shouldn’t be complacent either. The last section of Vlad’s book
introduces the notion of balanced coupling, and a process for thinking
through design implications as you “rebalance” the coupling in your
system.
Thanks, Vlad, for persisting in writing this comprehensive treatment of
coupling, balancing, and then rebalancing (design always involves trade-
offs). You provide us with a wealth of new and insightful ways to think
about structuring and restructuring complex systems to keep them working
and evolvable. This book gives me hope that in the hands of thoughtful
designers, software system entropy isn’t inevitable.
—Rebecca J. Wirfs-Brock
May 2, 2024
Foreword
At first as a programmer you don’t even know what the things are. You
learn about functions. You learn about types. You learn about classes and
modules and packages and services. You still haven’t learned to design. You
can make all the things, but you can’t design. Because design happens in
the cracks.
Design prepares change. The things, those are the change. Design makes
places for the new things, the functions and types and classes and modules
and packages and services.
What Vlad has done is catalog the cracks, the seams, the dark squishy in
between of software. If you want to not just make changes, but make
changes easy, this is the vocabulary you’ll need. The glossary. The
dictionary of cracks.
Vlad understands well that experts learn by doing and reflecting. The
review questions with each chapter are stepping stones to learning for those
willing to put in the work required to learn.
Since Vlad did me the honor of inviting me to invite you to read this book,
I’ll take a moment to complain about vocabulary. Vlad uses “integration
strength” to mean what I mean by “coupling”, the relationship between
elements where changing one in a particular way requires changing the
other. He uses “coupling” to mean a more general connection between
elements, at runtime or compile time. It’s not a huge deal but it’s important
for me to say.
Your software can get easier to change over time, but it’s hard work to make
that happen. With the concepts and skills you’ll gain from this book,
though, you will be well on your way.
—Kent Beck
2024
Preface
I had to write this book for the same reason I gave that conference talk. All
this knowledge that we have, but have forgotten, is far too important. So, if
you’re reading these words, it means that after long years of hard work, I
managed to finish this book before it could finish me. I wholeheartedly
believe that this material will be as useful to you as it has been to me.
Chapter 1, Coupling and System Design—In the first part of this chapter,
you will learn what systems are, how they are built, and the role coupling
plays in any system. The second part of the chapter switches the focus to
software systems and introduces the terminology that will be used to
describe coupling in the chapters that follow.
This book is grounded in practice: Everything you’ll read has been battle-
tested and proven useful across multiple software projects and business
domains. This real-world experience is reflected in the case studies you’ll
find in each chapter. Though I can’t divulge specific details about the
projects, I wanted to provide concrete case studies to make the material less
abstract. To do this, I transformed stories from the trenches into case studies
about a fictional company, WolfDesk. While the company is fictional, all
the case studies are drawn from real projects. Here’s a brief description of
WolfDesk and its business domain.
WolfDesk
WolfDesk uses a payment model that sets it apart from its competitors.
Rather than charging a fee per user, it allows tenants to set up as many users
as they need, charging based on the number of support cases opened per
billing period. There is no minimum fee, and automatic volume discounts
are offered at certain thresholds of monthly cases.
Haze Humbert is the book’s guardian angel or, more formally, its executive
editor. The work on this book lasted for four years, and I know I didn’t
make those years easy for Haze. Everything that could go wrong did, and
then some. Haze, you are among the most patient people I know. Thank you
for making this happen, and for your support in the moments when I needed
it most.
What a relief it was to finish writing this book! However, that was just one
battle. To win the war, it needed to be prepared for printing, and what an
array of curveballs this entailed. I want to thank Julie Nahil, the book’s
content producer, for being on my side and helping me get the book laid out
and formatted just as I envisioned.
Heartfelt thanks to the reviewers who were brave enough to read the book’s
early, unedited drafts, providing feedback that played a crucial role in
refining the manuscript: Ilio Catallo, Ruslan Dmytrakovych, Savvas
Kleanthous, Hayden Melton, Sonya Natanzon, Artem Shchodro, and Ivan
Zakrevsky.
Last but not least, I want to thank two special people from whom I’ve
learned so much, and it’s an immense honor to have them as foreword
authors: Rebecca J. Wirfs-Brock and Kent Beck. Thank you both for your
warm and inspiring words!
About the Author
That’s what you will learn. This book aims to achieve two main goals. The
first is to provide a comprehensive explanation of coupling and the various
ways it manifests in software design. Second, by learning to evaluate the
multidimensional forces of coupling, you will uncover the intriguing
possibility of turning the traditionally vilified concept of coupling into a
constructive design tool. This tool will help you guide your systems away
from complexity and toward modularity, embodying the principle of
balanced coupling.
Coupling
Before you can start using coupling as a design tool, it’s crucial to
understand the big picture. This part of the book discusses the goals to
achieve when designing a system, what to avoid, and the tools that are
available for steering the design.
Chapter 1 starts our exploration from the widest perspective: the role that
coupling plays in system design. You’ll learn what coupling really is and
why no system can function without it.
The term “coupling” is often used as a shorthand for poor design. When a
system’s structure actively resists changes we want to make, or when we are
trying to find our way out of a labyrinth of entangled dependencies, we
blame it on coupling. Not surprisingly, our natural tendency is to go ahead
and “decouple everything.” Whether they’re classes, modules, services, or
whole systems, we tend to break them apart so that the smaller components
will give us the freedom to implement and evolve each component
independently.
Imagine a system without any coupling at all. All of its components are
independent and interchangeable. Is it a good design? Would such a system
be able to achieve its business goals? To answer these questions, this
chapter starts our exploration of coupling in a rather general context: the
role that coupling plays in system design. You will learn what coupling is,
what is needed to make a system, and ultimately, the peculiar relationship
between coupling and system design.
What Is Coupling?
Whenever you encounter the word “coupled,” you can replace it with
“connected.” Saying that two services are “coupled” is the same as saying
they are “connected.” Similarly, “an object that is strongly coupled to a
database” is “an object that is strongly connected to a database.”
Magnitude of Coupling
Shared Lifecycle
The trivial reason for multiple components to have to change together is the
coupling of their lifecycles. Consider the two designs illustrated in Figure
1.2. Both describe two modules:1 Payments and Authorization . The
design in Figure 1.2A colocates the two modules in the same monolithic
system, whereas in the design in Figure 1.2B, the modules are located in
two different services: Billing and Identity & Access .
Judging solely from the information available in the figure, it’s safe to say
that the modules in Figure 1.2A have higher lifecycle coupling than those in
Figure 1.2B. Being colocated in the same monolithic application, the
modules have to be tested, deployed, and maintained together. On the other
hand, extracting the Payments and Authorization modules into
different services, as illustrated in Figure 1.2B, reduces their lifecycle
coupling and allows each to be developed and maintained more
independently of the other.
Figure 1.2 Colocating modules in the same encapsulation boundary (A) increases their lifecycle
coupling, whereas extracting the modules into different services (B) reduces their lifecycle coupling.
The way components share knowledge and how the knowledge propagates
across the system is one of the central themes of this book. In Part II,
Dimensions, you will learn three models for evaluating and categorizing
shared knowledge. The following section introduces a notation that I will
use in subsequent chapters to describe the flow of knowledge in a system.
Flow of Knowledge
In one way or another, almost all future chapters discuss the notion of flow
of knowledge in system design, albeit from different angles. To discuss it
effectively, I want to establish a shared language for describing how
components share knowledge.
Systems
In the previous sections, you saw that coupling is ubiquitous. You can
observe it everywhere: in cars, organisms, celestial bodies, and so on. The
common denominator in all these examples is that all of them are systems.
To understand the role that coupling plays in systems, it is essential to
define what a system is. In her classic book Thinking in Systems: A Primer,
Donella H. Meadows (2009) defines a system as an interconnected set of
elements organized in a way that achieves something. This succinct
definition describes the three core elements constituting any system:
components, interconnections, and a purpose.
Furthermore, you can delve deeper into the hierarchical nature of software
systems. At its core, a class can be regarded as a system, with its
components comprising methods and variables that implement the class’s
functionality. Taking it a step further, a method can be seen as a system in
itself, consisting of distinct statements that collectively fulfill the method’s
purpose. This hierarchical structure of software systems is another motive
of this book, which will be revisited in forthcoming chapters and will
culminate in Chapter 12, Fractal Geometry of Software Design.
What is needed to make the components at all levels work together and
achieve the goals of the overarching system? Interactions.
Coupling in Systems
(Image: Photocell/Shutterstock)
Coupling is the glue holding systems together. Does this mean that blindly
introducing relationships and interdependencies will result in a good
design? Of course not. Coupling can be essential or accidental. Modular
design requires eliminating accidental coupling while carefully managing
the essential interrelationships. In the forthcoming chapters, you will learn
to tell the two apart and how to use the right coupling to make systems
simpler and more modular. But first, let’s explore the concepts of
complexity and modularity and their tight relationship with coupling.
Key Takeaways
Quiz
1. What is coupling?
2. What is a system?
1. A software solution
2. A hardware appliance
3. Any set of components working together to implement a defined purpose
4. All of the answers are incorrect.
4. Look for examples of systems that you are interacting with in your day-
to-day life. How are these systems’ components integrated? Can you spot
the different ways the components share knowledge across their
boundaries?
Chapter 2
Chapter 1 defined the concept of coupling and its role in system design.
Despite its unfavorable perception, coupling is needed to hold systems
together. If that’s the case, however, what is the force that leads systems to
become disorganized and unmanageable? That force is complexity.
What Is Complexity?
Complexity, like coupling, is an essential part of our lives. We use the term
“complexity,” and variations of it, in a wide variety of domains. We
describe the behavior of financial markets as complex, as well as the
behavior of ecosystems, social and transportation systems, and others.
Complexity is everywhere. It is so ubiquitous that Stephen Hawking (2000)
proclaimed that we are living in the century of complexity. But what does it
mean for something to be complex? Complexity is a concept that we
understand intuitively, but it can be challenging to define it precisely.
Complexity Is Subjective
1. Pronounced kuh-nev-in. Or, as Dan North put it, say “Kevin” and sneeze
halfway through (https://twitter.com/tastapod/status/979390926903742465).
Clear2
In the clear domain, the effects of an action impacting a system are evident
and predictable; you know precisely what the outcomes will be. Thus,
decision making within this domain is straightforward.
Typically, when working in the clear domain, rules or best practices guide
the decision-making process. Hence, according to the Cynefin framework,
making a decision in the clear domain follows the sense–categorize–
respond approach:
1. Sense: Gather the available information and establish the relevant facts.
2. Categorize: Use the facts to identify the relevant rule or best practice.
3. Respond: Follow the chosen rule or best practice.
Complicated
1. Sense: Gather the available information and establish the relevant facts.
2. Analyze: Identify the missing knowledge and consult an expert in the
relevant field.
3. Respond: Follow the expert’s advice.
The example of car trouble that I used at the beginning of the chapter
belongs to the complicated domain:
You may have noticed that earlier in the chapter I used the same example of
car trouble to demonstrate the notion of complexity. Here, however, I used
it to demonstrate Cynefin’s complicated domain rather than a complex
domain. Let’s see what distinguishes complicated from complex.
Complex
Chaotic
As you learned in the preceding section, you cannot predict the result of an
action in a complex domain; you can only deduce it in retrospect. The
chaotic domain is where things go out of control and all hell breaks loose:
An action’s outcome cannot be predicted at all.
Since you can neither consult an expert nor conduct an experiment (it’s
either impossible or the results are random), chaotic domains are too
confusing for knowledge-based responses, according to the Cynefin
framework. Instead, you have to trust your instincts and commit any action
that makes sense at the moment. The goal of the action is to transform the
situation from chaotic to complex. Only when you are out of danger can
you assess the situation and plan the next steps:
1. Act: Commit any action that makes sense and can potentially help you to
get out of danger.
2. Sense: Gather the available information on the results of the action.
3. Respond: If you are still in danger, commit another action; plan
knowledge-based responses only when you are out of danger.
Disorder5
The key difference among the clear, complicated, complex, and chaotic
domains of the Cynefin framework lies in the cause-and-effect relationships
between decisions (or actions) and their outcomes:
Table 2.1 summarizes the key differences between the first four domains of
the Cynefin framework: clear, complicated, complex, and chaotic.
Table 2.1 The Key Differences Between the First Four Domains of the Cynefin Framework
Cause-and-
Required
Domain effect Required action
knowledge
relationship
Assume you are working on the WolfDesk system. You need to send SMS
messages notifying customers about changes in their support cases. Instead
of implementing the functionality from scratch, you’re considering
integrating an existing solution. Let’s call it NotificationMaster .
To which Cynefin domain does this integration belong? As the saying goes,
the devil is in the details, and the provided description doesn’t offer the
details needed to identify the relevant domain. Let’s examine four possible
scenarios.6
6. From now on, I’m going to focus on Cynefin’s first four domains: clear,
complicated, complex, and chaotic.
Clear
Complicated
This time, neither the API nor its documentation specifies in what format
you should supply phone numbers. A string can contain a phone number in
local or international format.
Complex
Let’s assume the same method signature as in the preceding section (Figure
2.3). This time, however, there is no one you can consult: The
NotificationMaster component is part of a legacy system, and
currently, no one in your company knows how it works. Moreover, you
can’t examine the component’s source code. The only way to find out the
integration format is through trial and error: Try providing phone numbers
in different formats and see which one works.
Finally, consider the same signature as in the previous two sections: The
module belongs to a legacy system, and you’re trying to find out the correct
data format through trial and error. However, in this case, one time the
method works with local numbers but fails with international numbers, and
another time it fails with local numbers but works with international
numbers. The behavior is inconsistent and unpredictable.
In this case, the integration scenario belongs to the chaotic domain: There is
no relationship between the cause and effect. The outcome of your design
decisions is random and depends on the specific server that was chosen by
the load balancer.
Following the Cynefin framework, in such situations you have to follow
your instincts and act. In this case, it’s probably going to be to avoid using
NotificationMaster altogether.
Clear
Complicated
Now consider that your microservice is no longer the sole user of the
database. Instead, you know that another team uses some of its data (Figure
2.6).
As a result, you can no longer be confident about the effects of the changes
you want to make. To avoid potentially negative impacts on the
performance of some queries, you need to discuss the planned changes with
the other team. That makes the decision of changing the indexes belong to
the complicated domain.
Complex
The need to “probe” the proposed changes before making the final decision
categorizes this scenario into the complex domain.
Chaotic
Consider that this time you are not aware that another team is using the
database you are working on. Moreover, there are no automated tests that
can shed light on the unintended effects of the changes you want to make.
After verifying the new indexes’ effects on your microservice, you deploy
the changes to production. A few minutes later, an unrelated component of
the system starts to fail with timeout errors.
Because you are in the chaotic domain, you have to act to move out of the
danger zone. Trusting your instincts that the outage might be related to your
recent deployment, you decide to roll back the changes.
Cynefin Applications
If you can consult an expert to whom it’s clear how the system works
and how different decisions will affect its behavior, you are dealing with
a complicated system, not a complex system.
Similarly, if there is no consistent relationship between actions
performed against the system and their outcomes, that’s not complexity
either. That’s chaos.
7. That said, if you look into the origins of the words “complicated” and
“complex,” you will see that their original meanings are not synonymous.
“Complicated” comes from the Latin word “compilcare,” meaning folded
together, whereas “complex” comes from the Latin word “complexus,”
meaning an aggregate of parts.
Now that you have a deeper understanding of what complexity entails, let’s
explore its causes and how it arises. Is it the size of a system, or the way it
was designed? That’s the topic of the next chapter.
Key Takeaways
Quiz
1. Assuming that you are not a watch repairer and your watch has stopped
working, to what Cynefin domain does repairing your watch belong?
1. Clear
2. Complicated
3. Complex
4. Chaotic
1. Clear
2. Complicated
3. Complex
4. Chaotic
3. Which Cynefin domain reflects a game of chess, but one in which you
are allowed to cheat and use a computer?
1. Clear
2. Complicated
3. Complex
4. Chaotic
1. Complicated
2. Chaotic
3. Complex
4. Such a situation is not possible.
1. Clear
2. Complicated
3. Complex
4. Chaotic
Chapter 3
Linear Interactions
Linear interactions are clear and predictable. Such interactions make the
dependencies between the components obvious, and the cause-and-effect
relationships in the system’s behavior are clear. In other words, if you
introduce a change in one component, it is clear how the change is going to
affect other parts of the system, as long as the interactions between them are
linear.
(Image: Besjunior/Shutterstock)
People with tacit knowledge leave the organization, until none of the
remaining staff can make sense of a system.
A system is plagued with accidental complexity. The team introduces
tools and techniques because they are trendy, and not because they are
really needed.
A team inherits a codebase that was built in a technology stack it has no
experience with.
Unintended Results
Complex interactions can achieve their goals, yet still lead to unintended
results. The unintended results are accidents and system failures. Such
interactions are quite common:
Hierarchical Complexity
Therefore, neither local nor global complexity is more important than the
other. Both forms of complexity must be addressed. Let’s see what happens
when only one type of complexity is being managed.
Let’s assume you are dealing with a complex system and are interested in
managing the complexity of interactions between its components—its
global complexity. Doing so is simple. All you have to do is merge all the
system’s components into one monolithic component (Figure 3.3). Now that
the system consists of a single component, its global complexity is
minimized; there are no cross-component interactions, because it only
consists of one component. On the other hand, the local complexity of the
sole component skyrockets!
Figure 3.3 A naive attempt to reduce the global complexity of a system (A) by merging all of its
components into a single monolithic system (B). Focusing solely on the global complexity results in
high local complexity.
The team decides to limit the microservices codebases4 to no more than 100
lines of code. The reason behind the limitation is that the “micro” codebases
will be easy to comprehend and, thus, to change and evolve.
Balancing Complexity
Both local and global complexities arise from the interactions between
components—their coupling. Failing to address forms of complexity
increases the risk of one of the complexities spinning out of control, and
thereby leading to an overall complex system.
In Chapter 10, Balancing Coupling, you will learn to balance both local and
global complexities by adjusting the way components are coupled and share
knowledge. For now, let’s get back to the notion of complex interactions
and explore a metric that can signal their presence: degrees of freedom.
Degrees of Freedom
Although the term has a positive connotation—after all, who doesn’t want
more freedom?—an excessive number of degrees of freedom might lead to
negative outcomes. As you will see, an increase in degrees of freedom often
results in more complex interactions. But first, let’s see an example that
illustrates the concept of degrees of freedom within the context of software
design.
Consider a square and a rectangle. A square can be defined with only one
variable: the length of its edge. Therefore, it has only one degree of freedom
(Listing 3.1).
struct Square {
// One degree of freedom
int Edge;
}
On the other hand, a rectangle requires two values to describe its state:
height and width. Hence, it has two degrees of freedom (Listing 3.2).
struct Rectangle {
// Two degrees of freedom
int Width;
int Height;
}
Figure 3.5 Replicating data across multiple storage mechanisms increases the system’s degrees of
freedom.
The same holds true for the example of replicated data (Figure 3.5). Having
the data duplicated in multiple databases introduces the possibility of the
data not being in sync, and therefore some components of the system
having to work with stale information.
class Triangle {
public int EdgeA;
public int EdgeB;
public int EdgeC;
}
class Triangle {
public int EdgeA { get; private set; }
public int EdgeB { get; private set; }
public int EdgeC { get; private set; }
EdgeA = edgeA;
EdgeB = edgeB;
EdgeC = edgeC;
}
}
Constraints in Cynefin Domains
The Cynefin domains are tightly related to the system’s constraints as well.
Order in a system, and its future outcomes, are predictable as long as the
system has constraints and its constraints can be sustained (Snowden 2020).
Therefore, the presence or absence of relevant constraints necessitates
different decision-making processes.
Constraints in the clear and complicated domains are present and bind the
causes to their effects. There are fewer constraints in the complex domain,
and this is the reason for loose coupling between causes and effects.
Ultimately, there are no constraints in the chaotic domain.
Therefore, the fewer constraints you can work with, the less certainty you
have about cause-and-effect relationships, and as a result, you face greater
complexity. Constraints are needed to tame complex interactions.
Fortunately, we have a design tool that defines how a system’s components
are integrated and how they are allowed to interact with each other. That
design tool is coupling.
Figure 3.7 The repository object described in the example aims to encapsulate the physical database
from other application components using it.
Design A: Using SQL to Filter Support Cases
Notice the design of the Query method: It allows the caller to specify a
SQL Where clause to filter the returned support cases. For example, the
following code will query for support cases that belong to a specific tenant
and were opened more than three months ago (Listing 3.6).
Listing 3.6 Using a SQL Where Clause to Query Support Cases That
Belong to a Specific Tenant and Were Opened More Than Three Months
Ago
On the one hand, such a design provides a very flexible solution for the
application developers to query support cases. On the other hand, in
Chapter 1 you learned that the ways components are coupled define the
knowledge that is shared across their boundaries. Let’s analyze the
knowledge that this design exposes:
Furthermore, assume that due to the success of the WolfDesk system, the
team decides to switch to a more scalable database engine. Given the need
to use SQL for queries, the team decides to switch to the Cloud Spanner
database. Although that database does support SQL, the team discovers that
its SQL dialect is different from the one used by the application, and much
of the querying code has to be rewritten.
The common thread in both examples is that the repository interface
permits the sharing of extraneous knowledge in the form of explicit
(matching property and column names) and implicit (SQL dialect)
assumptions. Failing to meet these assumptions is likely to cause
unexpected results and, thus, complex interactions.
Listing 3.7 Support Case Repository Interface That Uses a Query Object to
Filter Support Cases
Listing 3.8 Using a Query Object to Filter Support Cases That Belong to a
Specific Tenant and Were Opened More Than Three Months Ago
That said, this solution still fails to encapsulate some of the assumptions
made by the original design:
Database schema: The criteria used by the Query object still depend on
knowing the names of the database columns; thus, they assume that the
names of the SupportCase object’s properties and the corresponding
table’s columns are in sync.
Database indexes: The solution provides a flexible way to construct
database queries, still allowing the risk of missing an index and causing
high database load.
Now, the column names are encapsulated within the finder methods,
eliminating the need for consumers to be aware of them. Furthermore,
having the explicit list of supported queries makes it easier to configure
database indexes to ensure optimal performance.
Let’s analyze the three coupling options from the perspectives of degrees of
freedom and constraints.
The design that allows filtering support cases by specifying a SQL query
maximizes the range of possible querying options and the states of the
system—its degrees of freedom. It includes the states of errors and outages,
as demonstrated in the previous sections.
The Query object–based design limits the shared knowledge and helps to
avoid some of the possible issues by introducing additional constraints; for
example, by using a SQL dialect that is not supported by the actual
database. However, it still makes it possible for an ineffective query to
cause an outage, as that degree of freedom is still open.
Finally, from a functionality perspective, the third design imposes the most
constraints on the application’s ability to query support case data: Only a
limited set of queries is supported. Correspondingly, these constraints also
reduce the system’s degrees of freedom and, thus, the chances of complex
interactions.
As a result, the way components are integrated, the interface they use for
communication, and the assumptions the interface makes on the
environment directly impact the complexity of the overarching system.
5. You could argue that this case can fall into other Cynefin domains as
well. If it’s obvious that “something will break,” then it’s in the clear
domain. If you want to know what will break, then you have to conduct
experiments, which puts you in the complex domain. Ultimately, if things
break randomly without any relationship to the changes made, that’s the
chaotic domain.
Key Takeaways
The complexity of a system isn’t just about its size or the number of
components; it’s about the interactions between those components. When
making a design decision, consider whether it results in linear or complex
interactions. Try to detach yourself from the current context, and think
ahead: If you had to maintain the codebase years from now, would you be
able to predict the system’s behavior and understand the impact of changes
to any affected components? How likely would you be to resort to
experimentation?
Quiz
1. Size
2. Number of system components
3. Interactions between components
4. All of the answers are correct.
1. Local complexity
2. Global complexity
3. Both local and global complexity are equally important.
4. Both local and global complexity are equally unimportant.
5. This chapter emphasizes that not all constraints are desirable in system
design. What should be the main focus instead?
“95% of the words are spent extolling the benefits of modularity, and little,
if anything, is said about how to achieve it” (Myers 1979). These words
were written over 40 years ago, but the observation remains true to this day.
The significance of modularity is unquestionable: It is the cornerstone of
any well-designed system. Yet, despite the many new patterns, architectural
styles, and methodologies that have emerged over the years, attaining
modularity is still a challenge for many software projects.
The topic of this chapter is modularity and its relationship to coupling. I’ll
start by defining what modules are and what makes a system modular. Next,
you will learn about design considerations that are essential for increasing
the modularity of a system and avoiding complex interactions. Ultimately,
the chapter discusses the role of cross-component interactions in modular
systems, which paves the way for using coupling as a design tool in the
chapters that follow.
Modularity
Not only is the notion of modularity not unique to software design, but the
term “module” predates software design by about 500 years. At its core,
modularity refers to systems composed of self-contained units called
modules. At the same time, you may recall that in Chapter 1, I defined a
system as a set of components. This naturally raises an intriguing question:
What distinguishes the components of a traditional system from the
modules of a modular system?
Modules
Let’s consider two examples of modules from our everyday lives (Figure
4.1):
LEGO Bricks
Camera Lenses
Software Modules
Four years later, in their book Structured Design, Edward Yourdon and
Larry L. Constantine (1975) described a module as “a lexically contiguous
sequence of program statements, bounded by boundary elements, having an
aggregate identifier.” Or, in simpler terms, a module is any collection of
executable program statements meeting all of the following criteria (Myers
1979):
Throughout this book, I’ll use the term “module” to signify a boundary
enclosing specific functionality. This functionality is exposed to external
consumers and either is or has the potential to be independently compiled.
Function, Logic, and Context of Software Modules
Function
Logic
Context
Effective Modules
When I came on the scene (in the late 1960s) software development
managers had realized that building what they called monolithic systems
wasn’t working. They wanted to divide the work to be done into parts
(which they called modules) and each part or module would be assigned to
a different team or team member. Their hope was that (a) when they put the
parts together they would “fit” and the system would work and (b) when
they had to make changes, the changes would be confined to a single
module. Neither of those things happened. The reason was that they were
doing a “bad job” of dividing the work into modules. Those modules had
very complex interfaces, and changes almost always affected many
modules. —David L. Parnas, personal correspondence to author (May 3,
2023)
Modules as Abstractions
For an abstraction “to work,” it has to eliminate details that are relevant to
concrete cases but are not shared by all. Instead, to represent multiple things
equally well, it has to focus on aspects shared by all members of a group.
Going back to the previous example, the word “car” simplifies our
understanding by focusing on the common characteristics of all cars, such
as their function of providing transportation and their typical structure,
which often includes four wheels, an engine, and a steering wheel.
Note
interface CustomerRepository {
Customer Load(CustomerId id);
void Save(Customer customer);
Collection<Customer> FindByName(Name name);
Collection<Customer> FindByPhone(PhoneNumber phon
}
A concrete implementation of the repository could use a relational database,
a document store, or even a polyglot persistence–based implementation that
leverages multiple databases. Moreover, this design allows the consumers
of the repository to switch from one concrete implementation to another,
without being affected by the change.
The notion of effortlessly switching from one database to another often has
a somewhat questionable reputation within the software engineering
community. Such changes aren’t common.5 That said, there’s a more
frequent and crucial need to switch the implementation behind a stable
interface. When you’re altering a module’s implementation without
changing its interface, such as fixing a bug or changing its behavior, you’re
essentially replacing its implementation. For example, the kinds of queries
used in the FindByName() and FindByPhone() methods can be
changed even when retaining the use of the same database. It could be that
an index, name, and phone number are added to the database schema itself.
Or it could be that the data is restructured to better optimize queries.
Neither of these changes should impact the client’s use of the module
interface.
5. With the exception of running a suite of unit tests that replace a physical
database with an in-memory mock.
That said, the possibility of switching an implementation is not the only
goal of introducing an abstraction. As Edsger W. Dijkstra (1972) famously
put it, “The purpose of abstraction is not to be vague, but to create a new
semantic level in which one can be absolutely precise.”
6. Or layers of abstraction.
Deep Modules
addTwoNumbers(a, b) {
return a + b;
}
That said, the metaphor of deep modules has its limitations. For instance,
there can be two perfectly deep modules implementing the same business
rule. If this business rule changes, both modules will need to be modified.
This could lead to cascading changes throughout the system, creating an
opportunity for inconsistent system behavior if only one of the modules is
updated. This underscores the hard truth about modularity: Confronting
complexity is difficult.
Modularity Versus Complexity
In the beginning of the chapter, I defined modular design as one that allows
the system to adapt to future changes. But how flexible should it be? As
they say, the more reusable something is, the less useful it becomes. That is
the cost of flexibility and, consequently, of modularity.
When designing a modular system, it’s crucial to watch for the two
extremes: not making a system so rigid that it can’t change, and not
designing a system to be so flexible that it’s not usable. As an example, let’s
revisit the repository object in Listing 4.1. Its interface allows two ways of
querying for customers: by name and by phone number. What would that
interface look like if we were to attempt to address all possible query types;
for example, by a wider range of fields, or even by looking up customers
based on aggregations of values? That would make the interface much
harder to work with. Moreover, optimizing the underlying database to
efficiently handle all possible queries wouldn’t be trivial.
Coupling in Modularity
Key Takeaways
When designing modules, reason about their core properties. Can you state
a module’s function (purpose) without revealing its implementation details
(logic)? Is a module’s usage scenario (context) explicitly stated, or is it
based on assumptions that might be forgotten over time?
Quiz
1. Runtime performance
2. Maximizing the complexity it encapsulates
3. Maximizing the complexity it encapsulates while supporting the system’s
flexibility needs
4. Correct implementation of the business logic
1. Function
2. Logic
3. Context
4. Answers B and C are correct.
1. Services
2. Namespaces
3. Classes
4. All of the answers are correct.
5. What makes an effective abstraction?
Dimensions
Part I showed that the way components of a system are coupled makes the
overarching system modular or complex. To steer the design in the right
direction, it’s essential to understand the diverse ways in which coupling
affects the system. To this end, Part II delves into manifestations of
coupling in three dimensions: strength, space, and time.
Chapter 5 explores coupling from its inception. You will learn about the
first model that was introduced to evaluate coupling: structured design’s
module coupling.
Given the age of this methodology, introducing these concepts, let alone
applying them in practice, presents some challenges. To make this more
relatable, I’ll discuss the levels of structured design’s coupling within their
original context. Some of the chapter’s examples will be implemented in
programming languages such as assembly, COBOL, Fortran, and PL/I. Of
course, there’s no need for you to master these languages; I have simplified
the examples so that everyone can comprehend them, regardless of their
familiarity with the languages. In addition to the original historical
perspective, I’ll also bring these levels of coupling into a modern context,
using examples that are closer to our current technological reality.
Furthermore, to reinforce the idea that the dynamics of coupling can be
observed at all levels of abstraction, the examples in this chapter illustrate
the discussed concepts in the context of both in-process communication and
distributed systems.
Note
Structured Design
These are also the days when the term “software crisis” was introduced
(Naur and Randell 1969). That term highlighted the challenges faced in
creating useful and efficient computer programs within the constraints of
time. It sounds quite relatable, doesn’t it? It’s fascinating to see how much
has changed since then. The software industry has evolved substantially,
and we continually improve our tools, techniques, and methodologies. Yet,
half a century later, we still find ourselves grappling with the same
challenges (Standish 2015).
Module Coupling
The structured design methodology aimed to achieve the same goals we are
chasing today: designing cost-effective, more reliable, and more flexible
software by designing modular systems. One of the methodology’s core
methods for designing toward modularity involves assessing the nature of
interrelationships and integrations between a program’s modules. For that,
structured design proposes a model called module coupling that describes
six levels of interconnectedness: content, common, external, control, stamp,
and data coupling.
Content Coupling
01 ROUTINE MAIN
...
...
12 JUMP TO COMP + 18
...
...
20 END ROUTINE MAIN
21 ROUTINE PROCESS
...
...
35 COMP:
...
...
...
53 MOVE 0 TO REGISTER B
...
72 END ROUTINE PROCESS
Luckily, such death-defying stunts are much harder to pull off nowadays.
That said, today we have other ways of content-coupling modules. For
example, consider the code in Listing 5.2. The DoSomething method
uses reflection to execute the private method called VerifyInput that
belongs to the InvoiceGenerator object.
var t = typeof(InvoiceGenerator);
var privateMethod = t.GetMethod("VerifyInput",
BindingFlags.NonP
BindingFlags.Inst
Consider again the example in Listing 5.1. Changing the code in line 53, or
even simply adding a line before it, would in the best case break the
integration and in the worst case lead to incorrect behavior of the system.
Common Coupling
Two modules are common-coupled if they are using a globally shared data
structure. All of the common-coupled modules can read and change the
values stored in the global memory.
This level of coupling is named after the COMMON 1 statement in the Fortran
language. Consider the source code in Listing 5.3, which defines three
subroutines: SUB1 , SUB2 , and SUB3 (lines 01, 11, and 31). Instead of
passing arguments, the subroutines use the COMMON statement to define
their shared data consisting of four variables: ALPHA , BETA , GAMMA , and
DELTA (lines 06, 16, and 36). Even if one of the subroutines is interested
in working with only one of the variables, it still has to declare and
reference the whole block. Consequently, if one of the subroutines has to
change the type of one of the variables, or add another variable, the change
will affect all of the common-coupled subroutines.
1. See https://web.stanford.edu/class/me200c/tutorial_77/13_common.html
for an example from Stanford University.
01 SUBROUTINE SUB1 ()
05 REAL ALPHA, BETA, GAMMA, DELTA
06 COMMON /VARS/ ALPHA, BETA, GAMMA, DELTA
...
09 RETURN
10 END
11 SUBROUTINE SUB2 ()
15 REAL ALPHA, BETA, GAMMA, DELTA
16 COMMON /VARS/ ALPHA, BETA, GAMMA, DELTA
...
19 RETURN
20 END
...
31 SUBROUTINE SUB3 ()
35 REAL ALPHA, BETA, GAMMA, DELTA
36 COMMON /VARS/ ALPHA, BETA, GAMMA, DELTA
...
39 RETURN
40 END
2. No pun intended.
Figure 5.1 Common coupling through reading and writing to a file in globally accessible object
storage
The fact that components that are using a shared memory space for
integration are considered common-coupled begs the question: Why isn’t
that a case of content coupling? In a sense, the shared memory is an
implementation detail of all participating modules. And as we previously
discussed, using another module’s implementation details for integration is
considered content coupling. The intent, however, is different in the two
cases.
For content coupling, the offending module ignores the upstream module’s
public interface and integrates through its private implementation details.
Conversely, in the case of common coupling, the integration through a
shared memory space (persistent or transient) is a joint effort. All the
common-coupled modules are making the conscious decision to integrate in
such a way, whereas in the case of content coupling, the upstream module
didn’t approve, or possibly isn’t even aware, of the trespassing of its
boundaries.
External Coupling
01 ProcA: procedure;
02 declare A fixed decimal (7,2) external;
...
...
...
10 end ProcA;
11 ProcB: procedure;
12 declare A fixed decimal (7,2) external;
...
...
...
20 end ProcB;
class ClassA {
public static string Name { get; set; }
}
class ClassB {
public void SetName(string name) {
ClassA.Name = name;
}
}
class ClassC {
public void Greet() {
Console.WriteLine($"Hello, {ClassA.Name}!");
}
}
The major advantage of external coupling over common coupling is that the
amount of data that is shared between the modules is reduced; only the data
that is actually needed for integration is being shared. This makes the
integration through external coupling more explicit and easier to
understand. In addition, externally coupled modules are more stable, as less
data is shared, and thus, fewer degrees of freedom are exposed across the
integrated modules’ boundaries. Consequently, the changes in the modules’
implementation details will have a smaller chance of spreading across the
modules’ boundaries. That said, external coupling still shares the majority
of the negative effects introduced by common coupling.
Control Coupling
sendNotification(notificationType, message);
}
Control coupling makes the consumer aware of the internal structure of the
upstream functionality. Sharing this kind of knowledge introduces stronger
dependencies between the connected modules and can potentially lead to
complex interactions. Assume, for example, that a new version of
sendNotification no longer supports push notifications. Introducing
such a change requires going over all of the consumers’ code and making
sure that the push value is no longer being used; failing to do so will
result in runtime exceptions.
You may argue that replacing the string with an enumeration type can
greatly help in identifying such compatibility issues and avoiding runtime
failures, and you would be correct. In the heyday of languages such as
Fortran and COBOL, there were no native enumeration types. Instead,
numeric values were passed, making it much easier to make an integration
mistake. Nowadays, such an integration method is much less error prone.
However, the underlying design issues are still relevant.
The presence of control coupling signals that the upstream module fails to
encapsulate its logic. As a result, changes in the way one module handles its
control parameters may require changes in all the modules that call it. Such
cascading changes can make maintenance more difficult and the system less
flexible.
It cannot control its execution flow, and the corresponding logic has to
be implemented by the modules it integrates with.
The controlled module’s boundaries are suboptimal in terms of the
knowledge it shares. Instead of describing only the business problem the
module is supposed to solve—the module’s function—its public
interface also reveals its implementation details—the module’s logic.
Moreover, often control coupling imposes additional constraints on the
module’s context—again, all symptoms of suboptimal abstractions.
Consider the two modules described in Listing 5.7. At first, the code looks
innocent: The Analysis module calls the CRM module’s repository to
fetch an instance of a customer. However, at line 16 you can see that the
Analysis module is not interested in the whole Customer object but
only in the value of its Status field. What if that Customer object
consists of hundreds of different fields, and all of them are exposed to the
external module? That would limit the CRM module’s ability to change this
data structure in the future, as its authors have no choice but to assume that
whatever data is exposed through the public interface is potentially being
used by some of the downstream modules.
01 namespace Example.CRM {
02 public class CustomersRepository {
03 ...
04 public Customer Get(Guid id) {
05 ....
06 }
07 ...
08 }
09 }
10
11 namespace Example.Analysis {
12 public class Assessment {
13 ...
14 void Execute(Guid customerId) {
15 var repository = new CustomerReposito
16 var status = repository.Get(customerI
17 ...
18 }
19 }
20 }
The key difference between control coupling and stamp coupling is that
they reflect different kinds of knowledge shared across the boundaries of
the upstream component:
01 namespace Example.CRM {
02 public class CustomersRepository {
03 ...
04 public Status GetStatus(Guid customerId)
05 ....
06 }
07 ...
08 }
09 }
10
11 namespace Example.Analysis {
12 public class Assessment {
13 ...
14 void Execute(Guid customerId) {
15 var repository = new CustomerReposit
16 var status = repository.GetStatus(cu
17 ...
18 }
19 }
20 }
As you can see, now the CRM module exposes only the information needed
by the consumer: the customer’s status. The Analysis module is not
aware of how a customer is represented in the CRM module, nor how many
fields it has. That frees the CRM module to change, evolve, and optimize
the way it represents the customers internally.
01 namespace Example.CRM.Integration.DTOs {
02 public class CustomerSnapshot {
03 ...
04 public static CustomerSnapshot From(Custo
05 ....
06 }
07 ...
08 }
09 }
10
11 namespace Example.CRM {
12 public class CustomersRepository {
13 ...
14 public CustomerSnapshot Get(Guid customer
15 ....
16 }
17 ...
18 }
19 }
20
21 namespace Example.Analysis {
22 public class Assessment {
23 ...
24 void Execute(Guid customerId) {
25 var repository = new CustomerReposit
26 var customer = repository.Get(custom
27 }
28 }
29 }
The use of the DTOs further reduces the knowledge shared by the upstream
module. The internal design and the integration-specific objects (the DTOs)
can evolve at different rates. The goal is, of course, to allow the internal
design decisions to change at more frequent rates than the public interface,
resulting in a more stable public interface. The more stable a public
interface is, the fewer changes in the module’s implementation details will
propagate to its consumers.
In the case of control coupling, its weakness lies in the sharing of the
upstream module’s functionality with its consumers. This is because it
cannot make the necessary execution decisions independently.
Key Takeaways
As stated in the introduction to Part II, at this stage you are learning to
identify types of knowledge exposed across components’ boundaries.
Whether a design contributes to modularity or complexity, and how to
improve it in the latter case, will be discussed in Part III. For now, when
working on a codebase, try to spot the kinds of knowledge described by the
levels of module coupling:
Even though the structured design methodology was designed more than
half a century ago, the problems it was supposed to solve, and some of its
solutions, are still relevant today. In the next chapter, you will learn a model
for evaluating shared knowledge that was introduced in a different era:
connascence. Ultimately, the two models will be combined into a practical
solution in Chapter 7.
Quiz
1. Stamp coupling
2. Content coupling
3. Control coupling
4. It’s impossible to share all implementation details.
1. Common coupling
2. Control coupling
3. External coupling
4. All of the answers are correct.
Chapter 6
Connascence
In this chapter, you will learn the connascence model. As you familiarize
yourself with the model’s levels, try to focus on the differences between the
levels in terms of the complexity of the integration interfaces and the
explicitness of the knowledge communicated across the modules. Also,
recall the introduction to Part II: At this stage, the goal is to identify
different ways of integrating components and what knowledge is being
exchanged. Evaluating whether an integration design is good or bad is the
topic of Part III, Balance.
What Is Connascence?
Static Connascence
The levels of static connascence describe the types of knowledge that can
be communicated across module boundaries. Moreover, these levels also
reflect the explicitness of interfaces, with connascence of name sharing the
least knowledge and being the most explicit and connascence of position
being the most implicit (Figure 6.1). Let’s see what causes the differences
between the levels.
Figure 6.1 Shared knowledge as a function of different levels of static connascence
Connascence of Name
01 def greet(name):
02 message = f'Hello, {name}!'
03 print(message)
04
05 greet('world')
You can observe connascence of name on almost every line of this listing:
If any of the three names in the example change, it will inevitably impact
exactly two lines of code. Although this example demonstrates connascence
of name between individual lines of code and methods, the same rationale
extends to other levels of modularity as well. For instance, in scenarios
where multiple classes interact, they must agree on the names of public
methods and members. The same reasoning applies to web services:
Interacting web services have to agree on the allowed HTTP verbs, actions,
and argument names.
Connascence of Type
Connascence of Algorithm
3. Considering both static and dynamic levels of connascence that are going
to be introduced further.
Connascence of Position
In the case of connascence of position, you cannot safely integrate with the
upstream component without remembering to reference its integration
guidelines. Furthermore, connascence of position makes the integration
interface fragile. A seemingly innocent change in the position of an element
will break the existing integrations and will require the integrated
components to change as well.
Connascence of Execution
interface DbConnection {
void OpenConnection();
void BeginTransaction(Guid transactionId);
QueryResult ExecuteQuery(string sql);
void Commit(Guid transactionId);
void Rollback(Guid transactionId);
void CloseConnection();
}
The interface describes the standard procedure for working with a relational
database. First, a connection to the database is opened. Next, a transaction
is begun, queries are executed, the transaction is either committed or rolled
back, and ultimately, the connection is closed.
Connascence of Timing
The code in Listing 6.10 can be easily refactored to avoid the runtime
dependency on time, as illustrated in Listing 6.11. That said, such
refactoring is not always possible, as in the case of database timeouts
discussed earlier in this section.
Connascence of Value
01 class Triangle {
02 double EdgeA { get; private set; }
03 double EdgeB { get; private set; }
04 double EdgeC { get; private set; }
05
06 void SetEdges(double a, double b, double c)
07 ...
08 }
09 ...
10 }
As illustrated in Figure 6.3, these fields can’t be assigned arbitrary values.
For the class to represent a mathematically valid triangle, the values must
satisfy an arithmetic constraint: The sum of any two edges of a triangle
must be greater than the length of the third side.
Changing the value of one of the edges in the Triangle class (Listing
6.12) necessitates a corresponding change in at least one of the other two
edges. Consequently, the three variables are connascent by value.
01 class Customer {
02 Guid Id;
03 ...
04 bool isVerified;
05 bool priorityShippingEnabled;
06 ...
07 void ClearVerification() {
08 isVerified = false;
09 priorityShippingEnabled = false;
10 }
11
12 void AllowPriorityShipping() {
13 if (isVerified) {
14 priorityShippingEnabled = true;
15 } else {
16 ...
17 }
18 }
19 }
Connascence of Identity
However, it’s important to note that what is depicted in Figure 6.4 illustrates
connascence of identity only if the integration presumes that the two
services are updating the same set of data, and each service expects to
immediately observe changes made by the other service.
Consider the method call in Listing 6.14 being made from the retail
module to the accounting module.
Managing Connascence
Being aware of the different levels of connascence is useful for reducing the
interconnectedness between software modules. As you’ve seen, in some
cases, simple refactoring can dramatically reduce the level of connascence,
making the integration both simpler and more explicit. For example,
extracting an enumeration can reduce connascence of meaning to
connascence of type, or using named arguments reduces connascence of
position to connascence of name.
Data Coupling
Stamp Coupling
In the case of stamp coupling, modules still do not share any business logic;
however, they share complete data structures—more information than is
needed for integration. Once again, all the levels of static connascence
apply here as well, as knowledge about runtime behavior is not shared. That
said, the minimum possible level here is connascence of type, as the
interconnected modules must agree on the use of specific types: the data
structures that are used for communication between the modules.
Control Coupling
External Coupling
Given that the overall level of connascence is defined by the highest level,
the connascence that suits external coupling is connascence of identity.
Common Coupling
Content Coupling
Key Takeaways
What’s your take on the nature of the conflict between module coupling and
connascence, described at the end of the chapter? How would you resolve
it? That’s the topic of the next chapter.
Quiz
1. Connascence of identity
2. Connascence of value
3. Connascence of position
4. None of the answers are correct.
1. Connascence of algorithm
2. Connascence of value
3. Connascence of identity
4. Answers B and C are correct.
Chapter 7
Integration Strength
I will begin this chapter with a discussion of the essential aspects of inter-
module relationships reflected by structured design’s module coupling and
connascence. Then, I will use these insights to integrate both models into a
more flexible and robust tool for analyzing cross-module relationships:
integration strength.
Note
In this and the following chapters, I use the term “interface”
to refer to a module’s integration interface, or its way of
integrating with other modules. This distinguishes it from the
conventional programming concept of an “interface” in
languages such as Java and C#, where it typically denotes a
protocol or contract.
Strength of Coupling
Part I of this book explored how the connections and interactions between
components shape the overarching system. The design of these interactions,
the coupling, determines whether the system will be modular or complex.
Chapter 1 explained that the stronger the connection is between two
components, the greater the effect a change in one component will have on
the other component. Furthermore, different designs lead to different types
of interactions: Connections that are implicit and unpredictable result in
complex interactions, as you learned in Chapter 3. On the other hand,
Chapter 4 showed that an explicit design focusing on encapsulation results
in modular systems. This leads us to an important question: What are the
specific characteristics of coupling that result in linear or complex
interactions?
Part II of the book began by exploring two models for assessing the strength
of coupling between modules: module coupling (structured design) and
connascence. As I demonstrated at the end of Chapter 6, these models
highlight different aspects of component interactions. Specifically, the two
scales of interconnectedness reflect two key properties of inter-module
interactions:
Therefore, it may sound reasonable to try to merge the levels of the two
models into one linear scale. There are similarities between the two models’
levels. For example, common coupling and external coupling fit the
definition of connascence of identity: strong levels of interconnectedness in
both models. However, as I discussed at the end of the Chapter 6, these
similarities can be misleading:
A weak level of connascence may correspond to the strongest level of
module coupling. Such is the case when using reflection to execute a
private method: content coupling (strongest) and connascence of name
(weakest).
A strong level of connascence, such as connascence of time, may
perfectly fit the weakest level of module coupling: data coupling.
Moreover, there are aspects of coupling that are not reflected by both
models.
Figure 7.1 Business logic duplicated in two services. Changes in requirements have to be applied
simultaneously in both services.
Such a dependency between two modules is not reflected in any of the
module coupling or connascence levels. But they still have a shared reason
for change, and a very strong one. If the implementations of the rule are not
in sync, the services may make contradictory decisions, resulting in an
inconsistent state for the system. Hence, any change in the business
requirements affecting this rule must be applied simultaneously in both
services.
Note
As you can see, merging the levels of the two models into a single scale
isn’t just impossible, but both models also overlook significant ways in
which knowledge can be shared across modules. Let’s try a different
approach.
Structured design was developed in the late 1960s, while connascence was
introduced in the 1990s. With so many years between the definitions of the
two models, and even many more years after the latter, it’s about time to
consider a new approach for determining the strength of coupling!
Integration Strength
To make the model easy to grasp and to apply, it is structured around four
fundamental levels:
1. Contract coupling
2. Model coupling
3. Functional coupling
4. Intrusive coupling
These basic levels reflect the type of interface linking the modules,
essentially serving as the conduit for sharing knowledge between them.
Let’s start exploring the model from the strongest level, intrusive coupling.
Intrusive Coupling
Recall the example that was introduced in the preceding section: two
services using the same database. Assume that this is a microservices-based
system, the database belongs to service A , and it was never meant to
serve as an integration interface. That makes it a typical case of intrusive
coupling (Figure 7.4)— service B is trespassing the encapsulation
boundaries and introduces a dependency on the implementation details of
service A .
Figure 7.4 Accessing a database instance that wasn’t intended for integration leads to intrusive
coupling.
For these reasons, intrusive coupling is located at the very top of the
integration strength model. It maximizes the knowledge shared across the
upstream module’s boundary.
The next level of integration strength moves the focus from how a module
is implemented to what is implemented: the business domain, logic, and
requirements of a module.
Functional Coupling
Sequential Functionality
Transactional Functionality
Symmetric Functionality
The modules duplicating the same algorithm shown in Figure 7.1 are
coupled through symmetric functionality. The example explicitly states that
not only is the algorithm duplicated, but also any changes made to it must
be implemented simultaneously in both services.
There are two reasons why the symmetric functionality degree is rated so
high on the integration strength scale, next only to intrusive coupling. First,
any change in the functionality has to be applied in both modules
simultaneously. Second, it’s highly implicit: The components don’t have to
be physically connected, or even aware of each other, yet they are still
coupled and have to change together.
Going back to the example of two services sharing a database, consider the
case illustrated in Figure 7.6. Both services are reading and writing
information to the same table. Moreover, they have to follow the same
business rules to ensure the consistency of the data. That makes these
services functionally coupled, with the degree of connascence of identity.
Figure 7.6 Functional coupling due to two services working on the same set of data
Effects of Functional Coupling
Model Coupling
Model coupling takes place when the same model of the business domain is
used by multiple modules.
Imagine you are designing a medical software system. Of course, you can’t
incorporate into the software all the knowledge available in the entire
medical domain. That would be akin to requiring software engineers in the
medical field to obtain an MD degree. Instead, you are describing a subset
of that knowledge in the software; a subset that is relevant to the system
you’re working on and is needed for implementing its functionality. In
essence, you’re crafting a model and articulating it through software.
Modeling is an essential part of software design. Models reflect structures
and processes, and other elements of the business domain, along with their
associated behavior. For example, consider support cases managed by the
WolfDesk system. There are different ways to model support cases,
depending on the functionality of the specific module. Listing 7.1 describes
two alternative models, one designed for operational transactions and one
designed for analysis.
namespace WolfDesk.SupportCases.Management {
public class SupportCase {
public CaseId Id { g
public string Title { g
public string Description { g
public DateTime CreatedDate { g
public DateTime LastUpdatedDate { g
public AgentId Assignee { g
public CustomerId OpenedBy { g
public CaseStatus Status { g
public List<string> Tags { g
public List<Message> Messages { g
}
...
}
namespace WolfDesk.SupportCases.Analysis {
public class SupportCase {
public CaseId Id { g
public int ReopenedCount { g
public int ReassignedCount { g
public TimeSpan LongestResponseTimeBy
{ g
Although the two objects in the preceding example represent the same
business entity, a support case in a help desk system, each model’s purpose
is reflected in its structure. The model in the
WolfDesk.SupportCases.Management namespace focuses on fine-
grained details needed to manage the operational lifecycle of a support case.
The model in the WolfDesk.SupportCases.Analysis namespace
reflects aspects of support cases needed to allow the business intelligence
department to analyze the data over time and use the insights provided by it
to improve the business performance. Merging the two representations of a
support case that would address all possible needs would result in a
complex object that is not optimized for any task. The saying “Jack of all
trades, master of none” applies nicely to the notion of working with models
in software systems.
By its definition, a model isn’t expected to mirror the real world, but only to
reflect a particular aspect of it that is for addressing a specific need. In the
context of software systems, the need is implementing the required
functionality. This brings to mind a popular saying (Box 1976):
All models are wrong, but some are useful.—George E.P. Box
The usefulness of a model is subjective. One business function might be
served well by a particular model, while another business function might be
better served by a different model. That’s precisely why domain-driven
design (DDD) emphasizes the importance of creating effective models and
using multiple models to address different needs and functionalities of a
system.2
2. I could elaborate on this subject for many more pages, but this is not the
topic of this book. If you want to learn more, I strongly suggest looking into
domain-driven design.
Figure 7.7 An upstream module exposing its internal module as part of its public interface
Sharing the knowledge of internal models across boundaries can undermine
the goal of designing a modular system. First, a different model might be
more fitting for the needs of the Accounting module. Second, the more
knowledge is shared across boundaries, the more cascading changes will
follow. Any modification to the model used in Distribution would
need to be coordinated and reflected in Accounting . This restriction
hinders the ability to evolve and improve the model used by
Distribution . This is especially true for nontrivial functionalities that
require modeling complex business domains.
Just as models vary based on the problem they are intended to solve, their
complexity levels are different as well. The degrees of static connascence
provide a way to assess the shared knowledge that results from exposing
models across module boundaries, as depicted in Figure 7.8.
Figure 7.8 Degrees of model coupling
Figure 7.9 Reading data from another component’s operational database introduces model coupling.
Second, interfaces sharing knowledge about models are more explicit than
those involving interrelated functionalities. It’s possible to document which
models are used across component boundaries and identify breaking
changes.
Lastly, unlike intrusive coupling, the shared parts of a model are being
intentionally used for integration. The downstream component doesn’t need
to break encapsulation boundaries to access them. Therefore, the authors of
the upstream module are aware of the shared knowledge and can make
efforts to prevent integration-breaking changes.
Contract Coupling
Let’s revisit the operational model of a support case object that was used as
an example in the preceding section (for convenience, it is duplicated in
Listing 7.2). Recall that the model used various domain-specific objects,
such as CaseId , AgentId , CustomerId , CaseStatus , and
Message . Exposing these objects to consumers of the module—for
example, through an API—would make downstream modules aware of their
structure and functionality. Furthermore, it wouldn’t be trivial to
communicate all of them through the API, as not all data types are
supported by all platforms.4
namespace WolfDesk.SupportCases.Management {
public class SupportCase {
public CaseId Id { get;
public string Title { get;
public string Description { get;
public DateTime CreatedDate { get;
public DateTime LastUpdatedDate { get;
public AgentId Assignee { get;
public CustomerId OpenedBy { get;
public CaseStatus Status { get;
public List<string> Tags { get;
public List<Message> Messages { get;
...
}
...
}
namespace WolfDesk.SupportCases.Application.API {
public class SupportCaseDetails {
public string Id { ge
public string Title { ge
public string Description { ge
public long CreatedDate { ge
public long LastUpdatedDate { ge
public string AssignedAgentId { ge
public string AssignedAgentName { ge
public string CustomerId { ge
public string CustomerName { ge
public string Status { ge
public string[] Tags { ge
public MessageDTO[] Messages { ge
...
}
...
}
namespace WolfDesk.SupportCases.Application.API.Comma
public class EscalateCase {
public readonly string CaseId;
public readonly string CustomerId;
public readonly string EscalationReason;
...
}
}
...
}
namespace WolfDesk.SupportCases.Application.API {
interface SupportCasesAPI {
// Commands
ExecutionResult Execute(EscalateCase cmd);
ExecutionResult Execute(ResolveCase cmd);
ExecutionResult Execute(PutCaseOnHold cmd);
...
// Queries
IEnumerable<SupportCaseDetails> CasesByAgent(
IEnumerable<SupportCaseDetails> CasesByCustom
IEnumerable<SupportCaseDetails> EscalatedCase
...
}
}
In Figure 7.11, you may have noticed a thin dashed line dividing the
degrees of contract coupling and model coupling. Before I close this
section, I feel it’s important to discuss that border between the two levels.
Assume you are working on the WolfDesk system, and you encounter a
simple data structure, Message , representing a message that was sent by
either a customer or a support agent. The service’s API introduces a data
transfer object (DTO) (Fowler 2003) that is used to communicate message
data over the API, MessageDTO . As you can see in Listing 7.4, both
objects, Message and MessageDTO , describe exactly the same data and
in the same format.
namespace WolfDesk.SupportCases.Management {
public class Message {
public Guid Id { get; privat
public string Body { get; privat
public DateTime SentOn { get; privat
public Guid SentBy { get; privat
}
}
namespace WolfDesk.SupportCases.Api {
public class MessageDTO {
public Guid Id { get; privat
public string Body { get; privat
public DateTime SentOn { get; privat
public Guid SentBy { get; privat
}
}
You may challenge this point by saying that it’s not entirely unnecessary,
because it still allows the developer to change the Message object without
having to change MessageDTO . While that is true, the mirroring structures
of the two objects suggest that changes to Message are likely to be
reflected in MessageDTO as well. In addition, it’s still possible to share
the Message object across the boundary (model coupling), and introduce
an integration-specific object once it is actually valuable.
Let’s say there is a specific table in the database meant for integration
between the components, as illustrated in Figure 7.13. Its schema is limited
and decoupled from service A ’s implementation model. Now, even
though the services use a database for communication, they are still
contract-coupled, as the data is specifically designed as an integration
model.
Figure 7.13 Contract coupling through exposing a database schema containing integration-specific
data
Note
Except for intrusive coupling, the other three levels of integration strength
span an additional dimension: degree.
Whereas coupling strength defines the type of the integration interface, its
degree describes the complexity of the information communicated through
that interface. For that, we can use the connascence model:
1. Service B (4) records the messages in its database for audit purposes.
2. Service C (5) updates its state using the information provided in the
messages.
3. Service D (6) enriches the incoming messages with states generated
by service C (5) and forwards the enriched messages to a data
analysis database. To make sure that the data returned by service C
(5) is up-to-date, the messages are delivered to service D (6) with a
delay of 30 seconds.
Figure 7.15 An example of a distributed system
What levels of integration strength can you spot here? Take a moment to
analyze this before we compare our answers:
Key Takeaways
Integration strength has four basic levels. Each level reflects a type of
knowledge that is shared between modules. Three of these levels have an
additional dimension, degree, that reflects the complexity of the shared
knowledge:
Armed with the integration strength model, not only can you evaluate the
types and degrees of knowledge shared by components, but you can also
inspect the flow of knowledge at different levels of abstraction: from an
object’s methods, to services in a distributed system.
This chapter also discussed the notion of models; specifically, that a model
is not intended to copy a real-world system, but just to provide enough
information to solve a particular problem. Hence, no model is complete,
and integration strength is no exception. There are additional factors
affecting coupling between a system’s components and how the design
increases or decreases the shared reasons for change. The next two chapters
will explore two such factors: distance and volatility.
Quiz
1. Intrusive coupling
2. Functional coupling
3. Model coupling
4. Contract coupling
1. Intrusive coupling
2. Functional coupling
3. Model coupling
4. Contract coupling
1. Intrusive coupling
2. Functional coupling
3. Model coupling
4. Contract coupling
8. What does the degree of integration strength reflect?
1. Yes
2. No
1. Intrusive coupling
2. Functional coupling
3. Model coupling
4. Contract coupling
Chapter 8
Distance
It’s possible to write software without any partitioning. You could just put
all the statements in one huge listing and sprinkle it with enough goto
statements. Indeed, that’s how software was written at the dawn of our
industry. Of course, such code is far from being readable, maintainable, or
flexible. Since those early days, new programming paradigms have been
introduced, each bringing different types of encapsulation boundaries:
routines, functions, objects, namespaces/packages, libraries, services, and
more. As we discussed in Chapter 4, all of these encapsulation boundaries
are, in fact, software modules, and they form a hierarchy. Many of the
preceding chapters have delved into the multidimensional nature of
software design, albeit from different perspectives:
Cost of Distance
Figure 8.1A puts the duplicated logic in the same object. Implementing
and deploying the change requires modifying one object and deploying
the updated module.
In the case of Figure 8.1B, the duplicated logic is spread across
microservices. Both microservices have to be changed. Since having
different implementations of the IsPreferred rule will lead to
inconsistent behavior of the system, both microservices need to be
deployed simultaneously.
The peculiar thing about lifecycle coupling is that it affects even otherwise
unrelated modules. Consider the rather extreme case described in Listing
8.1.
Evaluating Distance
4. The full type name includes both the namespace and the name of the
object. In the first example,
WolfDesk.Routing.Agents.Competencies is the namespace and
Evaluation is the object.
1. WolfDesk.Routing.Agents.Competencies.Evaluation
2. WolfDesk.CaseManagement.SupportCase.Message
3. WolfDesk.CaseManagement.SupportCase.Attachment
The common ancestor across types 1, 2, and 3 is the first level, named
Wolfdesk , which is the root-level namespace. On the other hand, types 2
and 3 share a much closer first common ancestor: SupportCase . Thus,
the distance between types 1 and 2 is greater than the distance between
types 2 and 3.
Figure 8.4 summarizes the two ways in which distance between coupled
components affects the overarching system: cost of change and lifecycle
coupling.
Figure 8.4 Effects of distance between coupled components
The greater the ownership distance, the higher the coordination effort
needed to implement changes affecting multiple modules. Consequently, the
perceived distance between the modules also increases accordingly.
Let’s revisit the example in Figure 8.1. Suppose the microservices (Figure
8.1B) need to be updated to reflect the new definition of “a preferred
customer.” Ideally, microservices should not require simultaneous
deployment. That said, such a need might still occur. In that case, the
process will be simpler if the two microservices are owned by the same
team, containing all the necessary communication and coordination within
the team.
If, on the other hand, the two microservices belong to different teams,
deploying a simultaneous change would require a significantly higher level
of communication and coordination. As a result, the effective distance in
this scenario is greater.
It’s worth mentioning that ownership distance also reduces the lifecycle
coupling between the modules. The less related the teams in charge of the
modules are, the lower the lifecycle dependencies between their modules
will be. Going back to the example of two microservices, if they are
implemented by the same team, they are more likely to share the same
development and deployment schedule. That is much less likely in the case
of two microservices implemented by different teams.
Conway’s Law suggests that the way a software system is developed and
organized will mirror the social and communication patterns within the
team or organization responsible for its creation. Even if the initial design
of the system differs from the organizational structure, over time, the design
will evolve to reflect the communication and collaboration levels between
the relevant teams.
Now that you’re familiar with the effects of distance between coupled
components on a system, I want to mention a closely related concept:
proximity. Put simply, proximity is the opposite of distance. High distance
equates to low proximity, while low distance implies high proximity.
Keep in mind that “distance” and “proximity” are two sides of the same
coin. Both terms describe the same aspect of design, but from different
perspectives.
Key Takeaways
When making design decisions, pay attention not only to the knowledge
shared by components, but also to the distance the knowledge will traverse.
The distance can be evaluated by identifying the components’ closest
common ancestor module. Furthermore, don’t forget about the effect of
social factors on the distance, and as a result, the cost of evolving the design
in the future.
Quiz
1. Method
2. Object
3. Library
4. Service
1. Method
2. Object
3. Namespace
4. Service
3. Which of the following can affect the distance between modules?
1. Business requirements
2. Teams in charge of the modules
3. Integration strength
4. Answers A and B are correct.
Volatility
One of the core tenets of Agile software development states that you should
value responding to change over following a plan. On the one hand, we
should welcome change. Changes reflect new business opportunities, an
improved understanding of the business domain, or the discovery of more
efficient ways to implement the software. Changes are a sign of a healthy
system.
Have you ever designed an elegant solution that was thwarted by a slight
change in the requirements?
Did you ever find yourself adding awkward “if-else” statements
throughout your codebase to accommodate a change in the business
logic?
Or, even worse, have you witnessed a neat microservices-based system
turning into a distributed Big Ball of Mud the moment its functionality
had to change?
These scenarios share a common element: complex interactions. As you
learned in Chapter 3, complexity often arises from poorly designed
coupling. The more knowledge that is shared across the system, the harder
it is to change it. The more often the software has to change, the more
evident flaws in its design become. Changes make implicit complexity
explicit. A single change rippling through numerous components of a
seemingly decoupled system? That’s the change revealing the implicit
interactions among the affected components.
Software design revolves around two areas: the problem space and the
solution space. The problem space encompasses all the activities required to
define the business problem that the software system is supposed to solve.
The solution space is all about the software solution itself; how it is
designed, architected, and implemented.
Let’s see what are the common causes for changes in both spaces. Since the
solution space is closer to us as software engineers, I’ll start with that.
Solution Changes
You might argue that the shift to remote work during the COVID-19
pandemic demonstrated that companies can succeed even if all their
employees are distributed in different locations. That’s true. However,
organizational distance is not necessarily physical. Team members working
on the same project are destined to either succeed together or fail together.
Organizations are designed to facilitate intra-team communication. These
factors make collaboration more effective within a team rather than across
teams. As the organization grows, cross-team communication becomes less
effective (Hu et al. 2022). Only so many people can actively participate in
an all-hands call. Is that necessarily a problem or a case against remote-first
companies? Not at all. It’s an organizational factor that must be accounted
for by software design.
Problem Changes
Domain Analysis
Business Domain
Business Subdomains
For example, let’s analyze the WolfDesk company. Its business domain is
customer services and support software. The company requires the
following subdomains to compete in this field:
Although all the subdomains are required for the company to succeed in its
business domain, not all of them are made the same. From the company’s
strategic point of view, some of the subdomains are more important than
others. To illustrate these differences, DDD identifies three types of
subdomains: core, supporting, and generic.
Core Subdomains1
Since these are nontrivial business problems, finding the best solution
requires some trial and error. This involves experimenting with different
solutions and evolving them over time, until the most effective—or at least,
the most sufficiently good—solution is discovered. Referring back to the
Cynefin model introduced in Chapter 2, this places core subdomains within
Cynefin’s complex domain. Recall that according to Cynefin, decision
making in a complex domain involves conducting experiments, observing
the results, and applying the discovered insights in the next iteration.
Going back to the WolfDesk example, the following could be its core
subdomains:
Generic Subdomains
Generic subdomains are located at the opposite end of the spectrum from
core subdomains. Instead of necessitating a proprietary solution, generic
subdomains can utilize existing, battle-tested solutions. The solution
employed by your company is likely to be in use by its competitors as well.
Consequently, unlike core subdomains, there is no competitive advantage to
be gained from generic subdomains.
From the Cynefin perspective, generic subdomains fall into the complicated
domain. These are areas in which there are strong cause-and-effect
relationships; you just need to consult an expert to identify them. In this
case, the expert is the provider of the generic solution.
Supporting Subdomains
Volatility of Subdomains
Competitive Problem
Subdomain Complexity Volatility
Advantage Type
As you can see, identifying types of subdomains at play can help you
estimate the components’ expected rates of change. You can further use this
information to evaluate the design of a system.
Source control systems can provide insights into the volatility levels of
modules in a brownfield project. A simple measure of volatility could be the
frequency of changes, or in other words, the number of commits to a
particular module. More-volatile code tends to have more commits
associated with it, as it frequently evolves or is modified over time.
False Positives
Not all changes are equally significant. It’s worth distinguishing between
changes that are corrective, such as fixing bugs, and those that introduce
new functionalities or modify existing ones. If the latter case is more
prevalent, it is a stronger signal that you are working with a core
subdomain.
False Negatives
Consider the two systems illustrated in Figure 9.2. Don’t try to find
differences between them. Both contain exactly the same number of
components, with exactly the same integration strength between them.
Does that mean, however, that the two systems depicted in Figure 9.2 have
equal designs? To answer that question, first assume that each component of
the two systems implements exactly one business subdomain. Figure 9.3
expands the example with the types of the subdomains.
Figure 9.2 Two systems having the same number of components with the same integration strength
between them
Figure 9.3 Two systems having the same number of components with the same integration strength
between them, but with different types of subdomains implemented by the components
Pay attention to the most volatile components of the systems: the ones
implementing the functionalities of core subdomains. The components of
the system in Figure 9.3A are contract-coupled to the components
implementing core subdomains. The corresponding components of the
system in Figure 9.3B, on the other hand, use intrusive and functional
coupling to interact with the core subdomains. Given that a core subdomain
changes often, the changes will undoubtedly ripple relentlessly into the
dependent subdomains. Consequently, the design of system A is much more
stable. It minimizes the shared knowledge where it matters the most: in the
boundaries of the core subdomains.
As the example shows, high volatility can make a poor integration choice
especially painful in some cases and forgivable in others. Let’s see another
interesting intersection of volatility and integration strength.
Inferred Volatility
So far, I’ve described how subdomains can be used to estimate the volatility
of a component. However, as we discussed in Part I, you can’t evaluate a
system’s design by inspecting its components individually. Their
interactions are equally important, if not more so. This insight applies to
volatility as well.
Based on the available information, what can you deduce about its
volatility? Since it implements a supporting subdomain, it’s natural to
assume that its volatility is low. But is that necessarily the case?
Figure 9.5 expands this example with additional information: the types of
subdomains belonging to the components it depends on,2 as well as their
integration strengths.
Figure 9.5 High integration strength with volatile components
Now you can see that not only does the component depend on three
components implementing core subdomains, but those components are also
integrated with the highest level of integration strength: intrusive coupling.
Consequently, every change to any of the upstream components is likely to
trigger a corresponding change. Thus, even though component A
implements a supporting subdomain and is thus expected to have low
volatility, high integration strength with volatile components makes it
highly volatile as well.
Key Takeaways
Volatility can also stem from technical concerns, such as the need to
improve the system’s design or changes in the organizational structure.
Ultimately, the system’s design itself can make components volatile, as they
encompass the most volatile components with appropriate levels of
integration strength.
With the knowledge of the three dimensions of coupling and the ability to
estimate them, the next chapter will combine them into a framework for
evaluating and making design decisions.
Quiz
1. Core
2. Generic
3. Supporting
4. Answers A and B are correct.
1. High
2. Low
3. Depends on the components it integrates with
4. Depends on its integration strength with other components
5. Answers C and D are correct.
Balance
Part II delved into how coupled components can impact each other across
three dimensions: strength, space (distance), and time (volatility). Part III
combines the materials in Parts I and II and turns coupling into a design
tool. You will learn how to combine the three dimensions of coupling to
design modular software.
Chapter 12 addresses the most common, and arguably the most dangerous,
type of change in software systems: growth. It demonstrates how to
accommodate growth using the balanced coupling model, borrowing
insights from other industries to reveal the underlying fractal geometry of
modular software systems.
Balancing Coupling
In Part I, you learned that coupling is an integral aspect of any system. The way
components of a system interact can make the system either more modular or complex.
Part II delved into the manifestations of coupling across the following three
dimensions:
1. Strength: You learned that if two or more components are connected, they share
knowledge. The type of that shared knowledge can be evaluated using the
integration strength model. The more knowledge components share, the higher the
possibility of cascading changes rippling through the system.
2. Space: The physical arrangement of coupled components across different distances
influences the costs of making changes across multiple components.
3. Time: Ultimately, the actual impact of the costs of change depends on how
frequently the coupled components change, or their volatility.
The three dimensions of coupling are important, yet none can be deemed the most
important. They should all be considered and managed in unison. That’s what this
chapter is about: balancing the three dimensions of coupling.
This chapter bridges the topics of Parts I and II. It explores the interactions between
the three dimensions of coupling, and the effects of different combinations: whether
they increase modularity or lead to complexity. Ultimately, these insights will be used
to define a holistic model that turns coupling into a tool that helps design modular
systems.
Historically, coupling has always been associated with the dimension of strength, be it
connascence or structured design’s module coupling. But is achieving the lowest level
of, say, connascence—connascence of name—a goal worth pursuing? Such a goal is
not only impractical, but, as Chapter 7 showed, is impossible in a real-life system. No
amount of refactoring will reduce the integration strength if business requirements
dictate sequential or transactional coupling.
Even when refactoring to the minimal strength is possible, it is not always worth the
effort. As Eric Evans, author of the domain-driven design methodology, put it, not all
of a large system will be well-designed. Some components are more important
business-wise than others and require more advanced engineering tools and practices
—for example, core versus supporting subdomains, which we discussed in Chapter 9.
Efficacy requires healthy pragmatism.
In this light, when designing cross-component interactions, our goal is not to minimize
the strength of coupling. The goal is to design a modular system; that is, a system that
is simple to implement, evolve, and maintain. This can only be achieved by
considering all three dimensions of coupling.
Since this chapter relies heavily on the details of each dimension, I want to briefly
recap the key concepts covered in the previous parts of the book.
First, remember how knowledge is shared between coupled components. The flow of
knowledge is opposite from the direction of dependency. For instance, in Figure 10.1,
Module A communicates with, and thus depends on, Module B . This dynamic
positions Module B upstream and Module A as its downstream module. The terms
“upstream” and “downstream” describe the direction of knowledge flow within the
system. The upstream module exposes certain knowledge—say, via APIs or integration
contracts—enabling its downstream consumers to integrate with it.
Second, the interfaces used for integration reflect the nature of the knowledge that is
shared, and can be categorized into the following four types:
1. Intrusive coupling presumes that all knowledge about the implementation details of
the upstream module is shared with its consumers.
2. Functional coupling involves modules that implement closely related functionalities
and, consequently, share knowledge about the business domain or requirements.
3. Model coupling arises when a model of the business domain is exposed across
boundaries.
4. Contract coupling encapsulates as much of the upstream component’s knowledge as
possible by exposing an integration contract designed with the goal of sharing the
least amount of knowledge.
The knowledge can be shared across different distances: from methods of an object to
services in a distributed system. The more knowledge that is shared, the higher the
likelihood that the coupled modules will need to change together. The greater the
distance between the modules, the more effort is needed to implement a cascading
change. The extent of such changes depends on the volatility level of the modules
exposing the knowledge.
One important topic that we have not discussed, but is crucial for managing the values
of the three dimensions concurrently, is how to put them on the same scale.
Measurement Units
Integration strength, distance, and volatility each measure different facets of design.
But how can we compare them? Integration strength has multiple levels, and most of
them have degrees, but how do we quantify something as abstract as knowledge?
Moreover, how can we calculate the distance that this knowledge “travels” across the
system? And assigning a numerical value to volatility seems like an even more
puzzling task.
For the sake of simplicity, at this stage, I will use a basic binary scale: high and low.
“High” will be represented as 1, and “low” as 0. This approach allows discussing the
interactions between different dimensions and using binary logic to represent
significant combinations as equations. Here are some examples:
Later in this chapter I will revisit this topic and talk about how the discussed concepts
can be applied with a numerical scale. Let’s begin the exploration of how dimensions
interact, starting with the relationship between volatility and strength.
Stability: Volatility and Strength
In contrast, if both volatility and integration strength are high, the relationship between
the two components can be considered unstable. This is due to the frequent changes
expected in the upstream (high-volatility) component likely propagating to its
downstream components as a result of the high integration strength.
The physical distance between coupled components reflects the effort required for
communication and collaboration when implementing a change in the upstream
component. Such a change, when propagated, affects the downstream component. The
greater the distance, the more significant the required effort. When coupled with the
volatility of the upstream component, these two dimensions reflect the actual cost of a
change propagating from the upstream component to its downstream component.
As in the case of volatility and integration strength, if both distance and volatility are
high, the actual cost of changes is high. If, on the other hand, either the upstream
component is not volatile and/or it is located close to the downstream component, the
actual cost of cascading changes is low. This can be represented by the following
expression:
Table 10.2 Actual Cost of a Cascading Change as a Combination of Distance and Volatility
Next, let’s move on to what is probably the most interesting combination of coupling
forces: distance and integration strength.
The knowledge shared across the boundary of the upstream module, coupled with the
distance this knowledge traverses, signifies the nature of the relationship: whether it
makes the system more modular or more complex. Let’s examine the four possible
combinations:
1. High strength over high distance leads to frequent changes affecting distant
modules. This necessitates extra effort to coordinate and implement these changes.
Furthermore, dependencies over long distances increase the cognitive load and,
thus, invite complex interactions—it’s easy to forget to update a distant component
or even to be unaware of it needing to change. Therefore, the combination of high
strength over high distances leads to global complexity.
2. Low strength over high distance minimizes the chances of cascading changes
happening, thereby mitigating the effect of high distance. This relationship
embodies what we typically refer to as loose coupling: a relationship that separates
modules that are not expected to change together.
3. High strength over low distance, conversely, leads to frequent cascading changes.
However, due to the short distance between the coupled components, the effort
required to implement these changes is low. This relationship represents what
Chapter 4 defined as high cohesion: Modules expected to change together are
located close to each other.
4. Low strength over low distance is a peculiar combination. While low strength
minimizes cascading changes, low distance positions unrelated components close to
each other. This increases the cognitive load on the engineer maintaining the
overarching module: When something needs to change, the engineer must navigate
through unrelated components to find the one requiring modification. According to
the terminology in Chapter 3, this combination results in local complexity.
Ultimately, in addition to the increased cognitive load, such a relationship increases
the volatility of the overarching module: The more components it contains, the
more its contents are likely to change.
Table 10.3 Four Combinations of Low/High Values for Strength and Distance
The fact that complexity can be expressed as NOT MODULARITY further illustrates
the concept we discussed in Part I: The design of cross-component interactions nudges
the design either toward modularity or toward complexity.
Now, let’s see what insights we can gain by combining the three dimensions together.
Previous sections discussed how combining pairs of dimensions of coupling can allow
us to estimate the following properties of the resultant design: stability, cost of
changes, modularity, and complexity (local and global).
What can we identify by combining the three together? First, we can identify the
maintenance effort: the expected effort of maintaining a dependency between two
components. We can estimate this by multiplying the values of the three dimensions:1
1. Remember, I still assume a binary scale. The allowed values for each dimension are
high (1) and low (0).
MAINTENANCE EFFORT = STRENGTH * DISTANCE * VOLATILITY
Put differently, this metric represents the “pain” of maintaining an integration between
two components. High integration strength over high distance with a dependency on a
highly volatile component is, well, painful from a maintenance perspective. That
describes an upstream component that not only changes frequently, but the majority of
those changes propagate to its downstream consumers located at a distant part of the
system.
Naturally, we want to minimize the pain. For that, it’s enough to minimize the value of
any one of the equation’s elements. The zeroed dimension will compensate for the
high values coming from the remaining dimensions. Let’s examine the three possible
combinations:
A common case for this combination of coupling forces is when one needs to integrate
with a legacy system. Assuming that the legacy system doesn’t provide a weaker
integration interface, the integration strength is high—for example, when you have no
choice but to fetch data from one of its private interfaces, such as fetching the data
directly from its database. However, since the legacy system is not being evolved and
thus is not expected to change, the combination of high strength and low volatility
stabilizes the integration and minimizes the maintenance effort.
Table 10.4 summarizes these combinations of coupling forces and their outcomes.
However, it also shows a problematic combination that we haven’t discussed yet: low
strength, low distance, high volatility.
Table 10.4 Maintenance Effort as a Function of Integration Strength, Distance, and Volatility
The maintenance effort formula focuses on the evaluated coupled components, but it
has a higher-level blind spot. It doesn’t consider the complexity that can be introduced
through low strength over low distance. From a maintenance standpoint, that’s not an
issue in the context of the involved components. However, it is an issue for the module
that hosts these components. Even though the combination of low strength with high
volatility is considered stable, it is outweighed by local complexity induced by low
strength over low distance. That’s because the upstream component is volatile. Each
time it changes, the person implementing the change will have to go through the
unrelated modules located close to each, figuring out which one has to be modified
and why they are colocated overall.
Hence, to balance the coupling, it’s important to consider this scenario as well.
High strength, high distance, high volatility = global complexity + high volatility
Low strength, low distance, high volatility = local complexity + high volatility
They can be accounted for using the following expression:
Table 10.5 Balanced Coupling as a Function of Integration Strength, Distance, and Volatility
Using the binary scale for describing the values in different dimensions makes it easy
to describe the desired and undesired combinations of coupling forces. However, let’s
talk about how we can actually quantify the values in different dimensions.
I am obligated to start this section with a disclaimer. Imagine that not only is it written
in a boldface font, but it also is blinking like a banner from the 1990s:2
2. If you missed this wonderful era of website design, you can see examples here:
https://cyber.dabamos.de/88x31/index.html.
I’ll explain.
First, we need to put three different phenomena on the same numeric scale: integration
strength, distance, and volatility. That already presents a number of challenges: How
can we quantify the three values, let alone put them on the same scale?
Second, numbers are supposed to be objective. However, the dimensions of coupling
are subjective by definition. For instance:
Complexity shifts its form. A global complexity is also a local complexity when
observed from a higher level of abstraction. Similarly, a local complexity becomes
global when the perspective shifts to a lower level of abstraction.
Modules are hierarchical; hence, the evaluation of modularity may be different at
different levels of abstraction.
The integration strength model can be applied at different levels of abstraction as
well, yielding different results at different levels.
I hope that in the future, there will be a tool that will analyze a codebase and
automatically evaluate numeric values for integration strength, distances, and volatility
levels. Now, however, we still have to involve our gut feelings.
Scale
I’m going to use an arbitrary scale containing the values of 1 (lowest) to 10 (highest)
for the three dimensions:
Integration strength
1 = Contract coupling
3 = Model coupling
8 = Functional coupling
9 = Symmetric functional coupling
10 = Intrusive coupling
Distance
1 = Methods in the same object
2 = Objects in the same namespace/package
3–7 = Objects in different namespaces/packages
8 = Different libraries
9 = Services in a distributed system
10 = Systems implemented by different vendors
Volatility
1 = Legacy system that is not being evolved
3 = Supporting or generic subdomain
10 = Core subdomain, or inferred volatility from a core subdomain
Now let’s see how to use this (arbitrary) scale to evaluate the balancing of coupling.
For the balanced coupling equation, we need to evaluate modularity, and change the
result if a lower value of volatility can compensate for the resultant complexity (lack
of modularity).
Evaluating Modularity
The binary expression I used in the previous sections to determine whether the design
leans toward modularity or toward complexity compared the values of integration
strength and distance. A difference in the values signifies modularity, while similar
values represent complexity.
To express how far apart the values of strength and distance are, we can
simply subtract one from the other, then take the absolute value of the result and add 1
to keep the same range of values (1–10):
For example, in the case of intrusive coupling (10) across systems implemented by
different vendors (10), the modularity score will be the lowest value in the range; that
is, 1:
On the other hand, sharing only an integration contract results in the highest
modularity score, 10:
For the cases in between these two extremes, the modularity score will be adjusted
accordingly. In this example, functional coupling (8) is adjusted across two objects in
the same namespace (2):
The next step is to account for cases where the design leans toward complexity, but the
upstream module’s volatility is low. In such a case, the balance is reflected by the
following equation:
Here, the balance is the maximum score between modularity and the complementary
value of volatility. A complementary value is the difference between a given value and
the specified upper limit of the range. For example, in a range from 1 to 10, the
complement of 2 is 9. It is calculated using the following formula: (Minimum +
Maximum) − Value .
Let’s assume that WolfDesk’s distribution module, which assigns support cases to
agents, uses a machine learning (ML) algorithm that is running on a cloud vendor’s
managed service. For the ML algorithm to match cases to agents correctly, it is fed
with all the data from WolfDesk’s Support Cases Management module’s
operational database. That results in the following values for the three dimensions of
coupling:
Integration strength: Since the data is copied from the operational database as is, it
shares the model used by the Support Cases Management module. Therefore,
the integration strength is model coupling (3).
Distance: WolfDesk’s Support Cases Management module and the cloud
vendor’s managed service for running ML models are two different systems
implemented by different companies. Hence, the distance between them is 10.
Volatility: The upstream module—the one that shares the knowledge—is Support
Cases Management , and it’s one of WolfDesk’s core subdomains, which makes
it highly volatile (10).
The resultant coupling balance score for this integration scenario is as follows:
Example 2
In the same Support Cases Management module, there are two objects:
SupportCase and Message . The two have a strong functional relationship:
Receiving a message affects some of the business rules defined in the corresponding
instance of SupportCase , and thus, they have to be committed in the same
transaction.3 This case results in the following values of the three dimensions:
3. In domain-driven design (DDD) language, the two entities are part of the same
aggregate.
Integration strength: Since the two objects have to be committed as one transaction,
that’s functional coupling (8).4
Distance: The two objects are implemented in the same namespace; thus, the
distance between them is 2.
Volatility: In the functional coupling level, both modules expose knowledge about
the business functionality, and thus, both are considered upstream components.
Since both belong to the same module, Support Cases Management , and it’s
one of WolfDesk’s core subdomains, the volatility is 10.
The resultant coupling balance score for this integration scenario is as follows:
Example 3
When WolfDesk engineers heard about microservices, they started extracting some
functionalities from their monolithic codebase into microservices. That resulted in the
business rule that defines whether a support case can be escalated, to be duplicated in
the aforementioned Support Cases Managemen t and Distribution modules.
That resulted in the following:
The resultant coupling balance score for this integration scenario is as follows:
The balance score of 1, the lowest possible value, reflects the choice to duplicate
volatile, business-critical functionality in modules with minimal lifecycle coupling
(services).
Example 4
The same monolith from the previous example was at one point declared a legacy
system, and the team decided to implement all new functionalities in a modernized
solution, as well as gradually migrating the existing functionality out of the legacy
system.
As you can see, the low volatility balances out the lack of modularity introduced by
intrusive coupling over a high distance.
Before I close this chapter, I want to reiterate that the numeric approach demonstrated
in this section is not an exact science. The range I picked worked for me. However, a
different range might work better for you.
For instance, I assigned the largest value of distance (10) to systems implemented by
different vendors. Such a scenario might not be relevant for you. The largest distance
for you might be services in a distributed system, or even modules in a monolithic
codebase.
What’s much more important, in my opinion, is that you understand how the three
dimensions work together and balance each other out. The binary expression
succinctly illustrates this:
The overall cost of making changes is high if both volatility and distance are high:
If integration strength and distance have opposite values, the design increases the
modularity of the system, while equal values result in complexity:
Ultimately, balance in a numeric scale can be represented with the following equation,
where MIN_VALUE and MAX_VALUE are the numeric range’s limits:
Use the balanced coupling equation to estimate whether the design of coupling will
result in modularity (high balance) or complexity (low balance).
Quiz
Rebalancing Coupling
Unfortunately, that’s not the reality we are living in. A software system is
like a living organism: It evolves and adapts to survive in a changing
environment. No changes equal obsolescence. Systems providing business
value not only need to adapt, but often need to adapt frequently. Some of
the changes challenge prior domain knowledge, invalidate assumptions, and
shake the foundations of existing design decisions.
Chapter 9 discussed changes in the context of individual modules. This
chapter continues the discussion at a higher level, exploring system-wide
changes that reshuffle the forces of coupling. You will learn the
fundamental reasons behind such changes, and how to respond using the
balanced coupling model.
Resilient Design
When this occurs, it’s natural to question the integrity of prior design
decisions. Where were we wrong? How can we avoid such mistakes in the
future? Unfortunately, unless you have a crystal ball, there will always be
unforeseen changes and new requirements that are impossible to anticipate.
When these unexpected changes occur, it’s vital to leverage the new
information they provide. Rebalancing the system’s coupling is key to
enhancing design resilience.
The causes for software changes can be divided into tactical and strategic
changes. The definitions of the two are inherently different. Essentially,
strategy refers to the “what,” while tactics cover the “how.” A tactical
change impacts how a problem is solved, whereas a strategic change
redefines the problem itself.
Tactical Changes
Tactical changes alter the way the system or its components support
business goals. These are the changes that can be anticipated. They follow
the current understanding of the software’s goal and its business domain.
Consequently, an adequate software design should be able to accommodate
tactical changes.
Strategic Changes
Strategic changes are substantial. They shift the “what”: what is being
implemented, what problems the system addresses, or what
organization/structure executes it.
Functional Requirements
Figure 11.1 Adding components introduces more interactions, potentially escalating global
complexity.
Business Strategy
Companies often reassess and change their business strategies. This process
may involve identifying and assessing new profit opportunities—potentially
leading to new core subdomains—or they might entirely transform their
business domain. Take Nokia, for instance; while it’s primarily recognized
as a telecommunications manufacturer today, the company originally
concentrated on rubber manufacturing.
Organizational Changes
Environmental Changes
Changes in the execution environment can also impact software design. For
instance, migrating to a cloud computing infrastructure necessitates new
architectural approaches. The “lift-and-shift” strategy, where systems are
transferred from on-premises data centers to the cloud, often results in
limited success. This approach relies on assumptions rooted in the
experience of running on self-hosted servers and does not leverage the
advantages provided by cloud computing, thus undermining the financial
credibility of the overall migration.
Rebalancing Coupling
Contrary to tactical changes, which are supposed to fit within the software’s
design, strategic changes can disrupt both the design and the assumptions it
was built upon. As a result, the three dimensions of coupling—strength,
distance, and volatility—have to be rebalanced to ensure the system’s
modularity in the long term.
In some cases, the effect of a strategic change can be like an iceberg, where
only a small portion of it is visible and explicit. What may seem like an
innocent and even unrelated change could have a disastrous effect on the
software system’s design. For example, a few seemingly innocent “ifs”
added to address a new edge case might induce a fast-growing technical
debt. This is because in reality, it’s not just an “edge case.” Instead, a
significant change is needed in the model of the business domain. Such
changes, by their nature, are prone to affect one of the dimensions of
component coupling.
Strength
Figure 11.2 WolfDesk’s Distribution service uses HelpDesks' events to assign support cases to agents
only during their working hours.
As time passed, support agents asked for the ability to pause the assignment
of new cases when they felt overwhelmed with existing work. The decision
was to allow the agents to click a Pause Assignment button, which would
pause the assignment of new cases for one hour. An agent is allowed to
pause assignments up to three times during a day.
Shortly after this new functionality was deployed, some agents complained
that even though they had paused assignments, they were still receiving
new support cases. This was caused by a race condition: Due to the
asynchronous integration between the two microservices, new support cases
were assigned during the time it took to deliver schedule changes to the
Distribution component.
The support agents’ and management’s expectation was that the moment an
agent clicks the Pause Assignment button, it will take effect. In terms of
integration strength, this expectation describes transactional coupling. It is
expected that the information available to Distribution would be
strongly consistent, making it almost the highest degree of functional
coupling.
Exposing the agents’ schedules as a REST API: This would help reduce
the staleness of the data available to Distribution , as it would
synchronously query the HelpDesks API to read the data from the
operational database. However, this design wouldn’t eliminate the
possibility of Distribution working on stale data. Even if it queries
the API before each assignment, the underlying data could still change
after the API responds, but before a support case is assigned.
Furthermore, it would introduce higher runtime coupling between the
two components: Distribution would not be able to function if
HelpDesks is not available.
Merging the two components into a single service: While this would
allow the Distribution module to work on more consistent data, it
would result in high local complexity of the resultant service, as most of
the functionalities of the original components are not related to each
other (low strength over low distance).
For every new support case created in the WolfDesk system, the
management component emits an event describing the new support case.
There are two subscribers listening to these events (Figure 11.4):
Figure 11.4 WolfDesk’s Support Case Management service publishes events to notify other
components about new support cases.
Volatility
As the company became successful and started growing, more R&D teams
were added. To streamline the work of multiple teams, WolfDesk’s chief
architect decided to decompose the original monolithic codebase into
multiple services. This enabled each team to be in charge of one or more of
its own services.
Rebalancing Complexity
Key Takeaways
This chapter analyzed the causes for changes in software systems, and how
the changes affect the system’s design. Software changes can be categorized
into tactical changes and strategic changes.
Tactical changes affect the system’s how—how it implements the
functionality. These are new functionalities that fit the current design
decisions, or improved implementation of the system’s current
functionality.
Integration strength can grow over time, for example, due to new
functionality added to the system.
The distances between integrated components can increase due to
refactoring or organizational changes.
Volatility can change due to changes in the business domain.
Such changes have a profound effect on the system, and they need to be
addressed by adjusting the other dimensions of coupling to restore balanced
coupling. Failing to notice the resultant imbalance or neglecting to react by
adjusting other dimensions will inevitably lead to technical debt and overall
accidental complexity.
Quiz
1. Which type of change can result from changes in the types of business
subdomains?
1. Tactical
2. Strategic
3. Both tactical and strategic
4. Neither tactical nor strategic
1. Reduce volatility.
2. Reduce distance.
3. If possible, refactor the integration strength to its original value.
4. Answers B and C are correct.
1. Reduce distance.
2. Reduce integration strength.
3. Answers A and B are correct, depending on the situation.
4. None of the answers are correct.
1. Organizational
2. Refactoring
3. Answers A and B are correct.
4. None of the answers are correct.
Chapter 12
The preceding chapter concluded by pointing out that not all complexity
can be addressed by rebalancing the coupling forces. In some cases, all this
can do is merely transform local complexity into global complexity, or vice
versa. This is growth-induced complexity, and it is the focus of this chapter.
I will begin by exploring how systems grow. You will learn why growth is
so advantageous for systems in general, understand what constrains a
system’s potential for growth, and learn how to extend growth limits. For
this, prepare for a rollercoaster ride through multiple disciplines. This
chapter delves into the universal laws at the core of a wide variety of
systems, from physics and biology to social sciences. We’ll even seek
guidance from the father of modern science, Galileo Galilei. Ultimately, all
these topics will be used to illustrate the fractal nature of software design.
Growth
Network-Based Systems
1. Space filling: While knowledge can change its form, it ultimately reaches
all components of a system: from high-level modules all the way down
to the machine code instructions executed at the system’s lowest levels.
2. Invariant terminal units: In the end, all knowledge communicated across
a system’s modules is translated into machine code instructions.
Regardless of system size, the machine code and the hardware executing
it remain the same.
3. Optimization: The network delivering knowledge, represented by the
design of component boundaries, can be optimized. This optimization
reflects the process of improving a system’s design and evolving its
functionality.
Now that we’ve established that software design is a network-based system,
let’s see what insights we get by applying West’s research. Let’s start by
discussing why systems grow in the first place.
Not all aspects of a network-based system increase at the same rate as the
system grows. Some grow faster, and others are slower. Consider the
following example: If a city doubles in size, would it need twice as many
gas stations to supply fuel to its doubled population? Interestingly, the
answer is no. Studies show that if a city doubles in size, it will need only
85% more gas stations (Kuhnert et al. 2006).
These are examples of sublinear scaling: Even if the system doubles in size,
the required energy increases by a lesser factor. This is illustrated in Figure
12.2. Line “a” scales linearly, while line “c” scales sublinearly—slower
than any linear function.
Other aspects of a system can grow superlinearly—faster than any linear
function, as illustrated by line “b” in Figure 12.2. For instance, if a city is
twice as large as another one, its citizens will have more than twice as many
social interactions and opportunities (West 2018).
Figure 12.2 Growth dynamics: Linear (a), superlinear (b), and sublinear (c)
Larger animals consume fewer calories per unit of weight than smaller
ones.
A larger ship experiences fewer drag forces per unit of cargo and thus is
more efficient than a smaller ship.
As a city’s population grows, its productivity and innovation often
increase at a faster rate than the population itself.
That said, if a larger system is more effective than its smaller counterpart,
then why can’t a system keep growing indefinitely?
Growth Limits
Therefore, if a beam is scaled in size, its weight will grow at a faster pace
than its ability to resist fracture. Galileo used this argument to explain why
there are no sky-high horses or cats—their bones would collapse under their
own weight:
You can plainly see the impossibility of increasing the size of structures to
vast dimensions either in art or in nature; . . . also it would be impossible to
build up the bony structures of men, horses, or other animals so as to hold
together and perform their normal functions . . . for if a man’s height be
increased inordinately he will fall and be crushed under his own weight. —
Galileo Galilei
The different growth rates of the different elements of a system also explain
why network-based systems have an inherent growth limit. They can grow
and become more efficient until they break “under their own weight”: until
the negatives overpower the positives.
Assume you’ve just finished working on the first version of the WolfDesk
system. It currently has basic functionality for managing the lifecycles of
support cases and managing and authorizing users, and it has a basic user
interface that support agents can use.
The next version of the system should double its functionality.2 The
business needs a more elaborate management of support cases, as well as
automating tasks that currently have to be carried out manually (e.g.,
assigning cases to agents).
Figure 12.4 Extending functionality of a system involves sublinear growth of its knowledge.
That said, as Chapter 11 discussed, extending the functionality of a system
entails adding more and more components to it. Whether the components
are services, objects, or just methods, they are needed to host the new
functionality.
Components, however, are not the only artifact added to the system.
Interactions between them are needed to make the system work. Since a
component is likely to interact with more than one other component, the
overall interactions in a system grow superlinearly.
3. Exactly the same reasoning and example can be used to describe why
teams beyond a certain size become ineffective.
Figure 12.5 The number of interactions grows superlinearly relative to the number of interacting
entities.
Does this mean that beyond a certain size, software systems cannot be
extended with new functionality? No. Let’s go back to network-based
systems to understand how growth limits can be extended.
Innovation
Galileo not only defined and explained growth limits but also described
how to overcome them:
. . . increase in height can be accomplished only by employing a material
which is harder and stronger than usual, or by enlarging the size of the
bones, thus changing their shape until the form and appearance of the
animals suggest a monstrosity. —Galileo Galilei (emphasis added)
To overcome growth limits, Galileo proposes two solutions. The first one is
to use stronger materials. If, for example, a horse’s bones were made from
steel, it would be able to grow to much greater sizes. The second option is
to make changes in form and proportions in a way that will accommodate
the increased weight. In Figure 12.7 you can see Galileo’s sketch of how the
proportions of a bone would have to be changed to withstand a threefold
increase in size.
Figure 12.7 Galileo’s sketch of a change in the form of a bone needed to accommodate a threefold
increase in size
Historical data shows that about 5,000 years ago, the largest human
settlements counted about 50,000 inhabitants. Two thousand years ago, the
number rose to 1 million. One hundred years ago—6.5 million. Today
(2024)—37 million. What happened during these years? Innovations in
construction methods allowed us to dramatically increase the number of
people comfortably living in the same area. For example, steel-framed
skyscrapers vastly increased city capacities through vertical growth.
Moreover, innovations in transportation and communication enabled us to
push the limits even further, allowing people to live farther from their
workplaces. In other words, innovations have historically transformed both
the “materials” and “structure” of cities, enabling them to transcend their
previous growth limits.
A software system’s growth limit is the moment it turns into a Big Ball of
Mud (Foote and Yoder 1997):
Abstraction as Innovation
Software modules form the pipes through which knowledge flows across
the system. When the interactions and dependencies between these
components become too complex to comprehend, it signals the need to
optimize the flow of knowledge.
Chapter 11 discussed strategic software changes and how some of them can
be addressed by rebalancing coupling: adjusting integration strength or
distance to adapt to changes in other forces. However, as I reiterated in this
chapter’s introduction, rebalancing may not always be sufficient. In certain
cases, more comprehensive structural changes are needed.
Fractal Geometry
A fractal is a geometric shape that can be split into parts, each of which is a
reduced-scale copy of the whole. In other words, it’s a pattern that repeats
itself on different scales. This property is called self-similarity.
While the mathematical fractals are fascinating, there is more to the concept
of self-similarity. Nature extensively uses fractals as blueprints for various
phenomena, ranging from the infinitesimal to the immense (Figure 12.11).
We can observe self-similarity in the branching of trees, the network of
veins in a leaf, and the formation of clouds. Even on a cosmic scale, the
distribution of galaxies across the universe exhibits fractal properties. In a
sense, fractals represent nature’s favored formula for balancing chaos and
order.
4. West, G.B., J.H. Brown, and B.J. Enquist. 1999. “The fourth dimension
of life: Fractal geometry and allometric scaling of organisms.” Science 284,
no. 5420: 1677–79. https://doi.org/10.1126/science.284.5420.1677.
Fractal Modularity
Key Takeaways
This chapter concludes our exploration of the forces that shape software
design. You learned what network-based systems are, and what makes
software design an energy supply network. A system’s design “pumps” its
energy—knowledge—across interfaces of its components.
Not all aspects of a network-based system grow at the same pace as the
system itself. Some grow slower than the system—sublinearly—and others
grow faster than the system—superlinearly. The differences in growth rates
make larger systems more efficient. Expanding functionality of a system
allows building on top of the knowledge already implemented in it.
However, as a system grows, its undesired aspects become more efficient as
well. In the case of a software system, as its functionality is expanded, its
complexity grows superlinearly.
Quiz
1. Data
2. Knowledge of the business domain
3. Knowledge of how the system is designed
4. Answers B and C are correct.
1. Sublinear growth
2. Linear growth
3. Superlinear growth
4. All grow at the same speed.
Microservices
For the high-level architecture of the WolfDesk system, the teams adopted a
microservices-based approach. This decision was driven by the flexibility
and modularity that microservices offer, allowing independent
development, deployment, and scaling of system components. Let’s see
some of the design dilemmas that the teams faced.
[
{
"eventId": 200452,
"eventType": "CaseCreated",
"timestamp": "2023-07-04T09:00:00Z",
"caseId": "CASE2101",
"customerId": "CUST52",
"description": "Customer reports an issue wit
},
{
"eventId": 200453,
"eventType": "CaseAssigned",
"timestamp": "2023-07-04T10:30:00Z",
"caseId": "CASE2101",
"assignedTo": "AGNT007"
},
{
"eventId": 200454,
"eventType": "CaseUpdated",
"timestamp": "2023-07-04T14:15:00Z",
"caseId": "CASE2101",
"message": "..."
},
{
"eventId": 200455,
"eventType": "CaseEscalated",
"timestamp": "2023-07-05T16:45:00Z",
"caseId": "CASE2101",
"escalationLevel": "Level 2",
"message": "..."
},
{
"eventId": 200456,
"eventType": "CaseSolutionProvided",
"timestamp": "2023-07-08T11:30:00Z",
"caseId": "CASE2101",
"message": "..."
},
{
"eventId": 200457,
"eventType": "CaseResolved",
"timestamp": "2023-07-09T13:45:00Z",
"caseId": "CASE2101",
"message": "..."
},
{
"eventId": 200458,
"eventType": "CaseReopened",
"timestamp": "2023-07-11T09:15:00Z",
"caseId": "CASE2101",
"reason": "..."
},
{
"eventId": 200459,
"eventType": "CaseAssigned",
"timestamp": "2023-07-11T10:30:00Z",
"caseId": "CASE2101",
"assignedTo": "AGNT009"
},
{
"eventId": 200460,
"eventType": "CaseUpdated",
"timestamp": "2023-07-12T14:45:00Z",
"caseId": "CASE2101",
"message": "..."
},
{
"eventId": 200461,
"eventType": "CaseResolved",
"timestamp": "2023-07-13T16:00:00Z",
"caseId": "CASE2101",
"message": "..."
}
]
Figure 13.1 The Support Autopilot service subscribes to all events published by the Support Case
Management component.
The Support Autopilot team had access to all the information about support
cases and used it to train a machine learning (ML) model. This model is
later used for automatically generating solutions for new support cases.
Initially, the design was deemed successful. However, over time, friction
started to arise between the two teams: When the SCM ’s event-sourced
model evolved—either by adding new events or by modifying existing ones
—these changes had to be communicated and coordinated with the team in
charge of Support Autopilot . This often led to integration issues or
prolonged the time needed to evolve the model. Why did that happen? Let’s
analyze the design from the balanced coupling perspective.
{
"caseId": "CASE2101",
"caseVersion": 10,
"createdOn": "2023-07-04T09:00:00Z",
"lastModifiedOn": "2023-07-13T16:00:00Z",
"customerId": "CUST52",
"messages": [...],
"status": "RESOLVED"
"wasReopened": true,
"isEscalated": true,
"agent": "AGNT009",
"prevAgents": ["AGNT007"]
}
1. Private events that are used to model the lifecycle of support cases and
are utilized internally by the service
2. Public events that act as an integration-specific model by exposing the
minimum necessary knowledge for integration with other system
components
Internally, the private events are transformed into public events that act as
integration contracts with external services (Figure 13.2). Consequently, the
integration strength between the two services was reduced from model
coupling to contract coupling, effectively balancing the high values of
volatility and distance.
Figure 13.2 The new design of Support Case Management minimizes the knowledge it shares by
exposing a set of public events designed for integration with other microservices.
Although the integration strength and the distance between the two
microservices are identical to those in the preceding case study, this design
did not cause any integration issues. The key difference is in the rate of
changes. As a supporting subdomain, Desks has a low level of volatility,
which balances out the knowledge of its model shared across the boundary.
Architectural Patterns
All the following case studies in this chapter focus on the internals of the
Support Case Management service. This service is responsible for
implementing all use cases associated with the lifecycle of support cases.
Some use cases form an integral part of WolfDesk’s main help desk system,
while others are designed to be integrated into “customer” and “agent”
portals via the micro-frontend pattern (Mezzalira 2021). Although this
represents a relatively “broad” microservice, considering the system’s
implementation is still in its early stages, the team has opted to consolidate
related functionalities for now and plans to decompose them into finer-
grained services if the need arises in the future.
Note
During the initial design phases, the team decided to use the layered
architecture pattern to organize and orchestrate the components of the
Support Case Management service.
Figure 13.4 The layered architecture organizes components around their technical responsibilities.
Typically, each layer is implemented as a separate library or module. The
dependencies between them are downward facing: The presentation layer
depends on the application layer, the application layer depends on the
business logic layer, and the business logic layer is aware of the data access
layer.
Despite the clearly defined responsibilities of the layers, the Support Case
Management team encountered several issues with this approach. First and
foremost, the implementation of almost any feature required modification in
all four layers. From the integration strength perspective, that signifies
functional coupling across the layers.
Second, the team noticed that the components within each layer were
loosely related to each other. For example, the data access layer hosted code
for persisting different business entities. These objects were located close to
each other but rarely needed to change simultaneously (see Figure 13.5).
Or, in balanced coupling terms:
Within the perspective of a single service, the layers had high integration
strength over a high distance—global complexity.
Components within each layer had both low integration strength and low
distance between them—local complexity.
Figure 13.5 An architecture that focuses on technical responsibilities is prone to complexity.
Despite organizing the components both into vertical slices and layers, the
team still encountered difficulties, particularly when changes occurred in
the Support Cases Lifecycle slice. Let’s analyze why.
Figure 13.7 The team’s migration from layered architecture to ports and adapters
Now, the business logic layer—often called the domain or core layer—is
positioned at the very top of the hierarchy. The objects and processes
implemented in the domain layer are referenced and used by the application
layer. Ultimately, both the data access layer and the presentation layer are
combined into the infrastructure layer, responsible for integrating concrete
infrastructural components for storing and presenting the application’s data.
For the inverted dependencies to work, the application logic and the domain
logic use interfaces—ports—for describing the required infrastructural
components. In turn, the infrastructure layer contains concrete
implementations of these interfaces—adapters. Listing 13.3 defines a “port”
for a repository: an object encapsulating operations for saving and
retrieving products’ data. The listing also illustrates the repository’s
concrete implementation in the infrastructure layer.
namespace WolfDesk.SCM.CustomerPortal.Domain {
public interface IProductRepository {
Product Load(ProductId id);
void Update(Product product);
IEnumerable<Product> FindAll();
IEnumerable<Product> FindByStatus(ProductStat
}
}
namespace WolfDesk.SCM.CustomerPortal.Infrastructure.
class PostgresProductRepository : IProductReposit
...
}
}
The complexity of the business logic related to the support cases’ lifecycles
pushed the team to refactor its vertical slice to use the ports and adapters
architecture, as illustrated in Figure 13.8.
Figure 13.8 The vertical slice architecture allows adopting an architectural pattern that is most
suitable for each slice.
Business Objects
Next, let’s focus on the business logic of the support cases and how it was
modeled at different stages of the project.
A customer has many support cases, but a support case belongs to one
customer.
A support case has one support agent, but the same agent can handle
multiple cases simultaneously.
A support case has multiple messages, and each message has a sender
and a recipient (customer to agent, or agent to customer).
Figure 13.9 The relationships between the four classes: SupportCase, Customer, Agent, and Message
Although the solution seemed convenient initially, the team faced multiple
issues because of it. First, engineers “overused” the option to traverse the
objects—for example, opening a support case, fetching the customer that
opened it, and then loading all other cases belonging to the same customer.
This resulted in performance issues when reading the data, as well as when
committing changes to a large number of objects in the same database
transaction.
Let’s analyze the design issue from the perspective of coupling forces. The
distance between the four classes is low: They are located within the same
module and reference each other. The integration strength is a bit tricky:
3. You might be concerned about the size of the messages collection. In the
case study the example is based on, the number of messages was never
higher than 100. Moreover, the actual contents of the messages—both body
and attachments—were persisted in an external blob storage.
class SupportCase {
...
private CustomerId openedBy;
private AgentId assignedAgent;
private List<Message> messages;
...
}
class Message {
...
private CustomerId customer;
private AgentId agent;
...
}
WolfDesk
./SupportCaseManagement
./SupportCases
./Domain
./Entities/
./SupportCase.cs
./Message.cs
./Priority.cs
./Status.cs
./Status.cs
./MessageBody.cs
./Recipient.cs
...
./Events
./CaseInitialized.cs
./MessageReceived.cs
./CaseResolved.cs
./CaseReopened.cs
...
./Factories/
./SupportCaseFactory.cs
./MessageFactory.cs
./Repositories/
./ISupportCaseRepository.cs
Hence, the team decided to bring closer those files that change together,
while spreading apart the rest. The approach is the same as in Case Study 4:
Minimize the distance between functionally coupled types, as illustrated in
Listing 13.7.
WolfDesk
./SupportCaseManagement
./Domain
./SupportCases
./Events
./CaseInitialized.cs
./CaseResolved.cs
./CaseReopened.cs
...
./ISupportCaseRepository.cs
./SupportCase.cs
./Status.cs
./Priority.cs
...
./Messages
./Message.cs
./MessageBody.cs
./MessageFactory.cs
./MessageReceived.cs
./Recipient.cs
...
Methods
Next, let’s delve into the SupportCase.cs file to analyze its design and
observe how its coupling forces were rebalanced.
The code shown in Listing 13.8 violates the Single Responsibility Principle
(Martin 2003), which states that a class or module should only have one job
or responsibility. The shift to a ports and adapters architecture made it
explicit that the integration of infrastructural components for sending
notifications should not be located in the domain layer. Instead, an interface
(port) was defined in the domain layer, with a concrete implementation in
the infrastructure layer (Listing 13.9).
....
namespace WolfDesk.SCM.Infrastructure.Cases {
....
namespace WolfDesk.SCM.Domain.Cases.Notifications {
public interface IEmailNotificationProvider {
void Send(Email email);
}
public interface ISmsNotificationProvider {
void Send(PhoneNumber phone, SMS message);
}
...
}
When a customer replies to a support case, the assigned agent must reply
within a set amount of time, which depends on both the priority of the
support case and the SLAs established by the agent’s department (Listing
13.11).
Listing 13.12 Extracting the Code for Setting the Reply Due Date to a
Dedicated Method
Still, the code appears somewhat bulky. Let’s analyze it from the
perspective of knowledge sharing. Should the SupportCase object be
aware of how SLAs are calculated? In other words, does it need to know
that the calculation is based on the department to which the agent belongs,
and not some other strategy? For example, if it was decided that the
response time depends on the agent’s shift schedule, should the
SupportCase object be aware of that as well?
5. The name of the pattern may sound misleading, as “services” are usually
associated with physical boundaries; for example, web services. That’s not
the case here. A domain service is a logical boundary; it’s an object/class
implementing a business algorithm or process.
Listing 13.14 Extracting the SLA Calculation Logic into a Domain Service
Further distancing the logic for calculating the due date for the agent’s
response reduces the SupportCase ’s code and knowledge, allowing it to
focus on the functionality it is supposed to implement: the lifecycle of
support cases.
Key Takeaways
I hope this chapter sounded repetitive. Maybe you even thought, “Oh, here
he goes again with that weak strength—increase distance—and strong
integration—reduce distance.” If you did, that makes me happy, as that was
the goal of this chapter: to demonstrate the fractal nature of modular design,
and that no matter what architectural or design pattern you implement, all
share the same self-similarity principle: balanced coupling.
Quiz
The case studies I described in this chapter applied some of the well-known
design patterns and principles and discussed their relevance using the
balanced coupling model, including microservices, bounded contexts,
vertical slices, layered architecture, ports and adapters architecture, event
sourcing, aggregates, the interface segregation principle, and the single
responsibility principle.
As this chapter was intended to summarize the book’s material, there won’t
be any quiz questions this time. Instead, try using the balanced coupling
model to analyze other architectural styles, patterns, and principles that
weren’t covered in this chapter. I particularly suggest looking into the
following topics:
Conclusion
Shared knowledge reflects how familiar the components are about the
responsibilities and implementation details of each other. The greater the
shared knowledge, the more likely it is that coupled components will
need to change in tandem. The kind of knowledge shared across
components’ boundaries can be assessed using the four levels of the
integration strength model: contract, model, functional, and intrusive
coupling.
The physical distance between coupled components defines how
interrelated their lifecycles are. The closer the components, the higher
the likelihood that a change in one will trigger a change in the other.
Conversely, a greater distance requires more effort to implement changes
that affect both coupled components.
A software design decision can steer the system toward either modularity or
complexity—based on the shared knowledge and distance it results in.
However, striving for a perfect balance between the two is not always cost-
effective, or even possible. These are the cases where the upstream
component is not expected to change.
As you finish reading this book, consider the words you are reading. What
would happen if one word had to be changed? What would be the impact of
this change? Some other words in the same sentence would likely need to
change as well. What about other sentences in the same paragraph? Maybe
they would need to change too. If the original change was a significant one,
it could affect other paragraphs in the same chapter. Could it affect other
chapters? Possibly, but even less likely.
The trickier question, however, is how do we decide what things are related
and which aren’t. For example, when writing, it would be possible to group
words based on their lengths: the first paragraph listing one-letter words,
the next one listing two-letter words, and so on. Alternatively, we could
write words out in alphabetical order! Of course, that wouldn’t be useful.
Instead, we group words into sentences according to their interrelationships
so that sentences convey ideas. The same reasoning applies to sentences.
They are grouped into paragraphs to convey larger ideas. Paragraphs are
grouped into chapters to articulate bigger ideas, and chapters into books—
even bigger ideas.
I hope that the balanced coupling model has demonstrated how this
fundamental principle of organization is crucial to designing modular
software. Instead of ideas, we group components according to their goals—
their responsibilities. Although functionalities vary across different levels of
abstraction, the basic organizing principles remain the same.
Appendix A
Glossary of Coupling
Distance also affects the components’ lifecycle coupling: the closer the
components are, the higher the likelihood that they will have to be evolved,
tested, and deployed simultaneously.
The four basic levels of the integration strength model are based on module
coupling but condense and adapt the terminology to the context of modern
software systems.
Both the model coupling and contract coupling levels of the integration
strength model reflect different levels of semantic coupling. That said, high
semantic coupling of an integration contract (contract coupling) will often
result in fewer cascading changes than low semantic coupling in the context
of model coupling.
Question 1: B
Question 2: C
Question 3: D
Question 1: B
Question 2: C
Question 3: B
Question 4: B
Question 5: C
Question 1: C
Question 2: D
Question 3: B
Question 4: C
Question 5: D
Question 1: D
Question 2: C
Question 3: A
Question 4: D
Question 5: C
Question 1: C
Question 2: B
Question 3: B
Question 4: D
Chapter 6: Connascence
Question 1: B
Question 2: D
Question 3: B
Question 4: D
Question 1: F
Question 2: E
Question 3: E
Question 4: D
Question 5: A
Question 6: B
Question 7: D
Question 8: B
Question 9: B
Question 10: D
Chapter 8: Distance
Question 1: A
Question 2: D
Question 3: B
Question 4: B
Question 5: A
Question 6: D
Chapter 9: Volatility
Question 1: D
Question 2: A
Question 3: E
Question 4: D
Question 1: E
Question 2: B
Question 3: A
Question 4: D
Question 1: B
Question 2: D
Question 3: C
Question 4: C
Question 1: C
Question 2: D
Question 3: A
Question 4: D
Question 5: D
Bibliography
(Baldwin 2000) Baldwin, C.Y., and K.B. Clark. 2000. Design Rules: The
Power of Modularity. Cambridge, MA: The MIT Press.
(Ben-Jacob and Herbert 2001) Ben-Jacob, Eshel, and Herbert Levine. 2001.
“The artistry of nature.” Nature 409, no. 6823: 985–86.
(Booth et al. 1976) Booth A., S. Welch, and D.R. Johnson. 1976.
“Crowding and Urban Crime Rates.” Urban Affairs Quarterly 11, no. 3:
291–308. https://doi.org/10.1177/107808747601100301.
(Box 1976) Box, George E.P. 1976. “Science and Statistics.” Journal of the
American Statistical Association 71, no. 356: 791–99.
https://doi.org/10.1080/01621459.1976.10480949.
(Caldarelli 2007) Caldarelli, Guido. 2007. Scale-Free Networks: Complex
Webs in Nature and Technology. Oxford, UK: Oxford University Press.
(Foote and Yoder 1997) Foote, Brian, and Joseph W. Yoder. 1997. “Big Ball
of Mud.” Department of Computer Science, University of Illinois at
Urbana-Champaign, August 26, 1997.
(Fowler et al. 1999) Fowler, Martin, Kent Beck, John Brant, William
Opdyke, and Don Roberts. 1999. Refactoring: Improving the Design of
Existing Code, 1st Edition. Reading, MA: Addison-Wesley.
(Gamma et al. 1995) Gamma, Erich, Richard Helm, Ralph Johnson, John
Vlissides, and Grady Booch. 1995. Design Patterns: Elements of Reusable
Object-Oriented Software. Reading, MA: Addison-Wesley.
(Hohpe and Woolf 2004) Hohpe, Gregor, and Bobby Woolf. 2004.
Enterprise Integration Patterns: Designing, Building, and Deploying
Messaging Solutions. Boston: Addison-Wesley.
(Hu et al. 2022) Hu, Xinlan Emily, Rebecca Hinds, Melissa Valentine, and
Michael S. Bernstein. 2022. “A ‘Distance Matters’ Paradox: Facilitating
Intra-Team Collaboration Can Harm Inter-Team Collaboration.”
Proceedings of the ACM on Human-Computer Interaction 6, CSCW1,
Article 48: 1–36. https://doi.org/10.1145/3512895.
(Hunt and Thomas 2000) Hunt, Andrew, and David Thomas. 2000. The
Pragmatic Programmer: From Journeyman to Master, 1st Edition.
Reading, MA: Addison-Wesley.
(Jamilah et al. 2012) Jamilah, Din, A.B. AL-Badareen, and Y.Y. Jusoh.
2012. “Antipatterns Detection Approaches in Object-Oriented Design: A
Literature Review.” 7th International Conference on Computing and
Convergence Technology (ICCCT), Seoul, Korea (South), pp. 926–31.
(Maniloff 1996) Maniloff, J. 1996. “The Minimal Cell Genome: ‘On Being
the Right Size.’” Proceedings of the National Academy of Sciences of the
United States of America 93, no. 19: 10004–06.
(Parnas 2003) Parnas, David Lorge, and P. Eng. 2003. “A Procedure for
Interface Design.”
(Parnas 1985) Parnas, David L., P. Clements, and D. Weiss. 1985. “The
Modular Structure of Complex Systems.” IEEE Transactions on Software
Engineering SE-11, no. 3: 259–66. https://doi.org/10.1109/tse.1985.232209.
(Perrow 2011) Perrow, Charles. 2011. Normal Accidents: Living with High
Risk Technologies, Updated Edition. Princeton, NJ: Princeton University
Press.
(Santos et al. 2019) Santos, Pedro M., Marco Consolaro, and Alessandro Di
Gioia. 2019. Agile Technical Practices Distilled: A Learning Journey in
Technical Practices and Principles of Software Design. Birmingham, UK:
Packt Publishing.
(Taleb 2011) Taleb, Nassim Nicholas. 2011. The Black Swan: The Impact of
the Highly Improbable. London: Allen Lane.
(Thomas and Richard 1996) Bergin, Thomas J., and Richard J. Gibson.
1996. “Supplemental Material from HOPL II.”
A/B testing, 23
abstraction, 66, 136–137, 192, 226, 241
car, 66, 67–68
effective, 71
hierarchical, 68
as innovation, 226, 228
leaking, 72
modules as, 66–68
software module, 66
accidental complexity, 36, 37, 71
act–sense–respond, 25
adapter, 138
afferent coupling, 265
aggregate pattern, 248–249
Agile software development, 165–166
algorithm, 120
connascence of, 102
machine learning (ML), 195
anti-pattern, Big Ball of Mud, 71
API, 64, 135, 137, 138, 140
application layer, 239, 241–243
architecture, 239
layered, 239, 240, 243
ports and adapters, 243, 244
vertical slice, 241
arithmetic constraint, 107
assumptions, 72–73, 205
asynchronous execution, 146–147
asynchronous integration, 146–147, 159, 160
facade, 138
failure, system, 38
false negatives, 174
false positives, 174
flow of knowledge, 10, 15, 184
Foote, Brian, 71
Fortran, COMMON statement, 83
fractal geometry, 228–230
fractal modularity, 230
Fulfillment service, 119, 120
functional coupling, 125–126, 127, 128, 185, 196, 208, 209, 210, 268
function/ality, 58
boundary-enclosing, 63–64
business subdomains, 169, 170–171
constraints, 53
core subdomains, 171–172
generic subdomains, 172–173
interchangeable camera lenses, 61
LEGO brick, 61
module, 60, 68
objects, 156
software module, 62–63, 64
symmetric, 126, 127
G
Hawking, Stephen, 19
hierarchical complexity, 39, 40–41
high balance, 191
I
implementation, 67
implicit interface, 118
implicit knowledge, 53
implicit shared knowledge, 9
inferred volatility, 177, 178
infrastructure layer, 243, 244
innovation, 241
abstraction as, 226
construction, 224
software design, 225
integration, 89
asynchronous, 146–147, 159, 160
contract, 134, 135–138, 139–140
legacy system, 190
maintenance effort, 189–190, 191
-specific event, 237
strength, 161, 170, 175, 176–177, 195, 268. See also integration
strength
synchronous, 159
integration strength, 121, 143, 144, 147, 161, 170, 175–177, 195, 230,
268
contract coupling, 134, 135–138, 139–140, 141, 142
functional coupling, 125–126, 127, 128
intrusive coupling, 122, 123, 124
interactions, 39, 54, 178
complex, 37, 39, 118, 166
global complexity, 40–41, 42
linear, 36
superlinear growth, 221
system, 13, 14–15
interchangeable camera lenses, 59, 61
interface, 72
complexity, 118
implicit, 118
module, 67
transparency, 118
type, 118
Interface Segregation Principle, 253
intrusive coupling, 122, 123, 124, 184, 268
J-K
Kay, Alan, 73
knowledge, 50–51, 54, 72, 98, 203, 226, 237
boundary, 69
component, 14
constraints, 53
encapsulation, 140
expertise, 20, 22, 29
explicit, 52–53
flow, 10, 15, 184
hiding, 70
implicit, 53
shared, 8, 9, 15, 16–17, 68–69, 73, 89, 92, 133, 152, 166, 183, 185,
244, 257
sharing, 131
tacit, 37
known unknowns, 22
Page-Jones, Meilir, 97
Parnas, David L., 65–66, 127, 136, 228
“On the Criteria to Be Used in Decomposing Systems into
Modules”, 62
pathological coupling, 81–83
Perrow, Charles, Normal Accidents: Living with High-Risk Technologies,
36, 39
PL/I language, 86
ports and adapters architecture, 243, 244
presentation layer, 239, 240
private events, 237, 238
probe–sense–respond, 23, 25
programming language, object-oriented, 11
proximity, 160
public events, 237
purpose, system, 13, 14
push notification, 123
Q-R
queries, 137
Query object, 50–51, 53
race condition, 85
refactoring, 167, 184
reflection, 82, 113, 123
regulations, 205
relational database, 106
remote work, 168
repository, 48
REST API, 64
results
intended, 37–38
unintended, 38
Retail service, 119, 120
runtime coupling, 159, 269
tacit knowledge, 37
tactical changes, 202
teams, organizational distance, 167–168
technical debt, 167
testing, A/B, 23, 24
tolerances, 16
TrackCustomerEmail method, 253–254
transactional coupling, 125–126, 270
transparency, interface, 118
Triangle class, 107, 108
type, connascence, 100
uncertainty, 32
unknown unknowns, 22–23
upstream module, 10, 82–83, 184
variable, global, 87
vertical slice architecture, 241
volatility, 183, 192, 195, 210. See also change/s
core subdomains, 171
defined, 270
and distance, 187
high/low, 185–187
inferred, 177, 178
and integration strength, 175, 176–177
numeric scale, 192–193
subdomain, 173, 174
Yoder, Joseph, 71
Yourdon, Edward, Structured Design, 62, 73, 80
Code Snippets