Software Architecture · Modularity

Connascence: Coupling Measured by Strength and Distance

A finer taxonomy than fan-in and fan-out: nine kinds of change-propagating relationship, why strength only matters relative to locality, and how to trade one kind for another.

Fan-in counts arrows. It does not say what would break. Two modules can be connected by a single import and still be so tightly bound that neither can change without the other; two others can share twenty imports that are all type-only and safely ignored. Connascence is a taxonomy built around the question that fan-in cannot answer: if this changes, what else must change to keep the program correct, and how far away is it?

Page-Jones introduced the term for a relationship in which a change to one component requires a corresponding change to another for the system to remain correct. The relationship is not the same as a call or an import; it is a property of the knowledge the two components share. A method signature creates name connascence with every caller. A shared file format creates meaning connascence with every reader. A protocol handshake creates execution-order connascence across the network.

The taxonomy has two dimensions, and the second is what makes it useful.

Strength: nine kinds

The static kinds, roughly in increasing strength:

  • Name. One component refers to another by name. Every caller of processOrder is name-coupled to that identifier; renaming is a mechanical change if tooling exists, and a search-and-replace hazard if it does not.
  • Type. A value’s type must match. Passing an int where a long is expected is type connascence, checked by the compiler in statically typed languages and by nothing at runtime in dynamic ones.
  • Meaning. A value’s encoding must be agreed. The integer 3 meaning “cancelled” in one place and “refunded” in another is meaning connascence, and it is invisible to every type checker that sees only int.
  • Position. The order of values matters. The third argument of a function, the fourth column of a CSV row, the offset of a field in a binary record.
  • Algorithm. Two components must perform the same computation. A checksum computed by the sender and verified by the receiver, a hash function used to place and to look up, a rounding rule applied in two places.

The dynamic kinds, which only manifest while the program runs:

  • Execution order. One operation must precede another; open before read, begin before commit.
  • Timing. Correctness depends on when something happens relative to something else, not just in what order. A timeout value agreed between client and server, a lease duration, a retry window.
  • Value. Several values must change together. Two counters that must sum to a total, a set of flags that must be mutually consistent.
  • Identity. Two references must point to the same object rather than to equal copies. Caching, locking, and observer registration all rely on it.

The ordering is a claim about the cost of a mistake. A missed rename is caught by the compiler or by a test; a missed meaning is caught by a customer. Connascence strength is therefore about detection, not about aesthetics.

Degree and locality decide whether strength is a problem

The strength alone says little. Algorithm connascence inside a single function is not a defect; it is the function. Algorithm connascence between a client and a server, where both implement the same checksum, is a permanent coordination cost: every change to the algorithm must be deployed on both sides, ideally in an order that tolerates mixed versions.

Page-Jones’s rule is that connascence should be encapsulated, and where it cannot be encapsulated it should be as weak as possible. Encapsulation reduces the number of components that participate; strength reduction makes the remaining participation cheaper to detect. The two moves are different, and confusion between them produces the standard bad refactor: extracting a shared constant into a public module that every component imports. The name connascence got weaker (one definition instead of many), but the degree went up (every component now depends on the module), and the module became a change multiplier.

Degree is combinatorial when value connascence is involved. If n components must keep values consistent, there are n(n-1)/2 pairs whose disagreement is possible. Three components yield three pairs; ten yield forty-five. The usual resolution is not to test all pairs but to appoint one owner for the invariant and make the others derive from it, which converts value connascence into a one-to-many dependency on a single authority.

Meaning connascence is the kind tooling cannot see

The clearest everyday example is a status column encoded as integers.

# Two components agree that 3 means cancelled. Nothing enforces it.
def is_refundable(order):
    return order.status != 3

def cancel(order):
    order.status = 3

Renaming, retyping, or moving the field changes nothing about the hazard. The remedy is to reduce the strength by giving the encoding one definition and a type that cannot be confused with other integers:

from enum import IntEnum

class Status(IntEnum):
    PENDING = 1
    SHIPPED = 2
    CANCELLED = 3

def is_refundable(order):
    return order.status != Status.CANCELLED

def cancel(order):
    order.status = Status.CANCELLED

The connascence is now type and name connascence against Status, which a type checker and the import graph make visible. The integers still exist at the database boundary; the module boundary that owns the mapping is the one place where meaning connascence is allowed to remain, and it is a place with one reader.

