0% found this document useful (0 votes)
2K views557 pages

Balancing Coupling in Software Design Universal Design Principles For Architecting Modular Software Systems (Vlad Khononov)

This eBook provides information about the ePUB format and its customization options for enhancing the reading experience. It includes praise for the book 'Balancing Coupling in Software Design' by Vlad Khononov, highlighting its insights into coupling in software architecture and its importance for software professionals. The document outlines the structure of the book, which covers various aspects of coupling, complexity, and modularity in software design.

Uploaded by

photon628
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
2K views557 pages

Balancing Coupling in Software Design Universal Design Principles For Architecting Modular Software Systems (Vlad Khononov)

This eBook provides information about the ePUB format and its customization options for enhancing the reading experience. It includes praise for the book 'Balancing Coupling in Software Design' by Vlad Khononov, highlighting its insights into coupling in software architecture and its importance for software professionals. The document outlines the structure of the book, which covers various aspects of coupling, complexity, and modularity in software design.

Uploaded by

photon628
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 557

About This eBook

ePUB is an open, industry-standard format for eBooks. However, support of


ePUB and its many features varies across reading devices and applications.
Use your device or app settings to customize the presentation to your liking.
Settings that you can customize often include font, font size, single or
double column, landscape or portrait mode, and figures that you can click
or tap to enlarge. For additional information about the settings and features
on your reading device or app, visit the device manufacturer’s Web site.

Many titles include programming code or configuration examples. To


optimize the presentation of these elements, view the eBook in single-
column, landscape mode and adjust the font size to the smallest setting. In
addition to presenting code and configurations in the reflowable text format,
we have included images of the code that mimic the presentation found in
the print book; therefore, where the reflowable format may compromise the
presentation of the code listing, you will see a “Click here to view code
image” link. Click the link to view the print-fidelity code image. To return
to the previous page viewed, click the Back button on your device or app.
Praise for Balancing Coupling in Software Design

“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!”

—Gregor Hohpe, author of The Software Architect Elevator

“Get ready to unravel the multi-dimensional nature of coupling and the


forces at work behind the scenes. The reference for those looking for a
means to both assess and understand the real impact of design decisions.”

—Chris Bradford, Director of Digital Services, Cambridge Consultants

“Coupling is a tale as old as software. It’s a difficult concept to grasp and


explain, but Vlad effortlessly lays out the many facets of coupling in this
book, presenting a tangible model to measure and balance coupling in
modern distributed systems. This is a must-read for every software
professional!”

—Laila Bougria, solutions architect & engineer

“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.”

—Michael Plöd, fellow @ INNOQ

“Every software engineer is sensitive to coupling, the measure of


interconnection between parts. Still, many times the understanding of such
a fundamental property remains unarticulated. In this book, Vlad introduces
a much-needed intellectual tool to reason about coupling in a systematic
way, offering a novel perspective on this essential topic.”

—Ilio Catallo, senior software engineer

“Coupling is among the most slippery topics in software development.


However, with this book, Vlad simplifies for us how coupling, from a great
villain, can become a design tool when well understood. This is an
indispensable guide for anyone dealing with software design—especially
complex ones.”

—William Santos, software architect

“Balancing Coupling in Software Design is a must-read for any software


architect. Vlad Khononov masterfully demystifies coupling, offering
practical insights and strategies to balance it effectively. This book is
invaluable for creating modular, scalable, and maintainable software
systems. Highly recommended!”
—Vadim Solovey, CEO at DoiT International

“Balancing Coupling in Software Design by Vlad Khononov is an essential


read for architects aiming for quality, evolvable systems. Khononov
expertly classifies dependencies and reveals how varying designs impact
effort based on component distance and change frequency, introducing a
unified metric for coupling. With insightful case studies, he guides readers
toward achieving optimal modularity and long-term system adaptability by
illustrating and rectifying imbalances.”

—Asher Sterkin, independent software technology expert

“Khononov’s groundbreaking work unifies paramount forces of software


design into a coherent model for evaluating coupling of software systems.
His insights provide an invaluable framework for architects to design
modular, evolving systems that span legacy and modern architectures.”

—Felipe Henrique Gross Windmoller, staff software engineer, Banco do


Brasil

“This book systematizes over five decades of software design knowledge,


offering a comprehensive guide on coupling, its dimensions, and how to
manage it effectively. If software design is a constant battle with
complexity, then this book is about mastering the art of winning.”

—Ivan Zakervsky, IT architect


Balancing Coupling in Software Design
Universal Design Principles for Architecting Modular Software Systems

Vlad Khononov

Hoboken, New Jersey


Cover image: pernsanitfoto/Shutterstock

Many of the designations used by manufacturers and sellers to distinguish


their products are claimed as trademarks. Where those designations appear
in this book, and the publisher was aware of a trademark claim, the
designations have been printed with initial capital letters or in all capitals.

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.

For government sales inquiries, please contact


[email protected].

For questions about sales outside the U.S., please contact


[email protected].
Please contact us with concerns about any potential bias at
pearson.com/report-bias.html.

Visit us on the Web: informit.com/aw

Library of Congress Control Number: 2024942574

Copyright © 2025 Pearson Education, Inc.

All rights reserved. This publication is protected by copyright, and


permission must be obtained from the publisher prior to any prohibited
reproduction, storage in a retrieval system, or transmission in any form or
by any means, electronic, mechanical, photocopying, recording, or likewise.
For information regarding permissions, request forms and the appropriate
contacts within the Pearson Education Global Rights & Permissions
Department, please visit www.pearsoned.com/permissions/.

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

Series Editor Foreword


Foreword by Rebecca Wirfs-Brock
Foreword by Kent Beck
Preface
Acknowledgments
About the Author
Introduction
Part I: Coupling
Chapter 1: Coupling and System Design
What Is Coupling?
Magnitude of Coupling
Shared Lifecycle
Shared Knowledge
Flow of Knowledge
Systems
Coupling in Systems
Optional: Coupling and Cost Management in
Mechanical Engineering
Key Takeaways
Quiz
Chapter 2: Coupling and Complexity: Cynefin
What Is Complexity?
Complexity in Software Design
Complexity Is Subjective
Cynefin
Clear
Complicated
Complex
Chaotic
Disorder
Comparing Cynefin Domains
Cynefin in Software Design
Example A: Integrating an External Service
Example B: Changing Database Indexes
Cynefin Applications
Cynefin and Complexity
Key Takeaways
Quiz
Chapter 3: Coupling and Complexity: Interactions
Nature of Complexity
Complexity and System Design
Linear Interactions
Complex Interactions
Complexity and System Size
Hierarchical Complexity
Optimizing Only the Global Complexity
Optimizing Only the Local Complexity
Balancing Complexity
Degrees of Freedom
Degrees of Freedom in Software Design
Degrees of Freedom and Complex Interactions
Complexity and Constraints
Example: Constraining Degrees of Freedom
Constraints in Cynefin Domains
Coupling and Complex Interactions
Example: Connecting Coupling and Complexity
Design A: Using SQL to Filter Support Cases
Design B: Using a Query Object
Design C: Using Specialized Finder Methods
Coupling, Degrees of Freedom, and Constraints
Key Takeaways
Quiz
Chapter 4: Coupling and Modularity
Modularity
Modules
LEGO Bricks
Camera Lenses
Modularity in Software Systems
Software Modules
Function, Logic, and Context of Software Modules
Effective Modules
Modules as Abstractions
Modularity, Complexity, and Coupling
Deep Modules
Modularity Versus Complexity
Modularity: Too Much of a Good Thing
Coupling in Modularity
Key Takeaways
Quiz
Part II: Dimensions
Chapter 5: Structured Design’s Module Coupling
Structured Design
Module Coupling
Content Coupling
Common Coupling
External Coupling
Control Coupling
Stamp Coupling
Data Coupling
Comparison of Module Coupling Levels
Key Takeaways
Quiz
Chapter 6: Connascence
What Is Connascence?
Static Connascence
Connascence of Name
Connascence of Type
Connascence of Meaning
Connascence of Algorithm
Connascence of Position
Dynamic Connascence
Connascence of Execution
Connascence of Timing
Connascence of Value
Connascence of Identity
Evaluating Connascence
Managing Connascence
Connascence and Structured Design’s Module Coupling
Key Takeaways
Quiz
Chapter 7: Integration Strength
Strength of Coupling
Structured Design, Connascence, or Both?
Structured Design and Connascence: Blind Spots
Different Strategy
Integration Strength
Running Example: Sharing a Database
Intrusive Coupling
Examples of Intrusive Coupling
Running Example: Intrusive Coupling by Sharing a
Database
Effects of Intrusive Coupling
Functional Coupling
Degrees of Functional Coupling
Causes for Functional Coupling
Running Example: Functional Coupling by Sharing a
Database
Effects of Functional Coupling
Model Coupling
Degrees of Model Coupling
Running Example: Model Coupling by Sharing a
Database
Effects of Model Coupling
Contract Coupling
Example of Contract Coupling
Degrees of Contract Coupling
Depth of Contract Coupling
Running Example: Contract Coupling by Sharing a
Database
Effects of Contract Coupling
Integration Strength Discussion
Example: Distributed System
Integration Strength and Asynchronous Execution
Key Takeaways
Quiz
Chapter 8: Distance
Distance and Encapsulation Boundaries
Cost of Distance
Distance as Lifecycle Coupling
Evaluating Distance
Additional Factors Affecting Distance
Distance and Socio-Technical Design
Distance and Runtime Coupling
Asynchronous Communication and Cost of Change
Distance Versus Proximity
Distance Versus Integration Strength
Key Takeaways
Quiz
Chapter 9: Volatility
Changes and Coupling
Why Software Changes
Solution Changes
Problem Changes
Evaluating Rates of Changes
Domain Analysis
Source Control Analysis
Volatility and Integration Strength
Inferred Volatility
Key Takeaways
Quiz
Part III: Balance
Chapter 10: Balancing Coupling
Combining the Dimensions of Coupling
Measurement Units
Stability: Volatility and Strength
Actual Costs: Volatility and Distance
Modularity and Complexity: Strength and Distance
Combining Strength, Distance, and Volatility
Maintenance Effort: Strength, Distance, Volatility
Balanced Coupling: Strength, Distance, Volatility
Balancing Coupling on a Numeric Scale
Scale
Balanced Coupling Equation
Balanced Coupling: Examples
Key Takeaways
Quiz
Chapter 11: Rebalancing Coupling
Resilient Design
Software Change Vectors
Tactical Changes
Strategic Changes
Rebalancing Coupling
Strength
Volatility
Distance
Rebalancing Complexity
Key Takeaways
Quiz
Chapter 12: Fractal Geometry of Software Design
Growth
Network-Based Systems
Software Design as a Network-Based System
Why Do Systems Grow?
Growth Limits
Growth Dynamics in Software Design
Innovation
Innovation in Software Design
Abstraction as Innovation
Fractal Geometry
Fractal Modularity
Key Takeaways
Quiz
Chapter 13: Balanced Coupling in Practice
Microservices
Case Study 1: Events Sharing Extraneous Knowledge
Case Study 2: Good Enough Integration
Architectural Patterns
Case Study 3: Reducing Complexity
Case Study 4: Layers, Ports, and Adapters
Business Objects
Case Study 5: Entities and Aggregates
Case Study 6: Organizing Classes
Methods
Case Study 7: Divide and Conquer
Case Study 8: Code Smells
Key Takeaways
Quiz
Chapter 14: Conclusion
Epilogue
Appendix A: The Ballad of Coupling
Appendix B: Glossary of Coupling
Appendix C: Answers to Quiz Questions
Bibliography
Index
Series Editor Foreword

I recall meeting Vladik at a conference or two nearly a decade ago, by the


publication date of this, his new book. I recall Vladik, or Vlad if you like,
being a quiet and thoughtful person, and with a good sense of humor, which
scored high with me. He’s not overly quiet though, as he’s proven by his
insightful conference talks. Since that time, we met up now and then, with
our last in-person opportunity in New York City at a software architecture
conference just prior to the COVID-19 lockdown. Although I find that
reference point distasteful, it was thereafter a pivotal time when my
signature series got life. Shortly thereafter, I asked Vladik if he would write
a book for it. To my delight, he agreed. During the following years, Vladik
encountered several challenges, some of a personal family nature, and
others dealing with life and work during the crazy pandemic period. Yet, he
endured and persisted in his work. I reviewed Vladik’s book a few different
times and watched it transition from rough draft to finished product. I have
to say that experiencing the blend of past practices framed in a fresh and
powerful way was fascinating. I’ll explain more about that after I introduce
the purpose of this series.

My Signature Series is designed and curated to guide readers toward


advances in software development maturity and greater success with
business-centric practices. The series emphasizes organic refinement with a
variety of approaches—reactive, object, as well as functional architecture
and programming; domain modeling; right-sized services; patterns; and
APIs—and covers best uses of the associated underlying technologies.

From here I am focusing now on only two words: organic refinement.

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.

We can readily think of numerous organic objects—living organisms—from


the very large to the microscopic single-celled life forms. With the second
use of organism, though, examples may not as readily pop into our mind.
One example is an organization, which includes the prefix of both organic
and organism. In this use of organism, I’m describing something that is
structured with bidirectional dependencies. An organization is an organism
because it has organized parts. This kind of organism cannot survive
without the parts, and the parts cannot survive without the organism.

Taking that perspective, we can continue applying this thinking to nonliving


things because they exhibit characteristics of living organisms. Consider the
atom. Every single atom is a system unto itself, and all living things are
composed of atoms. Yet, atoms are inorganic and do not reproduce. Even
so, it’s not difficult to think of atoms as living things in the sense that they
are endlessly moving, functioning. Atoms even bond with other atoms.
When this occurs, each atom is not only a single system unto itself but also
becomes a subsystem along with other atoms as subsystems, with their
combined behaviors yielding a greater whole system.

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

Design happens in the cracks.

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.

Having said that, I heartily recommend reading Balancing Coupling in


Software Design. Strike that. I heartily recommend learning from
Balancing Coupling in Software Design. Read about a kind of crack, a
connection, go find it in your own code, find it in other people’s code, try
out variations on it, try out timings for changing it, watch how it affects the
behavioral changes you want to make. Then read about another kind of
crack. Compare and contrast. Dig in.

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

San Francisco, California

2024
Preface

Books on software design typically dedicate a few pages to coupling. On


rare occasions, you’ll find a whole chapter on the subject. Yet, while fads
come and go, coupling has been, is, and, I bet, always will be relevant.
Don’t believe me? Just take a moment and listen to the industry chatter. You
will hear the “coupling is bad” mantra everywhere. But what exactly is this
“coupling” thing? Is it always that bad, or does it become really bad after a
certain point? Can you even measure it? If so, how? These are the questions
I’ve sought answers to since I started working as a software engineer. All I
encountered was more and more of “Avoid coupling!” or “This architectural
pattern will save you from coupling!” or, even worse, “The only way to
avoid coupling is to use our product!” Sigh.

Around 2014, yet another “decoupling salvation” emerged: microservices. I


even remember a slide from some conference that read “Microservices is
the architecture for decoupling.” It was “microservices this” and
“microservices that,” but back then nobody could really define what a
microservice was. That didn’t stop me (or anyone else) from trying.
Pumped by the microservices/decoupling hype, we aimed to “decouple”
everything in the project I was working on. For that, we designed
microservices around business entities, with each API resembling mostly
CRUD1 operations. Each entity can be evolved independently, we said. The
result? A fiasco. No, a cosmic-scale fiasco.
1. Create, Read, Update, and Delete.

That failed project, however, turned out to be a blessing in disguise. I had to


figure out why what promised decoupling resulted in a coupling Godzilla. I
had to get it right. So, I set out to read all the papers and books that could
explain how to do microservices better. Eventually, I found an explanation.
All our design mistakes were described in Chapter 6 of the book Structured
Design (Yourdon and Constantine 1975). The title of the chapter?
“Coupling.”

That’s how my journey into coupling in software design started. I wanted to


learn everything that we knew but had forgotten. A few years later, all the
puzzle pieces started falling into place. Everything I learned began to form
a coherent picture—a three-dimensional model of how coupling affects
software projects. Gradually, I started applying this model in my day-to-day
work. It worked! What’s more, it completely changed the way I think about
software design.

At some point, I couldn’t keep it inside anymore, and I wanted to share my


findings. This led to a talk I gave at the Domain-Driven Design Europe
2020 conference, titled “Balancing Coupling in Distributed Systems.” As I
was walking off the stage, cortisol and adrenaline were conducting a stress
hormone conference of their own in my bloodstream. The only thing I
remember is Rebecca Wirfs-Brock telling me that I had to keep developing
these ideas and to write a book about it. Who am I to argue with Rebecca
Wirfs-Brock?

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.

Who Should Read This Book?

As I’m writing this Preface, Pearson’s style guidelines instruct me to “be


precise and resist the temptation to create a long list of potential readers.”
Well, then, I will define the book’s target audience as people who create
software.

Whether you are a junior, senior, or principal software engineer or architect,


as long as you are making software design decisions at any level of
abstraction, coupling can make or break your efforts. Learning to tame the
forces of coupling is essential for building modular and evolvable systems.

How This Book Is Organized

This book is divided into three parts.


Part I, Coupling—The first part of the book is about the big picture: how
coupling fits in the contexts of software design, complexity, and modularity.

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.

Chapter 2, Coupling and Complexity: Cynefin—Since complexity is


something we would rather avoid, it’s important to understand what it is in
the first place. To that end, the chapter introduces the basic principles of the
Cynefin framework that precisely defines what complexity is.

Chapter 3, Coupling and Complexity: Interactions—This chapter shifts the


discussion to systems in general and software design complexity in
particular. You will learn what makes a software system complex and what
that has to do with coupling.

Chapter 4, Coupling and Modularity—This chapter switches the focus to


what we would rather achieve: modularity. It defines the notions of
modularity and software modules. Most importantly, it discusses coupling:
the rudder that can steer a system toward either complexity or modularity.

Part II, Dimensions—The second part of the book homes in on coupling.


You will learn the different ways coupling affects systems and a number of
models for evaluating its effect.

Chapter 5, Structured Design’s Module Coupling—This chapter starts the


journey through time and introduces the first model of evaluating coupling
in software design, a model that was formulated in the late 1960s but is still
relevant today.

Chapter 6, Connascence—This chapter introduces a model that reflects a


different aspect of coupling: connascence. You will learn what it means for
modules to be “born together” and the different magnitudes of this kind of
relationship.

Chapter 7, Integration Strength—Here, we combine the aspects of coupling


reflected by structured design’s module coupling and connascence into a
combined model known as integration strength. You will learn to use this
model to evaluate the knowledge shared among the components of a
system.

Chapter 8, Distance—In this chapter, we switch the focus to a different


dimension: space. You will learn how the physical position of modules in a
codebase can affect their coupling.

Chapter 9, Volatility—Here, we switch the focus to the dimension of time.


We will discuss the reasons for changes in software modules, how a
module’s volatility can propagate across the system, and how you can
evaluate a module’s expected rate of change.
Part III, Balance—This part of the book connects the topics in Parts I and
II by turning the dimensions of coupling into a tool for designing modular
software.

Chapter 10, Balancing Coupling—In this chapter, we explore the insights


you can gain by combining the dimensions of coupling. The chapter also
introduces the balanced coupling model: a holistic model for evaluating the
effects of coupling on the overarching system.

Chapter 11, Rebalancing Coupling—Here, we discuss the strategic


evolution of a software system, the changes it brings, and how these
changes can be accommodated by rebalancing the coupling forces.

Chapter 12, Fractal Geometry of Software Design—In this chapter, we


continue the topic of system evolution, focusing on the most common and
important change: growth. This chapter combines knowledge from other
industries, and even nature, to uncover the underlying design principles
guiding software design.

Chapter 13, Balanced Coupling in Practice—We move from theory to


practical application in this chapter by discussing case studies that
demonstrate how the balanced coupling model can be used to improve
software design. The case studies also demonstrate that the balanced
coupling model can be observed at the heart of well-known architectural
styles, design patterns, and design principles.
Chapter 14, Conclusion—This chapter summarizes the book’s content and
provides final advice on applying the learned principles in your day-to-day
work.

Case Studies and WolfDesk

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 provides a help desk management system as a service. If your


startup company needs to offer support to your customers, WolfDesk’s
solution can get you up and running in no time.

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.

To prevent tenants from exploiting the business model by reusing existing


support cases, the lifecycle algorithm ensures that inactive support cases are
automatically closed, encouraging customers to open new ones when more
support is needed. Furthermore, WolfDesk implements a fraud detection
system that analyzes messages and identifies instances of unrelated topics
being discussed within the same support case.

In an effort to help tenants streamline their support-related work, WolfDesk


has implemented a “support autopilot” feature. This autopilot analyzes new
inquiries and attempts to automatically find a matching solution from the
tenant’s history. This function helps to further reduce the lifespan of cases,
encouraging customers to open new cases for additional questions.

The administration interface allows tenants to configure possible values for


support case categories, as well as a list of the tenant’s products that require
support. To ensure that support cases are routed to agents only during their
working hours, WolfDesk allows users to configure different shift schedules
for different departments and organizational units.

Register your copy of Balancing Coupling in Software Design on the


InformIT site for convenient access to updates and/or corrections as they
become available. To start the registration process, go to
informit.com/register and log in or create an account. Enter the product
ISBN (9780137353484) and click Submit. If you would like to be notified
of exclusive offers on new editions and updates, please check the box to
receive email from us.
Acknowledgments

I would like to extend my deepest gratitude to Vaughn Vernon, without


whom this book would not have become a reality. Vaughn not only
provided me with the incredible opportunity to bring these ideas to the
printed page, but he also supported me throughout the writing process.
Thank you for always being there when I needed help, and for your
invaluable advice and insights, which have significantly enriched this book.

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.

This book transcends different eras of software engineering and diverse


fields of study. This is hands down the most challenging project I have ever
worked on, and it wouldn’t have seen the light of day without the
contributions of so many people who helped me along the way. I want to
thank the subject matter experts whom I consulted during the writing
process2: Alistair Cockburn, Gregor Hohpe, Liz Keogh, Ruth Malan, David
L. Parnas, Dave Snowden, and Nick Tune.

2. Whenever I mention a group of people, the list is in alphabetical order by


last name.

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

Vlad (Vladik) Khononov wanted to make his own computer games, so at


eight years old, he picked up a book on BASIC. Although he has yet to
publish a game, software engineering became his passion and trade. With
over two decades of industry experience, Vlad has worked for companies
large and small, in roles ranging from webmaster to chief architect. As a
consultant and trainer, he currently helps companies make sense of their
business domains, untangle legacy systems, and tackle complex
architectural challenges.

Vlad maintains an active media career as an author and keynote speaker.


Besides the book you are holding, he has written Learning Domain-Driven
Design (O’Reilly, 2021), which has been translated into eight languages. As
a speaker, Vlad has presented at leading software engineering and
architecture conferences around the world. He is known for his ability to
explain complex concepts in simple, accessible terms, benefitting both
technical and nontechnical audiences. You can reach out to Vlad on X
(@vladikk) and LinkedIn.
Introduction

Imagine a top-notch Swiss watch: a marvel of engineering, tradition, and


craftsmanship combined. Its primary function, obviously, is accurate
timekeeping. The Official Swiss Chronometer Testing Institute requires that
watches not be off by more than 4 seconds slow or 6 seconds fast per day,
on average, during a testing period of 15 days. But that’s not all. Modern
watches also include complications, such as a chronograph, date display,
moon phase, or the simultaneous display of time in multiple time zones. All
this is achieved by hundreds of tiny components. Each part, no matter how
small, plays a crucial role in the overarching mechanism.

But that’s not all.

The precision of interactions between components is paramount. For


instance, if the connections are too tight due to insufficient lubrication,
excessive friction will cause the watch to run slow. Conversely, if the parts
are overly lubricated, the gears and springs will operate with too much
freedom, leading the watch to run fast. For the timepiece to maintain its
accuracy over time, it’s essential for the interactions between its
components to achieve perfect balance.

It’s just like coupling in software design.

Modern software systems consist of hundreds, sometimes thousands, of


modules. Even if a given module perfectly implements its functionality, it’s
not enough. To provide value, it has to be integrated into the system:
coupled to its other components. Just as with the delicate balance required
in the physical connections of a watch, software modules must be integrated
with careful consideration. If the coupling is too loose, the system may
become fragile and difficult to control; if it’s too tight, it lacks the flexibility
needed for efficient performance and future adaptation. A perfect balance is
essential for the system to be both robust and adaptable, mirroring the
meticulous harmony found in the world of precision watchmaking.

But how can balance be achieved?

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.

Why does it matter?

High-end mechanical watches represent more than just timekeeping


devices; they are tangible assets that can accumulate significant value over
time. This applies to successful software systems as well. The value of a
software system is not merely a reflection of its current functionality but
also its ability to evolve, to grow, and to address future requirements. As
you will learn in this book, an efficient design of cross-component
interactions is essential for architecting systems that withstand the test of
time.

At its core, it’s all about coupling.

Although this book is focused on coupling, its overall scope is broad.


Coupling affects everything we do: Whether you’re writing a function,
designing an object model, or architecting distributed systems, the
principles you will learn are universally applicable. Specifically, you will
learn to identify and evaluate the effects of coupling across multiple
dimensions. You will understand how these forces of coupling interact and
how to leverage them in making informed design decisions. The book
explores why certain design decisions result in complexity, while others
increase modularity of the system. It will take you beyond the typical “it
depends” answer to all software design questions; you will learn what “it
depends” on.
Part I

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.

Chapter 2 zeros in on the concept of complexity. It introduces the Cynefin


framework and uses it to define complexity.

Chapter 3 continues the discussion of complexity, this time in the context of


systems. You’ll discover what makes a system complex and why coupling is
an essential tool for managing complexity.

Chapter 4 concludes Part I by delving into the opposite of complexity:


modularity. You’ll learn what modularity is, why it is so sought after in
nearly every software system, and the trade-offs involved in designing
modules. Ultimately, the chapter will discuss the close relationship between
modularity, complexity, and coupling.
Chapter 1

Coupling and System Design

Strong or loose, we live in dread,

Of the coupling monster under the bed.

Yet without it, your system would flake,

Coupling is a pillar you can’t forsake.

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.

But is coupling necessarily the root of all evil?

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?

Coupling was around long before it became the nemesis of software


engineers worldwide. As illustrated in Figure 1.1, it comes from the Latin
word “copulare,” which originated from “co,” meaning together, and
“apere,” meaning fasten. Hence, “to couple” means to fasten together, or
simply to connect things.

Figure 1.1 The lexical origin of the word “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.”

That makes coupling a ubiquitous phenomenon. You can observe coupling


everywhere: If any two entities are connected, they are coupled. A
clockwork mechanism contains numerous gears and springs connected to
each other to measure time. Engines, axles, wheels, brakes, and other parts
are coupled to form vehicles. Organs are coupled to form living organisms,
including ourselves. On a smaller scale, particles interact with each other to
form everything in our universe. And on a much larger scale, celestial
bodies affect each other through gravitational forces—they remain coupled
despite the considerable distances between them.

Coupling suggests a relationship between connected entities. If they are


coupled, in some way they can affect each other. That said, as there are
different systems to build and different ways to design them, there are
different ways to connect components. Different designs result in different
outcomes and maintenance costs. When it comes to discussing coupling, we
often categorize it as either “loose/weak” or “tight/strong.” But what factors
actually drive coupling to be loose or tight?

Magnitude of Coupling

The magnitude of coupling reflects the interdependence of the connected


components; the stronger the connection is, the more effort will be needed
to maintain the relationship over time. That said, even “loosely coupled”
components are still connected and thus cannot be completely independent
of each other.
When it comes to software design, the higher the magnitude of the
coupling, the more often the coupled components will need to be changed
together. But what causes these shared reasons for change? There are two
main drivers: shared lifecycle and shared knowledge.

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 .

1. What a software module exactly is will be discussed in greater detail in


Chapter 4, Coupling and Modularity.

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.

In addition to encapsulation boundaries demonstrated in Figure 1.2, other


structural and organizational factors couple the lifecycles of components.
You will learn about them in more detail in Chapter 8, Distance.
Shared Knowledge

To be able to work together, coupled components have to share knowledge.


The shared knowledge can come in different forms: from awareness of
integration interfaces to functional requirements, to implementation details
of the corresponding module. When a portion of such shared knowledge
changes, a corresponding change needs to be applied in the connected
module. The more knowledge you share across the boundaries of coupled
components, the more cascading changes you will get.

Consider, for example, the CustomersService module illustrated in


Figure 1.3. It provides basic functionality to manage customer records and
works with—or is coupled with—a repository object that encapsulates
database access. The three alternative designs shown in the figure describe
different integration interfaces for the repository object, and thus, they
expose different amounts of knowledge:

The MySQLRepository object in Figure 1.3A, as communicated by its


name, uses a MySQL database to store customer information. The
knowledge of this implementation detail—the choice of a database—is
not encapsulated by the repository; thus, it is shared with the
CustomersService module. Switching to a repository that works
with a different storage mechanism, such as an in-memory
implementation used for testing, will require changes in the
CustomersService module.
The design in Figure 1.3B shifts from the CustomersService
module holding a dependency on a concrete implementation to
depending on the IRepository interface. On the one hand, the
knowledge of the exact database (MySQL) is now encapsulated, and the
CustomersService module is unaware of it. On the other hand, the
IRepository interface’s methods, such as BeginTransaction and
ExecuteSQL , reflect that the underlying storage mechanism belongs to
the family of relational databases. Switching to a key-value store that
doesn’t support executing SQL queries will require the
CustomersService module to change.
Ultimately, the IRepository interface in Figure 1.3C exposes two
rather abstract methods: Save and Query . This time, the knowledge
that a relational database is being used is also encapsulated. From the
CustomersService module’s perspective, the concrete
implementation of the IRepository interface can use a much wider
range of databases without the need to update the abstraction it uses.
Figure 1.3 Different designs share different amounts of knowledge.

To summarize, Figure 1.3A shares the most knowledge: the concrete


database that is being used, which is MySQL. Figure 1.3B reduces the
knowledge to the database family. Figure 1.3C encapsulates further and
exposes only the minimal knowledge needed for the CustomersService
module to implement its functionality. As you can see, the more knowledge
that is shared, the more shared reasons for change the coupled components
have; any changes to the “knowledge” have to be propagated across the
affected components.

To make things trickier, shared knowledge can be implicit. Components


may make implicit assumptions about the rest of the system, even when that
knowledge is not explicitly defined or shared—for instance, assuming that
the system runs on a specific version of an operating system or on particular
hardware.

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.

Consider the two coupled components in Figure 1.4. The Distribution


component references, and thus depends on, the CRM component.

The Distribution component must be aware of the CRM component’s


integration interface, functionality, and operational details. All of this
knowledge is shared by the CRM module through its integration interface.
As a result, the flow of knowledge occurs in the opposite direction of
dependency, as illustrated by the dashed arrow in Figure 1.4.
Figure 1.4 The flow of knowledge across coupled components

I use the terms “upstream” and “downstream” to describe the flow of


knowledge between components/modules:

An upstream component provides a functionality that other components


consume. Its interface exposes the knowledge of its functionality and
how to integrate with the component.
A downstream component consumes the upstream component’s
functionality. To use the upstream component, the downstream
component has to be aware of the knowledge shared through its
integration interface.

Going back to Figure 1.4, Distribution is the downstream component,


while CRM is the upstream component. In simpler terms, Distribution
is a consumer of CRM .

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.

Not only is software engineering abundant with different types of systems,


but software itself can be understood as a system of interconnected systems.
At a higher level, we encounter services, applications, scheduled jobs,
databases, and other components that are coupled together to fulfill the
system’s overarching purpose: its business functionality. However, even
these larger-scale components are systems in their own right, albeit on a
lower level. For example, consider the Processor service in Figure 1.5.
It is written in an object-oriented programming language, and as such, it is
composed of the classes needed to implement the service’s functionality.
The classes are the components required to fulfill the system’s purpose; that
is, the functionality of the service.
Figure 1.5 The components of a typical software system

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

The gears in Figure 1.6 are components of a clockwork system. The


purpose of a clockwork system is to measure and display time. However,
having the necessary gears and springs is not enough to achieve the
purpose. For the system to be useful, its components have to be connected
—coupled—so that they can work together. That said, putting them together
arbitrarily won’t do the trick. The components have to be coupled in a way
that achieves the goals of the system. Not only is coupling the glue that
holds a system together, but it also makes the value of the system higher
than the sum of its parts.

Figure 1.6 The components of a clockwork

(Image: Photocell/Shutterstock)

The three elements composing a system—components, interactions, and


purpose—are strongly interrelated:
A system’s purpose requires a certain set of components, and the
interactions between them.
The design of the components’ interfaces allows certain integrations and
prohibits others. Moreover, the functionality of the components enables
the system to achieve its purpose.
Interactions enable the system to achieve its purpose by orchestrating the
work of its components.

These interrelationships are illustrated in Figure 1.7.

Figure 1.7 The core elements of a system are interdependent.


Overall, given a system, you cannot change either one of the three elements
without affecting at least one of the other two. For example, let’s say you
want to extend the functionality of a software system; that is, its purpose.
Changing the purpose requires changes in its components: Services,
modules, and other parts will have to evolve to address the new
requirements. Furthermore, the changes in the components are likely to
trigger changes in the interactions, or the way the components are
integrated and are communicating with each other.

That brings us to another important concept in system design: boundaries.


As Ruth Malan said, “System design is inherently about boundaries (what’s
in, what’s out, what spans, what moves between) and about tradeoffs. It re-
shapes what is outside, just as it shapes what is inside” (Malan 2019). A
component’s boundary defines what knowledge belongs to the component
and what should remain outside of it—for example, what functionality
should be implemented by the component and what responsibilities should
be assigned to other parts of the system. Furthermore, the boundary
specifies how the component should interact with other parts of the system;
specifically, what knowledge is allowed to pass the boundaries. Ultimately,
components and interactions define the outcome that can be achieved with
the system design. That makes interactions—the design of how components
are coupled—an inherent part of system design.

As in systems in general, the concept of coupling is essential in software


design. It is often assumed that the goal should be to completely decouple
and have totally independent components. That’s not true. It’s impossible to
reduce coupling to zero. If two components are supposed to work together,
they have to share knowledge: Interactions require knowledge to be shared.
Without the interactions, or coupling, it would be impossible to achieve the
system’s purpose. It’s the interactions between the parts that make it a
system.

As software engineers, we often focus on the task of decomposing a system


into components. This involves examining the business domain and
deciding which parts can be broken up into services, modules, and objects.
Essentially, when architecting solutions we are often focusing only on the
boxes. However, the lines between them are at least as important. It is
essential to pay attention to the design of how those components interact—
what knowledge is being shared across them, how it is being shared, and
how it affects the overall system. Furthermore, as you’ve seen, the design of
components and their interactions are closely related. Coupling defines not
only what knowledge is allowed to flow between the components but also
what knowledge should never leave its component’s boundary. That makes
coupling a core tool for designing modular software systems.

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.

Optional: Coupling and Cost Management in Mechanical Engineering

Coupling as a design tool in mechanical engineering is not new. In


mechanical engineering, coupling is used to reduce costs by accounting for
unavoidable imperfections in the manufacturing process. Consider the two
parts with matching joints illustrated in Figure 1.8. If the cutouts of the two
joints were precisely identical, this could lead to waste due to
manufacturing imperfections. It’s expensive, and sometimes even
impossible, to precisely produce parts of an exact size. There will always be
manufacturing defects, however small.
Figure 1.8 Two parts that are designed to be connected through coupling joints

(Image: HL Studios/Pearson Education Ltd.)

To address this, the components’ connection points (coupling) are designed


