Chapter 3: Modularity

fsa modularity cohesion coupling connascence metrics

Status: Notes complete


Overview

Chapter 3 establishes the theoretical and practical foundation for one of the most important concepts in software architecture: modularity — the degree to which a system’s components can be separated, combined, and understood independently. Before architects can reason about architectural styles, they need a rigorous vocabulary and measurement framework for the relationships between components. This chapter provides exactly that, covering: (1) the definition of modularity and why it matters architecturally; (2) the three primary metrics architects use to measure modularity — cohesion, coupling, and connascence; (3) how these metrics translate into architectural health indicators; and (4) how poor modularity creates the conditions for Big Ball of Mud systems. The chapter is foundational — almost every architectural style decision in the rest of the book is, at its core, a decision about how to achieve the right kind of modularity for the problem at hand.


What Is Modularity?

The authors define modularity as a general concept describing a logical grouping of related code — whether as classes, functions, packages, or services. The key word is related: modularity means grouping things that belong together and separating things that do not.

Modularity exists at multiple levels of abstraction:

Levels of Modularity
─────────────────────────────────────────────────────────────────────
Class/Function level      → methods in a class, functions in a module
Package/Namespace level   → related classes in a package
Component level           → related packages forming a deployable unit
Service level             → related components exposed as a network service
─────────────────────────────────────────────────────────────────────

Each level of modularity involves the same fundamental questions:

  • What belongs together? (cohesion)
  • What depends on what? (coupling)
  • In what ways do elements share knowledge? (connascence)

The authors make a critical observation: 95% of the words developers and architects use about software structure — coupling, cohesion, reuse, layers, components — are really statements about modularity. Chapter 3 gives those words precise, measurable meaning.


Why Modularity Matters for Architecture

Modularity is not an aesthetic preference or a code-quality nicety. It directly determines:

Changeability: Poorly modular code creates unexpected cascading changes. Modifying one part causes failures in distant, apparently unrelated parts — the classic “stepping on a landmine” experience. High modularity means changes have predictable, bounded effects.

Testability: Modules with clear boundaries can be tested in isolation. Entangled code requires standing up large portions of the system to test any single behavior.

Deployability: At the architectural level, modularity determines what can be deployed independently. A system where everything depends on everything cannot deploy anything independently — it must deploy everything at once.

Team autonomy: Conway’s Law applies. If the code has poorly defined module boundaries, teams will step on each other’s work constantly. Clear module boundaries enable clear team ownership boundaries.

Evolvability: Systems that need to evolve their architecture (e.g., extracting a service from a monolith) can only do so cleanly if the code has been written with good modularity. Attempting to extract a tangled mass of coupled code is an extremely high-risk operation.

The authors link modularity directly to the First Fitness Function (structural integrity): an architecture that degrades its own modularity over time is an architecture drifting toward Big Ball of Mud.


Measuring Modularity: Cohesion

Cohesion measures how related the elements within a module are. A highly cohesive module contains elements that all serve a single, well-defined purpose. A poorly cohesive module is a grab-bag of unrelated functionality.

Types of Cohesion (Weakest to Strongest)

The authors present the classic Yourdon-Constantine cohesion hierarchy:

Cohesion Types (worst to best)
─────────────────────────────────────────────────────────────────
Coincidental cohesion   — elements are grouped arbitrarily (worst)
Logical cohesion        — elements do logically similar things but are unrelated
Temporal cohesion       — elements execute at the same time (e.g., startup)
Procedural cohesion     — elements follow a specific execution sequence
Communicational cohesion— elements operate on the same data
Sequential cohesion     — output of one element is input to the next
Functional cohesion     — elements all contribute to a single well-defined task (best)
─────────────────────────────────────────────────────────────────

Functional cohesion is the goal. A class or module with functional cohesion can be described with a single, clear sentence: “This module handles customer address validation.” If describing a module requires “and” — “This module handles customer address validation and sends notification emails and updates the audit log” — it has low cohesion.

Cohesion Metric: LCOM (Lack of Cohesion in Methods)

The authors introduce LCOM (Lack of Cohesion in Methods) as the primary measurable cohesion metric at the class level.

LCOM measures the degree to which the methods of a class use the class’s instance fields.

A class where every method uses every field has high cohesion (LCOM near 0). A class where methods each use different subsets of fields has low cohesion (LCOM near 1) — it is doing multiple unrelated things and should probably be split.