Position connascence has the same shape. A function taking five positional parameters has 120 possible orderings and no way to say which one the caller used. Named parameters, a parameter object, or keyword-only arguments reduce position connascence to name connascence. The change is mechanical in the language and expensive in the API, which is why it is better done before the function has a hundred callers.

Algorithm connascence is the hardest to remove because it often cannot be centralised. Two independent implementations of the same business rule — a tax calculation in the checkout service and in the reporting job — must agree for the reports to reconcile. Extracting a shared library creates a deployment coupling; duplicating creates divergence; making one call the other creates a runtime dependency where a batch job may not be able to reach a service. There is no free option, only a choice about which failure mode is acceptable. Connascence at least names the decision.

What encapsulation can and cannot absorb

Encapsulation is the technique of hiding a decision behind an interface so that the decision can change without the dependents changing. In connascence terms, it converts strong connascence with distant components into weak connascence with one nearby component. A hash table hides its hash function: callers see put and get, and the algorithm connascence is entirely inside the implementation.

The conversion only works if the interface does not leak the hidden decision. An interface that exposes a hash code, an iteration order derived from hashing, or a capacity that callers tune has re-exported the algorithm, and the encapsulation is cosmetic. The package encapsulation article covers the mechanism; connascence explains why a leak is expensive in a specific way: it restores a strong relationship with a large set of distant components, and the components have no way to know it happened.

Encapsulation cannot absorb dynamic connascence, which is why concurrency defects survive every structural improvement. Execution-order and timing connascence are properties of sequences, and a sequence spans components that may each be well encapsulated. A close that must precede a flush is order connascence between two calls, and no amount of visibility control will detect a caller that reversed them. The available mitigations are protocol design (make the operations idempotent or self-ordering), runtime checks (fail on use-after-close), and documented state machines.

Identity connascence is similar. A cache keyed by object identity, an observer registry holding references, a lock guarding a specific instance — all require that two references resolve to the same object. Serialising and deserialising breaks it silently, because the copy is equal and not identical. The fix is to change the key or the registration model, not to hide the reference more thoroughly.

Distance in distributed systems

Locality is usually physical proximity within a codebase, but the same reasoning applies across deployment boundaries, where the distance is a network and the cost of coordination is a release. A meaning connascence between two services is a schema contract that both must honour; if it changes, both must be deployed in a compatible order. A timing connascence is a timeout that must be shorter than a deadline elsewhere, and the two are configured in different repositories.

The standard distributed-systems practices are connascence management in disguise. Tolerant readers reduce meaning connascence by ignoring unknown fields. Versioned schemas reduce type connascence by allowing two representations to coexist. Idempotency keys reduce execution-order connascence by making repeated calls harmless. Deadlines propagated through call chains reduce timing connascence by making one component’s budget explicit to the next.

None of these eliminate the relationship. They move it from a strong kind to a weaker one at the same distance, which is exactly the trade the taxonomy recommends: if the components must stay far apart, make what they share as weak as possible.

Using the taxonomy in review

The practical value is in the question it forces. For any new dependency, the review can ask which kind of connascence it creates and how far apart the participants are. An import that adds name connascence across a package boundary is routine. An import that exposes a shared encoding is not, and the difference is invisible in the import statement itself.

The taxonomy also predicts where tests will be fragile. A test that asserts on the exact text of an error message is name and meaning coupled to the message; the test will break on a change that preserves behaviour. A test that constructs a fixture with positional arguments is position coupled to the constructor. Reading test failures through the taxonomy turns “the test is flaky” into “the test is strongly coupled to a detail that is not part of the contract”, which is a fixable diagnosis.

The limit of the approach is that it is qualitative. There is no accepted formula that combines nine kinds and a distance measure into a single score, and the attempts to build one tend to reproduce the fan-in counts they were meant to replace. Its use is in the conversation before the code is written, when the choice is still between two designs with different connascence profiles. Once the code exists, the compiler sees only names and types, and the other seven kinds are the ones that reach production.

References

  1. What Every Programmer Should Know About Object-Oriented Design — Meilir Page-Jones, Dorset House, 1995
  2. On the Criteria To Be Used in Decomposing Systems into Modules — D. L. Parnas, Communications of the ACM 15(12), 1972
  3. Refactoring: Improving the Design of Existing Code — Martin Fowler, Addison-Wesley, 2nd edition, 2018