with tolerances: permissible limits of variation in physical dimensions or
properties of a material. These tolerances allow for a certain amount of
slack, enabling reliable connections while also minimizing manufacturing
waste and thus production costs. The tolerances have to be designed
carefully. A tolerance factor that’s too high will lead to unreliable
connections, while a tolerance that’s too low can fail to address all the
possible manufacturing imperfections. As with any coupling, tolerances
have to be just right.

Key Takeaways

Coupling is an inherent part of system design. A system has a purpose and


components. The interactions between the components (coupling) are
needed for the components to achieve the system’s goals.

Coupling results from the components having to share knowledge, or


lifecycles, or both. There are different types of knowledge that can be
shared across the boundaries of coupled components. The more knowledge
that is shared, the higher the dependency between the components, and
thus, the more often the components will have to change together. Even if
components are not sharing knowledge, they can be coupled through shared
lifecycles.

When working on a codebase, assess how knowledge is shared across


coupled components and what their lifecycle dependencies are:

What do components need to know about other components in order to


work together? What will be the impact of a change in that shared
knowledge?
Can you identify components that have to be tested and redeployed only
because they share a lifecycle with other, more volatile components?
The goal of this chapter was to show that coupling should not be dismissed
as synonymous with bad design; instead, coupling is a design tool that you
shouldn’t forget about. What, then, is the force that derails software projects
and turns codebases into chaotic messes? That’s the topic of the next
chapter.

Quiz

1. What is coupling?

1. A common design flaw


2. A connection between two or more components
3. A design pattern
4. A dependency in an object model

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.

3. How does coupling affect system design?

1. Coupling affects how components are integrated with each other.


2. Coupling affects components’ boundaries.
3. Coupling defines whether the components can implement the system’s
purpose.
4. All of the answers are correct.

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

Coupling and Complexity: Cynefin

But if coupling isn’t the foe, then what can it be?

It’s complexity—the agent of anarchy.

To master the beast, its signs you must learn,

The Cynefin framework shows paths to discern.

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.

In the upcoming chapters, you will learn how to manage complexity in


software systems. But first, you must be able to define and identify it. In
this chapter, you’ll learn about the Cynefin framework, and you’ll use it to
define and recognize 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.

Put simply, we say that something is complex if it’s hard to comprehend. In


more formal terms, complexity reflects the cognitive load a person
experiences when interacting with any kind of system. The greater the
cognitive load, the more difficult it becomes to understand how the system
works, to control it, and to predict its behavior. Most importantly, in the
context of software design, the higher the complexity, the more effort is
needed to change the system.

Complexity in Software Design

Working on a complex codebase demonstrates all the aforementioned


effects of complexity. We consider a codebase to be complex if it makes
important information obscure. For example, it can be challenging to define
the responsibilities of components in a complex system. They are like
puzzle pieces: Although the components constitute the whole picture—the
system—you can’t reason about their individual functionalities. Instead,
you have to connect all the “puzzle pieces” to be able to see the behavior of
the resultant system.
Modifying or extending the functionality of a complex system is even more
daunting: The tangled components obfuscate which parts of the system need
to be changed. Moreover, it becomes increasingly difficult to predict how
these changes will impact the other components, not to mention the
overarching system as a whole.

Complexity Is Subjective

The relationship between cognitive load and the individual experiencing it


makes complexity a subjective concept: The level of complexity depends on
the observer. A person can evaluate complexity based only on their
expertise in the relevant domain. What may seem complex to one person
could be trivial to another. For example, a car problem that might take me
several days, or even weeks, to resolve could be a minor issue for a
seasoned mechanic.

This basic definition of complexity as the cognitive load on a person


explains how people use the term “complexity” colloquially. However, to
discuss and manage complexity in the domain of software design, a more
elaborate model is needed. To this end, I’m going to use the Cynefin
framework.
Cynefin

The process of crafting software systems involves making a multitude of


decisions, which is why we often use the term “design decision.” But what
underpins this decision-making process? What information is necessary for
making an informed decision? To address these questions, the remainder of
this chapter introduces a decision support framework known as Cynefin.
This framework guides decision making across various scenarios, proving
invaluable not only for software design decisions but also for revealing the
essence of complexity.

“Cynefin”1 is a Welsh term that embodies the multiple intertwined factors


in our environment and our experiences that influence our thoughts,
interpretations, and actions—all in ways we can never fully comprehend.
Dave Snowden (2007) formulated the Cynefin framework as a decision
support tool designed to match specific decision-making processes to the
situations where they are most effective.

1. Pronounced kuh-nev-in. Or, as Dan North put it, say “Kevin” and sneeze
halfway through (https://twitter.com/tastapod/status/979390926903742465).

The framework categorizes the situations in which we make decisions into


five domains: clear, complicated, complex, chaotic, and disorder. The
following sections will describe the differences between the five domains
and how each affects the decision-making process. Later in the chapter I
will show how the Cynefin framework can inform software design
decisions and aid in navigating the complexity of software systems.

Clear2

2. Formerly known as the “obvious” domain.

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.

For example, consider approaching a traffic light. The decision whether to


go on or to stop is obvious: You continue driving if the signal is green and
stop if it’s red or yellow. Or, using the sense–categorize–respond approach,
you do the following:

1. Sense: Identify the traffic light and its current status.


2. Categorize: Identify the meaning of the current traffic light color.
3. Respond: Keep driving or stop according to the traffic rules.

The clear domain ensures consistent decision making in predictable


situations through established rules.

Complicated

Complicated domains represent “known unknowns,” or areas where you


recognize your knowledge is insufficient for making an informed decision.
However, this awareness also enables you to identify and consult an expert
who possesses the required knowledge. With the expert’s advice, you can
decide the best course of action. Therefore, decision making in the
complicated domain involves the following three steps:

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:

1. Sense: Identify the car’s misbehavior or the available trouble codes.


2. Analyze: Consult a car mechanic to diagnose the issue and how it can be
fixed.
3. Respond: Follow the mechanic’s advice to fix the car.

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

In the complex domain, similarly to the complicated domain, you don’t


have the required knowledge to make an informed decision. However, there
is no expert you can consult. If complicated domains are “known
unknowns”—you know what knowledge you lack to make a decision—
complex domains are “unknown unknowns”—you don’t know what
knowledge is missing, or you do know, but no one possesses it, and thus,
you can’t consult an expert.

Complex domains are context specific. An action’s outcome depends not


only on some field of knowledge, but also on your specific context. In other
words, no generic advice or best practice can predict the outcome in your
concrete case. To make matters more “complex,” any action you take can
result in unpredictable changes to the overarching system and its future
behavior.

Therefore, according to the Cynefin framework, decision making in


complex domains requires experimentation. Prior to making any decisions,
you have to conduct a safe experiment, or even a number of them, analyze
the results, and then decide on the plan of action. Or, in the language of
Cynefin, you must probe–sense–respond:

1. Probe: Conduct a safe experiment to observe the results of different


decisions.
2. Sense: Gather the available information, establish the relevant facts, and
identify patterns.
3. Respond: Follow the course of action according to the experiments’
results.

Making decisions in the complex domain is an iterative process. Rarely can


a single experiment provide all the required information to make an
informed decision. You need multiple probes to gather all the available
information. Most importantly, you have to accept failure as an inherent
part of the learning process.

As a result, contrary to the clear and complicated domains, you cannot


precisely predict the effects of a decision in a complex domain; you can
only deduce them in retrospect. First, you have to conduct a safe
experiment and observe its outcomes. Only in this way can you get a
glimpse of the action’s possible outcomes.

A common example of making decisions in complex domains is A/B


testing.3 Say you are designing a website and have to choose the color of a
call-to-action button. No one can predict with a high level of certainty what
design will maximize the click-through rate.4 Different audiences may
prefer different designs. Furthermore, certain colors may work better for
some websites but worse for others. Therefore, the common course of
action when making such decisions is A/B testing (Figure 2.1): conducting
a safe experiment by rolling out two versions of the design, and observing
which one produces higher click-through rates. Or, in Cynefin’s probe–
sense–respond approach:

3. For more information on A/B testing, refer to the Wikipedia entry


(https://en.wikipedia.org/wiki/A/B_testing).

4. Click-through rate (CTR) is a metric used to measure the success of


online advertising campaigns. It is calculated by dividing the number of
users who click on an ad by the number of impressions or views of the ad.

1. Probe: Conduct a safe experiment to observe the results of using


different colors.
2. Sense: Gather the information regarding the efficacy of different designs.
3. Respond: Use the design that maximizes the click-through rate.
Figure 2.1 A/B testing is a common example of decision making in a complex domain. Different
designs are experimentally tested to observe their impact on the click-through rate, allowing for
informed decisions based on the gathered results.
Making decisions in complex domains is hard because of the inability to
predict the results of different plans of action. Even more challenging,
however, are chaotic domains.

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.

The inability to anticipate the results of actions is due to the inherently


unpredictable nature of the specific domain. For example, unless you fiddle
with them, it’s impossible to predict the outcome of rolling a die or playing
roulette. No amount of experimentation can predict future outcomes based
on past results.

In addition, a domain can be chaotic if the situation at hand makes it


impossible to conduct safe experiments. For example, think of a fire or
other natural disaster. When it happens, you don’t have the time needed to
analyze how things are going to unfold by conducting experiments, as you
would in a complex domain.

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.

In complex domains, the cause-and-effect relationships are obscure, hard to


identify, and error prone, but they are still there. When dealing with chaotic
domains, it is essential to assume that there is no discernible relationship
between causes and effects.

Disorder5

5. A newer version of the Cynefin framework distinguishes two cases


within the disorder domain: aporia and confusion. Aporia is when you
recognize that you lack the necessary information to categorize the situation
into one of the four previous domains, and in response, you undertake an
investigation to discover the missing information. Confusion occurs when
you mistakenly categorize the situation into one of the four previous
domains, not realizing that you lack the complete information about the
situation at hand that could influence your decision. However, these
distinctions are less relevant for our discussion, and for the sake of
simplicity, I’ll adhere to the version of the Cynefin framework that does not
differentiate between the two.

Finally, the Cynefin framework’s fifth domain indicates a lack of awareness


regarding the specific domain you are operating in. Therefore, the
appropriate course of action in the disorder domain is to identify whether
you are dealing with a clear, complicated, complex, or chaotic domain. To
accomplish this, let’s examine the fundamental differences between these
domains.

Comparing Cynefin Domains

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:

In the clear domain, all the necessary knowledge to make informed


decisions is explicit and readily available. Consequently, the results of
decisions within this domain are obvious and predictable. The cause-
and-effect relationship is strongly coupled.
Within the complicated domain, the outcomes are less explicit. You are
aware of what knowledge you lack, and thus you have to consult an
expert in the relevant field to make a decision. As in the clear domain,
the cause-and-effect relationship here is still tightly coupled, although
it’s not as explicit.
In the complex domain, either you can’t define the exact knowledge you
are missing to make informed decisions or such knowledge simply
doesn’t exist. Thus, this domain requires you to conduct experiments to
try to uncover the cause-and-effect relationships and thus be able to
make predictions about the outcomes of your decisions.
Furthermore, in complex systems, no amount of experimentation will be
able to predict an action’s outcome with absolute certainty. There is
always the possibility of not taking into account an important property of
the system, or even of the system changing its behavior because of the
experiment. That makes the cause-and-effect relationship loosely
coupled.
In the chaotic domain, the knowledge you lack is “unknowable.” Neither
consulting an expert nor conducting experiments can provide it.
Therefore, there are no discernible cause-and-effect relationships. In this
domain, the causes and their effects are decoupled.

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

Clear Tightly Known Categorization


coupled

Complicated Tightly Known Analysis


coupled, not unknown
obvious

Complex Loosely Unknown Experimentation


coupled unknown

Chaotic Decoupled “Unknowable” Trusting


instincts

Cynefin in Software Design

The possible applications of Cynefin are extensive. Here, we will explore


how the framework applies in the context of the book’s main subject,
software design; specifically, the interactions between components in a
system. To illustrate this, I will analyze two example scenarios using the
Cynefin framework.

Example A: Integrating an External Service

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

To issue a notification to the customer, the NotificationMaster ’s


SendSMS method needs to be called. In addition to the message itself, the
method accepts the target argument of type PhoneNumber that is
included in the client library you are using, as shown in Figure 2.2. The
PhoneNumber constructor clearly documents the format of the phone
number it expects: It has to be in the international format (E.164).

Figure 2.2 Integrating an external component: clear

The explicit definition of the information the NotificationMaster


module expects makes this integration scenario’s Cynefin domain “clear.”

Complicated

Now consider that instead of the explicit PhoneNumber type, the


NotificationMaster ’s SendSMS method accepts the phoneNumber
argument of type string (Figure 2.3).

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.

The decision of how to supply data to the external module is no longer


clear. You decide to consult the module’s authors regarding the expected
format. This categorizes this integration scenario into the complicated
domain.

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.

Figure 2.3 Integrating an external component: complicated

The need to conduct experiments to make integration design decisions


makes this scenario belong to the complex domain.

Notice, however, an interesting outcome of the experimentation. Once you


manage to identify the correct data format, the scenario turns into the
complicated Cynefin domain. If someone else in your organization needs to
integrate with the NotificationMaster component, they don’t have to
repeat your experiments. Instead, they can consult an expert: you.
Chaotic

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.

It turns out that someone has put the NotificationMaster ’s API


behind a load balancer that points to servers running different versions of
the service (Figure 2.4).

Figure 2.4 Integrating an external component: chaotic

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.

Example B: Changing Database Indexes

Suppose you wish to change the indexes of a relational database. What


might be the potential effects of these changes? Let’s examine four different
scenarios.

Clear

The database you are working on belongs to your microservice. No other


microservice, and for that matter, no other system in the organization,
accesses the database directly. All of its data can be accessed only through
your microservice’s API (Figure 2.5).

Figure 2.5 Changing database indexes: clear


The clear ownership boundaries make it obvious what the effects of
changing the database’s indexes are going to be: All the queries are
implemented by your microservice, and you know how the queries will be
affected by the changes. Hence, this scenario belongs to the clear domain.

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).

Figure 2.6 Changing database indexes: complicated

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

As in the preceding scenario, another team is using the database. However,


you do not know which team that is, nor do you know why or how the team
is using it. To change the indexes, you decide to rely on automated
integration and performance tests. You apply the changes in the staging
environment, and you make design decisions based on how the changes
affect the system’s performance scores.

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.

Your inability to anticipate the negative effects of your changes, neither by


consultation nor through experimentation, places this scenario in the chaotic
domain.

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

Although the examples used in this chapter are related to software


engineering, the Cynefin framework is a decision support tool that can be
used in a wide variety of situations. From healthcare to agriculture, its
applicability is broad. Even within the realm of software engineering,
Cynefin is useful for navigating its various facets. The preceding sections
illustrated how Cynefin domains can be used to analyze integration
scenarios and make implementation changes. Cynefin can also be used to
make strategic decisions, such as whether functionality should be built in-
house or whether it would be more cost-effective to purchase a ready-made
implementation. I will return to the discussion of strategic decisions in
Chapter 9, Volatility.

Cynefin and Complexity

This chapter opened with a definition of complexity as being the cognitive


load a person experiences when facing a system. In the previous sections,
you learned a more precise definition of complexity provided by the
Cynefin framework. However, not everything that induces a high cognitive
load is considered to be complex:

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.

Interestingly, the words “complex” and “complicated” are used


synonymously in the English language. Complexity thinkers, namely, Dave
Snowden, had to redefine7 the meanings of “complicated” and “complex”
to distinguish between different levels of uncertainty. Throughout the rest of
the book, I’ll adhere to Cynefin’s definition of complexity: situations that
are characterized by a high degree of uncertainty, where the cause-and-
effect relationships are loosely coupled. Consequently, complex domains
are not predictable, and they require conducting safe experiments to
uncover the underlying cause-and-effect relationships.

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

This chapter introduced the Cynefin framework and used it to define


complexity: domains in which the outcome of an action can only be
identified in retrospect. Or, to put it in the language of the Cynefin
framework, complexity results from loose coupling between actions and
their outcomes.

When making design decisions in your software systems, try to start by


categorizing the situation you are in. Are you in the clear, complicated,
complex, or chaotic domain? What is the decision-making process that suits
your specific scenario according to Cynefin?

Making a decision in a complex domain requires conducting safe


experiments to get a glimpse into the potential outcomes of decisions. When
making software design decisions you often do this inuitively; however, the
next time you find yourself having to experiment before making a decision,
stop and consider the source of the complexity you are facing.

Unfortunately, facing complexity is a fairly common scenario when


working with brownfield software projects. That’s our next destination. The
following chapter will delve into how complexity arises in both general and
software systems.

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

2. Which Cynefin domain reflects a game of chess?

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

4. Which of the following Cynefin domains describes situations with no


relationships between causes and their effects?

1. Complicated
2. Chaotic
3. Complex
4. Such a situation is not possible.

5. Assume you need to modify the behavior of a legacy system. The


engineers who built it are no longer with the company, there are no tests,
and the only way to learn the outcome of making the change is to deploy it
to a test/staging environment and see how it behaves. What type of Cynefin
domain is this?

1. Clear
2. Complicated
3. Complex
4. Chaotic
Chapter 3

Coupling and Complexity: Interactions

Complexity rises, not from parts’ number or size,

It’s interactions among them, where troubles can rise.

While linear ones are simple and clear,

With complex interactions, failures loom near.

The preceding chapter sought to define what complexity is. It started by


defining complexity as the cognitive load one experiences when working
with a system, and elaborated using the Cynefin framework. You learned
that in complex situations, the outcome of an action can only be identified
in retrospect. It is not immediately apparent, and you cannot consult an
expert; instead, you must conduct an experiment and observe the resultant
behavior. Because such uncertainty is not a desirable property for a
software system, its design should make the outcomes of changes clear and
explicit. Therefore, this chapter shifts the focus from defining what
complexity is and how it manifests itself, to explaining its causes. You will
learn what common factors induce complexity in systems in general and in
software systems in particular. We will explore the common symptoms of
complexity in software, the tools available to manage it, and the
relationship between coupling and complexity.
Nature of Complexity

Complexity is a multifaceted concept that stems from two primary sources:


essential complexity and accidental complexity. Essential complexity is
inherent to the business domain for which the system is designed. It arises
from the intricate nature of the business processes, rules, and requirements
that the software aims to address. For example, if you are building a novel
algorithmic trading system, the essential complexity might include the
diverse market dynamics, regulatory requirements, and varied financial
instruments involved, which are intrinsic to the nature of financial trading.
Since this complexity is a fundamental part of the system’s nature, it cannot
be eliminated. However, it should be managed through thoughtful system
design—for example, by chunking the system into a set of components that
reduce the cognitive load when reasoning about each component
individually or their interactions in the overarching system.

Accidental complexity is not inherent to the business domain; rather, it is a


by-product of suboptimal design decisions. As software engineers and
architects, it is our responsibility to manage essential complexity while
avoiding accidental complexity. In the following section, we’ll explore how
design decisions can lead to complexity within the broader context of
system design.
Complexity and System Design

In his book Normal Accidents: Living with High-Risk Technologies, Charles


Perrow (2011) conducts a detailed analysis of the causes of catastrophic
accidents in complex systems such as nuclear power plants, air traffic
control, and others. He concludes that all complex systems are doomed to
fail sooner or later. Thus, he centers his research on understanding what
causes complexity in systems.

According to Perrow’s research, it all boils down to how the components of


a system interact with each other; that is, whether those interactions are
linear or complex. Let’s see the differences between the two types.

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.

A straightforward example of a system containing linear interactions is a


mechanical watch. Although the mechanism consists of many parts, it’s
possible to track down how a gear or a spring influences other components
of the clockwork (Figure 3.1).
Figure 3.1 A clockwork mechanism is an example of a system that consists of linear interactions. It
contains many parts, but the interactions and dependencies between them are predictable.

(Image: Besjunior/Shutterstock)

In the language of the Cynefin framework, depending on your expertise,


linear interactions belong to both the clear and complicated domains. Going
back to the example of a clockwork, even if you are not a seasoned watch
repairer, you can still take care of a malfunctioning watch: Contact a watch
repairer, for whom the inner workings of the clockwork mechanism are
obvious.
Complex Interactions

Contrary to linear interactions, complex interactions are neither clear nor


predictable. Such interactions cause either unintended effects on the system
or intended effects but in unexpected ways.

Intended Effects in Unexpected Ways

Put simply, intended effects produced in unexpected ways describe a system


that works, but no one knows how it works. This can happen for many
reasons, including the following:

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.

Naturally, evolving such a system is risky, as even minor changes could


inadvertently disrupt its functionality.

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:

A change in the functionality of one component produces unanticipated


effects in other parts of the system.
A failure in one component causes cascading failures throughout the
system, even in seemingly unrelated parts.
Implicit—or explicit but incorrect—assumptions are made about the
system’s environment. Examples include not accounting for the fallacies
of distributed computing1 and assuming that networks are always
reliable. When a network partition occurs, or even when there’s just
lower network performance, it often leads to an outage.

1. For more information on the fallacies of distributed computing, see


the Wikipedia entry
(https://en.wikipedia.org/wiki/Fallacies_of_distributed_computing).

A single change in the functionality of a system must be coordinated


across multiple, distant components. It can be easy to miss the need to
make one of the changes, thus leading to inconsistent behavior of the
system.

Unintended results are usually a symptom of implicit knowledge, whether


it’s an implicit assumption about the system’s environment or just a poorly
structured codebase that lacks organization, consistency, and clear
boundaries between the system’s components.
Complex Interactions and Cynefin

While linear interactions are obvious, complex interactions distort the


cause-and-effect relationships. The unintended results of complex
interactions demonstrate our inability to anticipate the results of a change in
a complex system. Facing that, the only way to identify the effects of a
change is to conduct an experiment: that is, make the change and observe
its effects. That naturally places complex interactions in the complex
domain of the Cynefin framework.

Cynefin’s chaotic domain is an extreme example of complex interactions.


The nonexistent cause-and-effect relationships inherently make its
interactions complex as well.

Complexity and System Size

Charles Perrow’s research demonstrates that the complexity of a system is


not defined by its overall size or the number of its components. Not all
small systems are clear, just as not all large systems are complex. A system
with 5,000 components can be simpler than a system containing merely five
components. Systems become complex due to the nature of the interactions
between their components. If all the system’s components are integrated
through linear interactions, the system is not complex, even if it consists of
millions of components. On the other hand, a small system can be complex,
even chaotic, if its design leads to complex interactions.
That said, even if a system appears to consist solely of linear interactions at
first glance, complexity may still be lurking beneath the surface.

Hierarchical Complexity

System complexity is multidimensional. Complex interactions can take


place not only between components of a system, but also within the
components themselves. We discussed the reason for that in Chapter 1: On a
smaller scale, a system’s component is almost always a subsystem in its
own right. It consists of its own internal components, interacting to achieve
its goal. Or, as Tim Berners-Lee puts it, any system is a component in
another, larger system.2

2. Berners-Lee, Tim. 1998. “Principles of Design.” Last modified


December 1, 2023. https://www.w3.org/DesignIssues/Principles.html.

In other words, both systems and system complexity are hierarchical in


nature.

Consider, for example, a microservices-based implementation of the


WolfDesk system (Figure 3.2). Its goal is to provide a specific
implementation of a support case management system. Its components are
microservices. The microservices interact to implement WolfDesk’s
functionality.
Figure 3.2 Multilevel complexity

Case Management is one of WolfDesk’s microservices. Since it is


implemented in an object-oriented language, its components are objects.
The objects are interacting to implement its functionality (purpose): to
manage the lifecycles of support cases.

One of the objects constituting the Case Management service is the


Support Case aggregate.3 As such, it’s an object consisting of a number
of entities (components) working together (interactions) to implement the
structure and behavior of a support case (purpose).

3. Fowler, Martin. 2013. “D D D_Aggregate.”


https://martinfowler.com/bliki/DDD_Aggregate.html.
We can continue analyzing the underlying structure of the components until
we get all the way down to the level of processor instructions. However,
regardless of the level we are looking at, it can be analyzed as a system:
having its purpose, components, and interactions. The interactions
happening at any level can be complex.

To denote complexity at different levels, I’ll adopt the terminology


introduced by Glenford J. Myers (1979) in his book Composite/Structured
Design:

Global complexity is the complexity of interactions between the


system’s components.
Local complexity is the complexity of a single component—the
complexity of interactions happening inside it.

A complex interaction taking place in a system cannot be labeled as either a


local or a global complexity. The categorization of complexity is subjective
and depends on perspective. Global complexity turns into local complexity
when the system is observed from a higher level of abstraction. In other
words, changing the perspective turns the system into a component in
another, larger system. Consequently, global complexity turns into local
complexity when observed from a higher level of abstraction.

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.

Optimizing Only the Global Complexity

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.

Of course, merging the system into a single component didn’t really


manage its complexity. The initial complexity has been tucked into a lower
level of abstraction. Once you shift your perspective to the interactions
occurring within the component, the initial complexity remains.
Optimizing Only the Local Complexity

Let’s see what happens if we do the opposite. Assume a team works on a


monolithic system and decides to decompose it into (what it hopes will be)
a more modern, microservices-based architecture.

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.

4. Don’t ever do this. Never. Please.

This approach minimizes the microservices’ local complexities. However, it


completely overlooks the global complexity (Figure 3.4). Not surprisingly,
using such a decomposition strategy often leads to a distributed Big Ball of
Mud—the highest level of global complexity possible.
Figure 3.4 Attempting to manage the complexity of a system (A) by focusing solely on optimizing the
local complexity results in high global complexity (B).

Furthermore, such an approach falls into the trap of equating complexity


with size. As you learned in the preceding section, complexity is not a
function of the size of a system. Hence, as in the preceding example,
optimizing the “size” of something is not a way to manage complexity. In
the best-case scenario, you’ll end up with the same amount of complexity,
while more realistically, it will result in additional accidental complexities.

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

The term “degrees of freedom” is used in mechanics, thermodynamics, and


other fields to describe the movement and behavior of physical systems. It
refers to the number of independent variables that can take on different
values without being constrained by other variables.

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.

Degrees of Freedom in 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).

Listing 3.1 A Square Has One Degree of Freedom

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).

Listing 3.2 A Rectangle Has Two Degrees of Freedom

struct Rectangle {
// Two degrees of freedom
int Width;
int Height;
}

The simplest system would have no degrees of freedom: Nothing can be


changed, and, hence, its state is always obvious. On the other hand, the
more degrees of freedom a system has, the more information is needed to
define the system’s state and the more interactions are possible between its
values. The more interactions that are possible, the more possible states the
system has and the harder it is to control and predict its behavior: You have
to account for more possible combinations and interactions between the
different values.

Although the impact of the difference between degrees of freedom in a


square and a rectangle is not substantial, consider a software system that
replicates some of its data across multiple databases, as illustrated in Figure
3.5. The decision to replicate data involves the trade-off between
availability and data freshness. If the source database is offline, the
Shipping service can still work with its local copy of the data. However,
it has to assume eventual consistency of the data. Moreover, a network
partition lasting for a prolonged period can lead to unintended results, as the
Shipping service will work with significantly stale data.

Figure 3.5 Replicating data across multiple storage mechanisms increases the system’s degrees of
freedom.

Another common source of additional degrees of freedom in a software


system is duplicated business logic. Consider a system with two of its
components implementing the same business rule,
isQualifiedForFreeShipping , as illustrated in Figure 3.6.
Figure 3.6 A software system having two of its services implementing the same business rule

From the system design perspective, the


isQualifiedForFreeShipping business rule has two degrees of
freedom: Its implementation may vary between the Shipping and
Ordering services. If the two implementations go out of sync, the system
will end up in an inconsistent state: The customer may qualify for free
shipping during the ordering process, but they will be charged during the
shipment process.

Degrees of Freedom and Complex Interactions

Degrees of freedom reflect the system’s possible states. In the example of


duplicated business logic (Figure 3.6), the state of the
isQualifiedForFreeShipping algorithm can be in two states:

1. The two implementations result in the same behavior.


2. The system has two different implementations of the algorithm.
The fact that there is even a possibility of the two implementations falling
out of sync demonstrates how extraneous degrees of freedom can lead to
unwanted potential states in the system.

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.

Consequently, the more degrees of freedom that have to be accounted for


when changing a system, the easier it is to miss some of the possible
interactions and end up with complex interactions leading to unintended
results. Let’s see how Cynefin domains reflect degrees of freedom and how
you can leverage the insights to manage complexity.

Complexity and Constraints

The notion of degrees of freedom and the Cynefin framework have


something in common: constraints.

As you learned earlier, degrees of freedom represent the number of


independent variables whose values are not constrained by the values of
other variables. In other words, constraints, such as business rules and
invariants, limit the possible interactions between the system’s components
and, thus, reduce its degrees of freedom. Let’s see another example that
demonstrates how constraints limit the degrees of freedom.
Example: Constraining Degrees of Freedom

The Triangle object in Listing 3.3 holds three values— EdgeA ,


EdgeB , and EdgeC —representing the sizes of a triangle’s edges. Its
current implementation doesn’t constrain the possible values of the three
variables. This implies that it’s possible for an instance of the object to
represent a mathematically invalid triangle, where the sum of any two sides
isn’t greater than the third side.

Listing 3.3 An Object Holding Three Values Representing Sizes of a


Triangle’s Edges

class Triangle {
public int EdgeA;
public int EdgeB;
public int EdgeC;
}

The implementation in Listing 3.4 addresses this by introducing a


constraint. Instead of making the variables publicly modifiable, now their
values can only be changed by calling the SetEdges method. The method
introduces the constraint that the passed values have to be mathematically
correct; otherwise, an exception will be raised. Thus, the constraint reduces
the object’s possible states—its degrees of freedom—to only the valid
values.
Listing 3.4 Introducing a Constraint to Make It Impossible for the
Variables to Represent a Mathematically Invalid Triangle

Click here to view code image