Example: High LCOM (low cohesion)
─────────────────────────────────────────────────────────────────
class CustomerService {
    String name;
    String email;
    String streetAddress;
    String city;
    double accountBalance;
    Date lastPaymentDate;

    sendWelcomeEmail()  → uses: name, email
    validateAddress()   → uses: streetAddress, city
    processPayment()    → uses: accountBalance, lastPaymentDate
}

Each method uses a completely different subset of fields.
LCOM is high — this class has three separate responsibilities.
It should be split into EmailService, AddressService, PaymentService.
─────────────────────────────────────────────────────────────────

LCOM as an architectural fitness function: Automated tools can track LCOM across all classes in a codebase and flag classes with LCOM above a threshold (e.g., LCOM > 0.8). This turns cohesion from a subjective observation into a measurable, enforceable architectural constraint.

The Cohesion-Size Trade-off

There is a tension: maximizing cohesion naively leads to classes with one method and one field — tiny, perfectly cohesive but impractical. The goal is not to minimize class size but to ensure that everything in a module genuinely belongs together. Cohesion is about relatedness, not smallness.


Measuring Modularity: Coupling

Coupling measures the degree of interdependence between modules. High coupling means changes in one module are likely to require changes in another. Low coupling means modules can change independently.

The authors distinguish two types of coupling:

Afferent Coupling (Ca) — Incoming

Afferent coupling (also called fan-in) measures how many other modules depend on a given module. A module with high afferent coupling is depended upon by many others — it is a central, stable component. High Ca means high responsibility: changes to this module affect many dependents.

High Afferent Coupling (Ca):

  ModuleA ──┐
  ModuleB ──┤──→ CoreModule (Ca = 5)
  ModuleC ──┤
  ModuleD ──┤
  ModuleE ──┘

CoreModule is used by 5 other modules.
It must be extremely stable — any breaking change ripples widely.

Efferent Coupling (Ce) — Outgoing

Efferent coupling (also called fan-out) measures how many other modules a given module depends on. A module with high efferent coupling is fragile: it can be broken by changes in any of the many modules it depends on.

High Efferent Coupling (Ce):

OrderProcessor ──→ CustomerRepo (Ce = 5)
               ──→ InventoryService
               ──→ PaymentGateway
               ──→ NotificationService
               ──→ AuditLogger

OrderProcessor depends on 5 other modules.
Any one of them changing its interface may break OrderProcessor.

Instability Metric

The authors present the instability metric derived from afferent and efferent coupling:

Instability (I) = Ce / (Ca + Ce)

Range: 0.0 (maximally stable) to 1.0 (maximally unstable)

- I = 0.0: The module has no outgoing dependencies. Nothing it depends on can break it.
          It is maximally stable. (Example: a pure utility with no imports)
- I = 1.0: The module has no incoming dependencies (nobody uses it) but depends
          on many others. It is maximally unstable — any dependency change breaks it,
          and nothing would break if it disappeared.

The Stable Dependencies Principle: Modules should depend in the direction of stability — a module should depend on modules that are more stable than itself. If a stable module (I ≈ 0) depends on an unstable module (I ≈ 1), that stable module has inadvertently become fragile.

Abstractness

To complete the coupling analysis, the authors introduce abstractness:

Abstractness (A) = (Number of abstract classes + interfaces)
                  / (Total number of classes)

Range: 0.0 (no abstractions — all concrete) to 1.0 (all abstract)

A module with A = 1.0 is entirely abstract — it defines contracts but contains no implementation. It can be changed without breaking dependents (because dependents depend on the interface, not the implementation). A module with A = 0.0 is entirely concrete — all implementation, no abstractions.

The Main Sequence: Balancing Stability and Abstractness

The authors present the Zone of Uselessness and Zone of Pain diagram, showing how stability and abstractness interact:

                Abstractness (A)
        1.0 ─────────────────────────
            │ Zone of Uselessness   /
            │ (abstract but nobody /
            │  depends on it)      /
            │                     / Main Sequence
            │                    /  (A + I = 1.0)
            │                   /
            │                  /
            │   Zone of Pain  /
            │ (concrete and  /
            │  everything    /
            │  depends on it)/
        0.0 ────────────────────────
            0.0                   1.0
                    Instability (I)

