Two pieces of code are coupled when a change in one can require a change in the other. That definition is useful, but it leaves an important engineering question unanswered: which coupling should you fix first?

A shared constant, a parameter order, and a distributed workflow can all create coupling. Treating them as equally harmful leads to unnecessary abstractions in some places and fragile dependencies in others.

Connascence gives a more precise mental model. Two software elements are connascent when they must agree in some way for the system to work correctly. By asking what must agree, how difficult that agreement is to maintain, how far apart the elements are, and how many elements participate, you can make better refactoring decisions.

This article shows how to use that model in everyday design work without turning it into a taxonomy exercise.

Start with one question: what must agree?

Consider a function that creates a reservation:

createReservation(customerId, roomId, nights)

A caller uses it like this:

createReservation(customer.id, room.id, 3)

The caller and function must agree on the position of each argument. If the function changes to:

createReservation(roomId, customerId, nights)

and a caller is not updated, the call may still be syntactically valid while passing the identifiers in the wrong roles.

The important observation is not the name of this particular coupling. It is the agreement:

Argument position carries meaning, so both sides must preserve the same ordering convention.

That is connascence. It makes an otherwise vague statement such as “these modules are coupled” concrete enough to reason about.

A refactoring can move the agreement into a more explicit representation:

request = ReservationRequest(
    customerId = customer.id,
    roomId = room.id,
    nights = 3
)

createReservation(request)

Now the caller and callee still have a contract, but meaning is attached to names rather than position. Reordering fields no longer changes their meaning.

This does not remove all coupling. Software components must agree on something to collaborate. The goal is to make necessary agreements easier to understand and harder to violate accidentally.

Evaluate coupling on three dimensions

A useful connascence review does not stop after identifying an agreement. It asks three practical questions:

  1. Strength: how difficult is the agreement to discover and preserve correctly?
  2. Locality: how far apart are the participating elements?
  3. Degree: how many elements must maintain the agreement?

These dimensions explain why the same kind of dependency can be harmless in one context and expensive in another.

Strength: prefer agreements the tools can see

Some agreements are explicit enough for compilers, type checkers, linters, or tests to detect. Others exist only as conventions in developers’ heads.

Suppose two modules independently use the string "paid" to mean the same order state:

if order.status == "paid":
    releaseShipment(order)
if event.newStatus == "paid":
    recordRevenue(event)

Both pieces of code must agree on the spelling and meaning of "paid". Changing one occurrence to "payment_complete" without changing the other can break behavior.

If the language and design allow both modules to depend on one named definition, the agreement becomes more explicit:

OrderStatus.PAID

Now a rename can often be found mechanically, and invalid values may be rejected earlier depending on the type system.

The general lesson is not “replace every string with an enum.” A string that is local, obvious, and unlikely to change may be perfectly adequate. The lesson is:

When an agreement is costly to maintain, prefer a representation that makes violations visible to your available tools.

A stronger abstraction is valuable only when it reduces meaningful risk. Adding types and wrappers around every small convention can make simple code harder to read.

Locality: distance changes the cost of an agreement

Consider two local variables used together inside a ten-line function:

start = clock.now()
result = operation()
elapsed = clock.now() - start

The code relies on start being captured before the operation. That ordering dependency is easy to see because both operations are close together.

Now imagine a similar ordering requirement spread across services:

Billing -> emits InvoiceCreated
Fulfillment -> must wait for CreditApproved
Notifications -> assumes both happened before ShipmentReady

The agreement is harder to inspect because it crosses process boundaries, deployment units, logs, ownership, and possibly asynchronous delivery. A developer changing one participant may not even have the other participants open in the same repository.

Distance therefore amplifies maintenance cost. When connascent elements are far apart, prefer agreements that are especially explicit and independently verifiable: stable message schemas, documented invariants, compatibility tests, or a coordinator when ordering truly belongs in one place.

Do not infer from this that distributed systems are inherently bad design. Distribution may be required for ownership, scaling, reliability, or organizational reasons. It simply means hidden conventions become more expensive as locality decreases.

Degree: count how many places must agree

Suppose a tax rule is copied into two small functions. A future change requires two edits. That duplication may be tolerable.

Now suppose the same rule is copied into forty handlers. The underlying agreement has high degree: many places must stay synchronized.

The probability of an incomplete change rises because a developer must find every participant. Review also becomes harder because correctness depends on a repository-wide search rather than one authoritative definition.

Reducing degree often means giving one concept one owner:

TaxPolicy.rateFor(order)

Callers depend on that operation instead of reproducing the rule.

But centralization has a cost. If the forty pieces of code only look similar while representing genuinely different business rules, forcing them through one abstraction creates a false dependency. Before deduplicating, ask whether they must change together or merely happen to look alike today.

That question is one of the most useful consequences of the connascence model.

Static agreements are usually easier to manage than dynamic ones

Some agreements can be checked by examining code or declarations. Examples include names, types, argument positions, and shared constants.