class Triangle {
public int EdgeA { get; private set; }
public int EdgeB { get; private set; }
public int EdgeC { get; private set; }

public void SetEdges(int edgeA, int edgeB, int ed


if ((edgeA + edgeB) < edgeC) ||
(edgeA + edgeC) < edgeB) ||
(edgeB + edgeC) < edgeA)) {
throw ValueException(
"The values represent an invalid tria
}

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.

Coupling and Complex Interactions

As we discussed in Chapter 1, coupling is an inherent part of system design.


It defines how components constituting a system are allowed to
communicate with each other for the system to achieve its goals. Coupling
is a design decision that, in essence, defines the components’ integration
constraints: It enables some interactions and prohibits others. Hence, the
notion of constraints is the link that connects coupling and complexity.
Let’s demonstrate this with an example.

Example: Connecting Coupling and Complexity

Disclaimer: Treat this example as a thought experiment. It’s not intended to


give guidance on how to implement a proper repository. Its purpose is to
demonstrate that the way components are coupled imposes integration
constraints and, thus, affects complexity.

A software engineer is designing an object that implements the repository


pattern for the WolfDesk system. The repository is intended to encapsulate
the database being used to store the support cases’ data from its other
components (Figure 3.7). It should provide the standard operations for
managing data: adding, updating, deleting, and reading support cases.

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

Since the WolfDesk system needs to support numerous ways of querying


support cases, the software engineer is considering the repository interface
described in Listing 3.5.

Listing 3.5 Support Case Repository Interface Allowing the Caller to


Specify a SQL Where Clause for Querying Support Cases

Click here to view code image

public interface SupportCaseRepository {


void Insert(SupportCase case);
void Update(SupportCase case);
void Delete(SupportCase case);
SupportCase GetById(CaseId id);
IEnumerable<SupportCase> Query(
string sqlWhere,
Dictionary<string, ob
}

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

Click here to view code image

var paramValues = new Dictionary<string, object> {


["TId"] = 10,
["Months"] = 3
};

var q = "Tenant=@TId and OpenedOn<=dateadd(month, @Mo


var cases = repository.Query(q, paramValues);

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:

Database schema: Since application developers are expected to write


SQL queries, the working assumption is that columns used in the
database match those of the SupportCase object’s fields.
Database indexes: Writing complex queries can result in low
performance and downtime. Allowing consumers such a flexible way to
write queries assumes engineers’ knowledge of the database indexes.
Database family: The use of SQL implies that the database of choice
supports this querying option, which makes it most likely to be a
relational database.
Database itself: Although the use of SQL is widespread, its definition is
not a completely standard one. Different databases support different
dialects. Writing a SQL query for the Query method requires the
engineers to be aware of the dialect supported by the actual database.

The shared knowledge provides multiple opportunities for complex


interactions. For example, suppose an engineer modifies the name of the
OpenedOn property in SupportCase to Timestamp , but they don’t
update the table schema to match the new name, as this would necessitate
modifying all existing queries using that column. Later on, another engineer
writes a query that uses the name Timestamp . That leads to complex
interaction (an unintended effect) because, although the engineer followed
the accepted convention, the query is still invalid.

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.

Design B: Using a Query Object

Looking to prevent the complex interactions described in the preceding


section, the engineer considers a different design: replacing the SQL query
with a Query object (Listing 3.7).

Listing 3.7 Support Case Repository Interface That Uses a Query Object to
Filter Support Cases

Click here to view code image

public interface SupportCaseRepository {


void Insert(SupportCase case);
void Update(SupportCase case);
void Delete(SupportCase case);
SupportCase GetById(CaseId id);
IEnumerable<SupportCase> Query(QueryObject query)
}
The Query object, which is now being used as the argument for the
Query method, uses a structure of objects for defining queries. The
Query object hides the translation of the query to SQL. Thus, instead of
writing SQL code, consumers of the repository compose queries using an
object structure (Listing 3.8).

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

Click here to view code image

var query = new QueryObject(typeof(SupportCase))


.AddCriteria(Criteria.Equals("Tenan
.AddCriteria(Criteria.LessOrEquals(
DateTime.N
var cases = repository.Query(query);

The code responsible for transforming the QueryObject ’s criteria into


actual SQL belongs to the repository. In the case of switching to a different
database engine, the new repository implementation will have to implement
the translation of the query criteria into the database’s query language.
Furthermore, now the actual database integration code may use a different
querying mechanism. For example, instead of SQL, the criteria could be
transformed into MQL (MongoDB Query Language). Thus, this design
better encapsulates the choice of database and alleviates switching to a
different database family (as long as it supports the flexible querying
option). It achieves this by introducing constraints; the Query object
pattern’s implementation isn’t expected to mirror the full richness of
querying options provided by SQL. It constrains the allowed querying to a
subset of the database’s querying functionality that suffices for the
application’s needs.

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.

To further encapsulate the knowledge of database internals (schema and


indexes) and thus avoid corresponding complex interactions, the engineer
considers a different design.

Design C: Using Specialized Finder Methods

Consider the repository interface described in Listing 3.9.


Listing 3.9 Support Case Repository Interface That Uses Specialized
Finder Methods

Click here to view code image

public interface SupportCaseRepository {


void Insert(SupportCase case);
void Update(SupportCase case);
void Delete(SupportCase case);
SupportCase GetById(CaseId id);
IEnumerable<SupportCase> AllCases(TenantId tenant
IEnumerable<SupportCase> CreatedBefore(TenantId t
DateTime d
IEnumerable<SupportCase> MatchingStatus(TenantId
Status st
...
...
}

Instead of the flexible querying options provided by SQL or the Query


object, this design constrains the querying possibilities to a limited set of
parameterized finder methods, such as AllCases , CreatedBefore ,
MatchingStatus , and others. Now, instead of handcrafting queries, the
repository’s consumers need to execute the relevant finder method (Listing
3.10).
Listing 3.10 Using a Finder Method to Filter Support Cases That Belong to
a Specific Tenant and Were Opened More Than Three Months Ago

Click here to view code image

var cases = repository.CreatedBefore(10, DateTime.Now

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.

On the other hand, the constraints imposed by this design make it


impossible for consumers to form and execute ad hoc queries. That’s a
trade-off between flexibility and minimizing the chances of complex
interactions (as a result of sharing extraneous knowledge).

Let’s analyze the three coupling options from the perspectives of degrees of
freedom and constraints.

Coupling, Degrees of Freedom, and Constraints

Coupling connects components and defines the knowledge that is allowed


to flow across their boundaries. The knowledge can range from explicit, as
with the methods exposed by the repository interfaces in the previous
examples, to implicit, as with assumptions imposed by the design. As
discussed in Chapter 1, sharing extraneous knowledge increases the chances
of the connected components having to be changed together. Implicit
knowledge exacerbates the issue of cascading changes even further, making
it harder to anticipate the need for the shared changes, thus leading to
complex interactions.

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.

Of course, not all constraints are desirable. For example, consider a


software engineer’s worst nightmare: a Big Ball of Mud system, with all of
its components so tightly coupled that absolutely any change made to the
system will definitely break some of its functionality. From the Cynefin
standpoint, it can be said that the system belongs to the clear domain—you
can be absolutely sure that any change will result in something breaking.5
Of course, that’s not the simplicity we are interested in. Instead, when
designing component interactions we want to constrain the shared
knowledge in such a way that it enables the desired interactions and
prohibits the rest, especially the complex interactions.

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.

Through careful design of coupling, we define constraints both on the


functionality shared by the connected components and on the knowledge
shared between them. Consequently, proper coupling reduces the system’s
degrees of freedom. But what are the degrees of freedom that we do want to
enable in software systems? That’s the topic of the next chapter, which
discusses the interplay between coupling and modularity.

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?

When evaluating design decisions, consider their potential complexity on


both global and local levels. Global complexity involves interactions
between components, while local complexity describes interactions within a
single component. Complexity has to be managed at both levels.

Effective constraints encapsulate knowledge, reduce a system’s degrees of


freedom, and minimize the chances of complex interactions. Pay close
attention to the degrees of freedom exposed by your components’ public
interfaces. Do they allow unintended behavior due to overly flexible input?
Armed with a clear understanding of complexity, its signs, and its sources,
we can move on to the next chapter, which focuses on what we do want to
achieve in software design: modularity.

Quiz

1. Which of the following properties of a system affect(s) its complexity?

1. Size
2. Number of system components
3. Interactions between components
4. All of the answers are correct.

2. Which of the following statements is/are true?

1. Complex interactions can lead to unintended results.


2. Linear interactions can lead to unintended results.
3. Complex interactions can lead to intended results but in unintended
ways.
4. Answers A and C are correct.

3. What is the difference between local and global complexities?

1. Global complexity describes interactions inside a component, and local


complexity is the complexity of interactions between the components.
2. Local complexity describes interactions inside a component, and global
complexity is the complexity of interactions between the components.
3. Global and local complexities are synonymous in the context of software
design.
4. Global complexity results from the use of global variables, while local
complexity relates to local fields.

4. What type of complexity is more important in the context of software


design?

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?

1. Encourage complex interactions.


2. Constrain shared knowledge.
3. Enable desired interactions.
4. Answers B and C are correct.
Chapter 4

Coupling and Modularity

Modularity’s perks, we cannot ignore,

But its true essence, we still must explore.

What makes a design coherent and fluent?

It’s all about value—future and current.

“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?

A system has a goal: the functionality it has to implement. The components


of the system are working together to achieve the goal. For example, a
social media app enables people to connect, share, and interact, while an
accounting system streamlines financial tasks for businesses. However,
these functionalities only address the present requirements. As time goes
on, the users’ needs may change, and new requirements may emerge. That’s
where modularity comes into play.

Modular design aims to address a wider range of goals than a nonmodular


system can. It expands the system’s goal to accommodate requirements that
are currently unknown but may be needed in the future. Of course, the
future requirements are not expected to be available out of the box on day
one, but the design should make it possible to evolve the system with a
reasonable effort.

By investing in modularity, we design an adaptable and flexible system.


That is, the primary goal of modularity is to allow the system to evolve
(Cunningham 1992). A famous quote that is often (mis)attributed to Charles
Darwin1 captures this idea perfectly: “It is not the strongest of the species
that survives, but the most adaptable.” This principle applies to systems as
well. Even the most finely tuned, faultlessly performing system of today
will face obsolescence if it cannot flex and grow with tomorrow’s changes.
The less flexible a system is, the less stress it can tolerate. The less stress it
can tolerate, the more prone it is to breaking under the pressure of evolving
requirements. By being prepared to handle changes, a modular system is
better positioned for long-term success.

1. Quote Investigator delves into the history of this quotation


(https://quoteinvestigator.com/2014/05/04/adapt).

Modularity also serves as a cognitive tool, streamlining the comprehension


of a system. Instead of a monolithic, inscrutable black box, a modular
system presents as a collective of individual parts, each performing its
function yet able to function collaboratively. This separation into modules
allows for a clearer understanding of the system’s inner mechanics and how
it ultimately delivers the desired output.
But why does this matter? Is it simply a matter of satisfying intellectual
curiosity? Not really. A deep understanding of how a system operates is the
key to modifying and improving it. This might involve altering existing
behavior, such as fixing bugs, or it could involve evolution of the system by
introducing new functionalities. The simplicity and transparency of a
modular design enable you to tinker, adjust, and innovate more effectively
and with more confidence.

With this understanding of the importance of modularity, let’s dig deeper


into the concept of a module and its role in making a flexible system.

Modules

The terms “module” and “component” are often used interchangeably,


which causes confusion. As I mentioned earlier, any system is composed of
components. Therefore, a module is a component. However, not every
component is a module. To design a flexible system, it’s not enough to
decompose the system into an arbitrary set of components. Instead, a
modular design should enable you to alter the system by combining,
reconfiguring, or replacing its components—its modules.

Let’s consider two examples of modules from our everyday lives (Figure
4.1):

1. LEGO bricks are a straightforward illustration of modularity in action.


Each brick is a self-contained unit that can be connected with other
bricks to form a variety of structures. The ease with which these bricks
can be assembled and disassembled illustrates a perfectly modular
system.
2. Another widespread example of modularity is the interchangeable
camera lenses used by photography enthusiasts. The ability to switch
lenses enables photographers to adapt their cameras to different shooting
conditions and achieve various effects, all without requiring multiple
cameras.

Figure 4.1 Modularity in real-life systems

(Images: left, focal point/Shutterstock; right, Kjpargeter/Shutterstock)

The success of a modular system depends on the design of its modules. To


enable the desired flexibility of a system, its design has to focus on clear
boundaries and well-defined interactions between modules. To reason about
the design of a module, it’s helpful to examine three fundamental properties
describing a module: function, logic, and context:2
2. In later sources, you may encounter different terms used to describe the
properties: border, implementation, and environment. For consistency, I’ll
stick to the original terminology (Myers 1979).

1. Function is the module’s goal, the functionality it provides. It is exposed


to consumers of the module through its public interface. The interface
has to reflect the tasks that can be achieved by using the module, how
the module can be integrated, and its interactions with other modules.
2. A module’s logic is all about how the module’s function is implemented;
that is, the implementation details of the module. Unlike function, which
is explicitly exposed to consumers, a module’s logic should be hidden
from other modules.
3. Finally, a module’s context is the environment in which the module
should be used. This includes both explicit requirements and implicit
assumptions the design makes on the module’s usage scenarios and
environment.

These fundamental properties provide valuable insights into a module’s role


within the broader system, as summarized in Table 4.1.

Table 4.1 Comparison of the Three Fundamental Properties of a Module

Property Reflects Type of information

Function Module’s goal Public, explicit


Property Reflects Type of information

Logic How module works Hidden by the module

Context Assumptions about the Public, less explicit than


environment function

To effectively design a module, its function should be clear and explicitly


expressed in its public interface. The module’s implementation details, or
logic, on the other hand, should be hidden from consumers by the module’s
boundary. Ultimately, a clear and explicit definition of the context is
essential for consumers to be able to integrate the module, as well as to be
aware of how the module’s behavior might be affected by changes in its
environment.

Let’s have a look at how these properties are reflected in the


aforementioned modular systems: LEGO bricks and interchangeable
camera lenses.

LEGO Bricks

The goal of the overall system—the LEGO constructor—is to form


structures from individual building blocks. The modules of the system are
LEGO bricks. As a module, each brick has the following properties:
Function: A brick’s goal is to connect with other bricks. It’s explicitly
reflected by the “integration interface”: studs and holes through which it
can be easily attached to other bricks.
Logic: The bricks are made from a material that supports the required
weight to build sturdy structures and guarantees reliable attachment to
other bricks.
Context: Since LEGOs are (generally) a toy for children, they have to be
safe and appropriate for kids to play with. Furthermore, because of their
purpose as a creative and fun playtime tool, using LEGO bricks to build
actual houses would not be a good fit, as they are not designed or
intended for such a task.

Camera Lenses

As I mentioned earlier in this chapter, interchangeable camera lenses enable


photography enthusiasts to adapt to different shooting conditions without
having to use multiple cameras. Both the camera body and the attachable
lenses are modules. Let’s focus on the properties of camera lenses as
modules:

Function: Enable capturing images with specific properties, such as focal


length or aperture. The interface defines what kinds of cameras the
lenses can be used with, as well as the supported range of optical
capabilities.
Logic: The inner workings of lenses allowing them to be connected to a
camera and capture the required optical capabilities.
Context: The supported ranges of camera models, as well as varying
functionalities for different cameras (e.g., whether autofocus is supported
or not).

Now that we have established an understanding of modularity in general,


let’s explore how these concepts apply in the context of software design.

Modularity in Software Systems

Although the term “module” is used extensively in software engineering,


defining what a software module is, is not as straightforward as one might
expect. The ambiguity arises from the term’s long-standing use, during
which its original meaning was obscured as software engineering evolved,
leading to diverse reinterpretations and loss of a precise definition.

What makes a software module? Is it a library, a package, an object, a group


of objects, or a service? Furthermore, what is a nonmodule software
component, and how does it differ from a module?

Some argue that a module embodies a logical boundary, such as a


namespace, a package, or an object, while a component signifies a physical
boundary, encompassing artifacts such as services and redistributable
libraries. However, the juxtaposition of logical and physical boundaries is
not accurate. To understand why it’s not accurate, as well as what exactly a
software module is, let’s go back in time and examine what was meant by
“module” when the term was originally introduced to software design.

Software Modules

In his seminal paper “On the Criteria to Be Used in Decomposing Systems


into Modules,” David L. Parnas (1971) succinctly defined a module as “a
responsibility assignment” rather than just an arbitrary boundary around
statements of a program.

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):

The statements implement self-contained functionality.


The functionality can be called from any other module.
The implementation has the potential to be independently compiled.

The self-contained functionality criterion implies that a specific


functionality is encapsulated within a module, rather than, for example,
being spread across multiple modules. Next, the module makes this
functionality accessible to other modules of the system through its public
interface. Ultimately, the module’s implementation can potentially be
independently compiled. Consequently, according to this definition, the
type of a module’s boundary—physical or logical—is not essential. As long
as it has the potential of being extracted into an independent unit that can be
compiled, it is a module. What is more important than the type of the
module’s boundary is the functionality it implements and provides to other
modules.

This focus on the well-defined functionality rather than the type of a


boundary makes modules ubiquitous all across software design.
(Micro)services, frameworks, libraries, namespaces, packages, objects,
classes—all can be modules. Furthermore, because nowadays a class’s
methods can be compiled independently,3 even individual
methods/functions can be considered modules.

3. For example, extension methods in C# or functions in languages such as


Python and JavaScript.

That means a service-based system can be modular if its services are


designed as effective modules. A service of that system can be modular on
its own if, for example, it consists of modular namespaces. Modular objects
can form a modular namespace, and the same is true for methods or
functions constituting objects. “It’s turtles all the way down,” as illustrated
in Figure 4.2. Modules are not flat; modular design is hierarchical.
Figure 4.2 Hierarchical modular design

To reiterate, a module is a boundary encompassing a well-defined


functionality, which it exposes for use by other parts of the system.
Consequently, a module could represent nearly any type of logical or
physical boundary within a software system, be it a service, a namespace,
an object, or something else.

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

We can use the three properties of a module—function, logic, and context—


to describe all kinds of the aforementioned software modules.

Function

A software module’s function is the functionality it exposes to its


consumers over its public interface. For example:

A service’s functionality can be exposed through a REST API or


asynchronously through publishing and subscribing to messages.
An object’s function is expressed in its public methods and members.
The function of a namespace, package, or distributed library consists of
the functionality implemented by its members.
If a distinct method or a function is treated as a module, its name and
signature reflect its function.

Logic

A software module’s logic encompasses all the implementation and design


decisions that are needed to implement its function. It includes its source
code,4 as well as internal infrastructural components (e.g., databases,
message buses) that are not needed for describing the module’s function.
4. Rumor has it that this is where the term “business logic” comes from.
This implies that there are different kinds of “logics” encompassed in a
module: logic for integrating infrastructural components, and logic for
business tasks. That said, I couldn’t find any sources that can prove this
observation.

Context

All types of software modules depend on various attributes of their


execution environments and/or make assumptions regarding the context in
which they operate. For example:

At a very basic level, a certain runtime environment is needed to execute


a module. Moreover, a specific version of the runtime environment may
be required.
A certain level of compute resources, such as CPU, memory, or network
bandwidth, may be needed for the module to function properly.
A module may assume that the calls are pre-authorized instead of
performing authorization itself.

Going back to the definition of a module’s context, the main difference


between function and context is that the assumptions and requirements tied
to the context are not reflected in the module’s public interface—its
function.
Now that you have a solid understanding of what a software module is, let’s
delve into the design considerations for designing a modular system.

Effective Modules

As noted in the previous sections, an arbitrary decomposition of a system


into components won’t make it modular. The hierarchical nature of modules
doesn’t make it any easier. Failing to properly design modules at any level
in the hierarchy can potentially undermine the whole effort.

Effective design of modules is not trivial, and failures to do so can be


spotted all across the history of software engineering. For example, not so
long ago, many believed that a microservices-based architecture is the easy
solution for designing flexible, evolvable systems. However, without a
proper principle guiding the decomposition of a system into microservices
(modules), many teams ended up with distributed monoliths—solutions that
were much less flexible than the original design. As they say, history tends
to repeat itself, and almost exactly the same situation happened when
modularity was introduced to software design:

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)

Following that experience, Parnas (1971) proposed a principle intended to


guide more effective decomposition of systems into modules: information
hiding. According to the principle, an effective module is one that hides
decisions. If a decision has to be revisited, the change should only affect
one module, the one that “hides” it, thus minimizing cascading changes
rippling across multiple components of the system.

In Parnas’s later work (1985, 2003), he equated modules following the


information-hiding principle to the concept of abstraction. Let’s see what an
abstraction is, what makes an effective abstraction, and how to use this
knowledge to craft module boundaries.

Modules as Abstractions

The goal of an abstraction is to represent multiple things equally well. For


example, the word “car” is an abstraction. When thinking about a “car,” one
does not need to consider a specific make, model, or color. It could be a
Tesla Model 3, an SUV, a taxi, or even a Formula 1 race car; it could be red,
blue, or silver. These specific details are not necessary to understand the
basic concept of a car.

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.

By focusing only on the details that are shared by a group of entities, an


abstraction hides decisions that are likely to change. As a result, the more
general an abstraction is, the more stable it is. Or, the fewer details that are
shared by an abstraction, the less likely it is to change.

Note

Interestingly, the term “software module” is an abstraction


itself. As you learned in the preceding section, a software
module can represent a variety of boundaries, including
services, namespaces, and objects. That’s the concept of
abstraction in action. It eliminates details relevant to
concrete types of software boundaries, while focusing on
what is essential: responsibility assignment, or the
encapsulated functionality. Hence, you can use the term
“module” to represent all kinds of software boundaries
equally well.

A well-designed module is an abstraction. Its public interface should focus


on the functionality provided by the module, while hiding all the details that
are not shared by all possible implementations of that functionality. Going
back to the example of a repository object in Chapter 3, the interface
described in Listing 4.1 focuses on the required functionality, while
encapsulating the concrete implementation details.

Listing 4.1 A Module Interface That Focuses on the Functionality It


Provides, While Encapsulating Its Implementation Details

Click here to view code image

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.”

It may seem that using an abstraction introduces vagueness or lack of detail.


However, as Dijkstra argues, that’s not the goal. Instead, an abstraction
should create a new level of understanding—a “semantic level”—where
one can be “absolutely precise.” Balance is needed to reach a proper level
of abstraction to convey the correct semantics. Consider this: If you use an
abstraction called “vehicle” to represent cars, it might be an overly broad
generalization. Ask yourself: Are you actually modeling a range of vehicles,
such as motorcycles and buses, necessitating such a wide-ranging
abstraction? If the answer is no, then using “car” as your abstraction is more
appropriate and precisely conveys the intended meaning.

By focusing on the essentials—functionality of modules—while ignoring


extraneous information, abstractions allow us to reason about complex
systems without getting lost in the details. A common example of a modular
system is a personal computer. We can reason about the interactions of its
modules—CPU, motherboard, random-access memory, hard drive, and
others—all without understanding the intricate technicalities of each
individual component. When troubleshooting a problem, we don’t need to
comprehend how a CPU processes instructions or how a hard drive stores
data at a microscopic level. Instead, we consider their roles within the larger
system: a new semantic level provided by effective abstractions.

Finally, abstractions, like modules, are hierarchical. In software design,


“levels of abstraction”6 are used to refer to different levels of detail when
reasoning about systems. Higher levels of abstraction are closer to user-
facing functionality, while lower levels are more about components related
to low-level implementation details. Different levels of detail require
different languages for discussing the functionalities implemented at each
level. Those languages, or (as Dijkstra called them) semantic levels, are
formed by designing abstractions.

6. Or layers of abstraction.

Hierarchical abstractions also serve as further illustration of modularity’s


hierarchical nature. Since abstractions adhere to the same design principles
at all levels, modular design exhibits not only a hierarchical but also a
fractal structure. In upcoming chapters, I will discuss in detail how the same
rules govern modular structures at different scales. But for now, let’s revisit
the topic of the previous chapters, complexity, and analyze its relationship
with modularity.
Modularity, Complexity, and Coupling

Poor design of a system’s modules leads to complexity. As we discussed in


Chapter 3, complexity can be both local and global, while the exact
meaning of local/global depends on point of view: Global complexity is
local complexity at a higher level of abstraction, and vice versa. But what
exactly makes one design modular and another one complex?

Both modularity and complexity result from how knowledge is shared


across the system’s design. Sharing extraneous knowledge across
components increases the cognitive load required to understand the system
and introduces complex interactions (unintended results, or intended results
but in unintended ways).

Modularity, on the other hand, controls complexity of a system in two


ways:

1. Eliminating accidental complexity; in other words, avoiding complexity


driven by the poor design of a system.
2. Managing the system’s essential complexity. The essential complexity is
an inherent part of the system’s business domain and, thus, cannot be
eliminated. On the other hand, modular design contains its effect by
encapsulating the complex parts in proper modules, preventing its
complexity from “spilling” across the system.
In terms of knowledge, modular design optimizes how knowledge is
distributed across the components (modules) of a system.

Essentially, a module is a knowledge boundary. A module’s boundary


defines what knowledge will be exposed to other parts of the system and
what knowledge will be encapsulated (hidden) by the module. The three
properties of a module that were introduced earlier in the chapter define
three kinds of knowledge reflected by the design of a module:

1. Function: The explicitly exposed knowledge


2. Logic: Knowledge that is hidden within the module
3. Context: Knowledge the module has about its environment

An effective design of a module maximizes the knowledge it encapsulates,


while sharing only the minimum that is required for other components to
work with the module.

Deep Modules

In his book A Philosophy of Software Design, John Ousterhout (2018)


proposes a visual heuristic for evaluating a module’s boundary. Imagine that
a module’s function and logic are represented by a rectangle, as illustrated
in Figure 4.3. The rectangle’s area reflects the module’s implementation
details (logic), while the bottom edge is the module’s function (public
interface).
Figure 4.3 A shallow module (A) and a deep module (B)

According to Ousterhout, the resultant “depth” of the rectangle reflects how


effective it is at hiding knowledge. The higher the ratio between the
module’s function and logic, the “deeper” the rectangle.

If the module is shallow, as in Figure 4.3A, the difference between the


function and logic is small. That is, the complexity encapsulated by the
module’s boundary is low as well. In the extreme case, the function and
logic are precisely the same—the public interface reflects how the module
is implemented. Such an interface provides no value; it doesn’t encapsulate
any complexity. You could just as well read the module’s implementation.
Listing 4.2 shows an extreme example of a shallow module. The method’s
interface doesn’t encapsulate any knowledge. Instead, it simply describes its
implementation (adding two numbers).
Listing 4.2 An Example of a Shallow Module

addTwoNumbers(a, b) {
return a + b;
}

On the other hand, a deep module (Figure 4.3B) encapsulates the


complexity of the implementation details behind a concise public interface.
The consumer of such a module need not be aware of its implementation
details. Instead, the consumer can reason about the module’s functionality
and its role in the overarching system, while being ignorant of how the
module is implemented—at a higher semantic level.

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

Modularity and complexity are two competing forces. Modularity aims to


make systems easier to understand and to evolve, while complexity pulls
the design in the opposite direction.

The complete opposite of modularity, and the epitome of complexity, is the


Big Ball of Mud anti-pattern (Foote and Yoder 1997):

A Big Ball of Mud is haphazardly structured, sprawling, sloppy, duct-tape


and bailing wire, spaghetti code jungle. These systems show unmistakable
signs of unregulated growth, and repeated, expedient repair. Information is
shared promiscuously among distant elements of the system, often to the
point where nearly all the important information becomes global or
duplicated. The overall structure of the system may never have been well
defined. If it was, it may have eroded beyond recognition. —Brian Foote
and Joseph Yoder

In the preceding definition of the Big Ball of Mud anti-pattern, unregulated


growth, sharing information promiscuously among distant elements of the
system, and important information becoming global or duplicated all
demonstrate how unoptimized and inefficient flow of knowledge cripples
systems.

These points can also be formulated in terms of ineffective abstractions. An


effective abstraction removes all extraneous information, retaining only
what is absolutely necessary for effective communication. In contrast, an
ineffective abstraction creates noise by failing to eliminate unimportant
details, removing essential details, or both.

If an abstraction includes extraneous details, it exposes more knowledge


than is actually necessary. That causes accidental complexity in multiple
ways. The consumers of the abstraction are exposed to more details than are
actually needed to use the abstraction. First, this results in accidental
cognitive load, or cognitive load that could have been avoided by
encapsulating the extraneous detail. Second, this limits the scope of the
abstraction: It is no longer able to represent a group of entities equally well,
but only those for which the extraneous details are relevant.

On the other hand, an abstraction can fail if it omits important information.


For example, a database abstraction layer that doesn’t communicate its
transaction semantics may result in users expecting a different level of data
consistency than the one provided by concrete implementation. This
situation creates what is referred to as a leaking abstraction;7 that is, when
details from the underlying system “leak” through the abstraction. This
happens when the consumer of the abstraction needs to understand the
underlying concrete implementation to use it correctly. As in the case of an
abstraction sharing extraneous details, it increases the consumer’s cognitive
load and can lead to misuse or misunderstandings of the module,
complicating maintenance, debugging, and extension.
7. Spolsky, Joel. “The Law of Leaky Abstractions.” Joel on Software.
November 11, 2002. https://www.joelonsoftware.com/2002/11/11/the-law-
of-leaky-abstractions.

Hence, encapsulating knowledge is a double-edged sword. Going overboard


can make it hard or even impossible to use the module, but the same is true
when too little knowledge is being communicated. To make modularity
even more challenging, even if a system is decomposed into seemingly
perfect modules, it is still not guaranteed to be modular.

Modularity: Too Much of a Good Thing

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.

Hence, a modular design should focus on reasonable changes. In other


words, the system should expose only reasonable degrees of freedom. For
example, changing the functionality of a blog engine into a printer driver is
not a reasonable change.

Unfortunately, identifying reasonable future changes is not an exact science,


but is based on our current knowledge and assumptions about the system.
An assumption is essentially a bet against the future (Høst 2023). The future
can support or invalidate assumptions. However, if modular design entails
making bets, we can gather as much information as possible and do our best
to make informed bets.

Coupling in Modularity

Many aspects of modularity can be understood only by considering the


modules not as individual entities, but by examining them in relation to one
another. This was demonstrated in the Deep Modules section earlier: Even
perfectly “deep” modules can still introduce complex interactions. As Alan
Kay said, the big idea of object-oriented programming is messaging, not
classes;8 in other words, the relationships and interactions between objects.9
Traditionally, when systems are designed the main focus is on the
components, or boxes. But what about the arrows and lines connecting
them?

8. Kay, Alan. “Alan Kay on Messaging.” October 10, 1998.


http://wiki.c2.com/?AlanKayOnMessaging.

9. According to the original definition, objects are modules and, therefore,


the same design principles that apply to objects apply to modules at other
levels of abstraction.

The modularity of a system cannot be evaluated by examining designs of


individual modules in isolation. The goal of modular design is to simplify
the relationships between components of a system. Hence, modularity can
only be evaluated in the scope of the relationships and interactions between
the components. The knowledge that is shared among the components
controls whether the overarching system will be more modular or more
complex. Coupling is the aspect of a system that defines what knowledge is
shared between components of a system. Different ways of coupling
components share different types and amounts of knowledge. Some will
increase complexity, while others will contribute to modularity.

This is a good time to mention the counterpart of coupling: cohesion. The


concept of cohesion was introduced in tandem with coupling in Structured
Design (Yourdon and Constantine 1975). Cohesion refers to the degree to
which the elements inside a module belong together. In other words, it’s a
measure of how closely the responsibilities of a module are related to each
other. High cohesion is generally seen as a desirable characteristic because
it promotes a single, well-defined purpose for each module, improving
understandability, maintainability, and robustness.

Under the hood, however, cohesion is based on coupling. Some software


engineers even refer to cohesion as “good coupling.” That’s my preferred
approach as well. The chapters in Part II, Dimensions, scrutinize how
coupling affects system design and the dimensions in which effects of
coupling can be observed. Later in the book I will combine these insights
into a concise framework for guiding modular design that will also reflect
cohesion of the system’s modules.

Key Takeaways

Modularity strives to minimize complexity by managing the distribution of


knowledge across modules. However, the overarching goal of modularity is
to enable evolution of the system according to future goals and needs.
Hence, modular design requires awareness not only of the current
requirements, but also of those that might arise in the future.

Nevertheless, be aware of the “too much of a good thing” syndrome. A


system that is flexible to accommodate any change is likely to be overly
complicated. Striking a balance is crucial to prevent systems from
becoming exceedingly rigid or overly flexible.

To train your “muscle” of predicting future changes, learn about the


business domain of your system. Analyze the trends: what changes were
required in the past, and why. Learn about competing products: what they
are doing differently, why they are doing these things differently, and how
likely the functionality is to change in your system.

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?

Ultimately, to truly design modular systems, one must consider modules in


relation to each other, acknowledging that their interplay significantly
impacts modularity. Coupling defines what knowledge is shared between
components, and cohesion indicates how related a module’s responsibilities
are. These topics will be expanded upon in the forthcoming chapters,
eventually forming a robust framework to inform modular design.

Quiz

1. What are the basic properties of a module?

1. API, database, and business logic


2. Source code
3. Function and logic
4. Function, logic, and context

2. What makes an effective module?

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

3. Which property of a module is the most explicit?

1. Function
2. Logic
3. Context
4. Answers B and C are correct.

4. Which of the following software design elements can be considered


modules?

1. Services
2. Namespaces
3. Classes
4. All of the answers are correct.
5. What makes an effective abstraction?

1. Omitting as much information as possible


2. Retaining as much detail as possible
3. Creating a language that allows discussing about functionalities of
components, without having to know how they are implemented
4. Describing as many objects as possible
Part II

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.

Chapter 6 introduces connascence, another way to evaluate the knowledge


shared between coupled components. You will learn about the levels of
connascence, the differences between them, and connascence’s relationship
to module coupling.

In Chapter 7, I combine module coupling and connascence into integration


strength, a unified model for evaluating the strength of coupling. You’ll see
why integration strength is necessary and what is reflected by its levels.
Most importantly, you’ll learn to apply the model in practice.
Another important aspect of designing cross-component interactions is their
spatial location in the codebase. Chapter 8 discusses the effects of distance
on coupling. You’ll learn how physical distances can influence the
components’ shared reasons for change.

Chapter 9 concludes Part II by exploring the effects of coupling in the


dimension of time. You will learn to evaluate components’ expected rates of
change, as well as additional factors that can lead to cascading changes
across coupled components.

Before I delve into the different models of evaluating coupling, there is an


important remark I have to make. Chapter 5, 6, and 7 discuss different
models of evaluating coupling. Some of the coupling levels you will learn
may sound acceptable, while others can make you cringe. That said, at this
point, it’s too early to judge which levels are good and which should be
avoided. That will be discussed in Part III, Balance. Until then, let’s focus
on identifying the different ways to couple components and their effect on
the flow of knowledge in the design of the system.
Chapter 5

Structured Design’s Module Coupling

Old paradigms fade, replaced by new,

Yet, the same design principles are ever true.

Structured design first came to show,

How modules connect and knowledge can flow.

This chapter begins our exploration of the various forms of coupling in


software design. I start with the model for evaluating coupling that was
introduced in the structured design methodology. This methodology was
mentioned multiple times in Chapter 4. You may recall that it originated in
the late 1960s, a period when the field of software engineering was quite
different from what we are familiar with today.

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

This chapter and Chapter 6, Connascence, describe models


that were formulated quite a few years ago. These chapters
are not meant to be history lessons. Although these models
are not in wide use today, their underlying principles are at
the core of the model that will be introduced in Chapter 7,
Integration Strength. Therefore, it is important to understand
the levels of these models and the conceptual differences
between them.

Structured Design

Larry L. Constantine began developing the ideas behind the structured


design methodology as early as 1963 (Yourdon and Constantine 1975).
These ideas were then published in collaboration with Edward Yourdon in
the early 1970s. You may think that today, over half a century later, this
material is outdated and irrelevant. Hang on, however, and read the first
paragraph from Reliable Software Through Composite Design (Myers
1979):

The 1970s will probably be known as the decade in which software


considerations surpassed hardware considerations. Concerns about the
reliability and economics of data processing systems are now largely
focused on software rather than hardware. Software costs now greatly
exceed hardware costs, and issues of the prior two decades have taken a
back seat to software issues such as unsatisfactory reliability, excessive
cost, and schedule overruns.

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.

Before exploring these levels of module coupling, I want to reiterate that, as


we discussed in Chapter 4, a module is not a specific type of boundary.
Instead, a module is a responsibility assignment: a boundary that
encapsulates a functionality, exposes it to other parts of the system, and has
the potential to be independently compiled. Thus, module coupling applies
to a variety of boundaries, from methods/routines/functions to services and
whole systems.

Let’s discuss module coupling’s levels of interconnectedness. We’ll start


from the highest level, content coupling.

Content Coupling

The content coupling level of module coupling is also known as


pathological coupling. Pathological? It has to be bad. And indeed it is: A
downstream module is content-coupled to an upstream module if it
references the upstream module’s contents directly, instead of using its
public interface. That is, the offending module ignores the formal
integration methods and instead trespasses the module’s boundaries by
using its private interfaces, or integrates through other implementation
details.

The assembly code in Listing 5.1 demonstrates a classic example of content


coupling. The jump on line 12 in the routine MAIN moves the execution to
line 53, located exactly 18 lines after the COMP label—not a line more, nor
a line less. These routines are content-coupled, as the routine MAIN uses
the contents of the routine PROCESS to control the execution flow.

Listing 5.1 Content Coupling in Assembly

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.

Listing 5.2 Content Coupling Through Reflection in C#

Click here to view code image

public void DoSomething() {


var invoice = new InvoiceGenerator();

var t = typeof(InvoiceGenerator);
var privateMethod = t.GetMethod("VerifyInput",
BindingFlags.NonP
BindingFlags.Inst

var res = privateMethod.Invoke(invoice, "input");


}
Since the VerifyInput method is private, it wasn’t intended to be called
by consumers of the InvoiceGenerator object. Therefore, invoking it
through reflection is content coupling: The downstream module couples
itself to the contents—the implementation details—of the upstream module
(the InvoiceGenerator object).

Another common way to introduce content coupling in modern systems is


through directly accessing infrastructural components belonging to another
module. For example, assume that you have two microservices, and one of
them reads data from the other’s database directly instead of through its
public interface. As long as that database was not meant to be used as an
integration mechanism, that’s content coupling.

Effects of Content Coupling

The practical implication of content coupling is the collapsing of the


upstream module’s boundaries. The boundaries are no longer relevant, as
the downstream component reaches out to the implementation details and
uses them for integration.

Needless to say, such an integration method is both implicit and fragile. On


the one hand, it limits the upstream module’s ability to evolve and change
its implementation details, as any change has the potential to break the
integration. On the other hand, authors of the upstream module might not
even be aware of the integration taking place. Hence, even the smallest
change they make can unexpectedly cause integration issues in the
overarching system—a typical example of complex interactions.

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.

Listing 5.3 Content Coupling: Three Subroutines Operating on Globally


Shared Data

Click here to view code image

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

As in the case of content coupling, integrating modules through sharing


globally accessible memory space is not common in modern programming
languages.2 But it is possible. A trivial example of common coupling in
modern systems would be multiple modules reading and writing to the same
file stored in an object storage service such as AWS S3, Google Cloud
Storage, or Azure Storage, as illustrated in Figure 5.1.

2. No pun intended.
Figure 5.1 Common coupling through reading and writing to a file in globally accessible object
storage

A rather everyday example of common coupling in a modern system would


be multiple methods working with members of their overarching class. In
such a case, the methods enclosed in the same object are, naturally,
common-coupled. Of course, methods working on the same member
variables is by far not as problematic as multiple systems concurrently
modifying the same set of data, yet both are common-coupled. So, is
introducing common coupling a good design or a bad design? Chapter 10,
Balancing Coupling, will discuss the differences between these two cases in
detail, and how it is possible that the same level of interconnectedness can
be both problematic and acceptable.

Effects of Common Coupling

Common coupling is considered a high level of interconnectedness in the


module coupling scale. There are multiple reasons for this level being the
second-highest level, just below content coupling:

Common-coupled modules are sharing much more information than


might be needed. For example, even if one of the modules in Figure 5.1
doesn’t actually need all of the data that is stored in the shared data.json
file, it still has to be aware of it. The more data that is shared, the harder
it is to change it in the future. One of the modules might need to change
its data structures, data types, or simply the name of one of the variables.
As long as the data is shared among different modules, such a change
should be coordinated with all the common-coupled modules.
Because extraneous information is shared among common-coupled
modules, the integration contract between the modules is implicit. It is
hard to track what subset of the data is actually needed by each module.
The data flow in common-coupled modules is hard to track and
understand. If a module updates a globally shared value, the operation is
going to have side effects in the connected modules. The other modules,
on the other hand, cannot track how a particular value got there.
Some variables require validation before updating their values; for
example, according to the business domain’s rules and invariants. In
such a case, the business logic that validates the values has to be
duplicated across all of the common-coupled modules. If one of the
modules ignores the business rules and just updates a variable’s value, it
could potentially lead to other modules being in an invalid state.
Ultimately, multiple processes concurrently modifying the same set of
data produce a race condition. Concurrency management should be
implemented to “serialize” access to the shared state. Such serialization
can have a potentially negative impact on the system’s performance, as
only one module can modify the data at a time. Furthermore, depending
on the storage mechanism that holds the shared data, it might even be
impossible to implement sufficient concurrency control.

Common Coupling Versus Content Coupling

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

External coupling is still positioned high on structured design’s module


coupling scale and is somewhat similar to common coupling. Externally
coupled modules are still communicating through globally shared data.
However, contrary to common coupling, the integrated modules aren’t
exposing all of their data. Instead, the only data that is shared is the data
that is actually needed for the integration.

External coupling is named after the PL/I language’s EXTERNAL attribute,3


which is used to mark variables that are intended to be stored in a globally
accessible memory. In Listing 5.4, the procedures ProcA and ProcB use
the variable A , which is marked by the external attribute (lines 02 and 12).
In effect, it means that both lines 02 and 12 point to the same memory
address. If ProcA changes the value of A , the value of A in ProcB will
reflect this change, as both are pointing at the same location in memory.
3. A discussion of the external attribute is available at
https://www.ibm.com/docs/en/epfz/6.1?topic=declarations-internal-
external-attributes.

Listing 5.4 Externally Coupled Procedures in the PL/I Language

Click here to view code image

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;

There are multiple ways to implement external coupling in modern


programming languages. For example, Listing 5.5 demonstrates the use of a
global static variable for sharing values between multiple classes.
Listing 5.5 Externally Coupled Classes Through Using a Global Variable

Click here to view code image

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}!");
}
}

Effects of External Coupling

Despite the advantages over common coupling–based integration, external


coupling still involves global data and all of its shortcomings. It is still hard
to track the side effects of changing a value, as well as tracing where a
specific value came from.
As in common coupling, external coupling creates opportunities to
introduce duplicated business logic: If certain conditions should be checked
before setting a global variable’s value, then that business logic has to be
duplicated in all the modules that are modifying that shared variable.

Finally, if multiple modules are going to concurrently update a global


variable, you should implement a synchronization mechanism to prevent
concurrency issues.

External Coupling Versus Common Coupling

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

Two modules are control-coupled if one module controls the internal


execution flow of the other module. This is typically done by passing
information (such as flags, commands, or options) that tells the receiving
module not just what to do, but also how to do it. In a sense, one module
micromanages the other one. Instead of issuing a command specifying what
needs to be done, the caller also instructs the second module on how the
task should be handled.

Consider the two methods in Listing 5.6. The sendNotification


method’s argument type controls which branch of the switch statement
will be executed. Its consumers—the notifyUser method in this case—
are expected to “know” all the possible values that are supported: sms ,
email , and push .

Listing 5.6 Control Coupling: The sendNotification Method


Exposing an Argument Controlling Its Internal Execution Flow

Click here to view code image

function sendNotification(type, message) {


switch (type) {
case 'sms':
sendSMS(message);
break;
case 'email':
sendEmail(message);
break;
case 'push':
sendPushNotification(message);
break;
default:
throw new Error("Notification type not su
}
}
function notifyUser(user, message) {
let notificationType;

if (user.preferences.receiveSMS && user.phoneNumb


notificationType = 'sms';
} else if (user.preferences.receiveEmail && user.
notificationType = 'email';
} else if (user.preferences.receivePushNotificati
notificationType = 'push';
} else {
throw new Error('No suitable notification met
}

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.

Effects of Control Coupling

Going back to the terminology used in Chapter 4, control coupling entails


that the upstream module is not a proper abstraction; its boundary reveals
extraneous knowledge about its functionality. That makes the coupled
modules prone to sharing business logic, implementation details, or both.
As a result, control coupling reduces the ability to change the upstream
module without affecting its consumers:

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.

Control Coupling Versus External Coupling

Control coupling is a lower level of coupling than external coupling.


Instead of relying on a global state, the modules are communicating through
passing explicit arguments. However, control coupling is still considered a
strong level of coupling, as the upstream module doesn’t encapsulate its
functionality completely.

Control coupling’s shortcomings are addressed by the next level of module


coupling: stamp coupling.
Stamp Coupling

Two modules are considered stamp-coupled if they are communicating by


passing data structures that are revealing some of their implementation
details. In general, such data structures contain more information than is
actually needed by the integrated modules.

Effects of Stamp Coupling

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.

Listing 5.7 Stamp Coupling Through Sharing Extraneous Data

Click here to view code image

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 }

Although stamp coupling ranks quite low on structured design’s module


coupling scale, it is still not the lowest level of coupling, and it limits the
upstream module’s ability to evolve.

Stamp Coupling Versus Common Coupling

Stamp coupling might seem similar to common coupling, as both levels


share data that is not really needed for integration. However, there is a
striking difference that makes stamp coupling a much weaker level of
coupling: No business logic is shared across the boundaries.

Contrary to common coupling, with stamp coupling the data is shared


through method calls instead of a global modifiable state. Only the
originating module is in charge of managing the data structures and their
values. Hence, for stamp-coupled modules, there is no need to control
concurrency or duplicate business logic that validates the values.

Stamp Coupling Versus Control Coupling

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:

Control-coupled modules share knowledge about behavior, the


functionality that should have been encapsulated by the upstream
module.
Stamp-coupled modules share knowledge about data structures used by
the upstream module.

Knowledge of data structures is considered lower and more stable than


awareness of behavior. As a result, stamp coupling ranks lower than control
coupling.
Data Coupling

Structured design’s lowest level of module coupling is data coupling. Data-


coupled modules share no business logic, and they minimize the knowledge
of data structures that is revealed across module boundaries: Only the
minimum set of data that is actually needed for integration is shared.

The code in Listing 5.8 refactors the previous stamp-coupling example


(Listing 5.7) into data coupling–based integration.

Listing 5.8 Data Coupling Through Sharing Integration-Specific Data

Click here to view code image

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.

An extreme implementation of data coupling would see all of the upstream


modules’ internal data structures being converted to data transfer objects
(DTOs) (Fowler 2003) designed specifically for integration purposes, as
demonstrated in Listing 5.9. The CRM module no longer returns instances
of its internal Customer objects. Instead, an integration-specific object is
defined in the CRM.Integration.DTOs namespace:
CustomerSnapshot . Only the CustomerSnapshot object belongs to
the CRM module’s public interface and is shared with the downstream
components.
Listing 5.9 Data Coupling: Encapsulating Internal Data Structures by
Defining Integration-Specific Data Structures (DTOs)

Click here to view code image

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.

Comparison of Module Coupling Levels

Structured design’s model of module coupling illustrates how different


ways to connect components affect the system’s complexity and modularity.
Integrating through unintended interfaces (content coupling), exposing
implementation details, or failing to encapsulate functionality increases the
risk of complex interactions—innocent changes producing rippling effects
throughout the system.
Figure 5.2 highlights the differences between the levels of coupling
discussed throughout the chapter. The lower the coupling level, the clearer
its integration with other modules and the less knowledge exposed across
the boundaries.

Figure 5.2 Comparison of structured design’s levels of module coupling

In Figure 5.2, at the highest extreme is content coupling: Integration occurs


through the most implicit interface, one that is undocumented by the
module’s author. The consumers of the module consequently have more
knowledge about the upstream module’s implementation details than they
ideally should.

Common coupling and external coupling both hinge on the use of a


globally modifiable state for module communication. Such global states
necessitate the sharing of both the modules’ business logic and their internal
implementation details, all through an implicit integration interface.

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.

Stamp coupling does not share knowledge related to the functionality of


the upstream module, but it does expose the data structures it uses to
implement its business requirements.

At the lowest extreme, we have data coupling. Here, modules integrate by


sharing as little information about the upstream module as possible, and
always through explicitly defined public interfaces.

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:

Do you have components sharing a database? What knowledge do they


have to be aware of to keep the data consistent? (External and common
coupling)
Can you spot component interfaces sharing extraneous knowledge
through exchanging data structures? (Stamp versus data coupling)
Are you relying on the knowledge of implementation details to integrate
with an external component or a system? (Content 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. Which of the discussed coupling levels are prone to sharing business


logic?

1. Content, common, external, and stamp coupling


2. Common, external, and control coupling
3. Content, common, external, and control coupling
4. Content, common, external, control, and stamp coupling

2. Which of the discussed coupling levels exposes all of the upstream


module’s implementation details?

1. Stamp coupling
2. Content coupling
3. Control coupling
4. It’s impossible to share all implementation details.

3. Which of the discussed coupling levels can be reduced by limiting the


amount of data shared for integration needs?

1. External and data coupling


2. Common and stamp coupling
3. Control coupling
4. All of the answers are correct.

4. Which of the following coupling levels leads to sharing the knowledge of


function or logic implemented by a module?

1. Common coupling
2. Control coupling
3. External coupling
4. All of the answers are correct.
Chapter 6

Connascence

From structured design, the baton was passed,

Connascence detailed how ties can be cast.

A spectrum of coupling, to ponder and think,

Connascence reveals the depth of each link.

The preceding chapter discussed structured design’s model for evaluating


the strength of inter-module relationships. Module coupling was formulated
and introduced in the context of the procedural programming paradigm.
However, with the widespread adoption of object-oriented programming,
there emerged a need for a more detailed model that could account for the
nuances of object-oriented design. Meilir Page-Jones (1996) responded to
this need and introduced a model called 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?

“Connascence” is a Latin word that means “having been born together.”


Translating this to the realm of software design, we label two modules as
“connascent”—implying that they are born together—when their lifecycles
are intertwined. In essence, a change in one module necessitates a
corresponding change in the other, or at the very least, a meticulous
examination for potential breaking changes. Furthermore, modules gain the
status of connascent if you can postulate a reasonable change in
requirements that would induce a simultaneous change of both modules.

According to Meilir Page-Jones, the intent behind the connascence model


was to provide a tool for assessing the interrelationships spanning different
types of modules, from individual statements in a method to complex
interactions between objects. This adaptability resonates with the
multidimensional nature of modular design, as we discussed in Chapter 4.

Compared to structured design’s module coupling, connascence is a more


detailed model and describes a wider range of ways in which knowledge
can be shared across modules. Its levels are divided into two types:

1. Static connascence describes the interconnectedness between the


module at the source code level; that is, its compile-time relationships.
2. Dynamic connascence describes the runtime relationships between the
components, or how functionalities implemented in different modules
affect each other during execution.

Let’s start with static 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

Connascence of name is the weakest level of interconnectedness on the


connascence scale. It implies that to reference the same things, the
connected modules must agree on its name. For example:

The name of a variable whose value you need to read or update


The name of a method that you want to call
The name of a service to execute
Since the knowledge of the name is shared among multiple modules,
changing it will require the connected modules to change simultaneously
for the integration to work.

Consider the Python1 code in Listing 6.1.

1. I purposely chose a dynamically typed language for this example, and


will explain the reason for this in the next section.

Listing 6.1 Example of Connascence of Name

Click here to view code image

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:

Lines 01 and 02 have to use the name of the method’s argument


( name ).
Lines 02 and 03 have to use the name of the variable storing the
generated message ( message ).
Ultimately, lines 01 and 05 have to agree on the name of the method
( greet ).

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.

The next level of static connascence is strongly related, and oftentimes it is


considered to be of the same strength as connascence of name. Let’s see
what it’s about.

Connascence of Type

Connascence of type occurs if two components have to agree on the use of


a specific type. Consider the preceding example (Listing 6.1), but this time
implemented in a strongly typed language, as shown in Listing 6.2.

Listing 6.2 Example of Connascence of Type

Click here to view code image


01 private static void Greet(string name) {
02 string message = $"Hello, {name}";
03 Console.WriteLine(message);
04 }
05
06 static void Main() {
07 Greet("world");
08 }

Changing the implementation language from a dynamically typed language


(Python in Listing 6.1) to a strongly typed one (C# in Listing 6.2) changed
the connascence level of some of the relationships. For instance, now it’s
not enough to be aware of the Greet method’s name. Lines 01 and 07
must use the same type for the name argument: string .

Moreover, connascence of type is considered a slightly higher level of


connascence than that of name. Nevertheless, both connascence of name
and connascence of type usually appear in tandem. Type-agnostic
integrations are fairly uncommon. Even when types are implicit, as in
dynamically typed languages, certain types are still assumed. If a value of a
different type is provided, the code remains valid but can fail during
runtime.
Connascence of Meaning

Two components are connascent by meaning if both attribute a special


meaning to specific values. Simply stated, modules that are connascent by
meaning pass so-called magic values across their boundaries. The
knowledge of what the magic values mean has to be shared by the
connected modules.

A typical example of connascence of meaning is shown in Listing 6.3. On


line 03, the AppendResponse method is being called with the value of
the newStatus argument set to 7. What does the value of 7 actually
mean? Its exact meaning is not evident in the example. However, both the
authors of the code in Listing 6.3 and the authors of the SupportCase
objects do have to know and agree on the specific meaning of that value.

Listing 6.3 Example of Connascence of Meaning

Click here to view code image

01 void ProcessEmail(EmailMessage msg, CaseId caseId


02 var supportCase = repository.Load(caseId);
03 supportCase.AppendResponse(msg.Body, newStatu
04 }
Needless to say, such an integration design is not ideal. The values used for
communication between the modules cannot be verified by the compiler,
and it is easy to make a mistake. More importantly, the components’
interfaces are less explicit than in the previous two levels (name and type).

In many cases, you can refactor connascence of meaning into connascence


of name or of type by extracting constants or introducing an enumeration,
such as the one demonstrated in Listing 6.4. By introducing an enumeration
for the possible statuses (line 100), line 03 now explicitly states what the
new status is.

Listing 6.4 Introducing Enumeration to Reduce Connascence of Meaning


to Connascence of Type2

2. Technically, it is both connascence of name and of type, but traditionally,


only the highest level is used to denote the actual level of
interconnectedness.

Click here to view code image

01 void ProcessEmail(EmailMessage msg, CaseId caseId


02 var supportCase = repository.Load(caseId);
03 supportCase.AppendResponse(msg.Body, newStat
04 }
...

100 enum Status {


101 Open, FollowUp, OnHold, Escalated,
103 Closed, Resolved, Reopened
102 }

Connascence of Algorithm

Connascence of algorithm is, in a sense, similar to connascence of meaning,


but it takes the knowledge shared between connected modules a notch
higher. If two modules must agree on the usage of a particular algorithm to
understand the values transmitted across their interfaces, they are
connascent by algorithm.

A typical example of connascence of algorithm would be two modules


communicating by exchanging encrypted data. Unless they agree on what
algorithm will be used by both sides to encrypt and decrypt the data, the
integration won’t work.

Another common example of connascence of algorithm is illustrated in


Listing 6.5, in which two modules are sharing a file and calculating a
checksum to verify that the data was passed intact. If the remote storage
service doesn’t use the same hashing algorithm as the one used by the
UploadFile method (line 03), the integration won’t work.
Listing 6.5 Example of Connascence of Algorithm

Click here to view code image

01 static void UploadFile(string filePath) {


02 var data = ReadFile(filePath);
03 var checksum = CalculateMD5(data);
04 _storage.Upload(data, checksum);
05 }

Contrary to a popular misconception, connascence of algorithm is not about


duplication of code. It’s about the algorithm that has to be used to
understand the meaning of the passed values. In the context of this level of
connascence, it doesn’t matter whether the algorithm is duplicated in both
modules or is implemented in an external library and referenced by both
components. What matters is that the modules need to agree on one
algorithm to understand each other. For that reason, connascence of
algorithm is positioned relatively low on the connascence scale—4 out of
9.3 Duplicated business logic would deserve a much stronger level of
interconnectedness, if not the strongest level.

3. Considering both static and dynamic levels of connascence that are going
to be introduced further.
Connascence of Position

When multiple modules need to agree on a specific order of elements, they


are connascent by position. Consider a method that accepts an array of
values as its argument, where each value’s meaning is determined by its
position within the array, as illustrated in Listing 6.6.

Listing 6.6 Method Signature Introducing Connascence of Position

Click here to view code image

01 void SendEmail(string[] data) {


02 var from = data[0];
03 var to = data[1];
04 var subject = data[2];
05 var body = data[3];
06 ...
07 }

Consumers of the SendEmail method must know how the method


extracts values from the data array; this knowledge is shared between the
method itself and its callers. Such a method signature makes it quite easy to
commit an error and trigger invalid system behavior by inadvertently
passing values in the wrong order.
Fortunately, method signatures such as those shown in Listing 6.6 are not
very common. However, even a simpler signature that accepts a list of
unnamed arguments of the same type can suffer from the same drawbacks.
Consider, for example, a modified version of the method’s signature, shown
in Listing 6.7. It still makes it easy to confuse the order of passed arguments
and potentially cause incorrect behavior of the system.

Listing 6.7 Connascence of Position Due to Multiple Arguments of the


Same Type

Click here to view code image

01 void SendEmail(string from, string to


02 string subject, string body) {
03 ...
04 }

Another common example of connascence of position involves the passing


of unnamed tuples between modules. Take, for instance, the method
illustrated in Listing 6.8. It returns a tuple containing two values: the
current time in the local time zone and the current time in the UTC time
zone. The knowledge of which time value comes first and which follows is
shared by both the method and its callers.
Listing 6.8 Connascence of Position Due to a Method Returning Unnamed
Tuples

Click here to view code image

01 (DateTime, DateTime) GetCurrentDateTime() {


02 DateTime localTime = DateTime.Now;
03 DateTime utcTime = DateTime.UtcNow;
04 return (localTime, utcTime);
05 }

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 position is the highest level of static connascence. At first


glance, it might not appear significantly different from the weakest level,
connascence of name. However, the distinction between these two levels is
indeed significant. One level makes the integration interface explicit, while
the other makes it implicit and error prone.
Dynamic Connascence

The levels of static connascence discussed previously can be identified and


evaluated by reading and examining the code. In theory, this process can be
automated via the use of a static code analysis tool.

On the other hand, dynamic connascence characterizes a more intricate type


of relationship between modules: dependencies in their runtime behaviors.
As a result, the levels of dynamic connascence embody stronger
dependencies between modules than the levels of static connascence. This
also leads to a greater extent of knowledge that is shared between the
modules (Figure 6.2).
Figure 6.2 Shared knowledge as a function of different levels of dynamic connascence

Let’s explore dynamic connascence starting from its weakest level:


connascence of execution.

Connascence of Execution

Connascence of execution is the dynamic counterpart of the static


connascence of position. Modules exhibit connascence of execution when
their execution must follow a specific sequence. Take, for example, the
methods defined by the DbConnection interface illustrated in Listing
6.9.

Listing 6.9 Example of Connascence of Execution

Click here to view code image

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.

Each method defined by the interface is connascent by execution with


almost all the other methods:

All methods can only be executed following the OpenConnection


method.
Beyond OpenConnection , no other method can be executed after
CloseConnection .
A transaction can be committed ( Commit ) or rolled back ( Rollback )
only after it was initiated with BeginTransaction .
A query can be executed ( ExecuteQuery ) only after a transaction has
been started but before it has been committed or rolled back.

This example illustrates that connascence of execution shares more


knowledge than all of the levels of static connascence. The runtime
dependencies between the methods indicate that the methods’
functionalities are closely related to each other.

Connascence of Timing

Connascence of timing shares close ties with the previous level,


connascence of execution. In this case, not only should the functionalities of
two modules be executed in a specific order, but there should also be a
specific time interval between them.

Going back to the example of working with a relational database (Listing


6.9), let’s introduce another requirement: If a connection is opened, but no
operations are executed for 30 seconds, the connection should be timed out.
The exact time interval, 30 seconds, that has to pass between opening a
connection and when it should be timed out makes these two actions
connascent by timing.

Numerous examples of connascence of timing relationships exist in real-


time systems. For instance, many car doors lock automatically if they have
been unlocked but remain unopened for a set duration. Similarly, an X-ray
machine should deactivate a certain number of seconds after it was
activated.

Occurrences of connascence of timing that are more peculiar and harder to


spot can be observed in modules relying on the system’s clock. Consider the
method GetTime in Listing 6.10.

Listing 6.10 Connascence of Time Due to Reliance on the System Clock

Click here to view code image


01 (int, int) GetTime() {
02 int hour = DateTime.Now.Hour;
03 int minute = DateTime.Now.Minute;
04 return (hour, minute);
05 }

The GetTime method returns a tuple containing two numbers


corresponding to the current time. However, the system’s clock is queried
twice: on line 02 to retrieve the current hour and on line 03 to obtain the
current minute. This implementation assumes that both lines are executed
instantaneously and no time elapses between the two statements. Consider
what would happen if, for instance, the execution is delayed by the
operating system between lines 02 and 03, causing the hour to advance
during this delay. Or what if the system’s clock shifts to accommodate
daylight saving time right between the two calls? Both scenarios would
yield invalid results, illustrating that lines 02 and 03 are connascent by
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.

Listing 6.11 Refactoring Connascence of Time to Connascence of Type


Click here to view code image

01 (int, int) GetTime() {


02 DateTime now = DateTime.Now;
03 int hour = now.Hour;
04 int minute = now.Minute;
05 return (hour, minute);
06 }

Ultimately, though the example in Listing 6.10 might seem overly


theoretical, the fundamental issue it demonstrates is fairly common.
Systems that rely on querying the system’s clock multiple times within a
short period can encounter timing discrepancies if not carefully handled. In
real-world scenarios, such timing dependencies can lead to various issues.
Consider a scenario where a system logs events or transactions with
timestamps. If there is a delay or inconsistency between the timestamp
generation at different stages of the process, it can result in incorrect data
analysis or an invalid order of events. For instance, if event A is logged with
a timestamp earlier than event B due to a timing discrepancy, it may lead to
incorrect conclusions or misinterpretations of the data.

Connascence of Value

Connascence of value is almost the highest level of connascence, as this


level describes a strong functional relationship between different elements
of a system. Values that have to change simultaneously, such as an atomic
transaction, or that otherwise place the system in an incorrect state, are
connascent by value.

A straightforward example of connascence of value is an arithmetic


constraint (Santos et al. 2019). Consider the structure in Listing 6.12, which
contains three variables ( EdgeA , EdgeB , and EdgeC ) representing the
lengths of a triangle’s edges.

Listing 6.12 Connascence of Value Between Three Values

Click here to view code image

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.

Figure 6.3 Mathematical constraints for edges of a triangle

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.

Constraints driven by business rules and invariants are more common.


Consider a retail system that must implement the following business rule: If
a customer is verified, a sales agent can upgrade them to priority shipping.
Let’s assume the customer is represented by an object with the fields shown
in Listing 6.13.
Listing 6.13 Connascence of Value Imposed by Business Rules

Click here to view code image

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 }

The business rule introduces connascence of value between the fields


isVerified and priorityShippingEnabled (lines 04 and 05). A
valid value for priorityShippingEnabled depends on the value of
isVerified . If the customer’s verification status is revoked, it
necessitates changing priorityShippingEnabled to false as well.

Connascence of Identity

The highest level of connascence is connascence of identity. It arises when


two objects need to reference the exact same instance of a third object to
operate correctly. The functionality of such modules is strongly
interconnected, and is often orchestrated through the shared object.

Since the concept of connascence was introduced in the context of object-


oriented programming, the vast majority of examples of connascence of
identity are related to interactions between classes. For instance, consider
modules sharing the same database connection pool. For efficient use of
available database connections, all modules must use the exact same pool,
and that makes them connascent by identity.

To extrapolate this level to other types of modules, such as services, it’s


important to understand the essence of this relationship and what makes it
the strongest on the connascence scale: When multiple objects work with
the same instance of another object, it can be assumed that the
interconnected modules depend on a strongly consistent state provided by
the shared object. Every change in the shared object is immediately
observable by the connected modules. Moreover, the shared object can
orchestrate the functionalities of the connected modules.

In the context of distributed systems, connascence of identity can be


observed when services are integrated by writing to and reading from the
same database, as illustrated in Figure 6.4.

Figure 6.4 Connascence of identity due to integration via a database

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.

Conversely, if the services do not require transactional consistency of the


shared data, it would not be classified as connascence of identity—for
instance, if two services communicate via a message bus and do not assume
transactional consistency of the published messages.
Evaluating Connascence

Consider the method call in Listing 6.14 being made from the retail
module to the accounting module.

Listing 6.14 Example of Different Levels of Connascence

Click here to view code image

(res, balance, tran_id) = accounting.process_payment(


account_id='LVG141028',
transaction_type=3,
credit_card='S5hDn175mPiDL4D5ftbtMw=='
)

Let’s examine the levels of static connascence present here:

Connascence of name: The calling module ( retail ) and the


accounting module must agree on the name of the method
( process_paymen t) and the names of its arguments.
Connascence of type: Despite being implemented in a dynamically typed
language (Python), the arguments cannot accept just any value; the
values must be of specific types: account_id and credit_card are
strings, while transaction_type is numeric.
Connascence of meaning: The transaction _ type argument gets
the value of 3. This number bears a special meaning shared by the
calling and the called modules.
Connascence of algorithm: The value of the credit_card argument is
encrypted using the AES algorithm. Both modules must agree on the use
of this algorithm for exchanging encrypted data.
Connascence of position: The process_payment method returns a
tuple consisting of three values. The returned values should be accepted
in exactly the same order as they are passed.

When multiple levels of connascence are present, the overall level of


connascence is the highest one. The preceding example contains all the
possible levels of static connascence. Therefore, the overall connascence
between the modules retail and accounting is connascence of
position.4

4. Based on the information available in the example. In reality, higher


levels of dynamic connascence might be there as well.

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.

That said, in some cases, connascence cannot be downgraded. It’s a


common mistake to treat the levels of connascence as a design goal—
always trying to reduce the components’ relationships to connascence of
name or of type. In many cases it’s simply not possible. For example,
modules that are connascent by timing have to be executed after a set
period, and no amount of refactoring is going to change that business
requirement. The same is true for connascence of algorithm, of execution,
of value, and of identity.

A high level of connascence indicates that the components are, literally,


“born together” and shouldn’t be separated. Instead, they should reside
close to each other. I will revisit this notion in more detail in Chapter 8,
Distance, and more importantly, in Chapter 10, Balancing Coupling.

Connascence and Structured Design’s Module Coupling

Module coupling and connascence are two ways of evaluating the


interconnectedness of a system’s components. Because both concepts
seemingly measure the same phenomena, it makes sense to try to draw
parallels between the two models’ levels.
Let’s examine the levels of module coupling and determine which levels of
connascence are relevant to each one.

Data Coupling

This is the weakest level of module coupling, which assumes that no


business logic is shared and that the modules exchange only the minimum
data necessary for integration. From the perspective of connascence, all
levels of static connascence can fit here. Even if the highest level of static
connascence (connascence of position) is present, it still aligns with
structured design’s data coupling level, as long as the shared arguments
between the modules contain only the minimal data set required for their
integration.

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

Control coupling assumes that a module is closely familiar with the


functionality of the other module, and it is able to control its behavior. In
other words, functionality is not encapsulated by the upstream module and
is partially implemented in its consumers. No levels of connascence
describe such a relationship.

External Coupling

Externally coupled modules communicate via global variables. This method


of integration is compatible with both connascence of value and
connascence of identity:

Connascence of value: Both modules have to implement the same


business rules that verify the validity of the shared data.
Connascence of identity: Both modules depend on a strongly consistent
shared state.

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

Common coupling represents an extended case of external coupling. Rather


than sharing integration-specific values, extraneous data is managed as
globally accessible state. In addition, the knowledge of how the shared data
is structured in the shared memory is duplicated in both modules. In the
context of connascence levels, it is still a case of connascence of identity, as
an external component is used to store the state and make it accessible to
the connected modules.

Content Coupling

Content-coupled modules are integrated through the use of one of the


module’s private interfaces or implementation details; for example, by
accessing the value of a private property through reflection, as
demonstrated in Listing 6.15.

Listing 6.15 Content Coupling Through Reflection

Click here to view code image

01 var customer = LoadNextCustomer();


02 var value = typeof(Customer)
03 .GetProperty("_verificationStatus
04 .GetValue(customer);

From structured design’s perspective, this is clearly the strongest level of


coupling: content coupling. But what about connascence?
To read a value of a private field, all the code in Listing 6.15 needs to know
is the name of the private field. Thus, from connascence’s perspective, this
is the weakest level: connascence of name! What is going on here?

While both models ought to describe the same phenomenon (the


interconnectedness of components), structured design’s module coupling
and connascence address different aspects of coupling. This is also evident
by the fact that the majority of the dynamic connascence levels are not
reflected in module coupling.

The next chapter explores the notion of coupling from a different


perspective, focusing on the essence of both structured design’s module
coupling and connascence, and uses these insights to formulate an
integrated model of inter-module relationships known as integration
strength.

Key Takeaways

Compared to structured design’s module coupling, the connascence model


reflects different kinds of knowledge that can be shared across component
boundaries. The model’s levels are categorized into two types: static
connascence and dynamic connascence.

Static connascence describes the different interface decisions modules need


to coordinate to communicate with each other. Connascence of name and of
type are the weakest levels—the components have to agree on a name or a
type. Components that are connascent by meaning attribute special
meanings to specific values, while connascence of algorithm requires the
use of an agreed-upon algorithm to understand the meaning of a value.
Finally, elements that are connascent by position rely on the specific
position of the elements in source files.

Dynamic connascence moves the focus from compile-time to runtime


dependencies. Modules that are connascent by execution must be executed
in a particular order. If there is a specific time interval between the
executions, then the modules are connascent by timing. Connascence of
value arises if the modules’ data should change according to shared
business rules or constraints. If the related modules require an additional
component, and the same instance should be used by all the modules to
function properly, then the modules are connascent by identity.

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. Which of the following statements is true?

1. The levels of static connascence describe a higher level of


interconnectedness than the levels of dynamic connascence.
2. The levels of dynamic connascence describe a higher level of
interconnectedness than the levels of static connascence.
3. The levels of static and dynamic connascence are parallel.
4. The levels of static connascence describe compile-time relationships,
while the levels of dynamic connascence describe runtime relationships,
and thus, the two types cannot be compared.

2. Which level of connascence matches structured design’s content


coupling?

1. Connascence of identity
2. Connascence of value
3. Connascence of position
4. None of the answers are correct.

3. What is the difference between connascence of position and connascence


of execution?

1. The two levels of connascence are identical.


2. Connascence of position describes a compile-time relationship, while
connascence of execution is a runtime relationship.
3. Connascence of position describes a runtime relationship, while
connascence of execution is a compile-time relationship.
4. None of the answers are correct.
4. Which of the connascence levels reflects a strong relationship between
the functionalities of two modules?

1. Connascence of algorithm
2. Connascence of value
3. Connascence of identity
4. Answers B and C are correct.
Chapter 7

Integration Strength

Built on the backbone of structured design,

The integration strength model, a new paradigm.

Connascence on board, detailing the ties,

Exposing the nuances of knowledge flow paths.

The previous chapter ended with an example in which structured design’s


module coupling and connascence produced opposite results: Module
coupling pointed to the strongest level of interconnectedness, while
connascence pointed to the weakest level. Although this result might sound
surprising, it is reasonable and expected once you analyze what exactly is
reflected by each model.

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:

1. Interface type: Structured design shows that different types of interfaces


can be used to connect components. For example, modules can
communicate through private interfaces (content coupling) or public
interfaces (stamp and data coupling). Chapter 6 discussed how
communicating through private interfaces shares more knowledge, and
thus results in stronger coupling between the connected components. In
the next section, you’ll learn that there is another unique type of
interface: Components can change together even if they’re not physically
integrated, meaning their physical interface type is “none.”
2. Interface complexity: The way components are connected can lead to
complex interactions. Going back to the example of using
implementation details for integration (content coupling), any change in
the upstream component has the potential to break the integration with
its consumers. On the other hand, a module that exposes the minimum
data required for integration (data coupling) is more stable and
predictable. Overall, the more knowledge that is shared by a module, the
higher the chances of complex interactions with its consumers.
Another aspect of interface design that can increase complexity is its
transparency. Implicit interfaces are harder and more expensive to
maintain. For instance, if a component makes an assumption about how
another component is functioning, even if it is correct, it still creates an
opening for complex interactions if that functionality changes.
Implicit interfaces also make it possible to integrate components
incorrectly, and cause incorrect system behavior. For example,
connascence of position makes it easier to cause an integration error than
connascence of name.

The two factors describe different dynamics of cross-component


interactions and must be considered simultaneously. So, which of the two
models, module coupling or connascence, will do a better job?

Structured Design, Connascence, or Both?

Using either structured design’s module coupling or connascence to


evaluate the strength of coupling is not possible. Some of module
coupling’s coarse-grained levels are better at showing interface types,
whereas connascence is better at reflecting the complexity of interfaces.

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.

Structured Design and Connascence: Blind Spots

Consider a system with two modules implementing exactly the same


business functionality. For example, a business rule for defining whether a
customer qualifies for free shipping is implemented in two services:
Retail and Fulfillment , as illustrated in Figure 7.1.

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.

Furthermore, in Figure 7.1, I purposely didn’t draw a line representing a


connection, or a direct dependency between the two services. Therefore,
they are not physically integrated. Nevertheless, they still share knowledge
and still must change together. That’s another blind spot in the module
coupling and connascence models: They apply only if modules are
physically integrated.

Note

If you are wondering whether this is connascence of


algorithm, it’s not. Connascence of algorithm states that
modules must agree on the use of an algorithm to understand
the data they are exchanging. If the duplicated logic doesn’t
affect the data used for communication between the
modules, it’s no longer connascence of algorithm.
Different Strategy

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

The integration strength model incorporates the essence of both structured


design’s module coupling and connascence. Again, we are not merely
merging the levels, but rather incorporating them into a different structure.

The new model aims to achieve the following goals:

Practicality: Integration strength should be easy to grasp and to apply in


day-to-day work.
Versatility: Enable evaluating the strength of coupling for all kinds of
modules, from individual lines of code to services within a distributed
system.
Completeness: Address the shortcomings and blind spots of module
coupling and connascence.

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.

Running Example: Sharing a Database

Consider the system illustrated in Figure 7.2. It is composed of two services


working with the same database. What can you say about this design? Does
it make the two services strongly coupled or loosely coupled?
Figure 7.2 Two services working with the same database

Well, that was purposefully a misleading question. The diagram doesn’t


present enough information to evaluate coupling. As I describe each level of
integration strength, I’ll revisit this example to demonstrate each level.

Let’s start exploring the model from the strongest level, intrusive coupling.

Intrusive Coupling

Instead of communicating through public interfaces, the downstream


module communicates through, and thus depends on, the implementation
details of the upstream module (Figure 7.3).1

Figure 7.3 Intrusive coupling

1. Intrusive coupling is synonymous with structured design’s content


coupling. I decided against using the term “content coupling,” as originally
it related to the contents of a module, or its source code. Today we have a
wider range of options to introduce dependencies on implementation
details. Initially I called it “implementation coupling,” but Vaughn Vernon
came up with the term “intrusive coupling,” which brilliantly reflects the
nature of this integration interface.

The implementation details used for integration encompass all aspects of


the upstream module that were not originally intended for integration. This
level is called “intrusive” to emphasize that the means for integration were
not accounted for by the authors of the upstream module.

Examples of Intrusive Coupling

The examples of content coupling in structured design that we discussed in


Chapter 5 are applicable here as well. Working with private members and
methods via reflection is intrusive coupling: You are using reflection to
interact with elements that were never intended for integration. That said,
it’s not reflection per se that makes an integration intrusive, but the
underlying intent. Here is an example that might appear similar at first, but
is actually different.

Object-Relational Mapping (ORM) frameworks often use reflection to


interact with data models. Reflection allows the ORM to dynamically
access and manipulate properties of mapped objects. When using such a
tool, you acknowledge that the framework has to access fields of your
objects, be it via reflection or another mechanism. Therefore, this is no
longer a case of intrusive coupling.
There are creative ways to introduce intrusive coupling. Assume you are
using a commercial, off-the-shelf order management system. A new
business requirement comes in stating that you need to receive notification
about every new order. Unfortunately, the system doesn’t provide a way to
subscribe for such notifications. However, you can look at its source code
and easily modify the implementation to push the missing notifications.

Integration through such a hack is an example of “inverted” intrusive


coupling. Obviously, any upgrade of the order management system would
reset the source code, subsequently breaking the integration.

Running Example: Intrusive Coupling by Sharing a Database

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.

Effects of Intrusive Coupling

Intrusive coupling might be undesirable for many reasons. This is why


during the structured design era it was referred to as pathological coupling.

First, it is fragile. Any modification in the upstream module holds the


potential of breaking the integration with downstream components. Hence,
every change to the upstream module should be carefully examined and
treated as a possible integration-breaking change.

Second, intrusive coupling represents the most implicit integration


interface. Often, when such integration happens, the authors of the upstream
modules are not aware of it. As a result, the careful examination of each
release as a potentially integration-breaking change, mentioned earlier, isn’t
possible.

Third, it breaks encapsulation. Modular design requires hiding as much


knowledge as possible behind the boundaries of modules. Intrusive
coupling breaks boundaries. You have to assume that all knowledge about
the upstream component—its functionality and how it’s implemented—is
shared with the downstream components.

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

Two modules are functionally coupled if their functionalities are


interrelated.

The shared business responsibility comes in the form of interrelated


business rules, invariants, and algorithms, or what we usually call business
logic. As a result, when the functional (business) requirements change, the
changes are likely to affect all the coupled modules. Because cascading
changes are very likely with functional coupling, this level is positioned
high on the integration strength scale, just below intrusive coupling.

Functionally coupled modules share the knowledge of the functionalities


they are implementing. Interestingly, unlike in the other levels of
integration strength, there is no upstream–downstream relationship here.
Instead, the knowledge flows in both directions. If any of the functionally
coupled modules change, the change is likely to propagate to the other
module(s). Hence, if two components are functionally coupled, they both
can be considered upstream components.

Degrees of Functional Coupling

Contrary to intrusive coupling, functional coupling is not an all-or-nothing


scenario, where modules are exclusively classified as coupled or not.
Instead, this level of integration strength spans an additional dimension,
which describes the extent of knowledge shared by the modules.

Sequential Functionality

Sequential coupling (Jamilah et al. 2012), also known as temporal coupling,


happens when multiple actions must be called in a particular order. As the
connascence model shows, such a relationship indicates a strong level of
interconnectedness between the connected modules. Chances are they are
implementing the lifecycle of the same business process, work on the same
data, or share similar responsibilities in the overarching system.
Both connascence of execution and connascence of timing fall under the
definition of sequential coupling and can be used to identify a finer-grained
degree of functional coupling.

Transactional Functionality

Transactional coupling refers to a scenario where multiple operations have


to be carried out as a single unit of work: a transaction. All the operations
included must succeed for the transaction to be considered successful. If
any operation fails, then all operations within the transaction are typically
rolled back to their previous state to maintain the system’s integrity.

A common symptom of transactional coupling is the need to manage


concurrency. If two modules are modifying the same set of data,
concurrency control is essential for maintaining data integrity in multiuser
environments.

From a knowledge sharing perspective, transactional coupling entails a high


amount of knowledge shared between the connected modules. This
knowledge is reflected by the next two levels of dynamic connascence:

Connascence of value: Values have to change simultaneously, either to


equal values or according to algorithmic constraints or business
invariants.
Connascence of identity: Modules operate on the same set of data, and
the data has to be strongly consistent for the modules to operate
correctly.

Symmetric Functionality

Finally, the highest level of functional coupling is when two modules


implement the same functionality. It’s crucial to stress that not all cases of
duplicated logic result in symmetric functional coupling. Rather, symmetric
functional coupling occurs only in the following cases:

Both modules implement the same functionality. It doesn’t matter how it


is implemented. The modules can employ different algorithms to achieve
the goal.
When the requirements for the shared behavior change, the change has to
be implemented by all of the functionally coupled modules
simultaneously. Failing to do so will result in an invalid state of the
system (bugs).

A more succinct way to define symmetric functional coupling is as a


violation of the “Don’t Repeat Yourself” principle (Hunt and Thomas
2000):

Every piece of knowledge must have a single, unambiguous, authoritative


representation within a system.—Andrew Hunt and David Thomas

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.

Figure 7.5 summarizes the degrees of functional coupling.


Figure 7.5 Degrees of functional coupling

Causes for Functional Coupling

Returning to David L. Parnas’s definition, a module is essentially an


assignment of responsibility. Its purpose is to encapsulate decisions,
ensuring that changes to those decisions affect only that module. By
definition, functional coupling involves a high degree of knowledge
sharing, signaling ineffective module boundaries.
A useful heuristic for identifying functional coupling is to hypothesize a
change in business requirements that would impact the functionalities of
multiple modules. If such a scenario is plausible, it suggests the modules
may be functionally coupled.

Running Example: Functional Coupling by Sharing a Database

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

Even the lowest degree of functional coupling (sequential) ranks high on


the integration strength scale. The shared knowledge is wide, and changes
in the related functionalities of modules are likely to propagate across the
boundaries to other coupled modules.

Sharing knowledge about the functionalities implemented by modules


naturally results in broad and complex interfaces. Often such integration
results in relationships between the modules that are implicit and hard to
track. Consequently, it becomes easy to cause unintended system behavior
by failing to implement a change across all impacted modules.

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.

Listing 7.1 Different Models Representing the Same Business Entity

Click here to view code image

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

public TimeSpan LongestResponseTimeBy


{ g
public TimeSpan AverageResponseTimeBy
{ g
public TimeSpan AverageResponseTimeBy
{ g
public DateTime CreatedDate { g
public DateTime LastUpdatedDate { g
public int ReassignedCount { g
public List<AgentId> PreviouslyAssignedAge
{ g
public AgentId CurrentAgent { g
public CustomerId OpenedBy { g
public CaseStatus CurrentStatus { g
public List<CaseStatus> PastStatuses { g
public List<string> Tags { 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.

Reusing the same model across different modules can be detrimental to


modular design. Let’s say we have two modules, Distribution and
Accounting . The Distribution module exposes the model it uses
internally to implement its functionality as part of its public interface. The
Accounting module references distribution and, thus, reuses part of that
model (Figure 7.7).

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.

A model of the business domain used in software contains data and


behavior; however, this level of integration strength assumes sharing data
models only, as sharing behavior belongs to functional coupling.

Degrees of Model Coupling

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

As we discussed in Chapter 6, levels of static connascence describe


compile-time dependencies between modules; the lowest levels are
connascence of name and of type. As you move up the scale, the interface
becomes more implicit, increasing the chances of making an integration
mistake.

Running Example: Model Coupling by Sharing a Database

In the scenario illustrated in Figure 7.9, the database belongs to service


A , which is responsible for managing its data. However, service B is
granted permission to read this data. Unlike the scenario of intrusive
coupling, here the database serves as a public interface. Hence, because
service B is allowed to read any information it requires and the data is
reflected in service A ’s internal model, this qualifies as model coupling.

Figure 7.9 Reading data from another component’s operational database introduces model coupling.

Effects of Model Coupling

Because models are necessary for implementing functionality of modules,


exposing a model across a module’s boundary can be perceived as a leakage
of implementation details. This naturally leads to the question: How is this
better than intrusive or functional coupling?
First, model-coupled interfaces reveal data models rather than behavior.
Sharing knowledge about functional behavior falls into the functional
coupling level. Sharing data structures is far less concerning than sharing
business logic. Moreover, data structures tend to be more stable than their
associated behavior, and thus they result in fewer cascading changes.

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.

However, this type of interface can still introduce accidental complexity


into the system. If the downstream components only use a part of the shared
model, the upstream component is sharing unnecessary information, which
will make it harder to change.3 It will be even harder with higher degrees of
model coupling. Moreover, as models propagate throughout the system, an
increasing number of components may end up using models that are not
optimized for implementing their specific functionalities.
3. This is analogous to structured design’s stamp coupling.

Contract Coupling

Modules are contract-coupled if they communicate through an integration-


specific model: a contract.

A contract is an agreement outlining the terms of a collaboration. In


software design, a contract is the communication protocol between
components of a system. In a sense, it is a model of a model. It abstracts
away extraneous information from the business domain model used by a
module, thus reducing the knowledge exposed across its boundary.

As illustrated in Figure 7.10, an upstream module can maintain an


integration-specific model, or integration contract, designed for effective
communication with other modules. The contract model is exposed across
the boundary and shared with downstream modules. However, it is not used
to implement the upstream module’s functionality. Instead, the calls are
translated into the internal implementation model, which was discussed in
the model coupling level.
Figure 7.10 Contract coupling: Connecting downstream components through a model optimized for
integration

Having a dedicated integration model provides multiple advantages to the


upstream module:

The integration contract is more stable than the underlying


implementation model. The implementation model can be evolved,
changed, or expanded, and as long as it can still be translated into the
same integration contract, the changes won’t propagate to the
downstream modules.
It minimizes the knowledge shared by the upstream module. Only the
integration contract is exposed to the consumers, and the implementation
model remains encapsulated behind the boundary. As a result, the two
models can evolve at different rates, allowing the implementation model
to evolve at a faster pace.
The integration contract can be versioned. The same upstream module
can expose different integration contracts simultaneously—for example,
to allow its consumers to gradually migrate to a new version.
All of these advantages can be summarized by reiterating that the less
knowledge a module shares, the fewer cascading changes it will cause to its
consumers.

Example of 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

4. For example, when working in C#, it’s convenient to transform GUID


values to strings, as not all platforms will be able to natively parse the
original format.

The SupportCaseDetails object in Listing 7.2 abstracts those details


of the operational model behind primitive values, making them simpler for
the downstream modules to consume.

Listing 7.2 A Business Entity and an Integration-Specific Model


Click here to view code image

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
...
}
...
}

Remember David L. Parnas’s saying that a module is an abstraction, and


according to Edsger Dijkstra, the purpose of abstraction is to create a new
semantic level in which one can be absolutely precise? Introducing an
explicit integration contract takes this idea to the extreme. The integration
contract can create a new language that focuses entirely on the tasks that
can be carried out using the module, while completely abstracting how the
tasks are implemented. For example, the integration contract can be
formulated in terms of Commands —actions that can be executed; and
Queries —methods for fetching information from the module. Listing 7.3
demonstrates command objects that could have been an integration contract
for the Support Cases module, as well as the API for executing
commands and queries.
Listing 7.3 Using a Task-Oriented Language for Defining a Module’s
Integration Contract

Click here to view code image

namespace WolfDesk.SupportCases.Application.API.Comma
public class EscalateCase {
public readonly string CaseId;
public readonly string CustomerId;
public readonly string EscalationReason;
...
}

public class ResolveCase {


public readonly string CaseId;
public readonly string Comment;
...
}

public class PutCaseOnHold {


public readonly string CaseId;
public readonly string Comment;
public readonly long Until;
...

}
...
}

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
...
}
}

This example illustrates the notion of an integration contract at a relatively


high level of abstraction: a service exposing its functionality via an API. At
the beginning of the chapter, I said that the integration strength model
should be applicable for modules at all levels of abstraction. Let’s shift the
focus to lower levels. Consider the following design patterns as described in
the original Design Patterns book (Gamma et al. 1995):

Facade: “Provide a unified interface to a set of interfaces in a


subsystem. Facade defines a higher-level interface that makes the
subsystem easier to use.”
Bridge: “Decouple an abstraction from its implementation so that the
two can vary independently.”
Adapter: “Convert the interface of a class into another interface clients
expect. Adapter lets classes work together that couldn’t otherwise
because of incompatible interfaces.”

These design patterns propose different ways of implementing integration


contracts for groups of objects (namespace, packages, libraries, etc.). If you
look at a single object, it oftentimes will have both public and private
methods, and its public methods don’t implement the functionality directly
but forward the processing to one or more private methods, thereby acting
as an integration contract.

Finally, the integration contract doesn’t have to be tailor-made. A system


can use widely accepted protocols for specifying integration contracts. For
example, if you are implementing an email service, you will probably use
SMTP, IMAP, and/or POP3 as the integration contracts with the consumers.
Degrees of Contract Coupling

Because the knowledge that flows through a contract-based interface


depends on the format and structure of the shared data, we can evaluate its
degree in the same way as for model coupling: through the levels of static
connascence (Figure 7.11).

Figure 7.11 Degrees of contract coupling and model coupling


Using the same scale for evaluating the degrees of both contract coupling
and model coupling stresses the important difference between the two
interface types. Even the strongest degree of contract coupling shares less
knowledge than the weakest degree of model coupling. The difference is in
the underlying type of knowledge that is being shared; a model is related to
implementation details, while an integration contract is not. Thus, even the
strongest degree of contract coupling is much less prone to cascading
changes than the weakest degree of model coupling.

Depth of Contract Coupling

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.

Listing 7.4 An Integration Contract That Doesn’t Encapsulate Any Details


of the Implementation Model
Click here to view code image

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
}
}

Even though the MessageDTO ’s degree is connascence of name and of


type, it doesn’t encapsulate any knowledge about the operational object
Message . As such, even though it’s a separate object that is intended to be
part of the service’s integration contract, it’s still closer to model coupling
than to contract coupling.
Recall the visual heuristic for evaluating module depth that we discussed in
Chapter 4. If you chart a module as a rectangle, where the area represents
implementation details while the bottom edge is the module’s functionality,
the resultant “depth” reflects how effective it is at hiding knowledge (Figure
7.12).

Figure 7.12 Depth: A visual heuristic for evaluating modules

The MessageDTO object (Listing 7.4) doesn’t encapsulate any knowledge.


Thus, the rectangle’s area is equal to its bottom edge, making it a shallow—
ineffective—module.

As this example shows, it’s important to evaluate the knowledge


encapsulated by an integration model. If, as in this example, it doesn’t
encapsulate any knowledge, not only does it not make the system simpler,
but also it makes it more complicated by introducing an unnecessary
“moving part.”

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.

Running Example: Contract Coupling by Sharing a Database

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

Effects of Contract Coupling

Contract coupling minimizes the knowledge shared across the boundaries.


As a result, the modules’ boundaries are more stable and less prone to
cascading changes. Having an integration-specific model also makes this
integration method the most explicit.

Despite its numerous advantages, it’s important to treat contract coupling as


just another tool in the toolbox, and not as an absolute end goal. First, as the
example in Listing 7.4 shows, it’s not always beneficial to use contract
coupling. Second, contract coupling is not always possible. There will be
cases when you have to use the same implementation model, implement
closely related functionality, or introduce dependency on private interfaces.
Once I finish covering the remaining dimensions of coupling in Chapter 8,
Distance, and Chapter 9, Volatility, I will return to this topic and discuss it
in greater detail in Chapter 10, Balancing Coupling.

Note

In software design terminology, there is an accepted term


that could be used to describe both model coupling and
contract coupling: semantic coupling. I chose not to use it,
because of the rather significant differences in integration
strength resulting from model coupling and contract
coupling. Putting the two levels under the same umbrella
wouldn’t do justice to the differences in shared knowledge.

Integration Strength Discussion

The four levels of integration strength reflect the essence of integration


design: what the effect of a change in one component is going to be on the
rest of the system. The levels describe probabilities of a change being
contained in its module or rippling throughout the system. The stronger the
integration, the less predictable the effects of changes. Figure 7.14
illustrates the modules’ shared reasons for change as a function of interface
type (integration strength level) and its complexity (degree).
Figure 7.14 Shared reasons for change as a function of interface type (integration strength level) and
its complexity (degree)

In the beginning of the chapter, I said that structured design’s module


coupling and connascence reflect different aspects of cross-component
interactions, and thus they are best used in tandem. Let’s quickly summarize
the integration strength model by analyzing how it relates to structured
design’s coupling and connascence.
The parallels between the four levels of integration strength and structured
design’s levels of module coupling are evident:

Intrusive coupling is synonymous with structured design’s content


coupling.
Functional coupling mimics structured design’s common, external, and
control coupling. All these levels entail functional dependency between
the connected modules.
Model coupling is closest to stamp coupling. Modules exchange data
records that might contain more information than is actually needed for
integration.
The idea of contract coupling resembles data coupling: minimizing the
knowledge shared across module boundaries.

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:

The complexity of functional knowledge shared across component


boundaries is reflected by the four levels of dynamic connascence.
Furthermore, an additional degree—symmetric functional coupling—is
needed to account for cases of the same functionality implemented in
different modules.
The complexity of models can be evaluated with the five levels of static
connascence and can be applicable both to model coupling and contract
coupling levels. However, it’s also important to keep in mind that a
module should encapsulate knowledge, and use it to ensure that the
integration model (contract) is indeed valuable.

Let’s see a few concrete cases of these concepts in action.

Example: Distributed System

Consider the system illustrated in Figure 7.15. Service A (1) executes


operations submitted by user commands. A typical operation changes data
in the database (2) and publishes corresponding messages to the message
bus (3). The messages are forwarded to three subscribers:

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:

1. An action executed by a user command in service A (1) should result


in both updating the operational database (2) and publishing events to
the message bus (3). Updating the database and publishing events to the
message bus have to succeed or fail together. Hence, the two
components are supposed to be updated as a transaction. As a result,
components 2 and 3 are functionally coupled, with the degree of
connascence of value.
2. Service C (5) uses the data models published by service A (1),
which happen to reflect the service’s model of the business domain.
Hence, the two are model-coupled.
3. Service D (6) has to be executed after a specific period to make sure
service C (5) has enough time to process its messages. The services
are functionally coupled, with the degree of temporal coupling
(connascence of time).

Integration Strength and Asynchronous Execution

It is widely accepted that components communicating asynchronously are


considered less coupled than those that are integrated synchronously. At this
point, you might justifiably ask, “How come asynchronous integration
hasn’t been covered in the context of integration strength?”

Let’s examine the example in Figure 7.16 of two components


communicating synchronously and asynchronously through a message
queue.

Figure 7.16 Synchronously and asynchronously integrated components


Based on the communication mechanism, can you deduce that the
asynchronous design is necessarily less coupled than the synchronous
design? That was another tricky question. Figure 7.16 doesn’t provide
enough information to judge the integration strength of the two scenarios.
Just as in the example of two services working with the same database,
different additional details lead to different results:

If an integration-specific model is communicated through the message


queue, the services are coupled by contract.
If the producer just pushes data in the format of its model of the business
domain, and the consumer has to make sense of it, the services are
coupled by model.
Assume that the messages published by the producer are delayed; that is,
the consumers should process the messages only after a set period. An
example would be a case in which messages are used to verify
completion of a business process after a certain period, and are rolled
back if needed. In this case, the services are functionally coupled with
the degree of connascence of timing.
Assume that the operations executed by the producer and the consumer
have to either succeed together or fail together. If the consumer fails, the
producer has to execute a compensating action. Now the services are
functionally coupled with the degree of connascence of value.
Finally, what if the message bus is the producer’s internal
implementation detail and is not intended for integration? That would be
intrusive coupling.
As you can see, the type of communication between modules, synchronous
or asynchronous, doesn’t affect the knowledge shared across the module’s
boundaries, and thus doesn’t affect integration strength. It does, however,
introduce other aspects of software design, which we will discuss in the
next chapter.

Key Takeaways

Structured design’s module coupling and connascence reflect different


aspects of sharing knowledge across module boundaries. For a
comprehensive analysis of cross-component relationships, integration
strength combines the two models into one.

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:

1. Intrusive coupling: The upstream module’s implementation details that


are not meant for integration are used by its downstream consumers.
2. Functional coupling: Modules implement closely related functionalities.
The complexity of the shared knowledge can be evaluated using the
levels of dynamic connascence plus a special case of duplicated
functionality.
3. Model coupling: Modules share a model of the business domain. The
levels of static connascence reflect the extent of the shared knowledge.
4. Contract coupling: The upstream module shares an integration-specific
model, contract, for communication with downstream modules. The
shared knowledge can be evaluated with the levels of static connascence.

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. What factors affect integration strength?

1. The type of the integration interface


2. The complexity of the integration interface
3. The explicitness of the integration interface
4. Shared reasons for change
5. Answers A and B are correct.
6. Answers A, B, C, and D are correct.

2. Which of the following necessarily represent an increasing level of


degrees of integration strength?

1. Connascence of name, connascence of position


2. Connascence of name, connascence of execution
3. Connascence of timing, symmetric functional coupling
4. Degrees belonging to different levels of integration strength cannot be
compared.
5. Answers B and C are correct.

3. What information is necessary to evaluate integration strength?

1. The modules’ integration interfaces


2. The modules’ business functionality
3. Storage mechanisms involved in the integration
4. Whether synchronous or asynchronous communication is used
5. Answers A and B are correct.

4. What information is reflected by the levels of integration strength?

1. The complexity of the integration interfaces


2. The coupled modules’ shared reasons for change
3. The explicitness of the integration interfaces
4. All of the answers are correct.

5. Which level of integration strength occurs when one component depends


on the private implementation details of another component?

1. Intrusive coupling
2. Functional coupling
3. Model coupling
4. Contract coupling

6. Which type of integration describes two modules implementing the same


or closely related functionalities?

1. Intrusive coupling
2. Functional coupling
3. Model coupling
4. Contract coupling

7. Which level of integration strength minimizes the knowledge shared


across module boundaries?

1. Intrusive coupling
2. Functional coupling
3. Model coupling
4. Contract coupling
8. What does the degree of integration strength reflect?

1. The total number of software components


2. The complexity of the knowledge communicated through the interface
3. The programming language used
4. The design patterns applied in the software

9. Is asynchronous communication between components necessarily a sign


of weaker coupling than synchronous communication?

1. Yes
2. No

10. Which type of coupling occurs when an integration-specific model is


communicated through a message queue?

1. Intrusive coupling
2. Functional coupling
3. Model coupling
4. Contract coupling
Chapter 8

Distance

Knowledge can flow from near or far,

The distance it goes, sets the costs’ bar.

Beyond the code, distance can span,

Social factors—its way can expand.

Chapters 5 through 7 delved into why certain components appear destined


to change together. You learned that the stronger the integration is between
two components, the more knowledge they share. Naturally, the more
knowledge that is shared between components, the higher their likelihood
of needing to change simultaneously. However, Chapter 7 concluded by
noting that knowledge sharing is not the only trigger for cascading changes
within a system.

This chapter explores another crucial dimension of coupling: the dimension


of space. I’ll illustrate how the physical locations of components can affect
coupling and examine how this spatial aspect can affect cascading changes
and complexity of the system.
Distance and Encapsulation Boundaries

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:

Chapter 1 discussed the basic properties of a system and how software


systems are “systems of systems.”
Chapter 3 defined the concepts of local and global complexity,
demonstrating that complexity is multidimensional and defining “local”
and “global” as being dependent on the observer’s perspective.
Chapter 4 referenced the original definition of “software module,”
underscoring the fact that modules, as abstractions, have multiple levels.
Chapter 7 introduced the integration strength model, which is designed
to assess knowledge shared between modules at different levels of
abstraction.
As a result, knowledge can be shared across different physical distances.
Consider the examples in Figure 8.1, where the knowledge of what makes a
preferred customer is shared between two methods of the same object (A)
and two microservices (B).

Although the same knowledge is shared in both cases,1 intuition suggests


that Figure 8.1A would be easier to maintain than Figure 8.1B. Let’s
analyze why.

1. …and both designs are not optimal.


Figure 8.1 Sharing the same knowledge across different encapsulation boundaries: Methods of an
object (A) and microservices in a distributed system (B)

Cost of Distance

You can observe coupling between methods of an object, as well as between


objects, namespaces, libraries, (micro)services, and even entire systems.
The greater the distance between components that have to change together,
the higher the effort required to implement the shared change. This is
illustrated in Figure 8.2.
Figure 8.2 Coordination effort as a function of physical distance between coupled components

The increased effort encompasses coordination of changes within the


codebase(s), additional communication between the authors of the coupled
modules, and effective collaboration among those authors.

Having to co-evolve distant modules also increases the cognitive load on


the maintainer. The greater the distance is between the modules, the more
likely it is for the maintainer to forget to update one of the components,
which will result in incorrect behavior of the system.
Going back to the example illustrated in Figure 8.1, assume that the
definition of a preferred customer changes. Comparing the two designs,
which one will be easier to change?

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.

This example shows that the cost of changing coupled components is


proportional to the distance between them. The greater the distance is, the
more effort is needed to introduce a cascading change.

However, that’s not the only interesting effect of distance.

Distance as Lifecycle Coupling

The lifecycle of a typical software module involves multiple stages. It


begins with requirements gathering, during which the necessary
functionality is identified and defined. This is followed by the design phase,
outlining the module’s structure and interfaces. Subsequently, the module is
implemented, tested,2 and deployed. Ultimately, the deployed module must
be maintained and supported, which involves ongoing monitoring, bug
fixing, and updates.

2. If you follow TDD/BDD, of course, the implementation phase involves


continuous testing as well.

Lifecycle coupling causes modules to share their lifecycles—for example,


having to be implemented, tested, and deployed simultaneously. Distance
between components is inversely proportional to their lifecycle coupling, as
illustrated in Figure 8.3.
Figure 8.3 Lifecycle coupling as a function of physical distance between coupled components

The peculiar thing about lifecycle coupling is that it affects even otherwise
unrelated modules. Consider the rather extreme case described in Listing
8.1.

Listing 8.1 Unrelated Functionalities Implemented in the Same Object

Click here to view code image

01 public class SupportCase {


02 ...
03 public void CreateCase(...) { ... }
04 public void AssignAgent(...) { ... }
05 public void ResolveCase(...) { ... }
06 public void LogActivity(...) { ... }
07 public void ScheduleFollowUp(...) { ... }
08 ...
09 public void SendEmailNotification(...) { ...
10 public void SendSMSNotification(...) { ... }
11 ...
12 public void ProcessPayment(...) { ... }
13 ...
14 public double ConvertMilesToKilometers(...) {
15 }
This SupportCase object is intended to implement the functionalities
related to the handling of support cases. That’s what its first five methods
do (lines 03–07). Then, however, some unrelated methods appear, for
sending email notifications, sending SMS messages, and processing
payments, and it culminates in a method that converts miles to kilometers
(line 14). This object is an example of the crudest violation of the Single
Responsibility Principle (Martin 2003). However, let’s assume it was
designed this way for multiple reasons, and instead of judging, we’ll focus
on the resultant lifecycle coupling.

Colocating otherwise unrelated functionalities in the same object couples


their lifecycles. Changes to the functionalities of support cases,
notifications, payments, and the logic that converts miles to kilometers are
bound by the same lifecycle. For example, any change related to the
functionality of support cases requires any changes to the other
functionalities to be compiled, tested, and deployed as well.

The practical implication of lifecycle coupling is its introduction of


collateral changes: changes that otherwise wouldn’t be needed. In the
example of the SupportCase object in Listing 8.1, deploying the changes
made to its core functionality (managing support cases) would require
rolling back all changes to the other functionalities, which are still not ready
for deployment. Alternatively, the changes could be implemented in
different branches. That is not ideal either, as modifying the same file in
multiple branches is more than likely to introduce merge conflicts.
Dividing the four functionalities into four objects—for example,
SupportCase , Notification , Payment , and UnitConversion —
would reduce their lifecycle coupling. Even if the four objects would
remain in the same namespace,3 at least the changes wouldn’t require
modifying the same file. Taking the distance between the four objects to an
extreme—for example, spreading them across four different microservices
—would reduce their lifecycle coupling to the minimum.

3. In the same package in Java, or the same module in


Python/JavaScript/Ruby.

Evaluating Distance

The distance between coupled components is closely related to the concepts


of hierarchical modules and levels of abstraction, as we discussed in
Chapter 4.

Assuming the hierarchical design of a system, distance can be represented


as the closest common ancestor of two modules. Consider the following C#
type names:4

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.

Additional Factors Affecting Distance

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

So far, I have discussed only the physical location of modules in the


codebase(s). However, distance can also be influenced by organizational
structure and how components interact.

Distance and Socio-Technical Design

When assessing the distance between modules, their physical location


doesn’t reflect the full picture. There’s a socio-technical aspect to consider.
The coupled modules could be implemented by the same person, the same
team, different teams, different teams within the same department, different
departments, or even different organizations. This can be further
exacerbated by physical distances, such as physical locations of engineering
teams (e.g., colocated versus distributed) and their local time zones.

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.

The effect of organization structure on encapsulation boundaries, and the


distance between them, is described by Conway’s Law (Conway 1968):

Any organization that designs a system (defined broadly) will produce a


design whose structure is a copy of the organization’s communication
structure.—Melvin E. Conway

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.

For instance, if development teams are organized around separate modules,


with each team responsible for specific functionality, the resultant software
system is likely to consist of separate modules with well-defined boundaries
and interfaces. Conversely, if the teams lack organization or clear
communication channels, the resultant software system may become
fragmented, difficult to maintain, and prone to miscommunication. In other
words, over time, the ownership distances will influence the encapsulation
distance of the system’s modules.

Distance and Runtime Coupling

Runtime coupling is the extent to which one module’s availability impacts


the availability of another module. For example, consider the two
integration patterns illustrated in Figure 8.5, which depict the following:

A. Services are integrated synchronously: The consumer ( service B )


issues a synchronous request to the producer ( service A ). For
example, the request can be implemented as a remote procedure call
(RPC).
B. Services are integrated asynchronously: The producer ( service A )
publishes messages that the consumer ( service B ) subscribes to, and
processes asynchronously.

Figure 8.5 Synchronous and asynchronous integration between services


In the case of synchronous integration, the consumer expects a near-
immediate response from the producer. If the producer is not available, the
consumer’s functionality is directly impacted.

Conversely, in asynchronous integration, as long as the consumer can


retrieve messages from the message bus, it can continue functioning even if
the producer is offline.

Thus, runtime coupling is higher in the case of synchronous communication


—both services must be available for the system to function. In contrast, it’s
lower in the case of asynchronous communication—the consumer or the
producer can be offline without affecting the functionality of the other
service. This, in turn, affects the components’ lifecycle coupling. High
runtime coupling can lead to any outage propagating to the coupled
components, thereby binding their lifecycles and reducing the distance
between them.

Asynchronous Communication and Cost of Change

You may be wondering, given that asynchronous integration reduces


lifecycle coupling and thus increases distance, how this aligns with the
concept that the cost of changes grows with distance. After all,
asynchronous integration is generally regarded as a more flexible design
choice.
To clarify this, let’s examine the two services illustrated in Figure 8.5B.
Suppose these services are model-coupled, meaning that the messages used
for integration expose the producer’s internal model. Therefore, a change in
this model is likely to require corresponding changes in the format of the
published message and the consumer’s ability to process the new schema.
Coordinating such a change might demand more effort than if the services
were integrated synchronously (as in Figure 8.5A). For instance, an
intermediate version of the consuming service might be needed so that it
will be able to process messages in both the existing and new formats.

Distance Versus Proximity

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.

Although “proximity” has been used in software design, particularly in


association with levels of connascence, I prefer the term “distance” for the
following reasons:

I find “distance” easier to explain and use when reasoning about


software design. “Low/high distance” sounds more intuitive than
“low/high proximity.”
The direct effect of distance on the cost of change vividly illustrates its
impact.
“Proximity” is traditionally used to describe the “distance” between
encapsulation boundaries. However, it’s also important to consider the
effects of socio-technical design and runtime coupling on the overall
distance.

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.

Distance Versus Integration Strength

As the preceding examples demonstrate, distance and integration strength


represent different aspects of module integration. Integration strength
reflects the knowledge shared across module boundaries. The more
knowledge that’s shared between coupled components, the higher the
likelihood the two modules will need to change in unison. When such
cascading changes occur, distance signifies the effort required to implement
them.

However, there’s a common tendency to attempt to “decouple” systems by


focusing solely on the distance aspect. For instance, decomposing a
monolith into microservices involves concentrating solely on the
encapsulation boundaries (the distance). However, not considering the
resultant flow of knowledge (integration strength) can lead such projects to
result in a distributed Big Ball of Mud.

That’s also why an event-driven architecture (EDA), which relies on


asynchronous communication, cannot guarantee a modular design by itself.
Often, messaging is viewed as a cure-all. It’s as if introducing some events
into a legacy system and putting a message bus in the middle will result in a
“decoupled” system. However, unless the design optimizes how knowledge
is shared across the system, the asynchronous components will still have
shared reasons for change. Some of them are likely to lead to complex
interactions and steer the system away from modularity.

Therefore, both dimensions of coupling—distance and integration strength


—are equally important. Chapter 10, Balancing Coupling, will merge these
dimensions into a cohesive model for evaluating design decisions. But
before we get there, there’s another dimension of coupling I have to
introduce: volatility, the topic of the next chapter.

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.

Ultimately, be aware of the effects of lifecycle coupling. Components


located close to each other will affect each other’s lifecycles. Such
“collateral” changes can occur even if the components are not integrated in
any other way. In other words, even without shared knowledge, components
can still have to co-evolve due to lifecycle coupling.

Quiz

1. Components in which of the encapsulation boundaries are located closest


to each other?

1. Method
2. Object
3. Library
4. Service

2. Components in which of the encapsulation boundaries are located


farthest from each other?

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.

4. Which of the following statements is/are true regarding asynchronous


communication?

1. Asynchronous communication increases coupling.


2. Asynchronous communication decreases the components’ lifecycle
coupling.
3. Asynchronous communication increases the components’ lifecycle
coupling.
4. Answers A and C are correct.

5. What level and degree of integration strength is illustrated in Figure 8.1?

1. Symmetric functional coupling


2. Intrusive coupling
3. Model coupling, connascence of name
4. Contract coupling, connascence of name

6. Which of the following properties of a system affect(s) the distance


between coupled modules?
1. The modules’ encapsulation boundaries
2. Teams in charge of the modules
3. The modules’ runtime dependencies
4. All of the answers are correct.
Chapter 9

Volatility

Wide is the distance, much knowledge exchanged—

The design not ideal, somewhat deranged.

Yet if they’re static, never to change,

Should that design make anyone rage?

Imagine a strongly coupled system, one in which all components share


excessive and extraneous knowledge across their boundaries. Even intrusive
coupling is there. The design is flawed to such an extent that any change, to
any component, would inevitably trigger a ripple effect, leading to
cascading changes across all possible distances. However, consider this:
What if none of the system’s components are ever expected to change?
Would you still consider this a strongly coupled system? From a technical
standpoint, it certainly is. But if the components will never change, does the
potential for cascading changes even matter?

This chapter shifts the discussion of the effects of coupling to a different


dimension: the dimension of time. Specifically, it examines the volatility of
modules—how frequently they are anticipated to undergo changes. To this
end, you will learn how systems evolve over time, identify the common
evolution drivers, and acquire strategies for assessing modules’ expected
volatility levels.

Changes and Coupling

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.

However, software developers do not always welcome change. At times,


they may even fear it. This is because, quite often, changes wreak havoc in
systems:

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.

On the other hand, less-than-ideal coupling between components that are


not expected to change is not necessarily a severe issue. Thus, to gain a
holistic understanding of the effects of coupling on a system, it is crucial to
evaluate the expected rate of changes within its components.

To be able to evaluate volatility of a system’s components, let’s start by


building an understanding of why software changes and what are its
primary change drivers.

Why Software Changes

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

Both software design and its implementation are solutions to business


problems. The solution can change while the “problem” remains the same.
For example, if you refactor the solution to implement a different pattern,
the implementation (the solution) may be modified; however, the business
requirements and the system’s expected behavior remain unchanged.

The obvious and probably one of the most common causes of


implementation changes is bug fixes. Software bugs can be as trivial as
typos, but they can also result from overlooked business rules and
requirements. Apart from being much more challenging to fix, the latter
type of bug requires modifications across component boundaries and thus
exposes implicit complexity. As a result, the encapsulation boundaries can
change, or otherwise, technical debt will be accumulated. Technical debt is
by itself a reason for refactoring software. The later the technical debt is
repaid, the higher the toll incurred by the implicit and explicit interactions
among the affected components.

Another common reason for changes in the solution space is Conway’s


Law, which we discussed in the preceding chapter. To reiterate, according to
Mel Conway, an organization designing a system will produce a design with
its structure mimicking the organization’s communication structure. Hence,
when the structure of the organization implementing the system changes,
the system’s design will be affected. Therefore, changes in the
organizational structure are likely to trigger changes in the design of the
system.

A trivial example of such organization-driven change in software design is a


small start-up growing into a big company. During the garage days, the
engineering team is small, communication is polished, and all team
members are pursuing shared goals. The team members’ work can be
integrated in an ad hoc manner; there is no need for formal processes or
documents. If a change should be introduced in one of the components’
public interfaces, it is easy to communicate and coordinate the change with
whomever is going to be affected by it. Everybody has the same goal; they
either succeed together or fail together.

Collaboration becomes much more challenging when a small start-up


becomes a large organization. Now, the work has to be coordinated across
different teams. Even though the teams are part of the same company, they
might still have different goals. Now, it’s no longer possible to coordinate a
change in a service’s public interface over a watercooler conversation.
Suddenly, more formal integration protocols are needed. Design and
implementation changes affecting multiple teams need to be discussed,
planned, and coordinated.
Following Conway’s Law, to reduce the chances of the teams stepping on
each other’s toes, the system design should accommodate the organization’s
communication structure. As I explained in Chapter 8, the distance between
components of a system can grow due to reduced lifecycle coupling
resulting from teams becoming more distant.

To minimize conflicts between teams and promote smoother collaboration,


it is essential for the system design to align with the organization’s
communication structure, as highlighted by Conway’s Law. As teams
become more dispersed, the distance between components in a system
increases due to reduced lifecycle coupling. On the one hand, this increased
distance between components promotes their independence. On the other
hand, when a change impacts these distant components, it requires a higher
level of coordination effort.

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

As Edward V. Berard has famously stated, walking on water and developing


software from a specification are easy if both are frozen (Berard 1993). Our
lives would be so much easier if software requirements indeed were static.
But they are not. These changes fall within the problem space and can
manifest in various forms. New functionalities may need to be added to the
system, or existing behaviors might require modifications. Such requests
result from new business insights, opportunities, or customer demands.

When working on a decent-sized project you may observe that certain


system components are changing much more frequently than others. It may
seem like business stakeholders prioritize specific functionalities while
giving little attention to other components. Why does this happen, and how
can you identify those components that are going to change the most?
That’s the topic of the next section.

Evaluating Rates of Changes

Trying to predict future changes in a system’s components results in a good


news/bad news situation. The bad news: Predicting the future is hard. The
good news: There are tools that can help to identify which components are
going to change the most and which are going to change the least.

I’ll start with my favorite: domain-driven design’s domain analysis.

Domain Analysis

According to domain-driven design (DDD), we can’t design a software


solution without a strong understanding of the business problem. Only
when we are equipped with the relevant knowledge of the business domain
can we choose the suitable engineering techniques. With this in mind, let’s
define what a business domain is.

Business Domain

A company’s business domain is its overall area of activity. This is the


service the company is providing to its customers. For example, FedEx’s
business domain is courier delivery, and for Walmart, it’s retail. A company
can work in multiple business domains. For example, Amazon provides
both retail services and on-demand cloud computing. Furthermore, business
domains are not static, and it’s not uncommon for a company to change its
business domain over time. For example, Nokia went from rubber
manufacturing to telecommunications and other business domains during its
lifetime.
Naturally, many companies can compete in the same business domain. Do
all of them offer the same services and the same solutions to their
customers? Of course not. Companies have their own unique ways of
competing with others in the industry. What differentiates one company
from its competitors is its subdomains.

Business Subdomains

Subdomains are finer-grained areas of business activity that are needed to


operate in the company’s business domain. These are business building
blocks that are required for the company to succeed. Inspecting the
company’s departments or other organizational units—accounting,
marketing, sales, and so on—is a good starting point for identifying its
subdomains.

From a more technical perspective, a subdomain can be defined as a set of


interrelated use cases that operate on closely related data and describe
related functionalities, as illustrated in Figure 9.1.
Figure 9.1 A subdomain can be represented as a set of interrelated use cases.

Going back to “integration strength” terminology, a subdomain consists of


“functionally coupled” use cases: functionality that is likely to be bound by
temporal/sequential or transactional relationships.

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:

Identity and access: Authenticating and authorizing its users


Support case management: Describing and implementing the lifecycles
of support cases
Distribution: The functionality required for distributing support cases
among the available support agents
Desk management: Managing the available help desk offices, their
departments, and the available support agents
Knowledge base: Managing and providing information that may be
useful for support agents in addressing customer requests
Billing: Clearing and billing WolfDesk customers

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

1. If you are an experienced DDD practitioner, you may be wondering why


I use the term “core subdomain” rather than “core domain.” In the book
Domain-Driven Design, Eric Evans (2004) uses both terms. I prefer “core
subdomain” because it doesn’t imply a hierarchical relationship with other
subdomain types, and it is more aligned with subdomains’ tendency to
morph from one type to another; more on that in Chapter 11, Rebalancing
Coupling.

A core subdomain describes the functionality that gives the company a


competitive advantage. This includes inventions, intelligent optimizations
of existing processes, unique knowledge, or other intellectual property. The
primary purpose of a core subdomain is to differentiate the company from
its competitors. It can involve providing a unique service to customers, or
offering the same service as competitors but with enhanced effectiveness,
whether in terms of operations or cost efficiency.

Inherently, core subdomains involve complexity. Strategically, companies


seek to maintain high entry barriers for their core subdomains. It should be
challenging for a competitor to duplicate your core subdomain; if they
manage to do so, your company stands to lose its competitive edge. In other
words, core subdomains typically aim to tackle intricate problems, or
problems that have no straightforward solutions.

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.

Core subdomains are inherently volatile. This volatility is particularly


evident in the early stages of development. During this phase, not only is
the optimal solution yet to be discovered, but even the problems the
company aims to address may not be clear. But there is more to it.
Competitors will always try their best to mimic solutions provided by
successful companies. This means that to stay one step ahead of its
competitors and maintain a competitive advantage, a company must evolve
its core subdomains over time. That’s another factor that contributes to the
volatility of core subdomains.

To identify core subdomains, you need to analyze what sets a company


apart from its competitors and uncover its unique value proposition, or what
is often referred to as its “secret sauce.” In brownfield projects, look for
areas that are getting the most attention from the business, and where
requirements change the most.

Going back to the WolfDesk example, the following could be its core
subdomains:

Support case management: The company continuously analyzes and tries


to improve the way support cases are handled, through automatic
responses and state transitions.
Distribution: WolfDesk works on an algorithm that considers case
details, agent specialization, and past experience to match cases with the
most suitable agents for quick resolution.
Knowledge base: WolfDesk implements its proprietary knowledge
management system that automatically retrieves relevant documentation
for each support case.
In summary, core subdomains enable companies to differentiate themselves
and gain a competitive edge by providing unique and effective solutions.

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.

Examples of implementing generic subdomains include purchasing a


commercial, off-the-shelf product or adopting an open source solution. In
contrast to core subdomains, generic subdomains have a low barrier to
entry. Every company in the field has access to the same, proven solutions.

Similar to core subdomains, generic subdomains are complex. This


complexity is why others have already invested time and effort into
designing and building these solutions. It’s also why it’s not worth investing
in your own solution for a generic subdomain. Have you ever seen
somebody trying to implement their own encryption algorithm? The time
and effort required to develop a comparable solution are better invested in
core subdomains, where they can contribute to the company’s competitive
advantage.
As the available solutions are already battle-tested, they are unlikely to
change significantly. There might be occasional changes, such as security
fixes, but these will be considerably less frequent than changes in core
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.

In the WolfDesk example, the “Identity & Access” and “Billing”


subdomains are generic. In both cases, the company uses a commercial
product that offers each functionality as a service.

Supporting Subdomains

Supporting subdomains fall somewhere between core and generic


subdomains. Like core subdomains, these represent problems the company
must solve itself, rather than using an existing solution. However, akin to
generic subdomains, they do not provide any competitive advantage.

Supporting subdomains lack competitive advantage because their business


logic is straightforward: The entry barriers are low. Not only could any
competitor replicate the solution rapidly, but its implementation would also
not impact the company’s bottom line or competitiveness. Strategically, the
company might prefer if someone offered a solution for a supporting
subdomain, turning it into a generic one. This would allow the company to
spend less time implementing non-business-critical solutions.

If supporting subdomains offer no competitive advantage, why should a


company bother implementing them? As the name suggests, they exist to
support one or more of the company’s core or generic subdomains. For
example, “Desk management” is one of WolfDesk’s supporting
subdomains. Here, there is little business complexity and mostly data entry
screens. No ready-made solution is available, so WolfDesk had to develop it
on its own.

In the context of the Cynefin framework, supporting subdomains fall within


the clear subdomain. Cause-and-effect relationships are strong and obvious.
Consequently, the solution required for a supporting subdomain is also
obvious.

Because supporting subdomains are simple solutions to simple problems,


they are not going to change much, if at all. The business is not interested in
optimizing solutions for supporting subdomains, as doing so would have no
effect on its profits.

Volatility of Subdomains

To summarize, core subdomains are expected to be the most volatile.


Conversely, both supporting and generic subdomains have much lower
volatility. Table 9.1 illustrates the key differences between the three types of
subdomains.

Table 9.1 Key Differences Between the Three Types of Subdomains

Competitive Problem
Subdomain Complexity Volatility
Advantage Type

Core High High High Interesting

Generic Low High Low Solved

Supporting Low Low Low Obvious

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 Analysis

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.

This approach is more engineer-friendly than analyzing business strategy,


but it is susceptible to inaccuracies. Consider possible false positives and
false negatives that can result from considering module volatility by source
code changes.

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

Apparent low volatility can be artificial or forced. For instance, I once


encountered a module that exhibited a low frequency of changes. However,
something didn’t add up, as its description suggested it was a core
subdomain. It turned out that its code was such a mess that all attempts to
modify it led to extensive outages. Consequently, the business abandoned
efforts to evolve its functionality and focused on other areas. Of course,
refactoring the module to enable its future evolution was one of the
business’s top priorities.
Volatility and Integration Strength

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.

Consider the highlighted component in Figure 9.4. According to the


illustration, the component implements the functionality of a supporting
subdomain. It also references, and thus depends on, three other components.
Figure 9.4 A component implementing a supporting subdomain. What is its expected volatility?

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

2. Reminder: Arrows on diagrams show the direction of dependency.


However, the knowledge flows in the opposite direction.

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.

This concludes our exploration of the dimensions of coupling: the three


forces affecting how changes propagate across the system. The next chapter
will discuss how to achieve a modular design by balancing the three
dimensions of coupling.

Key Takeaways

This chapter expands on the ideas described in Chapter 4, albeit from a


different angle. Your ability to design a modular system depends on your
knowledge of the system’s business domain. Identifying the subdomains
involved, and their types, gives you an understanding of the components’
volatility levels. Core subdomains are inherently the most volatile.

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. Which of the following can trigger changes in software systems?

1. New business requirements


2. Organizational changes
3. Software design improvements
4. All of the answers are correct.

2. Which subdomain type(s) is/are expected to change the most often?

1. Core
2. Generic
3. Supporting
4. Answers A and B are correct.

3. What is the expected rate of change of a component implementing a


supporting subdomain?

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.

4. How do coupling and volatility relate to each other?

1. Volatility can be caused by coupling.


2. Volatility makes coupling explicit.
3. Volatility is a property of the business domain and doesn’t affect
coupling.
4. Answers A and B are correct.
Part III

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 10 introduces the concept of balanced coupling, a model for


evaluating the overall effects of coupling. You will learn how to use the
dimensions of coupling to identify the complexity, costs, and modularity of
the resultant system.

Chapter 11 continues the discussion of balanced coupling in the context of


system evolution. It shows how to identify critical changes in the system’s
environment and how to adapt to such changes by rebalancing the coupling
forces.

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.

Lastly, Chapter 13 demonstrates how to apply the balanced coupling model


in practice through eight case studies. These real-world examples illustrate
how the model can be applied across various levels of abstraction.
Chapter 10

Balancing Coupling

A change in the east echoes in west,

Software design is like a game of chess.

Near or far, how to balance the ends?

As consultants proclaim—it depends!

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.

Combining the Dimensions of Coupling

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.

Figure 10.1 Knowledge flows in the opposite direction of dependencies.

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:

VOLATILITY AND STRENGTH: Both volatility and strength are high.


NOT STRENGTH OR NOT DISTANCE: Strength is low or distance is low, or both
are low.
VOLATILITY XOR DISTANCE: Either one of volatility and distance is high, and
the second is low.

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

The integration strength of coupled components indicates the likelihood of changes in


the upstream component propagating to its downstream consumers. Volatility, on the
other hand, reflects the frequency of changes in the upstream component.
Consequently, the combination of these two factors reflects the stability of the
relationship between components.

If either volatility or integration strength is low, the likelihood of cascading changes is


minimized. This could be due to the upstream module being unlikely to change (low
volatility) or the minimal integration strength keeping changes within the boundaries
of the upstream component. Of course, the same would apply if both volatility and
integration strength were low. All of these scenarios describe stable coupling between
the components.

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.

Therefore, we can represent the stability of coupling as the following expression:

STABILITY = NOT (VOLATILITY AND STRENGTH)

Table 10.1 illustrates the extreme combinations of volatility and strength.


Table 10.1 Stability as a Combination of Integration Strength and Volatility

Volatility Strength Low High

Low Stable Cascading changes


avoided due to low
volatility

High High volatility Unstable


contained by low
strength

Actual Costs: Volatility and Distance

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:

CHANGES COST = VOLATILITY AND DISTANCE


Table 10.2 illustrates the cost of a cascading change.

Table 10.2 Actual Cost of a Cascading Change as a Combination of Distance and Volatility

Volatility Distance Low High

Low Low Low due to low


volatility

High Low due to low High


distance

Next, let’s move on to what is probably the most interesting combination of coupling
forces: distance and integration strength.

Modularity and Complexity: Strength and Distance

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 summarizes the four combinations of strength and distance.

Table 10.3 Four Combinations of Low/High Values for Strength and Distance

Strength Distance Low High

Low Local complexity Loose coupling

High High cohesion Global complexity


When both forces are either high or low, the design leans toward complexity. On the
other hand, contrasting values describe a more balanced relationship, steering away
from complexity and toward modularity. Therefore, we can say the following:

M ODU LARI T Y = ST REN GT H XOR DI ST AN CE

COM P LEXI T Y = N OT M ODU LARI T Y

= N OT ( ST REN GT H XOR DI ST AN CE)

LOCAL COM P LEXI T Y = N OT ST REN GT H AN D N OT DI ST AN CE

GLOBAL COM P LEXI T Y = ST REN GT H AN D DI ST AN CE

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.

Combining Strength, Distance, and Volatility

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).

Maintenance Effort: Strength, Distance, Volatility

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:

1. Low strength, high distance, high volatility: As we discussed in the preceding


section, the combination of low strength over high distance results in loose
coupling. The high volatility of the upstream component doesn’t spoil the increase
in modularity of the system, as the frequent changes are still contained by low
integration strength, and thus, the resultant maintenance effort is low:
MAINTENANCE EFFORT = STRENGTH * DISTANCE * VOLATILITY = 0 * 1 * 1
=0
2. High strength, low distance, high volatility: High strength with high volatility
represents integration that is unstable and prone to cascading changes. However,
low distance neutralizes the negative impact of this by minimizing the effort needed
to implement those cascading changes. As in the preceding case, this relationship
contributes to modularity of the system. Thus, it results in low maintenance effort:
MAINTENANCE EFFORT = STRENGTH * DISTANCE * VOLATILITY = 1 * 0 * 1
=0
3. High strength, high distance, low volatility: High integration strength over a long
distance results in global complexity. However, it is balanced out by low volatility.
The stable relationship (high strength, low volatility) minimizes the maintenance
effort, as the upstream component is not expected to change. Hence, this situation
likewise results in low maintenance effort:
MAINTENANCE EFFORT = STRENGTH * DISTANCE * VOLATILITY = 1 * 1 * 0
=0

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

Strength Distance Volatility Maintenance Effort

High High High High

Low High High Low


Strength Distance Volatility Maintenance Effort

High Low High Low

High High Low Low

Low Low High ?

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.

Balanced Coupling: Strength, Distance, Volatility

Both of the undesired combinations of coupling forces discussed in the previous


section involve complexity coupled with high volatility:

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:

Click here to view code image

BALANCE = NOT (COMPLEXITY AND VOLATILITY)


= MODULARITY OR NOT VOLATILITY
= (STRENGTH XOR DISTANCE) OR NOT VOLATILITY

High balance signifies modularity, while low balance signifies complexity. As


illustrated in Table 10.5, except for the two cases of complexities combined with high
volatility, all other combinations result in 1, signifying a balanced design of coupling.

Table 10.5 Balanced Coupling as a Function of Integration Strength, Distance, and Volatility

Strength Distance Volatility Balance

Low High High High

High Low High High

High High Low High

High High Low High

Low High Low High

High High Low High


Strength Distance Volatility Balance

Low Low Low High

Low Low High Low

High High High Low

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.

Balancing Coupling on a Numeric Scale

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.

DISCLAIMER: THIS IS NOT AN EXACT SCIENCE

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.

Balanced Coupling Equation

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):

Click here to view code image


MODULARITY = |STRENGTH - DISTANCE| + 1

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:

Click here to view code image

MODULARITY = |STRENGTH - DISTANCE| + 1


= |10 - 10| + 1 = 1

On the other hand, sharing only an integration contract results in the highest
modularity score, 10:

Click here to view code image

MODULARITY = |STRENGTH - DISTANCE| + 1


= |1 - 10| + 1 = 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):

Click here to view code image

MODULARITY = |STRENGTH - DISTANCE| + 1


= |8 - 2| + 1 = 7
Balancing Complexity and Volatility

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:

Click here to view code image

BALANCE = max(MODULARITY, (1 + 10 - VOLATILITY))

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 .

Ultimately, the two equations can be combined into one:

Click here to view code image

MODULARITY = |STRENGTH - DISTANCE| + 1


BALANCE = max(MODULARITY, (10 - VOLATILITY + 1))
BALANCE = max(|STRENGTH - DISTANCE| + 1, (10 - VOLATILITY + 1
BALANCE = max(|STRENGTH - DISTANCE|, 10 - VOLATILITY) + 1

Balanced Coupling: Examples

Let’s apply this equation to a number of example scenarios.


Example 1

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:

Click here to view code image

BALANCE = max(|STRENGTH - DISTANCE|, 10 - VOLATILITY) + 1


= max(|3 - 10|, 10 - 10) + 1 = 8

The result points to a fairly balanced relationship, as model coupling shares


significantly less knowledge than the functional and intrusive coupling levels.
However, if instead of sharing the model, an integration contract were introduced, the
result would be as follows:

Click here to view code image

BALANCE = max(|STRENGTH - DISTANCE|, 10 - VOLATILITY) + 1


= max(|1 - 10|, 10 - 10) + 1 = 10

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

4. With the degree of connascence of identity.

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:

Click here to view code image

BALANCE = max(|STRENGTH - DISTANCE|, 10 - VOLATILITY) + 1


= max(|8 - 2|, 10 - 10) + 1 = 7

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:

Integration strength: This is symmetric functional coupling, as the same business


algorithm is duplicated, and has to change simultaneously (9).
Distance: Because these are two services in a distributed system, the distance
between them is 9.
Volatility: Because both services belong to a core subdomain, the volatility is high
(10).

The resultant coupling balance score for this integration scenario is as follows:

Click here to view code image


BALANCE = max(|STRENGTH - DISTANCE|, 10 - VOLATILITY) + 1
= max(|9-9|, 10 - 10) + 1 = 1

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.

Because this is a gradual migration, some business functionalities are still


implemented in the legacy system. The team decided to “cut corners” and allow the
newer microservices to fetch data from the monolith using “unconventional”
interfaces; that is, working directly with its infrastructural components, databases, and
message buses. That resulted in the following:

Integration strength: This is intrusive coupling (10), as integration is done via


private interfaces.
Distance: Because these are two systems that are still implemented by the same
company, the distance is 9.
Volatility: Because the upstream component—the legacy system—is not being
actively developed and evolved anymore, its volatility is low (1).
The resultant coupling balance score for this integration scenario is as follows:

Click here to view code image


BALANCE = max(|STRENGTH - DISTANCE|, 10 - VOLATILITY) + 1
= max(|10 - 9|, 10 - 1) + 1 = 10

As you can see, the low volatility balances out the lack of modularity introduced by
intrusive coupling over a high distance.

Balanced Coupling: Discussion

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:

Click here to view code image

BALANCE = MODULARITY OR NOT VOLATILITY


= (STRENGTH XOR DISTANCE) OR NOT VOLATILITY
Key Takeaways

In this chapter, you learned to leverage the dimensions of coupling to evaluate


software design decisions.

An integration is stable as long as either integration strength, volatility, or both are


low:

Click here to view code image

STABILITY = NOT (VOLATILITY AND STRENGTH)

The overall cost of making changes is high if both volatility and distance are high:

Click here to view code image

CHANGES COST = VOLATILITY AND DISTANCE

If integration strength and distance have opposite values, the design increases the
modularity of the system, while equal values result in complexity:

Click here to view code image

MODULARITY = STRENGTH XOR DISTANCE


COMPLEXITY = NOT MODULARITY = NOT (STRENGTH XOR DISTANCE)

Global complexity is represented by high values of integration strength and distance,


while in the case of local complexity, both values are low:

Click here to view code image


GLOBAL COMPLEXITY = STRENGTH AND DISTANCE

LOCAL COMPLEXITY = NOT STRENGTH AND NOT DISTANCE

These insights can be combined to evaluate the balance of coupling:

Click here to view code image

BALANCE = (STRENGTH XOR DISTANCE) OR NOT VOLATILITY

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:

Click here to view code image

BALANCE = max(|STRENGTH - DISTANCE|, MAX_VALUE - VOLATILITY)

Use the balanced coupling equation to estimate whether the design of coupling will
result in modularity (high balance) or complexity (low balance).

Achieving modularity is important. Yet, even more important is preserving modularity


over time. That’s the topic of the next chapter.

Quiz

1. Which of the following combinations increase(s) the complexity of a system?

1. High volatility, high strength


2. Low strength, low distance
3. Low volatility, low strength
4. High strength, high distance
5. Answers B and D are correct.

2. Which of the following combinations results in unstable coupling?

1. High distance, high volatility


2. High strength, high volatility
3. Low strength, low volatility
4. High strength, low distance

3. Which of the following combinations maximizes the cost of implementing


cascading changes?

1. High distance, high volatility


2. High strength, high distance
3. Low strength, low volatility
4. High strength, low distance

4. Which of the following combinations of coupling forces reflects balanced coupling?

1. High strength, low distance, high volatility


2. Low strength, low distance, high volatility
3. Low strength, high distance, high volatility
4. Answers A and C are correct.
Chapter 11

Rebalancing Coupling

Systems change, they twist, they shout,

From planned paths, they sometimes flout.

Rebalancing coupling will save the day,

Defending against complexity’s game.

In an ideal world, software would exist in a static state of perfection. The


initial release would meet all business objectives and would perfectly
address all current and future needs of its users. Should a need for change
ever arise, it would be seamlessly accommodated by the existing design.
Each modification would fall into its place, like a missing puzzle piece
finding its spot.

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

Software is supposed to change. That’s why it’s called software. Chapter 4


examined the challenges in designing systems that can withstand changes.
Lack of modularity will result in a system unable to accommodate changes.
Conversely, attempting to address all reasonable and unreasonable changes
will result in an unusable system. Achieving modularity necessitates
balancing these two extremes, which involves making predictions about the
system’s future—effectively, making a bet against the future. Sometimes we
lose that bet.

While some changes can be accommodated by the prior architectural


decisions, others can be resisted by the design. The latter invalidates prior
assumptions about what can change within the system.

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.

Software Change Vectors

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.

A common example of tactical changes in software development is bug


fixes and other implementation improvements. These changes involve
adjusting existing code to resolve logical issues or to create a more efficient
way of meeting the same business requirements. Additionally, if changes do
not necessitate readjusting the existing boundaries of components or
altering their interrelationships, they are considered tactical changes as
well. This includes the introduction of new functionalities that align with
current business needs while adhering to existing business rules and
assumptions. Examples of such new functionalities could be adding a new
payment option to an e-commerce platform, implementing a user preference
setting in an application, or integrating a new reporting feature into an
existing system. These additions enhance the system without challenging its
fundamental design structure.

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.

Let’s see concrete causes for strategic changes.

Functional Requirements

The most common instance of strategic changes is the expansion of a


system’s functionality. New functionality adds knowledge. The new
knowledge can affect one or more modules, or even reshape the landscape
of the entire system. It introduces new components that can appear at
various levels of abstraction, ranging from adding methods on objects to
forming new services. With new components, cross-component interactions
are expanded as well. At a certain point, these additional interactions have
the potential to significantly increase the cognitive load and eventually
inflate the complexity of the system, as illustrated in Figure 11.1.

Figure 11.1 Adding components introduces more interactions, potentially escalating global
complexity.

In addition to new functional requirements, strategic changes can be


prompted by business, organizational, and environmental shifts.
Furthermore, changes in a company’s business strategy can often have an
even bigger impact on the software’s design.

Business Strategy

Chapter 9 introduced the concept of subdomains in domain-driven design.


You learned that different parts of a business have varying strategic values.
Core subdomains are the areas where the company excels: That’s how the
company provides value to its customers and gains its competitive
advantage. Thus, improving the efficiency of core subdomains is of the
utmost importance for the company. Supporting and generic subdomains,
while necessary, don’t have the same impact on the company’s competitive
advantage and tend to evolve less frequently.

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.

Such shifts in business strategy can reshape the company’s subdomain


structure. New subdomains might be introduced, or existing ones can
transition from one type to another. Consider the following examples:

A generic subdomain could become one of the company’s core


subdomains if it discovers a more cost-effective solution to a common
problem than one available to its competitors.
A supporting subdomain could reveal itself as a potential competitive
advantage and transition into a core subdomain.
If a company’s expectations are not met in one of its core subdomains, it
might decide to demote its strategic importance. It could be reclassified
as a supporting subdomain or even become a generic subdomain by
open-sourcing the solution.
A core subdomain could be converted into a generic subdomain if
another company starts providing the same solution, either as a service
or as an off-the-shelf product.

As we discussed in Chapter 9, the type of a subdomain influences the


volatility of the module implementing it, and this should be factored in
when designing its interactions with other system components. Any shift in
a subdomain’s nature should be mirrored by an equivalent modification in
the design to preserve the balance.

Organizational Changes

Companies naturally evolve and undergo organizational shifts over time.


Such changes can significantly influence the design of the software a
company develops.

Growth is a common type of organizational change. Start-up companies


often originate in a garage or apartment and grow into enterprises with
multinational research and development (R&D) centers. Communication
and collaboration patterns that were effective in the start-up’s early days are
often ineffective for midsize organizations, let alone international
companies.

Organizational restructuring can also disrupt the dynamics within


engineering teams. For instance, a division of responsibility for
implementing a functionality among multiple teams can increase
communication and collaboration efforts. Similarly, if teams with well-
established collaboration routines are separated into different organizational
units, or even time zones, their ability to work together effectively can be
compromised.

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.

Regulatory changes represent another form of environmental adjustments


that can significantly influence software design. Regulations such as the
Sarbanes-Oxley Act (SOX), the Health Insurance Portability and
Accountability Act (HIPAA), and the General Data Protection Regulation
(GDPR) have all had profound impacts on the design of certain software
systems. GDPR, for instance, is a comprehensive data protection law that
not only impacts how personal data of European Union residents is handled,
but also imposes stringent requirements on software systems. This has
resulted in software designers having to incorporate principles such as data
minimization, privacy by design and by default, and rights of the data
subject, like the right to personal data erasure, as part of the system’s design
to ensure compliance. These regulatory requirements often necessitate
significant changes to the system’s structure and functionality to ensure that
they align with the legal standards.

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.

As you learned in the previous chapter, a change in one dimension is likely


to destabilize the balance:

BALANCE = (STRENGTH XOR DISTANCE) OR NOT VOLATILITY


Let’s see how, and how to address design-disturbing changes in the three
dimensions.

Strength

Changes in components’ integration strength are primarily driven by


extending the functionality of a system. As illustrated in Figure 11.1,
extending the system’s functionality leads to new connections and
integrations between its components, potentially inflating its complexity.
Let’s examine a number of case studies.

Case Study: Hidden Integration Strength

During the early stages of WolfDesk’s implementation, the teams designed


the HelpDesks microservice to host the following functionalities:

Configuring available help desk offices and their organizational units


Setting the help desks’ working hours
Managing support agents’ schedules, including occasions when an agent
is not available, such as personal time off, sick leave, national holidays,
and others

When a change in a schedule takes place, the HelpDesks microservice


emits an event describing the change, and which agents are affected by it.
The Distribution microservice subscribes to these events and uses the
information to assign agents new support cases only during their working
hours. The event itself is modeled to contain only the information needed
by Distribution (Figure 11.2).

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.

Naturally, this new functionality was implemented in the HelpDesks


microservice. When an agent pauses an assignment, the time frame for no
assignments is persisted in the operational database and published as an
event.

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.

Since the Distribution logic is one of WolfDesk’s core subdomains


(high volatility), the distance had to be reduced to balance the high
integration strength. The teams evaluated the following alternative designs:

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).

Ultimately, the team decided to extract the “pause assignment” functionality


out of HelpDesks and move it to the Distribution microservice
itself, as illustrated in Figure 11.3.

Figure 11.3 Moving the assignment-pausing functionality to the Distribution microservice to


balance the combination of high strength and high volatility

As a result, the information available to the Distribution logic is


strongly consistent. Furthermore, from the balanced coupling perspective,
high integration strength and high volatility were balanced out by
minimizing the distance between assignment-pausing and Distribution
logic.
Case Study: From No Integration Strength to Functional Coupling

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):

1. The Distribution microservice listens to notifications about new


support cases and selects the most suitable available support agent to
handle each individual case. The assigned agent is communicated back
to the Support Case Management component via a dedicated event.
2. WolfDesk’s Real-Time Analytics subsystem uses notifications
about new support cases to monitor the volume of new cases, identify
trends, and monitor the overall performance of the help desk.

Figure 11.4 WolfDesk’s Support Case Management service publishes events to notify other
components about new support cases.

The team in charge of the Distribution component noticed that at peak


times, the system isn’t able to process all incoming support cases due to a
lack of available agents. To alleviate this issue, the team decided to
prioritize processing of support cases based on the following criteria: a)
support cases with high urgency, b) support cases from customers marked
as “strategic,” and c) support cases from customers who recently escalated
other support cases. The criticality assessment logic was added to the
Distribution service, as illustrated in Figure 11.5.

Figure 11.5 Criticality assessment logic added to the Distribution component

Over time, a requirement came in to reflect support cases’ criticality in


Real-Time Analytics as well. Since the Distribution and Real-
Time Analytics components are not directly integrated, the team
decided to calculate the value according to the same logic and enrich the
incoming events in Real-Time Analytics (Figure 11.6).
Figure 11.6 Criticality assessment logic duplicated in the Distribution and Real-Time Analytics
components

As a result of duplicating the criticality assessment logic, the components


that previously were not integrated with each other in any way—that is, had
no integration strength—became functionally coupled. Moreover, as the
distance between the components is high, the new functionality increased
the system’s global complexity. Since the duplicated logic is relatively
simple and not expected to change, the teams decided to leave it as is: Low
volatility balances out high strength and distance.

Volatility

The balanced coupling formula tolerates complexity when an upstream


component is not volatile. However, changes in business strategy can
transform a supporting or generic subdomain into a core subdomain,
thereby making it highly volatile. Let’s analyze two examples.
Case Study: From Supporting to Core

Recall the previous case study, as illustrated in Figure 11.6: WolfDesk


engineers decided to duplicate the criticality assessment logic in two distant
components. This functionality was both simple and not expected to
change; it was considered a supporting subdomain.

However, as time passed, the approach of prioritizing some support cases


proved to be effective. It allowed the company to address customers’ needs
during peak hours even when there were not enough support agents to
handle all incoming requests.

To optimize this further, WolfDesk’s analytics department decided to


experiment with a number of other ways to identify critical support cases.
The goal was to find a method that maximizes customer satisfaction and
optimizes the number of support agents needed during peak hours.

This change in business strategy transformed what was considered a


supporting subdomain into a core subdomain. Now, the coupling between
the Real-Time Analytics and Distribution microservices is no
longer balanced: high strength, high distance, and high volatility.

The analytics department addressed this imbalance by “eliminating” the


distance: Instead of duplicating the functionality in two components, it was
moved to the Support Case Management service, as illustrated in
Figure 11.7.
Figure 11.7 Eliminating distance by extracting the duplicated functionality and moving it into the
Support Case Management component

Case Study: From Generic to Core

In WolfDesk’s initial implementation, the system leveraged a simple open-


source content management system (CMS) to manage its knowledge base.
The CMS enabled WolfDesk to maintain articles, FAQ documents, and
other informational resources for its support agents.

As the knowledge base management function was considered a generic


subdomain—low volatility—the team in charge of the Support Case
Management component directly populated the knowledge base’s
operational database with information coming from support cases. This
essentially introduced an intrusive coupling between the two subsystems, as
illustrated in Figure 11.8.
Figure 11.8 Intrusive coupling with a generic subdomain

As WolfDesk continued to grow, the company realized that its needs


surpassed what could be accomplished with the existing CMS. Therefore,
management decided to develop an in-house knowledge management
system specifically tailored to WolfDesk’s unique needs and requirements.
This decision transformed the knowledge base from a generic subdomain
into a core subdomain.

The transition from a generic to a core subdomain involved a significant


increase in volatility. Consequently, the intrusive coupling that once existed
between the Support Case Management solution and the new
Knowledge Base solution was no longer suitable. The combination of
high volatility and high distance necessitated a reduction in integration
strength. Therefore, the design of the new Knowledge Base service
emphasized its boundary and permitted the submission of information only
through its API, which utilizes an integration-specific model: contract
coupling.
Distance

The distance between components is defined by their physical location in


the codebase and the organizational structure. Changes in these factors can
increase or decrease the distance and, as a result, spoil a balanced
integration. Let’s analyze two case studies demonstrating such changes.

Case Study: Components’ Physical Locations and Organizational


Changes

The initial versions of the WolfDesk system were implemented as a


monolithic solution. Support Case Management , Distribution ,
Help Desks , and other modules were all encapsulated within the same
monolithic codebase.

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.

The initial proximity of the modules within the monolithic structure


facilitated quick adjustments to the modules’ boundaries. If a more effective
interface was needed, it could be refactored with relative ease. However, the
decomposition of the codebase into separate services increased the distance
between modules. This effectively reduced their lifetime coupling, and
subsequently increased the communication effort needed to implement
changes spanning multiple modules.

To avoid cascading changes, the teams had to “tighten” the boundaries of


the modules. This involved reducing the knowledge shared across module
boundaries, or more specifically, lowering the integration strength from
model coupling to contract coupling between some of the integrated
services.

Rebalancing Complexity

The preceding case studies have demonstrated how functional, business, or


organizational growth can impact the design of a system. However, not all
growth can be managed by rebalancing the forces of coupling. For instance,
in some cases, adjusting the distance merely transforms local complexity
into global complexity, or vice versa. So, how do we manage such
complexity? The next chapter delves deeper into the nature of system
growth and evolution to address this question.

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.

Strategic changes can render a modular design obsolete by unbalancing the


components’ relationships:

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

2. How should we react to an increase in a component’s integration


strength?

1. Reduce volatility.
2. Reduce distance.
3. If possible, refactor the integration strength to its original value.
4. Answers B and C are correct.

3. How should we react to an increase in an upstream component’s


volatility?

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.

4. What type of change can lead to an increase in the distance between


integrated components?

1. Organizational
2. Refactoring
3. Answers A and B are correct.
4. None of the answers are correct.
Chapter 12

Fractal Geometry of Software Design

With balanced coupling, a system will thrive:

Achieving its goals and staying alive.

But fractal geometry empowers its growth—

Reshaping the structure and knowledge flows.

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

Growth is an inherent part of a healthy system’s lifecycle. Software often


starts small—perhaps as a handful of features—and expands over time as its
business viability is proven. This might involve adding new modules,
integrating with other systems, or scaling to support more users. Such
growth isn’t unique to software systems. Numerous kinds of systems, from
organisms and companies to cities, begin small and expand over time.

What’s even more interesting is that systems classified as “network-based


systems” share the same growth dynamics. Understanding what network-
based systems are and how they respond to growth will help us tackle the
challenges inherent in extending the functionalities of software systems.

Network-Based Systems

Prof. Geoffrey West has been researching the dynamics of growth in


complex systems for many years. His research revealed that the same laws
of physics govern growth in a broad range of systems; more specifically,
network-based systems. West (2018) characterizes a system to be a
network-based system by having the following three properties:

1. Space filling: The system sustains itself by transporting energy through a


hierarchical branching network, ensuring that the energy reaches all parts
of the system.
2. Invariant terminal units: The energy is delivered to terminal units, which
maintain consistent size and characteristics regardless of the overall size
of the system.
3. Optimization: The system continuously evolves to minimize energy
wastage and maximize available energy.

Essentially, a network-based system is an energy supply network. It


supplies certain energy/energies to all of its components. The terminal units
receiving the energy at the boundary of the system are the same for all sizes
of a system. Finally, the network through which the energy is delivered can
be optimized to maximize the effectiveness of the system.

Network-based systems are ubiquitous. Our own body—or any living


organism, for that matter—is a prime example of a network-based system.
The circulatory system’s blood vessels transport oxygen and nutrients to the
cells (Figure 12.1). The blood vessels service all the cells in the body.
Importantly, living organisms are not static. They are continuously changing
—both in the short term, as the organism grows during its lifecycle, and in
the long term, as species evolve to better adapt to their environment.
Figure 12.1 Circulatory systems and cities are examples of network-based systems.

(Images: left, Matthew Cole/Shutterstock; right, watchara/Shutterstock)

Cities are another example of network-based systems. A city delivers


energy to its inhabitants in the form of water, electricity, roads,
communication lines, and so on (Figure 12.1). A city’s infrastructure aims
to deliver this energy to all its residents. Like organisms, cities are not
static, but continuously evolving and adapting.

Additional examples of network-based systems include companies, social


structures, and the internet, among others. Now, is software design—the
subject of this book—a network-based system? Indeed, it is.
Software Design as a Network-Based System

What is the energy supplied by software design? As defined in Chapter 1


and discussed throughout the subsequent chapters, this energy is knowledge.
The “pipes” through which this knowledge flows in software systems are
the design of its modules and, more significantly, the interactions between
the modules.

In the context of balanced coupling, the knowledge flowing through the


system is described by the levels of integration strength, while distance
reflects the paths traversed by knowledge across the system.

Going back to the three properties of a network-based system, all of them


are evident in software design:

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.

Why Do Systems Grow?

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).

Similar economies of scale can be observed in living organisms. For


example, if one dog is twice as big as another, does it need twice as much
food? On average, a beagle weighs twice as much as a pug. However, a
beagle requires only 75% more calories per day than a pug.1

1. Kleiber, M. 1947. “Body size and metabolic rate.” Physiological Reviews


27, no. 4: 511–41. https://doi.org/10.1152/physrev.1947.27.4.511.

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)

The different rates of growth make scaling systems so beneficial. As a


system gets larger, it becomes more effective. Following are some
examples:

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

As a system grows larger, it becomes more efficient. However, undesired


aspects of the system become more efficient as well. For instance, as a city
becomes larger, the cost of living, the crime rate, the spread of disease, and
other negative aspects grow superlinearly.

Another interesting example of this phenomenon was discussed by Galileo


Galilei (1638) in his book The Discourses and Mathematical
Demonstrations Relating to Two New Sciences. In it, he explains why
organisms cannot grow past a certain size. To illustrate this, Galileo
proposes considering a wooden beam. If it is scaled to twice its size, its
weight will increase in three dimensions: It will be twice as high, twice as
wide, and twice as long. Overall, its weight will increase eight times.

However, the beam’s resistance to fracture is defined by its cross-sectional


area, which only increases in two dimensions: width and height, as
illustrated in Figure 12.3.
Figure 12.3 Scaling a wooden beam to twice its size increases its weight eight times, but increases its
resistance to fracture only four times.

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.

How do growth limits manifest in software?

Growth Dynamics in Software Design

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).

2. Doubling the functionality in a single version is highly likely to indicate


a waterfall process. Hence, this is a hypothetical example.

However, does doubling the functionality of a system require doubling the


knowledge implemented in it? Unless the new functionality has to be
implemented in a new, completely separate system, the new functionality
can be at least partly built upon the existing knowledge. For example, the
basic functionality of how support cases are handled is still relevant, and the
new features just extend it.

In other words, doubling the functionality of a system doesn’t require


doubling the knowledge. As illustrated in Figure 12.4, while the
functionality of the system grows linearly, its knowledge grows sublinearly.
That’s why it’s often so tempting to add more and more features to the same
system—you can build upon its existing knowledge.

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.

For simplicity’s sake, assume that each component of a system interacts


with every other component. The number of possible interactions for
systems with different numbers of components is illustrated in Figure 12.5.3

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.

The more cross-component interactions present in a system, the higher the


cognitive load on the person trying to comprehend how it works, or trying
to change it. Unfortunately, our cognitive abilities are almost static and
can’t compete with the superlinear growth of possible interactions. Hence,
at some point, the increase in cognitive load surpasses our cognitive
abilities, and at that point, the system becomes complex (Figure 12.6).
Figure 12.6 Growth dynamics in software systems: Functionality = linear growth; knowledge =
sublinear growth; complexity = superlinear growth; our cognitive limits = no growth

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

Both options involve innovation: either using a stronger material or


designing a different form that would withstand the increased weight. In
both cases, innovation is the key to enabling continuous growth.

Let’s revisit cities as an example of a network-based system. An


overcrowded city presents many challenges for its inhabitants, from
increased rates of respiratory diseases to insufficient infrastructure and high
crime rates. That sets the limit upon which a city can grow. But what is this
limit beyond which a city is considered overcrowded? Would the same limit
be true for a medieval city and a modern one?

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.

Following Galileo’s second alternative, growth-enabling innovation doesn’t


have to be technical. Organisms innovate by evolving their “shapes” to
support larger bodies: Bones can become denser, wider, or both. Consider,
for instance, a lion and a domestic cat. To support the lion’s larger body
size, its skeletal structure has evolved to become more robust, with thicker,
denser bones. On the other hand, a domestic cat, being smaller, has a
lighter, less dense skeletal structure that is sufficient to support its weight.
Yet, despite these differences, the basic skeletal design remains remarkably
similar between the two, demonstrating how nature innovates within
existing frameworks to accommodate diverse sizes and needs.

Ultimately, this same principle was articulated by business management


expert Eliyahu Goldratt. He used to say that technology can bring benefits
if, and only if, it diminishes at least one limitation (Goldratt 2005). This
notion reinforces the role of innovation, irrespective of the field—be it
biology or business management—in overcoming barriers that limit growth
potential.

Innovation in Software Design

A software system’s growth limit is the moment it turns into a Big Ball of
Mud (Foote and Yoder 1997):

A Big Ball of Mud is a haphazardly structured, sprawling, sloppy, duct-


tape-and-baling-wire, spaghetti-code jungle. These systems show
unmistakable signs of unregulated growth, and repeated, expedient repair.
—Brian Foote and Joseph Yoder (emphasis added)
As the software’s functionality grows, so do the negative aspects of growth:
the unintended interactions between the system’s components. These
unintended interactions take the form of bugs or entangled modules that
have to co-evolve. In either case, these interactions increase the system’s
complexity: The current architecture cannot withstand the growth of
knowledge codified in the system.

Following Galileo’s reasoning, we have two ways to combat the complexity


driven by growth: Use different materials or adapt the system’s shape.

The first option—different materials—would be plausible if we could


replace ourselves with artificial intelligence that is more capable of
handling complexity than we are. We are (luckily) not there (yet). This
leads us to the second option—adapting the system’s shape—which is
currently more relevant for software systems.

When viewed through the prism of network-based systems, software design


is a network of modules through which knowledge is distributed across the
system. Adapting the system’s shape to combat complexity means
optimizing the way the knowledge is shared. Which concept discussed in
Part I optimizes how knowledge is shared?

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.

Chapter 4 discussed that software modules are abstractions. The purpose of


abstraction is to create a new semantic level in which one can be absolutely
precise (Dijkstra 1972). An ineffective abstraction is one that either exposes
extraneous knowledge, removes essential knowledge, or both. At certain
stages of system growth, it may be necessary to “innovate the design”—in
other words, to introduce new abstractions that can better handle the
system’s increasing functionality. Let’s see an example.

Consider the first version of the WolfDesk system, illustrated in Figure


12.8. Since it was the initial release, it had limited functionality, and all of
its ten objects were located in the same namespace.
Figure 12.8 Objects implemented in the first version of WolfDesk

As I mentioned in the Growth Dynamics in Software Design section,


WolfDesk’s second release had to double its functionality by automating
previously manual tasks: the assignment of support cases to agents and
billing. Implementing these functionalities in the same boundary would
result in unrelated objects (low strength) located close to each other (low
distance), increasing the local complexity of the WolfDesk system. Hence,
the team decided to introduce four overarching modules—
CaseManagement , IdentityAndAccess , Distribution , and
Billing , as illustrated in Figure 12.9.
Figure 12.9 Introducing a level of abstraction to tackle the growth in local complexity

At the micro level, we employ similar techniques to organize the


components within a software module, be it interactions between
namespaces, classes, or methods within a class. This approach is mirrored at
the macro level in company structures—each organizational unit presents
the services it provides, while abstracting its internal processes. As a
company expands, a single department might be split into two distinct units,
with each one assuming a subset of the original department’s
responsibilities. In some cases, a company may even be divided into several
independent entities, each tasked with separate responsibilities.
Paraphrasing David L. Parnas (1971), eventually, all restructurings boil
down to assignment of responsibilities.

The fractal-like nature of software systems, a theme echoed in previous


chapters discussing systems of systems, fractal complexity, and hierarchical
modules, is not a mere coincidence. Fractal geometry is nature’s way of
mitigating complexity. But what is it exactly?

Fractal Geometry

Highly complex, self-sustaining structures, whether cells, organisms,


ecosystems, cities, or corporations, require the close integration of
enormous numbers of their constituent units that need efficient servicing at
all scales. This has been accomplished in living systems by evolving fractal-
like, hierarchical branching network systems. —Geoffrey West (2018)
(emphasis added)

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.

Fractals are usually associated with psychedelic patterns produced by


mathematical models, such as the Mandelbrot set, Julia set, Sierpinski
triangle, or Koch snowflake (Figure 12.10). These intricate and visually
captivating patterns are created through the repetition of simple
mathematical rules or equations, resulting in self-similar structures that
reappear at different scales.

Figure 12.10 Fractal patterns produced by mathematical models

(Images: left, Florin Capilnean/Shutterstock; center, Albisoima/Shutterstock; right, Reinhold


Leitner/Shutterstock)

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.

Figure 12.11 Fractal patterns in nature

(Images: top left, vvoennyy/123RF; top right, Morgenstjerne/Shutterstock; bottom left,


dimitris_k/Shutterstock; bottom right, Unsplash (Courtesy of NASA))
Recall the concept of sublinear and superlinear growth rates, which enable
network-based systems to become more efficient as they grow. This
increase in efficiency is a direct result of the fractal geometry inherent in
those energy supply networks.4

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

Because previous sections established that software design is a network-


based system, we can take a page from nature and apply fractal geometry to
optimize the distribution of knowledge. For that, we have to define a self-
similarity principle that can be applied across all scales.

Chapter 10 already defined that self-similarity principle: balanced coupling.

The integration strength model defined four types of knowledge:


implementation details (intrusive), functional, model, and integration-
specific contracts. These four types are relative. An integration contract at
one level might be seen as an implementation detail at a higher level. For
example, unless an object is exposed across a microservice’s boundary, the
object’s public interface becomes an implementation detail for another
microservice. Similarly, a microservice’s integration contract can be
considered an implementation detail when viewed from the perspective of a
completely separate system.

Distances are relative as well. Different types of a language’s standard


library might be considered to be located far from each other. However, the
scale of “distance” changes when viewing the system through a higher level
of abstraction, such as the level of services.

Therefore, the balanced coupling model can be applied at all levels of


abstraction to evaluate the design of the interactions of components. The
next chapter will show how these principles can be applied in practice.

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.

Growth-driven complexity limits a software system’s growth potential.


However, as in nature, complexity of a software system can be tackled
through fractal modularity: Component interactions should be balanced at
all levels of abstraction. That makes the balanced coupling model the self-
similarity principle guiding the design of modular systems.

Quiz

1. Why can’t a system grow indefinitely?

1. It will require immense engineering efforts to implement all of its


functionality.
2. Because of runtime performance constraints.
3. Negative aspects of the system grow superlinearly, increasing the
system’s complexity.
4. All of the answers are correct.

2. Considering software as a network-based system, what is the energy that


is passed through its vessels?

1. Data
2. Knowledge of the business domain
3. Knowledge of how the system is designed
4. Answers B and C are correct.

3. Which of the growth dynamics grows the slowest?

1. Sublinear growth
2. Linear growth
3. Superlinear growth
4. All grow at the same speed.

4. What type of innovation enables further growth of a system beyond its


growth limit?

1. Changing its shape


2. Introducing more efficient materials
3. It’s not possible to grow past the growth limit.
4. Answers A and B are correct.

5. What type of innovation enables sustainable growth of a software


system?

1. Redesigning component interactions to balance the coupling forces


2. Introducing abstractions necessary to encapsulate the growing
complexity
3. Faster databases
4. Answers A and B are correct.
Chapter 13

Balanced Coupling in Practice

A developer’s life is like an endless learning spree,

Yet patterns and principles are branches of the same tree.

From a method’s line, to the system’s high-level design,

The dimensions of coupling must be balanced and prime!

Chapter 12 explored the growth dynamics in network-based systems and


explained why innovation is crucial for sustainable growth. You also
learned how the theory of complex networks applies to software design.
This chapter puts the theory into practice. It presents eight case studies that
demonstrate the discussed concepts at different levels of abstraction, from
microservices to methods. Each case study analyzes the design decisions
made by teams and how they were improved by applying the balanced
coupling model.

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.

Case Study 1: Events Sharing Extraneous Knowledge

The Support Case Management ( SCM ) microservice manages the


lifecycles of support cases. As this is a core subdomain, the team decided to
implement it using the event sourcing pattern: All changes to the states of
support cases are modeled as events (Listing 13.1).

Listing 13.1 Example of Events Used to Represent State Transitions in


Support Cases

Click here to view code image

[
{
"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": "..."
}
]

The event-sourced model enables WolfDesk to analyze all system decisions


and use the insights to optimize its business processes. It also allows
WolfDesk to generate multiple representations of support cases. For
instance, the events can be utilized to create models for operational decision
making, analysis, and more.

The team responsible for the Support Case Management component


decided to publish all of the internal events and allow other microservices
to subscribe to them. One such subscriber is the Support Autopilot
microservice, as illustrated in Figure 13.1.

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.

Support Case Management is a core subdomain, and thus its volatility


is high. The distance between the two components is also high; these are
two microservices implemented by different teams. Consider the events
generated by SCM to the model support cases’ state transitions. Exposing
these events across the service’s boundary leads to model coupling: The
downstream component— Support Autopilot —is now aware of what
model SCM uses internally.

Although “model coupling” is situated in the lower range of the integration


strength model, it still shares a significant amount of knowledge. The
combination of high volatility and high distance exacerbates its effect and
leads to friction between the involved teams.

Furthermore, a properly designed microservice is a bounded context.1


According to the definition, a bounded context is a boundary within which a
model can be used. That implies that the model should be encapsulated
within its bounded context—the microservice. Not surprisingly, exposing
the model results in integration difficulties between the teams.

1. On the other hand, a bounded context is not necessarily a microservice.


See Chapter 14 of my book Learning Domain-Driven Design: Aligning
Software Architecture and Business Strategy (O’Reilly, 2021).

To address the issue, the Support Case Management team decided to


minimize the knowledge exposed by the microservice. The team members
met with the Support Autopilot team members and asked what information
they actually need, and what would be the most convenient format for them
to receive the information. Together, the two teams converged on the ideal
integration event schema for the Support Autopilot team. Instead of having
to work with numerous types of events, the schema combined all the
required information into a single event. The integration event should
contain the following information for each case: its creation timestamp, the
timestamp of the last modification, identification of the relevant customer, a
collection of all exchanged messages, its status, flags indicating whether the
case has been reopened or escalated, the currently assigned agent, and a
collection of all agents who have worked on the case. The schema of the
resultant integration event is illustrated in Listing 13.2.

Listing 13.2 Example of an Integration-Specific Event


Click here to view code image

{
"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"]
}

Effectively, this refactoring divided the Support Case Management


service’s events into two sets:

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.

Case Study 2: Good Enough Integration

The Desks microservice manages WolfDesk’s help desks, their


organizational units, and the schedules of support agents at different
geographical locations. This functionality doesn’t offer a competitive
advantage for the company, and thus, it can be categorized as a supporting
subdomain.

The Distribution microservice, responsible for assigning agents to


handle support cases, subscribes to the schedule changes published by
Desks . The structure of these events reflects the models used internally
within the service. Therefore, any change to the internal model would
inevitably alter the events published by the service. As a result, the
integration strength between these two microservices is model coupling
(Figure 13.3).

Figure 13.3 Changes in agents’ schedules are published as events.

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

Architectural patterns define high-level organizational principles for


coordinating the work of components within a service: business logic, APIs,
user interface, persistence mechanisms, and other infrastructural
components. Let’s examine the challenges and considerations made by the
WolfDesk teams when choosing 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

Since Support Case Management is a core subdomain,


high volatility is assumed for all of its components.

Case Study 3: Reducing Complexity

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.

The layered architecture pattern organizes the application’s components


according to their technical responsibilities. A typical implementation of the
pattern includes the following logical layers (see Figure 13.4):

The presentation layer includes all components needed to expose


information to users. This includes user interfaces, APIs, CLIs, and so
on.
The application layer serves as the bridge between the user interface and
the business logic. It’s responsible for defining the application’s overall
functionality from a user’s point of view; that is, its use cases.
The business logic layer describes the rules and processes specific to the
business functionality, including business objects and processes, data
validation, algorithms, and other business-related functionalities.
The data access layer implements the low-level functionality needed to
store and retrieve data. This includes integration with databases, message
buses, and other external information providers, such as other
microservices.

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.

To balance the coupling forces, the team decided to adopt a different


approach: the vertical slice architecture (Bogard 2018).

The vertical slice architecture changes the focus of the organization


principle from technical to functional. First, the internals are divided into
vertical slices, where each slice represents a specific business functionality.
Second, the components within each slice are divided into horizontal layers,
as illustrated in Figure 13.6.
Figure 13.6 Vertical slices architecture

As a result, the distance between the SCM service’s modules located in


separate vertical slices is high, while the integration strength between them
is low. On the other hand, the integration strength between components
within each slice is high (functional coupling), while the distance is
reduced.

Furthermore, returning to the discussion of modules as abstractions, the


vertical slice architecture exemplifies “innovation” that reduces complexity.
Essentially, a vertical slice is a new level of abstraction. As such, it creates a
new semantic level: one that allows reasoning about the components of the
service in terms of the functionality they implement.

Case Study 4: Layers, Ports, and Adapters

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.

As mentioned in the preceding section, the direction of dependencies


between layers is top-down. This implies that the business logic layer
depends on the data access layer. Since the same functional changes affect
both the business logic and data access layers, they are strongly integrated
—functional coupling. Ultimately, as the knowledge flows opposite to the
direction of dependency, the business logic layer ends up being aware of
design decisions taken at the data access layer.

In the case of a core subdomain (support case management), the business


logic is usually complex and nontrivial to implement. Mixing it with even a
subset of data access–related knowledge makes it even more challenging to
implement and test the service’s business logic. To address this, the ports
and adapters architecture2 (Cockburn 2005) proposes a different strategy:
making the business logic and the application logic independent of
infrastructural concerns. For that, the pattern defines two layers: application
and infrastructure. Both the domain logic, and the use cases logic that
orchestrates it, now belong to the application layer. However, since the team
already had two separate layers for the business logic and application logic,
it decided to keep them separated. Figure 13.7 illustrates the migration from
the initial layered architecture to ports and adapters.

2. Also known as hexagonal architecture.

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.

Listing 13.3 An Example of a Port and a Concrete Adapter for It

Click here to view code image

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 inversion of dependencies affects coupling in two dimensions:

1. No knowledge is shared from the infrastructure layer to the domain layer


or the application layer. This simplifies reasoning about the business
functionality, without needing to consider specific infrastructural design
decisions.
2. Recall the concept of inferred volatility discussed in Chapter 9:
Dependence on a volatile component can make the downstream
component volatile as well. Eliminating any infrastructural knowledge
from the domain layer helps to eliminate such accidental volatility.

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.

Another interesting aspect of the ports and adapters architecture is that it


both minimizes the knowledge shared between layers and maximizes the
distance between them. The interfaces—ports—are integration contracts
between the layers. Recall that in the layered architecture the business logic
layer and data access layer are functionally coupled. Inverting the
dependencies between them reduces the integration strength to model
coupling or contract coupling. Ultimately, since the distance between the
layers is high, low integration strength over a high distance increases the
system’s modularity.

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.

Case Study 5: Entities and Aggregates

The initial implementation of WolfDesk’s functionality included the


following four classes: SupportCase , Message , Customer , and
SupportAgent . The process begins when a customer opens a support
case, which is then assigned to an agent to handle it. As the case is being
handled, both the customer and the assigned support agent exchange
messages until the case is marked as resolved.

The relationships between the four classes are as follows:

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).

These one-to-many relationships are illustrated in Figure 13.9.

Figure 13.9 The relationships between the four classes: SupportCase, Customer, Agent, and Message

Initially, the one-to-many relationships were implemented bidirectionally,


allowing traversal of objects in both directions (Listing 13.4). For example,
they allowed fetching the customer that opened a support case, as well as
seeing all the cases opened by a customer.

Listing 13.4 The Initial Design of Bidirectional One-to-Many Relationships


in the Object Model

Click here to view code image


class SupportCase {
...
private Customer openedBy;
private Agent assignedAgent;
private List<Message> messages;
...
}
class Customer {
...
private List<SupportCase> openedCases;
...
}
class Agent {
...
private List<SupportCase> assignedCases;
...
}
class Message {
...
private Customer customer;
private Agent agent;
...
}

The team decided to use an Object-Relational Mapping (ORM) library to


map the classes’ data to the underlying database. Combined with the design
of bidirectional relationships between the objects, the library allowed
committing changes to any number of objects in a single database
transaction.

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.

Second, although the solution provided a flexible way to include multiple


objects in the same transaction, it also allowed the “flexibility” of
committing changes in multiple transactions that should have been
committed atomically. For example, consider the following business rule: If
an agent doesn’t respond within the service level agreement (SLA) time, the
customer can escalate the case. Essentially, escalating a support case and
the creation of a new message should be an atomic transaction. However,
the current design doesn’t enforce it. For instance, it allows the following
implementation:

1. Load the last message submitted by the assigned support agent.


2. If enough time has passed since the last message, mark the support case
as escalated, and commit the change to the database.
If, however, the support agent responds right between the two steps, the
message will be ignored by the algorithm, and the case will still be
escalated.

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:

No business requirements dictate that there should be transactional


coupling between the SupportAgent , Customer , and
SupportCase classes. Yet, the design makes it possible to orchestrate
changes to all of them within the same transaction.
On the other hand, there are business requirements dictating
transactional coupling between SupportCase and Message .
Although it is possible to transactionally change the two classes, it is not
enforced by the design.

The team decided to address these design issues by implementing the


aggregate pattern. An aggregate is a cluster of entities sharing the same
transactional boundary (Evans 2004). This means that all changes to entities
within the aggregate are committed as an atomic transaction. From an
integration strength perspective, only entities bound by functional strength
with the transactional coupling degree are allowed to be included in the
aggregate.
From a balanced coupling perspective, the aggregate pattern minimizes the
distance between entities having a high level of integration strength
between them. The entities that should not participate in the same
transaction should be kept out of the aggregate, and thus, the distance with
them is increased. As a result, the SupportCase class references objects
of type Message ; however, it has no direct references to the Agent and
Customer entities. Instead, it holds their IDs only. That prevents
traversing functionally unrelated entities and makes the weaker integration
between them explicit (Listing 13.5).3

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.

Listing 13.5 The Aggregate Pattern, Which Minimizes the Distance


Between Entities Bound by Functional/Transactional Coupling

Click here to view code image

class SupportCase {
...
private CustomerId openedBy;
private AgentId assignedAgent;
private List<Message> messages;
...
}
class Message {
...
private CustomerId customer;
private AgentId agent;
...
}

Case Study 6: Organizing Classes

As the number of classes belonging to the Support Case Management


component increased, the team decided to organize the files in folders
according to the technical role each plays. In other words, they created a
folder for each design pattern they used, and the folder included all types
implementing that specific pattern, as shown in Listing 13.6.

Listing 13.6 Organizing Files According to Their Technical Roles

Click here to view code image

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

The organization of the artifacts according to the technical definitions


presents the same issues that were discussed in Case Study 4, the higher-
level case study on layers, ports, and adapters. The elements that are located
close to each other—in the same folder—are unlikely to change together.
However, a change in functionality is likely to involve files located in more
distant folders.

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.

Listing 13.7 Organizing Files According to Their Functional Roles

Click here to view code image

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
...

In terms of complexity, organizing code artifacts around their functional


responsibilities reduced the engineers’ cognitive load: It made it easier to
locate files that had to be changed. In other words, it reduced both local and
global complexities.

Methods

Next, let’s delve into the SupportCase.cs file to analyze its design and
observe how its coupling forces were rebalanced.

Case Study 7: Divide and Conquer

The SupportCase class models and implements the lifecycle of a support


case. Chapter 8 mentioned that, initially, it included methods that weren’t
functionally related to support cases: sending email and SMS notifications,
as shown in Listing 13.8.

Listing 13.8 Functionally Unrelated Methods Located in the Same Class

Click here to view code image


public class SupportCase {
public void CreateCase(...) { ... }
public void AssignAgent(...) { ... }
public void ResolveCase(...) { ... }
public void LogActivity(...) { ... }
public void ScheduleFollowUp(...) { ... }
...
public void SendEmailNotification(...) { ... }
public void SendSMSNotification(...) { ... }
}

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).

Listing 13.9 Functionally Unrelated Methods Spread Across Different


Classes

Click here to view code image


namespace WolfDesk.SCM.Domain.Cases {
public class SupportCase {
public void CreateCase(...) { ... }
public void AssignAgent(...) { ... }
public void ResolveCase(...) { ... }
public void LogActivity(...) { ... }
public void ScheduleFollowUp(...) { ... }
....
}

....

public interface INotificationProvider {


void SendEmail(Email email);
void SendSMS(PhoneNumber phone, SMS message);
}
}

namespace WolfDesk.SCM.Infrastructure.Cases {
....

public class AWSNotifications : INotificationProv


void SendEmail(Email email) {
....
}

void SendSMS(PhoneNumber phone, SMS message)


....
}
}
....
}

Distancing the infrastructural implementations of the SendEmail and


SendSMS methods from the domain layer has already increased the
modularity of the system. But let’s consider these two methods. What
knowledge do they share?

They both have a shared functional goal: sending notifications. However,


from the interface definition and implementation perspectives, they share
no knowledge. Hence, the distance between the two can be increased by
extracting each method into its own interface, as shown in Listing 13.10.

Listing 13.10 Increasing the Distance Between the Definitions of Interfaces


for Sending Email and SMS Notifications

Click here to view code image

namespace WolfDesk.SCM.Domain.Cases.Notifications {
public interface IEmailNotificationProvider {
void Send(Email email);
}
public interface ISmsNotificationProvider {
void Send(PhoneNumber phone, SMS message);
}
...
}

Essentially, this refactoring applies the Interface Segregation Principle


(Martin 2003). It states that code should not depend on methods it does not
use. In other words, if no knowledge is shared between two methods, the
distance between them should be increased. Furthermore, separating them
also reduces the interface footprint of the overarching module (class),
helping to reduce cognitive load further.

Case Study 8: Code Smells

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.11 Setting a Response Time Threshold

Click here to view code image


01 public class SupportCase {
02 ...
03 private AgentId assignedAgent;
04 private Priority priority;
05 private List<Message> messages;
06 private DateTime? replyDueDate;
07 ...
08 public void TrackCustomerEmail(Email email,
09 IDepartmentRep
10 var message = Message.FromEmail(email);
11 this.messages.Append(message);
12
13 if (this.AgentAssigned) {
14 var department = departments.GetDepar
15 var sla = department.SLAs[this.priori
16 this.replyDueDate = DateTime.Now.Add(
17 }
18 }
19 }

The implementation of the TrackCustomerEmail method in Listing


13.11 displays a number of code smells4 (Fowler et al. 1999). First, after the
incoming customer email is transformed into an instance of Message and
added to the relevant collection, the method proceeds to calculate the due
date for the assigned agent’s reply (if an agent is assigned). The majority of
the method’s code, lines 13-17, is not related to the incoming email and
doesn’t depend on any shared knowledge.

4. A code smell is a characteristic in the source code of a program that


indicates a deeper problem, often signifying issues with design that could
lead to decreased maintainability or increased complexity.

Moreover, this functionality may be needed at other stages of a support


case’s lifecycle—for instance, when an agent is assigned. Therefore, it
makes sense to “increase the distance” between the code that imports
incoming messages and the code that sets the replyDueDate value
(Listing 13.12).

Listing 13.12 Extracting the Code for Setting the Reply Due Date to a
Dedicated Method

Click here to view code image

01 public class SupportCase {


02 ...
03 public void TrackCustomerEmail(Email email,
04 IDepartmentRep
05 var message = Message.FromEmail(email);
06 this.messages.Append(message);
07 SetReplyDueDate(departments);
08 }
09
10 private void SetReplyDueDate(IDepartmentRepos
11 if (!this.AgentAssigned) {
12 return;
13 }
14
15 var department = departments.GetDepartmen
16 var sla = department.SLAs[this.priority];
17 replyDueDate = DateTime.Now.Add(sla);
18 }
19 }

The change makes it possible for other methods of the SupportCase


class to use the SetReplyDueDate method if that functionality is
required. However, note what happens in line 16. The method assumes that
the Department object holds a dictionary that maps support case
priorities to the department’s SLAs. Although the SupportCase and
Department objects reside within the same microservice, the distance
between them is significant: They belong to different vertical slices. This
usage of the SLAs field implies model coupling—knowledge of how the
module models its business domain. Furthermore, it creates potential for
unexpected edge cases. For instance, what if there is no suitable value in the
SLAs field? Should the TrackCustomerEmail method fail because of
that, or perhaps department managers would prefer to default to a standard
SLA value? The team decided to address both concerns by reducing the
integration strength between the two classes to contract coupling.

The new version of the Department class exposes a dedicated method


called GetSLA that receives the relevant case’s priority and returns the
SLA time, as shown on line 06 of Listing 13.13.

Listing 13.13 Turning Model Coupling into Contract Coupling by Exposing


a Method

Click here to view code image

01 public class SupportCase {


02 ...
03 private void SetReplyDueDate(IDepartmentRepos
04 ...
05 var department = departments.GetDepartmen
06 var sla = department.GetSLA(this.priority
07 replyDueDate = DateTime.Now.Add(sla);
08 }
09 }

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?

Since this knowledge isn’t related to other implementation details of the


SupportCase object, the distance between them can be extended. The
logic can be extracted into a separate object, such as one implementing the
domain service pattern:5 an object implementing an algorithm or a process
that doesn’t fit the functional definition of any specific entity (Evans 2004).
Furthermore, instead of getting an instance of the
IDepartmentRepository interface, the domain service itself can be
injected (Listing 13.14).

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

Click here to view code image


01 public class SupportCase {
02 ...
03 private void SetReplyDueDate(CalcSLA slaServ
04 ...
05 replyDueDate = slaService.CalcDueDate(th
06 th
07 }
08 }

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:

Event-driven architecture, and distributed systems in general


Domain-driven design’s strategic and tactical patterns, such as open-host
service, published language, anti-corruption layer, value objects, and the
difference in approaches used for modeling core and supporting
subdomains
Design principles such as the Dependency Inversion Principle, the
Liskov Substitution Principle, Don’t Repeat Yourself, the Law of
Demeter, and others
Refactorings and code smells
Chapter 14

Conclusion

Modularity and complexity—opposing in aim,

Yet forces behind them, are quite the same.

Balance the coupling, show knowledge its way,

Make those bugs and code gremlins vanish away!

Modularity and complexity are diametrically opposed properties of a


system’s design. Modularity facilitates ease of changes, whereas a complex
system can only be changed through a tedious process of trial and error.
Complexity entangles the internals of a system, while modularity promotes
an intuitive understanding of the system and the interactions of its
components.

If two things are exact opposites, they have to share dimensions or


characteristics that enable the comparison in the first place. In the case of
modularity and complexity of systems, the common dimensions are shared
knowledge and distance of coupled components:

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.

Modularity requires colocating components that have to change together


and to spread apart those that are not meant to co-evolve. Complexity is the
exact opposite. As illustrated in Figure 14.1, both modularity and
complexity reflect the combination of shared knowledge and distance.
Aligning the coupling forces increases complexity of the system, while
modularity requires shared knowledge and distance to be inversely
proportional.
Figure 14.1 Modularity and complexity as a combination of shared knowledge and distance

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.

Different models can evaluate the volatility of a system’s components. The


one I used in this book is domain-driven design’s subdomains. The most
volatile type of a subdomain is core, whereas support and generic
subdomains are not expected to change as frequently.
The forces driving a system toward modularity or complexity, combined
with volatility, result in the balanced coupling formula:

Balance = (Strength XOR Distance) OR NOT Volatility

The balanced coupling formula highlights the importance of modularity


when a system undergoes changes. In the face of high volatility, placing
components that need to co-evolve close to each other, while distancing
those that do not, reduces the cognitive load needed to implement changes.

Adjusting the dimensions of coupling to achieve a balanced relationship is


important, but it’s not enough. It has to be kept. Even a perfect design
decision today may be rendered under-engineering tomorrow. As the system
evolves, any of the three forces can change and, thus, break the balance.
Hence, it’s crucial to be alert for relevant changes, and to readjust the
design decisions to keep the balance.

The notion of coupling is an inherent part of system design. It’s not


unidimensional—either good or bad. The model of balanced coupling
shows that modular design requires keeping its three dimensions in balance.
Epilogue

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.

This simple organizational principle affects everything around us. We put


related things close to each other, while distancing things that are less
related.

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

The Ballad of Coupling

Strong or loose, we live in dread,

Of the coupling monster under the bed.

Yet without it, your system would flake,

Coupling is a pillar you can’t forsake.

But if coupling isn’t the foe, then what can it be?

It’s complexity—the agent of anarchy.

To master the beast, its signs you must learn,

The Cynefin framework shows paths to discern.

Complexity rises, not from parts’ number or size,

It’s interactions among them, where troubles can rise.

While linear ones are simple and clear,

With complex interactions, failures loom near.


Modularity’s perks, we cannot ignore,

But its true essence, we still must explore.

What makes a design coherent and fluent?

It’s all about value—future and current.

Old paradigms fade, replaced by new,

Yet, the same design principles are ever true.

Structured design first came to show,

How modules connect and knowledge can flow.

From structured design, the baton was passed,

Connascence detailed how ties can be cast.

A spectrum of coupling, to ponder and think,

Connascence reveals the depth of each link.

Built on the backbone of structured design,

The integration strength model, a new paradigm.

Connascence on board, detailing the ties,


Exposing the nuances of knowledge flow paths.

Knowledge can flow from near or far,

The distance it goes, sets the costs’ bar.

Beyond the code, distance can span,

Social factors—its way can expand.

Wide is the distance, much knowledge exchanged—

The design not ideal, somewhat deranged.

Yet if they’re static, never to change,

Should that design make anyone rage?

A change in the east echoes in west,

Software design is like a game of chess.

Near or far, how to balance the ends?

As consultants proclaim—it depends!

Systems change, they twist, they shout,

From planned paths, they sometimes flout.


Rebalancing coupling will save the day,

Defending against complexity’s game.

With balanced coupling, a system will thrive:

Achieving its goals and staying alive.

But fractal geometry empowers its growth—

Reshaping the structure and knowledge flows.

A developer’s life is like an endless learning spree,

Yet patterns and principles are branches of the same tree.

From a method’s line, to the system’s high-level design,

The dimensions of coupling must be balanced and prime!

Modularity and complexity—opposing in aim,

Yet forces behind them, are quite the same.

Balance the coupling, show knowledge its way,

Make those bugs and code gremlins vanish away!


Appendix B

Glossary of Coupling

Coupling is an extremely overloaded term, used in various scenarios and


often combined with other terms. While reading the book, you might have
wondered why I didn’t mention one or more types of coupling that you
have probably encountered in other sources. To address this, this appendix
consolidates the most common coupling-related terms, defines their
meanings, and explains how they relate to the contents of this book.

afferent coupling A metric in object-oriented design for the number of


classes that depend on a given class. In terms of the balanced coupling
model, afferent coupling is the number of downstream components that a
component shares knowledge with.

balanced coupling Refers to the optimal state of the three dimensions of


coupling (strength, distance, and volatility), in which the design increases
the modularity of the system’s volatile components. An unbalanced
combination of these dimensions results in increased cognitive load needed
to evolve the system, and consequently, increased complexity.

On a binary scale (low or high), the balance of coupling is evaluated using


the following equation:

BALANCE = (STRENGTH XOR DISTANCE) OR NOT VOLATILITY


cohesion The degree to which the elements within a component are related
and work together to achieve a single, well-defined purpose. In the balanced
coupling model, cohesion is represented by the combination of high
integration strength and low distance.

common coupling (module coupling) Components integrated through


shared access to a common memory space. The knowledge of schema and
of what data is valid for storage is shared by the coupled components.

complexity The cognitive load a person experiences while working with a


system. In the context of software design, complexity is the cognitive load
needed to change a system or to understand its structure and intended
behavior. The balanced coupling model defines complexity as a symmetric
relationship between integration strength and distance:

Local complexity: Packaging unrelated functionalities (low strength)


together (low distance) makes it harder to comprehend and change the
component.
Global complexity: Spreading related components (high strength) over a
large distance results in a volatile relationship with high efforts needed to
make a change.

On a binary scale (low or high), complexity is represented by the following


equations:

COMPLEXITY = NOT (STRENGTH XOR DISTANCE)


GLOBAL COMPLEXITY = STRENGTH AND DISTANCE

LOCAL COMPLEXITY = NOT STRENGTH AND NOT DISTANCE

connascence A model for evaluating dependencies between different parts


of a system, where changes in one necessitate changes in another. The
model consists of two parts: static connascence reflects compile-time
relationships, while dynamic connascence describes runtime dependencies.

In the balanced coupling model, connascence is used to describe degrees of


integration strength levels: static connascence is used for the contract and
model coupling levels, while dynamic connascence partially describes the
degrees of functional coupling.

content coupling (module coupling) Integration through access to or


modification of the internal data or workings of another component. Any
change to the external component’s implementation details is likely to break
the integration.

contract coupling (integration strength) Integration of components through


sharing an integration-specific model: contract. The integration contract
abstracts the details of the model used internally in the upstream
component. The degree of knowledge shared by an integration contract can
be evaluated using the levels of static connascence.
control coupling (module coupling) An integration scenario in which one
component controls the behavior of another. Intricate knowledge of the
intended functional behavior must be shared among the coupled
components.

coupling An essential aspect of system design, enabling the system’s


components to work together to achieve the overarching system’s goals.
Coupling results in components sharing knowledge about each other and
having to share lifecycles. The higher the magnitude of coupling, the higher
the likelihood of the coupled components having to change together. To
evaluate coupling and its effects, the three dimensions of coupling (strength,
distance, and volatility) have to be inspected, as the combination of these
forces can result in increased modularity or increased complexity.

Cynefin A decision-making framework defining problem-solving


approaches for different situations. Cynefin uses the relationship between
an action and its outcome to distinguish the nature of the challenges the
decision-maker faces.

data coupling (module coupling) Communication through the exchange of


data, ensuring that no unnecessary data is shared.

design-time coupling Refers to the dependencies established during the


design phase of development. In the context of balanced coupling, the
levels of the integration strength model reflect such dependencies: the
greater the extent of shared knowledge, the higher the design-time coupling.

development coupling Describes the interdependencies between


components during the software development process. Development
coupling causes changes in one component to impact the development,
testing, and deployment of other components. In the context of the balanced
coupling model, low distance results in development coupling, even if the
components do not share any knowledge.

distance The space knowledge “travels” across coupled components. The


distance affects the coordination and communication efforts needed to
implement a change affecting the coupled components.

Distance is influenced by several technical and social factors:

Level of abstraction: The physical location of the coupled components’


source code impacts the effort needed to introduce cascading changes.
For example, it is easier to change two components located in the same
file rather than those located in different projects.
Runtime coupling: Lower runtime coupling, i.e., not having to
introduce cascading changes simultaneously, reduces the coordination
efforts.
Organization structure: Ownership of the coupled components affects
both collaboration and communication efforts needed to implement
cascading changes. For example, more effort will be needed if the
components belong to different teams.

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.

efferent coupling A metric in object-oriented design for the number of


classes that a given class depends on. In terms of the balanced coupling
model, efferent coupling is the number of upstream components that are
sharing knowledge with a given a component.

external coupling (module coupling) Integration design in which the


coupled components exchange data through global variables or external
systems, which are outside their control. The knowledge of what data is
valid for storage in the global variables is shared by the coupled
components.

functional coupling (integration strength) A level of the integration


strength model for components implementing closely related business
functionalities. Changes in the business requirements are highly likely to
affect functionally coupled components.

integration strength A model for evaluating the type and extent of


knowledge shared by coupled components. Integration strength adapts the
levels of structured design’s module coupling to differentiate between four
kinds of knowledge: contract, model, functional, and intrusive couplings.
The model also leverages the levels of connascence to describe the
complexity of shared knowledge (degrees).

intrusive coupling (integration strength) Integration through using the


upstream component’s private interfaces or other implementation details
that weren’t meant for integration. Such an integration method makes the
downstream component sensitive to all future changes in the upstream
component.

lifecycle coupling Components sharing high levels of lifecycle coupling


have to be implemented, tested, and deployed together. Distance between
coupled components is inversely proportional to their lifecycle coupling:
the shorter the distance, the higher their lifecycle coupling.

model coupling (integration strength) An integration that results in multiple


components using a shared model of the business domain. The evolution of
the model necessitates changes in all model-coupled components. The
degree of shared model knowledge can be evaluated using the levels of
static connascence.

modularity The ability of a system’s design to support future goals. The


goal is to minimize the cognitive load needed to implement reasonable
changes in the system’s functional requirements. Modular design entails
management of the system’s essential complexity, as well as the elimination
of possible accidental complexities.

module coupling A model for evaluating coupling introduced in the


structured design methodology. The model consists of six levels of
interconnectedness: data, stamp, control, external, common, and content
couplings. These levels describe different kinds of knowledge shared for
integration, ranging from awareness of implementation details (content
coupling) to minimizing the schema of the shared data (data coupling).

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.

operational coupling See runtime coupling.

pathological coupling (module coupling) See content coupling.

runtime coupling An operational dependency in which one component


cannot work without another. In other words, the availability of one
component depends on the availability of another component. In the context
of the balanced coupling model, such a relationship signals high lifecycle
coupling and, as a result, effectively reduces the distance between the
components.
semantic coupling Arises from software components assigning common
meaning and interpretation of the data they exchange. This type of coupling
occurs when changes in the business logic, data formats, or the meaning of
data in one component necessitate changes in another component that relies
on that data.

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.

sequential coupling Occurs when methods, classes, or other components


have to be invoked in a specific order. The sequential relationship can be
defined both by a specific order of executions and specific time periods
between executions. Unless such a dependency can be avoided, it indicates
closely related business functionalities of sequentially coupled components.

In the context of balanced coupling and integration strength, sequential


coupling is the lower degree of the functional coupling level.

stamp coupling (module coupling) Communication by passing data


structures or entire objects, potentially sharing more information than
needed for integration.
structured design A methodology developed in the late 1960s for
designing modular software systems. Structured design emphasized the
concepts of coupling and cohesion. It introduced the module coupling
model to help manage the interdependencies between system components.
See also module coupling.

temporal coupling See sequential coupling.

transactional coupling Occurs when multiple components are involved in


a single transaction, requiring all components to either complete
successfully or fail as an atomic unit. Failing to implement transactional
behavior that encompasses the coupled components will result in potential
corruption of the system’s state.

In the context of balanced coupling and integration strength, transactional


coupling is a degree of the functional coupling level.

volatility Reflects a component’s expected rate of change. Volatility is


driven by internal and external factors. Internal volatility results from
changes in the component’s business requirements. The probability of such
changes can be evaluated using domain-driven design’s notion of
subdomains: components related to core subdomains will change the most.
External volatility is caused by changes in upstream components that a
given component depends on. Sharing extensive knowledge causes
cascading changes that affect other (downstream) components, even if the
downstream components’ internal volatility is low.

The balanced coupling model uses volatility to introduce pragmatism in the


design: a lack of modularity can be tolerated as long as the upstream
component’s volatility is low.
Appendix C

Answers to Quiz Questions

Chapter 1: Coupling and System Design

Question 1: B

Question 2: C

Question 3: D

Chapter 2: Coupling and Complexity: Cynefin

Question 1: B

Question 2: C

Question 3: B

Question 4: B

Question 5: C

Chapter 3: Coupling and Complexity: Interactions

Question 1: C
Question 2: D

Question 3: B

Question 4: C

Question 5: D

Chapter 4: Coupling and Modularity

Question 1: D

Question 2: C

Question 3: A

Question 4: D

Question 5: C

Chapter 5: Structured Design’s Module Coupling

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

Chapter 7: Integration Strength

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

Chapter 10: Balancing Coupling

Question 1: E

Question 2: B

Question 3: A

Question 4: D

Chapter 11: Rebalancing Coupling

Question 1: B

Question 2: D

Question 3: C

Question 4: C

Chapter 12: Fractal Geometry of Software Design

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.

(Berard 1993) Berard, Edward V. 1993. Essays on Object-Oriented


Software Engineering, Volume 1. Englewood Cliffs, NJ: Prentice Hall.

(Bettencourt et al. 2007) Bettencourt, L.M.A., J. Lobo, and D. Strumsky.


2007. “Invention in the City: Increasing Returns to Patenting as a Scaling
Function of Metropolitan Size.” Research Policy 36, no. 1: 107–20.

(Bogard 2018) Bogard, Jimmy. “Vertical Slice Architecture.” Jimmy


Bogard (blog). April 19, 2018. https://jimmybogard.com/vertical-slice-
architecture.

(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.

(Cockburn 2005) Cockburn, Alistair. 2005. “Hexagonal Architecture.”


Accessed July 8, 2022. https://alistair.cockburn.us/hexagonal-architecture.

(Conway 1968) Conway, Melvin E. 1968. “How do committees invent.”


Datamation 14, no. 4: 28–31.

(Cunningham 1992) Cunningham, Ward. 1992. “The WyCash Portfolio


Management System.” ACM SIGPLAN OOPS Messenger 4, no. 2: 29–30.

(Dijkstra 1972) Dijkstra, Edsger W. 1972. “The Humble Programmer.”


Communications of the ACM 15, no. 10: 859–66.

(Evans 2004) Evans, Eric. 2004. Domain-Driven Design: Tackling


Complexity in the Heart of Software. Boston: Addison-Wesley.

(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 2003) Fowler, Martin. 2003. Patterns of Enterprise Application


Architecture. Boston: Addison-Wesley.

(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.

(Galilei 1638) Galilei, Galileo. 1638. “The Discourses and Mathematical


Demonstrations Relating to Two New Sciences.”

(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.

(Goldratt 2008) Goldratt, Eliyahu M. 2008. The Choice. Great Barrington,


MA: North River Press.

(Goldratt 2005) Goldratt, Eliyahu M. 2005. Beyond the Goal: Theory of


Constraints. Old Saybrook, CT: Gildan Audio.

(Hawking 2000) Hawking, Stephen. 2000. “Unified Theory Is Getting


Closer, Hawking Predicts.” Interview in San Jose Mercury News, January
23, 2000.

(Hohpe and Woolf 2004) Hohpe, Gregor, and Bobby Woolf. 2004.
Enterprise Integration Patterns: Designing, Building, and Deploying
Messaging Solutions. Boston: Addison-Wesley.

(Høst 2023) Høst, Einar W. “Modeling vs Reality.” NDC Oslo 2023.

(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.

(Khononov 2021) Khononov, Vlad. 2021. Learning Domain-Driven


Design: Aligning Software Architecture and Business Strategy. Sebastopol,
CA: O’Reilly Media.

(Kuhnert et al. 2006) Kühnert, Christian, Dirk Helbing, and Geoffrey B.


West. 2006. “Scaling Laws in Urban Supply Networks.” Physica A
Statistical Mechanics and Its Applications 363, no. 1: 96–103.

(Lee 2020) Lee, Linus. 2020. “Software Complexity and Degrees of


Freedom.” Thesephist.com. Accessed October 31, 2021.
https://thesephist.com/posts/dof.
(Liskov 1972) Liskov, B.H. 1972. “A Design Methodology for Reliable
Software Systems.” In Proceedings of the December 5–7, 1972, Fall Joint
Computer Conference, Part I (AFIPS ‘72 (Fall, part I)). Association for
Computing Machinery, New York: 191–99.
https://doi.org/10.1145/1479992.1480018.

(Malan 2019) Malan, Ruth. 2019. “Architectural design is system design”


(@VisArch, January 5, 2019).
https://twitter.com/ruthmalan/status/1081578760271523840?s=20.

(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.

(Martin 2003) Martin, Robert C. 2003. Agile Software Development,


Principles, Patterns, and Practices. Upper Saddle River, NJ: Pearson.

(Meadows 2009) Meadows, Donella H. 2009. Thinking in Systems: A


Primer. White River Junction, VT: Chelsea Green Publishing.

(Mezzalira 2021) Mezzalira, Luca. 2021. Building Micro-Frontends:


Scaling Teams and Projects Empowering Developers. Sebastopol, CA:
O’Reilly Media.

(Myers 1979) Myers, Glenford J. 1979. Reliable Software Through


Composite Design. Hoboken, NJ: Wiley.
(Naur and Randell 1969) Naur, P., and B. Randell. 1969. “The 1968/69
NATO Software Engineering Reports.” NATO.

(Nygard 2018) Nygard, Michael. 2018. “Uncoupling.” GOTO 2018,


November 8, 2018.

(Oasis 2008) Oasis. 2008. “Reference Ontology for Semantic Service


Oriented Architectures v1.0.” (2008). Oasis-open.org. Accessed October 31,
2021. http://docs.oasis-open.org/semantic-ex/ro-soa/v1.0/pr01/see-rosoa-
v1.0-pr01.html.

(Ousterhout 2018) Ousterhout, John. 2018. A Philosophy of Software


Design. Palo Alto, CA: Yaknyam Press.

(Page-Jones 1996) Page-Jones, Meilir. 1996. What Every Programmer


Should Know About Object-Oriented Design. New York: Dorset House
Publishing Company.

(Page-Jones 1988) Page-Jones, Meilir. 1988. The Practical Guide to


Structured Systems Design, 2nd Edition. Boston: Pearson Education.

(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.

(Parnas 1971) Parnas, David L. 1971. “Information Distribution Aspects of


Design Methodology.” Technical Report, Depart. of Comput. Science,
Carnegie-Mellon U., Feb., 1971. Presented at the IFIP Congress, 1971,
Ljubljana, Yugoslavia, and included in the proceedings.

(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.

(Sinclair-Smith 2009) Sinclair-Smith, Ken. 2009. “The Expansion of Urban


Cape Town.”
https://www.researchgate.net/publication/333998645_The_Expansion_of_U
rban_Cape_Town.

(Smith 2020) Smith, Steve. 2020. “Encapsulation Boundaries Large and


Small.” Ardalis.Com. Accessed October 31, 2021.
https://ardalis.com/encapsulation-boundaries-large-and-small.
(Snowden 2020) Snowden, Dave, Sonnja Blignaut, and Zhen Goh. 2020.
Cynefin: Weaving Sense-Making into the Fabric of Our World. Colwyn
Bay, Wales: Cognitive Edge - The Cynefin Co.

(Snowden 2007) Snowden, David J., and Mary E. Boone. 2007. “A


Leader’s Framework for Decision Making.” Harvard Business Review 85,
no. 11: 68–76.

(Standish 2015) Standish Group. 2015. “CHAOS report 2015.” The


Standish Group International, Inc.

(Swanson 1976) Swanson, E. Burton. 1976. “The Dimensions of


Maintenance.” In Proceedings of the 2nd International Conference on
Software Engineering (ICSE ‘76). IEEE Computer Society Press,
Washington, DC: 492–97.

(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.”

(Vernon 2013) Vernon, Vaughn. 2013. Implementing Domain-Driven


Design. Boston: Addison-Wesley.
(West 2018) West, Geoffrey. 2018. Scale: The Universal Laws of Life and
Death in Organisms, Cities and Companies. London: Weidenfeld &
Nicolson.

(Wirfs-Brock and McKean 2003) Wirfs-Brock, Rebecca, and Alan McKean.


2003. Object Design: Roles, Responsibilities, and Collaborations. Boston:
Addison-Wesley.

(Wirfs-Brock and Wilkerson 1989) Wirfs-Brock, Rebecca, and B.


Wilkerson. 1989. “Object-Oriented Design: A Responsibility-Driven
Approach.” Conference Proceedings on Object-Oriented Programming
Systems, Languages and Applications (OOPSLA ’89), September 1989, pp.
71–75. https://doi.org/10.1145/74877.74885.

(Yourdon and Constantine 1975) Yourdon, Edward, and Larry L.


Constantine. 1975. Structured Design. New York: Yourdon Press.
Index

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

balance, 191, 192, 206


balanced coupling, 230, 258–259
defined, 265
equation, 194–195
examples, 195–198
high/low, 191, 192
numeric scale, 192–193
balancing complexity, 43
Berard, Edward V., 168
Berners-Lee, Tim, 39
best practice, 21–22
Big Ball of Mud, 42, 53, 71, 225
boundary/ies
component, 14–15, 69, 70
distance, 152
encapsulation, 151, 152–153, 158, 167
knowledge, 69
upstream module, 82–83
bounded context, 236–237
bridge, 138
bug fixes, 167, 202
business
domain, 35–36, 169
logic, 242, 244
strategy, 203, 204
subdomains, 169, 170–171
business logic layer, 239, 240

cause-and-effect relationship, 21, 26, 36


change/s, 201–202
collateral, 156, 161
cost of, 157, 160, 187
distance, 212
environmental, 205
organizational, 204–205
organization-driven, 167
problem, 168
in software development, 165–166
solution, 167–168
source control analysis, 174
strategic, 203, 205–206, 226
tactical, 202
chaotic domain, 24–25, 26, 29, 31, 38
class/es, 11, 109, 246
one-to-many relationships, 246, 247
organizing, 249–250
Triangle, 107, 108
clear domain, 26, 27, 29–30
linear interactions, 36
sense–categorize–respond, 21–22
click-through rate, 23
clockwork system, 11, 36
Cloud Spanner, 50
CMS (content management system), 211
code/codebase
assembly, 81–82
complexity, 20
smells, 253–255
cognitive load, 20, 36, 68–69, 71, 152, 187, 188
cohesion, 73, 188, 265
collaboration, 167–168
collateral change, 156, 161
Commands, 137
common coupling, 83–84, 85, 87, 89–90, 95, 112
defined, 265
versus content coupling, 86
versus external coupling, 88
versus stamp coupling, 91
complex domain, 22–25, 26, 28–29, 31
complex interactions, 54, 118, 166
coupling, 47, 48, 49–50, 51, 52–53
and Cynefin, 38
degrees of freedom, 45–46
intended effects in unexpected ways, 37–38
unintended results, 38
complexity, 19, 22, 54, 258
accidental, 36, 37, 71
balancing, 43
Cynefin, 31–32
defined, 266
essential, 35–36, 69
global, 40–41, 42, 68, 188, 191, 192
hierarchical, 39, 40–41
interface, 118
local, 40–41, 42, 68, 188, 191, 192
modularity and, 69, 71–72
software design, 20
subjectivity, 20
and system size, 39
complicated domain, 26, 28, 30–31
linear interactions, 36
sense–analyze–respond, 22
components, 13, 14, 54. See also modules
boundary, 14–15, 69, 70
complex interactions, 39
downstream, 10
local complexity, 40–41, 42
shared knowledge, 8, 9
upstream, 10
concurrency management, 85, 126
connascence, 97, 118
of algorithm, 102, 110, 120
blind spots, 119, 120
defined, 266
dynamic, 114
evaluating, 110
of execution, 105
of identity, 109, 126
managing, 111
of meaning, 100–101, 110
of name, 98, 99, 100, 110, 113
of position, 102–104, 110
static, 98, 113, 132
of timing, 105–107
of type, 100, 110
of value, 107, 108, 126
Constantine, Larry L., Structured Design, 62, 73, 80
constraint/s, 46, 47, 48, 52–53, 54
arithmetic, 107
business rule, 108
construction, innovation, 224
content coupling, 81–83, 86, 113, 123, 266
context, 22–23, 60
bounded, 236–237
interchangeable camera lenses, 61
LEGO bricks, 61
software module, 65
contract coupling, 134, 135–138, 139–140, 141, 142, 185, 255, 266
control coupling, 88–90, 112
defined, 266
versus external coupling, 90
versus stamp coupling, 91
Conway’s Law, 158, 167–168
core layer, 243
core subdomains, 171–172, 176–177, 178, 207, 243, 270
cost management, 15
coupling, 5, 6, 11, 15, 52–53
afferent, 265
balanced, 191, 192–193, 194–195, 196–198, 258–259
cause-and-effect relationship, 26
common, 83–84, 85–86, 95, 112
content, 81–83, 113, 123
contract, 134, 135–138, 139–140, 141, 142, 185, 255
control, 88–90, 112
data, 92–94, 95, 111
defined, 267
distance, 153, 154
external, 86–88, 112
flow of knowledge, 10, 15
functional, 125–126, 127, 128, 185, 196, 208, 209, 210
“good”, 73
implicit shared knowledge, 9
intrusive, 122, 123, 124, 184
lifecycle, 7–8, 154, 155–156, 159
loose, 188, 189
magnitude, 6, 7
maintenance effort, 189–190, 191
in mechanical engineering, 15, 16
model, 128–131, 132, 133–134, 185, 254
in modularity, 73
module, 80–81
runtime, 159
semantic, 142
sequential, 125
shared knowledge, 8, 9, 15
shared lifecycle, 7–8
stability, 186
stamp, 90–92, 95, 112
strength, 146–147
strength of, 118–119
symmetric functional, 126, 127
time dimension, 165
tolerances, 16
transactional, 125–126
Cynefin, 20, 171
act–sense–respond, 25
applications, 31
chaotic domain, 24–25, 29, 31, 38
clear domain, 21–22, 26, 27, 29–30, 36
complex domain, 22–25, 26, 28–29, 31
complex interactions, 38
and complexity, 32
complicated domain, 22, 26, 28, 30–31, 36
defined, 267
disorder domain, 26
probe–sense–respond, 23
sense–analyze–respond, 22
sense–categorize–respond, 21–22
in software design, 27–32

data access layer, 240


data coupling, 92–94, 95, 111, 267
data transfer objects (DTOs), 93, 140, 141
database, 44, 67
changing indexes, 29–31
Cloud Spanner, 50
relational, 106
schema, 51
sharing, 121, 122, 123, 132, 141, 142
SQL, 48–50, 53
decision-making. See also Cynefin
act–sense–respond approach, 25
experimentation, 23
known unknowns, 22
probe–sense–respond approach, 23, 25
sense–analyze–respond approach, 22
sense–categorize–respond approach, 21–22
unknown unknowns, 22
decomposition, 15, 42, 65, 66
decoupling, 15
deep modules, 70–71, 73
degrees of freedom, 52–53, 72
and complex interactions, 45–46
constraints, 46, 47
in software design, 43, 44, 45
dependencies, 244
compile-time, 132
flow of knowledge, 10
design patterns, 138
design-time coupling, 267
Desks microservice, 238
development coupling, 267
Dijkstra, Edsger W., 67, 136
disorder domain, 26
distance, 195, 230, 257–259
boundaries, 153
changes, 212
cost of, 153, 154
defined, 267–268
high/low, 185–187
versus integration strength, 161
as lifecycle coupling, 154, 155–156
numeric scale, 192–193
organizational, 168
ownership, 158
versus proximity, 160
runtime coupling and, 159
socio-technical aspect, 157, 158
and strength, 187, 188
and volatility, 186, 187
distributed system, 144, 145
Distribution microservice, 238
domain/s. See also Cynefin
analysis, 169
business, 169
constraints, 47
-driven design, 169
downstream module, 10, 81, 184
dynamic connascence, 104, 114
connascence of execution, 105
connascence of identity, 109, 126
connascence of timing, 105–107
connascence of value, 107, 108, 126

economies of scale, 218


effective modules, 65–66
efferent coupling, 268
efficiency, growth and, 219, 230
encapsulation, 140, 152–153, 158, 167
environmental change, 205
essential complexity, 35–36, 69
Evans, Eric, 184
events, 234–235, 236
integration-specific, 237
private, 237, 238
public, 237
experimentation, 21, 23, 26, 28, 29
expertise, 20, 22, 29
explicit knowledge, 52–53
external coupling, 86–88, 112
versus common coupling, 88
versus control coupling, 90
external coupling, 268
F

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

Galilei, Galileo, The Discourses and Mathematical Demonstrations


Relating to Two New Sciences, 220, 223
General Data Protection Regulation (GDPR), 205
generic subdomains, 172–173, 211, 212
GetTime method, 106
global complexity, 40, 41, 42, 54, 68, 188, 191, 192
global variable, 87
growth
and efficiency, 219, 230
innovation, 223–224, 225, 226
limits, 219–220, 225
limits, overcoming, 223, 224–225
linear, 221
software, 215–216
sublinear, 218
superlinear, 221
system, 218, 219, 220

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

layered architecture, 239, 240, 243


leaking abstraction, 72
LEGO bricks, 59, 60
lifecycle
coupling, 154, 155–156, 159
shared, 7–8
lifecycle coupling, 268
“lift-and-shift” strategy, 205
linear interactions, 36, 54
local complexity, 40–41, 42, 43, 54, 68, 188, 191, 192
logic, 60
duplicated, 153
interchangeable camera lenses, 61
LEGO bricks, 61
software module, 64
loose coupling, 188
low balance, 191

machine learning (ML) algorithm, 195


magic values, 101
magnitude of coupling, 6, 7
maintenance effort, 189–190, 191
Malan, Ruth, 14
Meadows, Donella H., Thinking in Systems: A Primer, 11
mechanical engineering, coupling, 16
method/s, 11, 64, 156
GetTime, 106
SendEmail, 103
sendNotification, 88–89
SetEdges, 46
SetReplyDueDate, 254
TrackCustomerEmail, 253–254
Myers, Glenford J. Reliable Software Through Composite Design, 80
microservices, 29, 39, 42, 65, 153, 158, 206, 207, 233, 239
Desks, 238
Distribution, 238
Support Autopilot, 236
Support Case Management (SCM), 234–235, 236–237, 238
model coupling, 128–131, 132, 133–134, 185, 254, 268
modules and modularity, 15, 57–58, 59, 74, 192–193, 257–259. See also
balanced coupling; coupling
as abstraction, 66–68
cohesion, 73
common-coupled, 85
comparison of coupling levels, 94, 95
complexity and, 69, 71–72
context, 60, 65
control coupling, 88–90
coupling, 15, 73, 80–81, 268
data-coupled, 92–94
defined, 268
deep, 69, 70–71, 73
distance between, 153, 154
downstream, 81, 184
effective, 65–66
fractal, 230
function, 60, 64
functionality, 62–63, 68
interchangeable camera lenses, 59, 61
interface, 67
LEGO bricks, 59, 60
lifecycle, 154
logic, 60, 64
properties, 74
queries, 137
shallow, 70
shared lifecycle, 7–8
software, 62–63, 64
stamp coupled, 90–92
Myers, Glenford J., Composite/Structured Design, 40
MySQL, 9

name, connascence, 98, 99, 100


nature, fractal geometry, 229
network-based system/s, 225
growth limit, 220
properties, 216, 218
software design as, 217

Object-Relational Mapping (ORM) library, 123, 247


object/s, 15, 39
business, 245
duplicated logic, 154
functionality, 156
-oriented programming, 11, 73, 97
Query, 50–51, 53
SupportCase, 156
one-to-many relationships, 246, 247
optimizing
global complexity, 41, 42
local complexity, 42, 43
organizational change, 204–205
organizational distance, 168
Ousterhout, John, A Philosophy of Software Design, 69, 70
overcoming growth limits, 223, 224–225
ownership distance, 158

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

scaling, 220. See also growth


sublinear, 218
superlinear, 218
self-similarity, 228, 229, 230
semantic coupling, 142, 269
SendEmail method, 103
sendNotification method, 88–89
sense–analyze–respond, 22
sense–categorize–respond, 21–22
sequential coupling, 125, 269
serialization, 85
service/s, 11, 15
-based system, 63
Fullfilment, 119, 120
Retail, 119, 120
SetEdges method, 46
SetReplyDueDate method, 254
shallow module, 70
shared knowledge, 8, 9, 15, 16–17, 68–69, 73, 89, 92, 131, 133, 153,
166, 183, 185, 244, 257. See also integration strength
implicit, 9
shared lifecycle, 7–8
Single Responsibility Principle, 156, 251
size, system, 39, 43
SMS messages, 27, 28
Snowden, Dave, 21, 32
socio-technical design, distance and, 157, 158
software development
Agile, 165–166
tactical changes, 202
software/software design. See also modules and modularity
bug fixes, 167
changing database indexes, 29–31
complexity, 20
contract, 134
Conway’s Law, 158, 167–168
coupling, 15
“crisis”, 80
Cynefin, 27–32
degrees of freedom, 43, 44, 45
effective modules, 65–66
growth, 215–216
growth dynamics, 221, 222, 223
innovation, 225
integrating an external service, 27–29
integration, 27
model, 129–130, 131
modularity, 57–58
module, 62–63, 64, 66
as network-based system, 217–218
problem space, 166
proximity, 160
refactoring, 167
solution space, 166–168
strategic changes, 203
system, 11, 14
tactical changes, 202
source control analysis, 173
SQL, 48–50, 53
stability, 186
stamp coupling, 90, 95, 112, 269
versus common coupling, 91
versus control coupling, 92
start-up, 167
state, system, 45
static connascence, 98, 110, 113, 132
connascence of algorithm, 102
connascence of meaning, 100–101
connascence of name, 98, 99–100
connascence of position, 102–104
connascence of type, 100
strategic changes, 203, 205–206, 226
strategy
business, 203
“lift-and-shift”, 205
strength, integration, 121, 143, 144, 147, 161, 170, 175–177, 195, 230,
268
changes, 206–207, 208, 209, 210
and distance, 187, 188
high/low, 185–187
numeric scale, 192–193
structured design, 79, 118, 144, 147
blind spots, 119, 120
control coupling, 88–90, 95
data coupling, 92–94, 95
defined, 269
external coupling, 86–88, 95
module coupling, 80–81
subdomains
business, 169, 170–171
core, 171–172, 176–177, 178, 207, 210, 243
generic, 172–173, 211, 212
supporting, 173, 210, 238
volatility, 173, 174
subjectivity, complexity, 20
sublinear scaling, 218
superlinear growth, 218, 221
Support Autopilot microservice, 236
Support Case Management (SCM) microservice, 234–235, 236–237, 238
SupportCase class, 251–253
SupportCase object, 156
supporting subdomains, 173, 210, 238
symmetric functional coupling, 126, 127
synchronous integration, 159
system design
complexity, 36
flow of knowledge, 10
system/s, 10–11
accidental complexity, 36
change, 201–202
class, 11
classes, 11
clockwork, 11, 36
complex interactions, 37
complexity, 19–20, 22, 54
components, 13, 14
coupling, 11
decomposition, 15, 42, 65–66
degrees of freedom, 43, 44, 45–46, 72
distributed, 144, 145
essential complexity, 35–36
evolution, 74
failure, 38
growth, 215, 218, 219–220
interactions, 13, 14–15
linear interactions, 36
method, 11
modularity, 57–58
monolithic, 65
network-based, 216, 217–218
purpose, 13, 14
service-based, 63
size, 39, 43
software, 11, 14
state, 45
T

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

West, Geoffrey, 216


Where clause, SQL, 48–50
WolfDesk, 27, 39, 48, 129–130, 140, 219, 224. See also microservices
business subdomains, 170–171
core subdomains, 172
Distribution microservice, 206, 207, 208
microservices, 233
one-to-many relationships between classes, 246, 247
organizing classes, 249–250
Real-Time Analytics subsystem, 208
Support Case Management (SCM) microservice, 234–235, 236–237,
238
support cases, 246, 247, 248
SupportCase class, 251–253
X-Y-Z

Yoder, Joseph, 71
Yourdon, Edward, Structured Design, 62, 73, 80
Code Snippets

Many titles include programming code or configuration examples. To


optimize the presentation of these elements, view the eBook in single-
column, landscape mode and adjust the font size to the smallest setting. In
addition to presenting code and configurations in the reflowable text format,
we have included images of the code that mimic the presentation found in
the print book; therefore, where the reflowable format may compromise the
presentation of the code listing, you will see a “Click here to view code
image” link. Click the link to view the print-fidelity code image. To return
to the previous page viewed, click the Back button on your device or app.

You might also like