Zone of Pain (stable + concrete, top-left): A module with low instability (many dependents) and low abstractness (all concrete, no interfaces). Highly depended upon, difficult to change. Example: a widely-used utility class with no interfaces. Changes are painful because they break many dependents.

Zone of Uselessness (unstable + abstract, bottom-right): A module with high instability (nobody uses it) and high abstractness (all interfaces, no implementation). Technically well-designed but serving no purpose.

Main Sequence (the diagonal): The ideal zone where abstractness and instability are in balance — A + I ≈ 1.0. Stable modules are abstract (can be depended upon safely because dependents use interfaces). Unstable modules are concrete (okay to be concrete since nobody is depending on them).

Distance from Main Sequence (D) is a fitness function:

D = |A + I − 1| / √2

D near 0.0 → module is near the main sequence (healthy)
D near 1.0 → module is in the zone of pain or uselessness (problematic)

Measuring Modularity: Connascence

Connascence is the most nuanced and powerful modularity concept in the chapter. Introduced by Meilir Page-Jones and extended by the authors, connascence describes the type of knowledge two components must share in order to change together.

The key insight: not all coupling is equal. Saying “ModuleA is coupled to ModuleB” obscures a crucial question — in what way are they coupled? Two modules that share a method name are coupled differently than two modules that share an execution order assumption. Connascence provides vocabulary to distinguish these cases.

The Two Dimensions of Connascence

Strength: How difficult is the connascence to detect and refactor? Weak connascence is easy to find and fix (e.g., a renamed method is caught by the compiler). Strong connascence is hard to detect (e.g., a timing assumption between distributed services — the compiler won’t catch it).

Locality: How close together are the connected elements? Connascence between elements in the same method is far less risky than connascence between elements in different services across a network.

Types of Static Connascence (Source Code)

Static connascence can be detected by analyzing source code (compilers or static analysis tools can flag it):

Connascence of Name (CoN) — weakest static form
Elements share knowledge of a name (method name, variable name, field name). If the name changes, all referencing elements must change.

// CoN: all callers must know the method is named "getCustomer"
Customer c = customerRepo.getCustomer(id);

CoN is the weakest and most acceptable form of connascence. Renaming is compiler-detected and IDE-refactorable.

Connascence of Type (CoT)
Elements share knowledge of a type. If the type changes, all using elements must change.

// CoT: caller and callee must agree on the type "CustomerId"
Customer c = customerRepo.getCustomer(CustomerId id);

Connascence of Meaning (CoM) / Connascence of Convention (CoC)
Elements share knowledge of what a value means. Example: using integer codes (1 = active, 2 = inactive, 3 = suspended) where both producer and consumer must know the meaning of the raw value.

// CoM: magic numbers — both sides must know 1 means "active"
if (customer.status == 1) { ... }
// Better: use an enum — reduces CoM to CoN/CoT
if (customer.status == CustomerStatus.ACTIVE) { ... }

CoM is dangerous because it is invisible to the type system but can cause subtle bugs.

Connascence of Position (CoP)
Elements share knowledge of the order of values. Example: a method with positional parameters, or a CSV record where field order matters.

// CoP: caller must know parameter order
createAddress("123 Main St", "Springfield", "IL", "62701");
// Better: use named parameters or a builder/value object

CoP is particularly dangerous in dynamically typed languages where parameter order is not compiler-checked.

Connascence of Algorithm (CoA)
Elements share knowledge of an algorithm — both must implement the same algorithm to communicate. Example: a client and server that both independently implement the same custom hashing or serialization algorithm without sharing the code.

Types of Dynamic Connascence (Runtime)

Dynamic connascence cannot be detected by analyzing source code — it only manifests at runtime:

Connascence of Execution (CoE)
Elements must execute in a particular order, but the code does not enforce that order.

// CoE: initialize() must be called before process(), but nothing enforces this
service.initialize();
service.process(data);  // will fail silently if initialize() was skipped

This is a significant source of hard-to-reproduce bugs, especially in concurrent or distributed systems.

Connascence of Timing (CoT)
Elements share a timing dependency — the result is correct only if actions happen within a certain time window. Classic example: race conditions in multi-threaded code, or distributed systems that assume a message arrives within N milliseconds.

Connascence of Values (CoV)
Elements share knowledge that certain values must be consistent with each other at runtime. Example: a transaction must have both a debit and a credit that net to zero — the code in two different places must maintain this invariant.

