A senior engineer at a mid-size startup inherits a codebase. The tests pass. The service runs. But every time the team ships a feature, something unrelated breaks. Adding a new payment method changes behaviour in the notification system. Fixing a bug in the user service requires touching the analytics pipeline. Nobody planned this. It emerged from three years of growth without deliberate boundaries. The engineers are not bad — the module structure is.
The symptom is clear: changes ripple through code in ways that should be impossible given what the code is supposed to do. The diagnosis requires a different tool than a debugger. It requires examining the import graph.
You have already seen divide and conquer (T8) in Book 1, where large problems are split into smaller problems that can be solved independently and whose solutions are combined. Here it appears at code level as modularity: a codebase is divided into units that can be developed, tested, and deployed independently. The shape is the same — decompose to reduce complexity, recombine to deliver capability. The constraint is the same too: the decomposition is only useful if the pieces genuinely do not depend on each other’s internals.
A module is a unit of code with a defined boundary: a public interface through which other modules interact with it, and private internals that no other module can see or depend on. Modules exist at every scale — a function, a class, a package, a service. The principle is the same at each scale: hide what can change, expose what must be stable.
The import graph makes the module structure visible. Every import statement is a directed edge from the importing module to the imported module. Cycles in the import graph are architectural problems: if module A imports B and B imports A, neither can be understood, tested, or deployed independently. The cycle has created a single entangled unit masquerading as two.
Connascence is a vocabulary for measuring the degree of coupling between modules. Two components are connascent if a change in one requires a change in the other. The types, ordered from weakest to strongest:
Connascence of Name — two components agree on an
identifier. Module B calls module_a.process(). If A renames
process, B breaks. This is the weakest form — renaming is
mechanical.
Connascence of Type — two components agree on the type of a value. A passes an integer; B expects an integer. Changing to a float breaks the contract.
Connascence of Meaning — two components agree on
what a value means. A passes status = 1 to mean “active”. B
treats status = 1 as “pending”. This coupling is invisible
to the type system and can be silent for months.
Connascence of Position — two components agree on
the order of values. process(user_id, account_id) vs
process(account_id, user_id) — positional connascence
produces silent bugs when arguments are transposed.
Connascence of Execution — two components agree on
the order of operations. initialize() must be called before
process(). Violating the order causes runtime failures, not
compile-time errors.
Connascence of Timing — two components depend on relative timing. Service A must write before Service B reads. This is the hardest to detect and the most expensive to violate.
The progression from weak to strong corresponds to increasing cost of change. Connascence of Name is tolerable and addressable with refactoring tools. Connascence of Timing is a distributed systems problem.
Consider a package structure for an e-commerce service:
orders/
order.py (Order entity, pure domain logic)
order_repo.py (OrderRepository interface)
order_service.py (OrderService: orchestrates domain logic)
payments/
payment.py (Payment entity)
payment_service.py
notifications/
notification_service.py
infrastructure/
postgres_order_repo.py (implements OrderRepository)
stripe_payment_adapter.py
email_notification_adapter.py
The import graph in this structure:
orders/order_service.py → orders/order.py
orders/order_service.py → orders/order_repo.py (interface only)
payments/payment_service.py → payments/payment.py
notifications/notification_service.py → (nothing in orders or payments)
infrastructure/postgres_order_repo.py → orders/order_repo.py
infrastructure/postgres_order_repo.py → orders/order.py
No cycles. Domain packages do not import infrastructure. Infrastructure imports domain interfaces. The dependency direction is controlled.
Now consider what goes wrong when a developer adds a shortcut:
orders/order_service.py → payments/payment_service.py
payments/payment_service.py → orders/order_service.py
The orders package now depends on the payments package and vice versa. Neither can be tested without the other. Neither can be deployed independently. The import cycle has merged two intended modules into one coupled unit. A cycle detector in CI would have caught this before it merged.
AT8 — Coupling/Cohesion: High connascence within a boundary, low connascence across boundaries is the target state. A module with strong internal connascence is cohesive — its parts belong together. Across boundaries, strong connascence is a liability. Every import creates a dependency; every dependency constrains independent evolution. The tradeoff is between having a natural home for each concern (cohesion) and keeping boundaries thin enough that modules can change without affecting each other (low coupling).
AT3 — Simplicity/Flexibility: Strict module boundaries make systems flexible — each module can be replaced independently. The cost is structure. A small codebase with two developers does not need elaborate module boundaries; the cognitive overhead of enforcing them exceeds the benefit. A codebase with thirty engineers and eight services needs explicit boundaries enforced by tooling, or the import graph will collapse into a ball of mud within a year.
FM8 — Schema/Contract Violation: When a module’s public interface changes without coordinating dependents, every module that depended on the old interface breaks. The risk is proportional to the number of dependents. A module depended on by twenty others is a high-risk change. A module depended on by zero others can be changed freely.
FM2 — Cascading Failures: A module that holds shared mutable state — a global configuration object, a singleton registry — creates hidden coupling. Changes to that shared state propagate to every module that reads it. The coupling is invisible in the import graph because it flows through data rather than code imports.
Linux kernel modules: The kernel enforces module boundaries through explicit symbol exports. A module can only depend on exported symbols. Unexported internals are invisible to other modules. This boundary enforcement has allowed the kernel to evolve over thirty years without the dependency graph collapsing.
Java package-private visibility: Methods and classes declared without a visibility modifier are accessible only within their package. This is a language-enforced module boundary. Teams that ignore this and make everything public pay the connascence cost later — they cannot change internal implementation without affecting external callers.
npm package ecosystem: Each npm package is a module
boundary enforced by version. A package’s package.json
declares exactly what it depends on. The downside: dependency resolution
is a graph problem, and circular dependencies between npm packages cause
resolution failures — the same architectural problem at ecosystem
scale.
Go module system: Go enforces package boundaries
through explicit exports (capitalised identifiers are public; lowercase
are private). The go mod system makes the dependency graph
explicit and version-pinned. Import cycles are compilation errors, not
runtime surprises — the compiler refuses to build a program with a
cycle.
Concept: Modularity and Boundaries
Thread: T8 (Divide & Conquer) ← Book 1, Ch 5 (Recursive decomposition) → Ch 13 (Domain-Driven Design)
Core Idea: Modules divide a codebase into units that can change independently; the import graph makes the true dependency structure visible, and cycles in that graph merge intended-separate modules into one coupled unit.
Tradeoff: AT8 — Coupling/Cohesion: strong internal connascence (cohesion) is healthy within a module boundary; strong connascence across boundaries creates resistance to independent change.
Failure Mode: FM8 — Schema/Contract Violation: module interfaces that change without coordinating dependents break every caller; the risk scales with the number of modules that import the changed interface.
Signal: When a change to one module unexpectedly breaks tests in an unrelated module, the import graph contains coupling that the team did not intend.
Maps to: Book 0, Framework 8 (Patterns); P2 (Modularity), P4 (Separation of Concerns)
1. What is a module, and what two things does its boundary define?
2. List the six types of connascence described in this chapter, ordered from weakest to strongest. Which type is hardest to detect and most expensive to violate?
3. What does a cycle in the import graph imply about the modules involved, and what is the consequence for testing and deployment?
Draw the import graph for the following module structure and
identify all cycles: auth imports users,
users imports billing, billing
imports auth, notifications imports
users. Which pairs of modules are entangled? What would you
need to extract to break the cycles?
A function signature is
processOrder(userId, accountId, orderId, productId, quantity).
Identify which type of connascence is highest risk here and explain why.
Propose a refactoring that reduces that risk.
A module declares a global CONFIG dictionary that
seven other modules read directly. What type of connascence does this
create? What failure mode does it produce? Propose an alternative
structure.
A complete answer will: (1) specify the tooling: a CI-enforced
import cycle detector (e.g., pydeps, madge, or
a custom script) that fails the build if new cycles are introduced —
this gate prevents regression while the existing cycles are addressed; a
dependency visualisation tool to identify the highest-fan-out nodes that
participate in the most cycles, (2) order cycle elimination by impact:
start with cycles involving the most files or the most frequently
changed modules (high churn cycles cause the most pain); within a cycle,
extract the shared dependency into a new module that both sides can
import, rather than reversing import direction, (3) name AT3
(Simplicity/Flexibility): extracting a shared module is simpler to
reason about but introduces a new module that must be maintained;
inlining dependencies eliminates the module boundary but increases file
size — state which is preferred and under what conditions, and FM8
(silent semantic drift) for cycles that embed implicit contracts between
files that are never documented, (4) describe the enforcement boundary:
define allowed import layers (e.g., api → services → domain →
infrastructure) and enforce directional imports only — any upward import
(domain importing from api) triggers a CI failure — specify the tool
configuration and the process for requesting exceptions.
A complete answer will: (1) make the cohesion argument using connascence vocabulary: authentication and profile management share connascence of identity (the user entity) and connascence of meaning (the concept of “logged-in user state”) — they change together when the user model changes, suggesting high cohesion that favours a single module, (2) make the coupling argument: authentication is consumed by every service in the platform (high fan-in), while profile management is consumed by a smaller set (settings, notifications) — placing them together creates connascence of execution (callers must import authentication logic to access profile operations) that spreads authentication’s blast radius to unrelated services, (3) state the conditions under which each argument wins: cohesion wins when the team is small, the user model is stable, and the overhead of a service boundary (network calls, separate deployment, schema migration coordination) is high; coupling wins when authentication scales independently of profile management, when separate SLAs are required (auth must be 99.99%, profile management can tolerate planned downtime), or when separate teams own each domain, and (4) name AT8 (Coupling/Cohesion) as the governing tradeoff and identify the connascence type that forces the decision: if profile management changes require authentication schema changes, the connascence of meaning is too tight for separation; if they can evolve independently, the coupling argument wins.