Other agreements only become visible while the program runs. Ordering is a common example:

session.load()
session.authorize()
session.execute()

If authorize() must run after load() and before execute(), callers must know a temporal protocol. The individual method calls may all type-check while the sequence is wrong.

One option is to make the valid sequence harder to misuse:

loaded = session.load()
authorized = loaded.authorize()
authorized.execute()

Here each step returns a value representing the next valid state. This is only a teaching sketch; whether such a design is appropriate depends on the language and the complexity of the protocol.

Another option is simpler: move the sequence behind one operation when callers do not need control over intermediate steps.

session.executeAuthorized(command)

The important design move is to replace a runtime convention with an agreement that is easier to see or has fewer participants.

Not every dynamic dependency should become a type-level state machine. For a short local sequence with clear tests, the additional structure may cost more than it saves.

Use connascence to choose refactoring targets

Imagine a code review reveals three dependencies:

  • two adjacent lines rely on a variable name;
  • twelve modules duplicate the same status string;
  • four independently deployed services rely on an undocumented event order.

A generic instruction to “reduce coupling” does not tell you where to spend effort.

Connascence does.

The first dependency is local and low degree. Even if it is technically coupling, it is cheap to understand and change.

The duplicated status has higher degree. A shared representation may reduce incomplete edits if those modules truly share one domain concept.

The cross-service ordering rule has poor locality and is only observable at runtime. It deserves the most attention because discovering and coordinating a change is difficult. You might document the protocol, add compatibility or workflow tests, redesign the events so order is unnecessary, or introduce an explicit coordinator.

The exact solution depends on the system. The model helps prioritize the problem before choosing a mechanism.

Refactor toward weaker, nearer, lower-degree agreements

A practical direction for improvement is:

Prefer agreements that are easier to detect, keep strongly coupled elements close together, and limit how many elements must agree.

These goals interact.

Suppose five modules compare raw numeric error codes:

if response.code == 17:
    retry()

Moving 17 into a named constant reduces the chance of inconsistent values and gives the concept a searchable name:

if response.code == ErrorCode.TEMPORARY_FAILURE:
    retry()

Moving retry policy behind a component can reduce degree further:

if retryPolicy.shouldRetry(response):
    retry()

But the second refactoring changes ownership as well as representation. It is appropriate only if retry classification is genuinely one policy. If different callers intentionally have different retry semantics, one centralized policy may erase an important distinction.

Connascence is therefore a diagnostic tool, not a command to maximize abstraction.

Watch for changes that merely move the coupling

A refactoring can hide an agreement without reducing its cost.

Suppose ten callers use a shared string constant. You introduce a helper:

isPaid(order):
    return order.status == "paid"

This is useful if callers need the business question “is this order paid?” The status representation is now owned in one place.

It is less useful if every new status requires adding dozens of narrowly named helpers while callers still need to understand the full state machine. The coupling has moved behind methods, but the conceptual dependency remains widespread.

Similarly, introducing a shared library between distant services can reduce duplicated definitions while increasing deployment coordination. A library upgrade may now become another agreement that teams must manage.

After a refactoring, ask again:

  • What must agree now?
  • Who owns that agreement?
  • How will a violation be detected?
  • How many places must change when the concept changes?

If the answers are not better, the refactoring may only have changed the shape of the coupling.

Do not optimize harmless local coupling

Connascence becomes counterproductive when every agreement is treated as a defect.

Two lines inside one function often need to agree on names, order, or values. That is normal. Trying to eliminate such local dependencies can produce layers of indirection that make the actual behavior harder to follow.

Prefer leaving coupling alone when it is:

  • close together and easy to inspect;
  • limited to a small number of participants;
  • explicit in the language or tools;
  • stable enough that coordinated changes are cheap.

Spend design effort where agreements cross module, team, process, or deployment boundaries, or where many participants must preserve a convention that tools cannot check.

A small review technique

When a change feels unexpectedly difficult, pick the code that must change together and write one sentence:

These elements must agree on ________.

Fill the blank with something concrete: a name, value, type, position, algorithm, order, timing assumption, or protocol.

Then ask:

  1. Can the agreement be represented in a way that is easier to detect or verify?
  2. Can the elements that share it be brought closer together?
  3. Can fewer elements own the rule?
  4. Would the proposed abstraction actually reduce change cost, or only add indirection?

This technique is intentionally lightweight. You do not need to classify every dependency in the codebase before improving one troublesome design.

Conclusion

Coupling is unavoidable because collaborating software must share agreements. The useful question is not whether coupling exists, but how expensive that agreement is to preserve.

Connascence makes that cost easier to reason about. Identify what must agree, then examine its strength, locality, and degree. Prefer agreements your tools can expose, keep tightly related decisions near each other when possible, and avoid making many distant elements preserve the same hidden convention.

Most importantly, do not refactor coupling simply because you can name it. Improve the agreements that make real changes risky, surprising, or expensive, and leave simple local dependencies simple.