Connascence of Identity (CoI) — strongest dynamic form
Elements share knowledge that they are referring to the same object instance. Classic example: two components that both hold a reference to the same mutable object and rely on the other not modifying it unexpectedly. This is the hardest form of connascence to detect and the most dangerous.

Connascence Refactoring Principles

The authors provide guidance on how to manage connascence:

Rule 1: Minimize overall connascence — fewer shared knowledge dependencies means more independent change.

Rule 2: Minimize connascence across boundaries — the most dangerous connascence is between separately-deployed components (microservices, external APIs). CoV, CoE, and CoI across service boundaries are architectural red flags.

Rule 3: Maximize the strength of connascence you cannot eliminate — if you must have connascence, prefer the weakest form. Convert CoM to CoN (replace magic numbers with enums). Convert CoP to CoN (replace positional parameters with named structs). Convert CoA to CoN (share the algorithm as a library rather than reimplementing it independently).

Rule 4: Locality matters — CoI within a single function is often unavoidable and harmless. CoI across service boundaries is a severe architectural problem.

Connascence vs. Classical Coupling

The connascence taxonomy provides finer-grained vocabulary than traditional coupling:

Classical Coupling ConceptConnascence Equivalent
”These classes are coupled”CoN, CoT, or CoM — depends on the type
”Tight coupling”CoA, CoE, CoI, CoV — implies algorithmic or behavioral knowledge sharing
”Loose coupling”CoN or CoT only — only name/type agreements
”These services share a data contract”CoT (schema) + CoM (field semantics)
“These services have a timing dependency”CoT (dynamic) — a runtime timing assumption

From Metrics to Architectural Fitness Functions

Chapter 3 introduces all three metrics — cohesion, coupling, connascence — but also establishes the principle that these metrics should become automated fitness functions in the build/CI pipeline.

The authors argue that architectural properties like modularity should not be assessed only during manual code reviews (which are infrequent and subjective). Instead, they should be continuously measured:

Example Fitness Functions for Modularity
─────────────────────────────────────────────────────────────────
Cohesion:
  - Flag any class with LCOM > 0.8 as a build warning
  - Fail the build if any class has LCOM > 0.95

Coupling:
  - Flag packages with distance-from-main-sequence D > 0.5
  - Fail the build if any component has Ce > 20 (excessive fan-out)

Connascence:
  - Detect CoM: flag magic numbers not assigned to named constants
  - Detect CoP: flag methods with > 5 positional parameters
  - Detect cross-component CoA: flag duplicate algorithm implementations
─────────────────────────────────────────────────────────────────

This connection between metrics and fitness functions is a preview of Chapter 6 (fitness functions), but it is introduced here because modularity metrics are the primary subject of structural fitness functions.


Big Ball of Mud: The Consequence of Ignored Modularity

The authors use Big Ball of Mud (a term coined by Brian Foote and Joseph Yoder) as the name for the architectural anti-pattern that results from consistently ignoring modularity.

A Big Ball of Mud is a system with:

  • No identifiable module boundaries
  • Undifferentiated interconnection between all components
  • No clear separation of concerns at any level
  • Changes that require touching dozens of files across the codebase
  • No team that can be said to “own” any part of the system

The Big Ball of Mud is not usually designed — it accretes. Each individual decision that ignores modularity is small and locally justifiable (“let’s just grab this method from the other class — it’s faster”). But the cumulative effect is structural collapse.

The authors identify the forces that drive systems toward Big Ball of Mud:

  • Schedule pressure: modularity requires upfront discipline; skipping it saves time now
  • Developer turnover: new developers don’t know the intended structure and create shortcuts
  • Lack of architectural fitness functions: no automated enforcement means drift goes undetected
  • “Just this once” exceptions: each exception to module boundaries that is never cleaned up
  • Lack of shared architectural vocabulary: developers can’t describe or enforce structure they can’t name

Chapter 3’s entire vocabulary — cohesion, coupling, connascence, LCOM, instability, main sequence — is ultimately in service of preventing or reversing Big Ball of Mud conditions.


Comparison Tables

Connascence Strength Reference

