I got interested in this after reading the Domain-Driven Design book by the same author, because of the historical perspective he applied to his treatment of modular design. This new book sounded like it would expand on that, and it didn’t disappoint: it’s well researched, building on the ideas of Parnas, Myers, Brooks, Conway, and Ousterhout, to produce something new and useful. Vlad Khononov does something that I find is fundamental to the software design discussion: he provides principled definitions to reduce our reliance on gut feeling when making decisions. That being said, while insightful, it’s also a rather specialized book, one that I recommend to software design nerds but not necessarily to every programmer (as, say, I do recommend A Philosophy of Software Design). I may change this assessment if I find myself frequently using its heuristics to success.
The thesis of the book: despite its reputation, coupling is not something fundamentally bad that we need to remove but an attribute that we should manage strategically. Similar to essential complexity, a useful software system can’t exist without some form of coupling of its components. An easy way to internalize this idea is remembering that cohesion is sometimes defined as good coupling.
Software has a fractal nature, one can reason about modularity at the system, service, namespace, class, and function levels. At each level there are interfaces and implementation, module depth and complexity. Complexity is twofold: local and global. And since software is fractal, this level’s global complexity becomes a higher level’s local one. Naively splitting modules into smaller ones just pushes local complexity up, making the overall system more complicated. Our job then is not chasing local minima but striking a balance: a good enough trade-off that reduces total maintenance cost.
Two thirds of the book is spent on definitions: there are the usual suspects (complexity, modularity, coupling), some historically relevant concepts (structured design’s module coupling, connasence), and a few new ones introduced by the author (integration strength, distance, volatility). These are used to compose heuristics (represented as formulas) to assess the modularity and balance of components in a system, to detect problems and hint at possible solutions. This activity is what the author calls balancing (and re-balancing) coupling. The final chapters show concrete applications of these heuristics on a series of case studies.
To a large extent, the process can be summarized as: if modules are strongly integrated (they share much knowledge), reduce their distance; if modules are weakly integrated (they don’t share much knowledge), increase their distance. Notice how this differs from the notion that smaller, more spread out components necessarily yield simpler systems (a notion that leads to microservice hell and left-pad).
But the balancing is not limited to “moving things around”. Given that software systems are fractal networks, one can introduce new levels of abstraction to allow them to grow beyond their structural limits—and the cognitive load its maintainers can support. This is one of the strengths of the theory presented by the book: the same principles apply at all levels of a system; there’s no hard distinction between software design and architecture—not even between software systems and the human systems they integrate—just different scales, different semantic levels: different perspectives.
Coupling results from the components having to share knowledge, or lifecycles, or both.
Complexity reflects the cognitive load a person experiences when interacting with a system. The greater the cognitive load, the more difficult it is to understand, control, predict, and change the behavior of the system.
A module is a type of component designed to improve the flexibility of a system, allowing it to evolve to support future goals.
A system’s design should not be evaluated by examining individual modules in isolation, but the relationships between them: how they are coupled. Different ways of coupling components share different types and amounts of knowledge. Some will increase complexity, while others will contribute to modularity.
Integration strength models the type and extent of knowledge shared by coupled components. There are four levels of coupling strength:
Distance refers to the space knowledge “travels” across coupled components. It affects the coordination and communication efforts needed to implement a change affecting the coupled components. Distance is influenced by several factors:
Volatility is a component’s expected rate of change.
-- there's global complexity when significant knowledge
-- travels long distances
global_complexity := strength() AND distance()
-- there's local complexity when unrelated components
-- are collocated
local_complexity := NOT strength() AND NOT distance()
-- modularity is the absence of local and global complexity
complexity := local_complexity OR global_complexity
= NOT (strength() XOR distance())
modularity := NOT complexity
= strength() XOR distance()
-- there's balance when the volatile components are modular
-- and the complex components don't change
balance := modularity OR NOT volatility()
= (strength() XOR distance()) OR NOT volatility()