TypeCategoryStrengthDetectable ByAcross Boundaries?
Connascence of NameStaticVery weakCompiler / IDELow risk
Connascence of TypeStaticWeakCompiler / type systemLow risk
Connascence of MeaningStaticModerateCode review / linterModerate risk
Connascence of PositionStaticModerateCode reviewModerate risk
Connascence of AlgorithmStaticStrongManual reviewHigh risk
Connascence of ExecutionDynamicStrongTesting / runtimeHigh risk
Connascence of TimingDynamicVery strongIntegration testingVery high risk
Connascence of ValuesDynamicVery strongDomain analysisVery high risk
Connascence of IdentityDynamicStrongestConcurrency testingExtreme risk

Cohesion Types Reference

Cohesion TypeDescriptionQuality
CoincidentalRandom groupingWorst
LogicalSimilar category but unrelatedPoor
TemporalExecute at the same timePoor
ProceduralFixed execution sequenceFair
CommunicationalOperate on the same dataGood
SequentialOutput feeds inputGood
FunctionalSingle well-defined purposeBest

Coupling Metrics Summary

MetricFormulaMeaningIdeal Range
Afferent Coupling (Ca)Count of incoming dependenciesHow many depend on this moduleHigh for core modules — but they must be stable
Efferent Coupling (Ce)Count of outgoing dependenciesHow many this module depends onKeep low — excessive fan-out = fragility
Instability (I)Ce / (Ca + Ce)Susceptibility to changeMatch to abstractness on main sequence
Abstractness (A)Abstract types / total typesDegree of abstractionMatch to instability on main sequence
Distance (D)|A + I − 1| / √2Deviation from main sequenceNear 0.0
LCOMDisjoint method-field setsLack of cohesion in methodsNear 0.0

Common Antipatterns

Utility classes as anti-cohesion: The Utils or Helpers class is a canonical low-cohesion antipattern. It is a coincidental cohesion grab-bag. Every team should have a policy against generic utility classes — each utility should live in the module most closely related to its purpose.

Anemic domain model with high CoM: Domain objects that are just data holders with no behavior force all logic into service classes, which then rely on knowing the meaning of raw data fields — creating Connascence of Meaning everywhere.

Distributed monolith: A system partitioned into microservices, but where the services share a database schema, must deploy together, and have high Connascence of Values across service boundaries. Achieves the operational complexity of microservices with none of the modularity benefits.

Circular dependencies: ModuleA depends on ModuleB, which depends on ModuleC, which depends on ModuleA. This creates a dependency cycle that makes it impossible to deploy, test, or understand any one component in isolation. Instability metrics cannot be computed for a cycle.

Inappropriate intimacy (high CoI across boundaries): Services that share mutable state or pass object references across service boundaries. The classic example is two microservices that both directly manipulate the same database table.


Key Takeaways

  1. Modularity is measurable, not subjective: LCOM, instability, abstractness, distance from main sequence, and connascence types all provide quantifiable, automatable metrics for modularity health.
  2. Cohesion = relatedness: Elements within a module should all serve a single, well-defined purpose. Functional cohesion is the goal. LCOM quantifies cohesion violations at the class level.
  3. Coupling direction matters: Stable modules should not depend on unstable ones. The Stable Dependencies Principle ensures that the direction of dependency matches the direction of stability.
  4. The Main Sequence balances stability and abstractness: Stable modules should be abstract (depended upon safely via interfaces). Unstable modules can be concrete (okay since nothing critical depends on them).
  5. Connascence adds precision to “coupling”: The type of shared knowledge matters as much as its presence. CoN is nearly always acceptable. CoI across service boundaries is nearly always a severe problem.
  6. Convert strong connascence to weak: Replace magic numbers with enums (CoM → CoN). Replace positional parameters with structs (CoP → CoN). Share algorithms as libraries rather than reimplementing them (CoA → CoN).
  7. Dynamic connascence is the most dangerous: Execution order assumptions, timing dependencies, and shared mutable state are invisible to static analysis and the hardest to test reliably.
  8. Modularity metrics become fitness functions: These metrics should be continuously measured in CI pipelines, not assessed only in manual code reviews. Unmonitored modularity always drifts toward Big Ball of Mud.
  9. Big Ball of Mud is the consequence of ignored modularity: It accretes gradually through individually reasonable shortcuts. Prevention requires vocabulary, fitness functions, and cultural discipline — all of which Chapter 3 provides.
  10. Connascence locality matters: Strong connascence within a single function is often acceptable. The same strength of connascence across service boundaries is an architectural crisis.

Last Updated: 2026-05-29