Be Software Architecture Wise
Be Wise Apps

Leer en español · Lire en français · Auf Deutsch lesen

Section 3.2 of 3.8 · Intermediate · 24 min read

Coupling and cohesion

Video lesson

5 min · narrated, with captions
Listen only (audio)
Read the transcript

Coupling and cohesion are the two oldest measures of good structure. In this lesson you'll learn their classic levels, how to count dependencies, the extra kinds of coupling between services, and how to loosen it.

Think of two neighbours who share a garden. If they only lend each other the occasional tool, either can redesign their own garden without fuss. They're loosely coupled. If they share one shed, one hose and one key under a flowerpot, every change needs a conversation. They're tightly coupled. Now think of a kitchen drawer. If it holds only cutlery, you know where to look. It's cohesive. If it holds cutlery, batteries, string and old receipts, it isn't. Good software has cohesive parts that are loosely coupled to each other.

Coupling is the degree to which one module depends on another, so that a change to one can force a change to the other. Cohesion is the degree to which the elements inside one module belong together, because they serve a single purpose. But coupling isn't a defect. A system whose parts never depended on each other would do nothing useful. The aim is to choose it deliberately: few dependencies, through narrow and stable interfaces, and as weak as the job allows.

Structured design, from the 1970s, ranked the ways modules can be connected, from tightest on the left to loosest on the right. Content coupling means reaching into another module's internals, like a service writing to another service's database tables. Common coupling means sharing global data, or one database schema. External coupling means sharing an imposed format, such as a partner's file layout. Control coupling means passing a flag that tells the other module what to do. Stamp coupling passes a whole record when only part is needed. Data coupling, the loosest, passes only simple data that's all needed, through a narrow, documented interface. Cohesion asks why things in a module are together. At the weak end, coincidental: a utils module holding date formatting and a VAT rate. At the strong end, functional: a Tax module that computes VAT, and only that.

The levels describe the kind of dependency. Architects also count them. Afferent coupling is the number of modules that depend on a component: its incoming arrows. Efferent coupling is the number it depends on: its outgoing arrows. The terms come from anatomy, where afferent nerves carry signals in, and efferent nerves carry them out. A component with high afferent coupling, such as a money library used by forty modules, is responsible. Any change can break many others, so it must change carefully and rarely. One with high efferent coupling, such as a checkout workflow calling five other modules, is exposed. The trouble comes when a component is both.

Between services, three more kinds appear, and none shows in a function signature. Temporal coupling means one part works only if another is available at the same moment. If checkout calls payments directly and payments is down, so is checkout. Deployment coupling means two parts must be released together. If every release of Orders needs a release of Billing, they're one unit in practice, whatever the diagram says. Semantic coupling means agreeing on meaning no interface expresses, such as status four meaning shipped, or prices being in pence. These agreements break silently.

Here's a worked refactoring. Before, an online shop's Orders module calls Billing directly on every order, passing a mode flag. That's control coupling, and temporal coupling too: Orders waits for Billing. Billing also reads the orders table directly, decoding its status codes and its VAT column. That's common coupling, and semantic coupling as well. After, Orders publishes an order placed event through a message broker, carrying exactly the facts Billing needs, including the totals Orders has already worked out. Each service owns its own data. What's left is data coupling, through a small, published contract. And Orders no longer waits for Billing.

Let's measure what changed. Before, a shared table, a mode flag and hidden status codes. After, one versioned event with five fields. Before, an order failed if Billing was down. Now the event waits in the broker until Billing recovers. Before, changing the orders table needed a joint release. Now Orders can change it freely. But there are new costs: a broker to run, invoices a few seconds later, and an event that's now a public contract. Coupling wasn't removed, it was chosen. It's now explicit, narrow and versioned.

The full section has tables of every level, a worked example counting dependencies, and the tactics for loosening coupling, each with its price. Next, in Section three point three, we look at information hiding. See you there.

What you need to know

Two measures of structure

Coupling is the degree to which one module depends on another, so that a change to one can force a change to the other. Cohesion is the degree to which the elements inside one module belong together because they serve a single purpose. Both ideas were introduced by Larry Constantine in the late 1960s as part of the structured design movement and published in the 1974 IBM Systems Journal paper Structured Design by Wayne Stevens, Glenford Myers and Constantine, then at greater length in Edward Yourdon and Constantine's book Structured Design (1979). Section 0.3 introduced them informally; this section makes them precise.

The two are linked. If the things that change together live in the same module (high cohesion), then fewer changes cross module boundaries, and the modules need to know less about each other (low coupling). Yourdon and Constantine went as far as saying that, in practice, cohesion was the more important of the two. The modifiability tactics in Section 2.6 (increase cohesion, reduce coupling) rest on exactly this pair.

One clarification at the start: coupling is not a defect. A system whose modules never depended on each other would do nothing useful. Martin Fowler, writing on the subject in 2001, warned that trying to remove coupling entirely only pushes everything into one huge module where the coupling is hidden rather than gone. The aim is to choose coupling deliberately: few dependencies, through narrow and stable interfaces, pointing in sensible directions, and as weak as the job allows.

The classic scales

The classic coupling scale from tightest (content) to loosest (data) and cohesion scale from weakest (coincidental) to strongest (functional)Coupling between modules: aim for the right-hand endContentCommonExternalControlStampDatatighter, worselooser, betterCohesion within a module: aim for the right-hand endCoinci-dentalLogicalTemporalProceduralCommuni-cationalSequentialFunctionalweaker, worsestronger, better
The structured design scales: coupling from content (tightest) to data (loosest), and cohesion from coincidental (weakest) to functional (strongest).

The levels of coupling

Structured design ranked the ways two modules can be connected, from the most damaging to the most benign. The six-level form below is usually traced to Glenford Myers's books on composite design in the mid-1970s and was later popularised by Meilir Page-Jones; names and boundaries vary slightly between sources, but this is the form found in most textbooks. They were defined for procedures in one program, but each has a direct equivalent between components and services, which is where architects meet them today.

Coupling levels, tightest first

LevelWhat it meansIn codeBetween services
ContentOne module reaches into the internals of another: changes its private data or jumps into its code.Setting cart._items from outside; monkey-patching another module's function.Reading or writing another service's database tables directly.
CommonSeveral modules read and write the same global data.A global config dictionary that any function may change.Several services sharing one database schema or one shared cache key space.
ExternalModules share an externally imposed format, protocol or device.Two modules both parse the same vendor file format.Several services depending on a partner's file layout or a specific message protocol version.
ControlOne module passes a flag that tells the other what to do, so the caller knows the callee's internal logic.process(order, mode="refund")An API with an action field that selects completely different behaviours.
StampA module receives a whole data structure but uses only part of it.Passing the full Customer object to a function that needs only the email address.Sending the entire customer record in every event when consumers need two fields.
DataModules communicate only through parameters of simple data that are all needed.send_receipt(email, order_id, total)A narrow, documented API or event with only the fields consumers need.

Two cautions. First, the scale is a guide to risk, not a law. Stamp coupling is often a sensible trade: passing a whole object is simpler than unpacking six fields, and the cost is only that the receiver may now break when fields it does not even use are changed. Second, the scale says nothing about how many dependencies there are or how often the other side changes. A single control-coupled call to a module that never changes may be harmless; data coupling to a module that changes weekly may hurt. Later in this chapter, connascence offers a finer vocabulary, and Vlad Khononov's Balancing Coupling in Software Design (2024) argues that the strength of coupling should always be judged together with the distance between the modules and how volatile they are.

# Control coupling: the caller chooses the callee's internal branch
def handle_payment(order, mode):
    if mode == "charge":
        ...  # call the provider, store the transaction
    elif mode == "refund":
        ...  # look up the transaction, reverse it
    elif mode == "dry_run":
        ...  # validate only

handle_payment(order, "refund")

# Data coupling: separate operations, each taking only what it needs
def charge(order_id: str, amount_pence: int) -> str: ...
def refund(transaction_id: str, amount_pence: int) -> None: ...

refund("tx_81f2", 1999)
Control coupling (top) against data coupling (bottom): the flag forces the caller to know how the callee is organised inside.

The levels of cohesion

Cohesion asks why the elements of a module are together. Yourdon and Constantine described seven kinds of association, again from worst to best. The lower levels group things for reasons unrelated to the problem (they happened to be written at the same time, or they run at the same moment); the higher levels group things because they work on the same data or towards the same single purpose. Later textbook summaries agree on the broad ranking: coincidental and logical cohesion are poor, communicational and sequential are good and functional is best, though the steps between levels are not evenly spaced.

Cohesion levels, weakest first

LevelElements are grouped because...Example
Coincidentalof no meaningful relationship at all.A utils module holding date formatting, a retry decorator and a VAT rate.
Logicalthey do the same kind of thing, selected by a flag.An exports module with one function that writes CSV, PDF or XML depending on a parameter.
Temporalthey run at the same time.A startup() routine that opens the database, warms caches and sends a monitoring ping.
Proceduralthey follow each other in a sequence of control, though on different data.A function that validates a form, then logs the user's IP address, then refreshes a menu.
Communicationalthey operate on the same data.A module that computes several reports from the same order history.
Sequentialthe output of one is the input of the next.Parse a bank statement, then categorise its lines, then total each category.
Functionaltogether they perform one well-defined task and nothing else.A Tax module that computes VAT for a basket, and only that.

At architecture scale, cohesion is the question of whether a component or service has one reason to exist. A service called CustomerService that handles login, loyalty points and marketing preferences is logically cohesive at best: three teams will change it for three unrelated reasons. A good heuristic is to describe a module's purpose in one sentence without using and; Section 3.5 develops this as the single responsibility principle, defined in terms of the people who request changes.

Afferent and efferent coupling

The levels above describe the kind of a dependency. Architects also need to count them. Afferent coupling (Ca) is the number of modules outside a component that depend on it: its incoming arrows. Efferent coupling (Ce) is the number of modules outside it that it depends on: its outgoing arrows. The terms come from anatomy, where afferent nerves carry signals in and efferent nerves carry them out. Robert C. Martin defined them for packages in 1994 by counting classes; tools today count classes, modules or packages, so always check what a tool is counting before comparing numbers.

The two numbers tell different stories. A component with high Ca, such as a shared money library used by forty modules, is responsible: any change to it can break many others, so it must change carefully and rarely. A component with high Ce, such as a checkout workflow that calls pricing, stock, payment, delivery and notifications, is exposed: it may need to change whenever any of those change. Neither is wrong in itself; the problem arises when a component is both heavily depended upon and itself depends on many volatile things. Section 3.4 turns Ca and Ce into Martin's instability metric and the rules built on it.

Worked example

Worked example: counting Ca and Ce in a food-delivery app

Seven modules (Checkout, Menu, Pricing, Payments, Notifications, Courier tracking and Admin) have these dependencies (A depends on B is written A to B): Checkout to Menu, Checkout to Pricing, Checkout to Payments, Checkout to Notifications; Courier tracking to Notifications; Pricing to Menu; Admin to Menu, Admin to Pricing.

MenuCa = 3 (Checkout, Pricing, Admin); Ce = 0
PricingCa = 2 (Checkout, Admin); Ce = 1 (Menu)
NotificationsCa = 2 (Checkout, Courier tracking); Ce = 0
CheckoutCa = 0; Ce = 4 (Menu, Pricing, Payments, Notifications)
ReadingMenu is the most depended upon, so its interface needs the most care and the slowest change. Checkout depends on the most, so it should be the easiest to change and the one that absorbs change from others.

Counting takes minutes with a dependency tool and turns a vague feeling ("Menu is central") into a number you can track over time.

Coupling in distributed systems

When modules become services that talk over a network, the classic levels still apply, but three further kinds of coupling become prominent. None of them is visible in a function signature, which is why teams that split a system into services are often surprised by how coupled the services still are.

Temporal coupling means that one part works only if another is available and responsive at the same moment. A synchronous HTTP call from Checkout to Payments couples their availability: if Payments is down, so is checkout, and the combined availability is the product of the two (Section 2.5). Asynchronous messages through a queue remove this, at the cost of eventual rather than immediate results. (In code-level design the same term sometimes means that calls must happen in a fixed order, such as open() before read(); context usually makes the meaning clear.)

Deployment coupling means that two parts must be released together, or in a fixed order, because a change in one breaks the other. If every release of Orders needs a simultaneous release of Billing, the two are one deployable unit in practice, whatever the diagram says. Backward-compatible changes, versioned APIs and the expand-and-contract pattern are the usual cures.

Semantic coupling means that two parts must agree on meaning that no interface expresses: that an order with status = 4 is shipped, that prices are in pence and include VAT, that an 'active customer' has ordered in the last 90 days. Such agreements break silently when one side changes its interpretation. The cure is to make the meaning explicit in the contract (named states, units in field names, published schemas) or to keep the rule in one place that others ask.

Tactics to reduce coupling

Narrow interfaces. Expose the fewest operations and fields that clients need. Every extra field in an API response is something a client may come to depend on (Section 3.3 on Hyrum's law). Intermediaries. Put something between the two parties so neither knows the other's details: an adapter or anti-corruption layer that translates a supplier's model, an API gateway, or a message broker that decouples sender from receiver in time and in identity. Events instead of commands. A command (Billing, create invoice) makes the sender know the receiver and its job; an event (an order was placed) lets the sender state a fact and lets any number of receivers react, so new consumers can be added without changing the producer. Data ownership. Each piece of data has one owner that writes it; other modules ask the owner through its interface or keep their own copy fed by events, never reading the owner's tables (Section 7.1).

Every tactic has a price. Intermediaries add a hop and something else to run. Events give up immediate consistency and make the flow of a business process harder to see in one place. Copies of data can be stale. Data ownership means building and versioning APIs that a shared table gave you for free. The trade-off is worth making where change is frequent or teams are separate, and often not worth making between two classes that one person maintains.

Worked example: refactoring a tightly coupled order and billing pair

An online shop's Orders module calls Billing directly whenever an order is placed. Billing then reads the orders table to find the details, interprets the status column's numeric codes and recalculates VAT from the line prices. The code below is a simplified version; it shows three of the problems at once.

# orders.py
def place_order(basket, customer_id):
    order_id = db.insert("orders", customer_id=customer_id, status=1,
                         lines=basket.lines)
    billing.process(order_id, mode="invoice")   # Orders waits for Billing
    return order_id

# billing.py
def process(order_id, mode):
    row = db.query_one("SELECT * FROM orders WHERE id = %s", order_id)
    if row["status"] != 1:              # 1 means 'placed', by convention
        return
    net = sum(l["price"] * l["qty"] for l in row["lines"])
    vat = round(net * 0.2)               # VAT rule duplicated here
    if mode == "invoice":
        db.insert("invoices", order_id=order_id, net=net, vat=vat)
    elif mode == "credit_note":
        db.insert("credit_notes", order_id=order_id, net=-net, vat=-vat)
Before: control coupling (the mode flag), common coupling (both modules use the orders table) and semantic coupling (status codes and VAT rules known to both).

Any change to the orders table's layout, to the status codes or to the VAT rules now has to be made in both modules at once. Orders cannot accept an order if Billing is slow or failing. And Billing's two behaviours are selected by a flag that Orders must know about. The refactoring proceeds in four steps: Orders stops calling Billing and instead publishes an OrderPlaced event carrying exactly the facts Billing needs, including the totals Orders has already calculated; Billing subscribes and records invoices in its own store; the two behaviours become two operations; and the orders table becomes private to Orders.

# orders.py
def place_order(basket, customer_id):
    order = Order.create(customer_id, basket)      # computes net and VAT once
    repository.save(order)
    events.publish("OrderPlaced", {
        "schema_version": 1, "order_id": order.id,
        "customer_id": customer_id,
        "net_pence": order.net_pence, "vat_pence": order.vat_pence})
    return order.id

# billing.py
@events.subscribe("OrderPlaced")
def on_order_placed(event):
    create_invoice(event["order_id"], event["net_pence"], event["vat_pence"])

def create_invoice(order_id, net_pence, vat_pence): ...
def create_credit_note(order_id, net_pence, vat_pence): ...
After: Orders states a fact through a versioned event; Billing owns its own data and no longer knows Orders' tables, codes or tax rules.

The order and billing pair before and after

Before: Billing reads the Orders database and is called with a mode flag. After: Orders publishes an OrderPlaced event and Billing keeps its own dataBeforeOrdersBillingmode flagorders table(owned by Orders)reads rows,status codes,VAT columnCommon coupling via the table,control coupling via the flag,temporal coupling: Orders waitsfor Billing on every orderAfterOrdersBillingMessagebrokerOrderPlaced event(ids, totals, version)orders DBOrders onlyinvoices DBBilling onlyData coupling through a publishedcontract; each service owns its data;Orders no longer waits for Billing
The refactoring replaces a direct call with a flag and a shared table by a published event and separate data stores.

Worked example

What changed, measured

Comparing the two versions against the coupling vocabulary in this section.

Kind of couplingBefore: content-like and common (shared table), control (mode flag), semantic (status 1, VAT rule). After: data coupling through a five-field versioned event.
Efferent coupling of OrdersBefore: Ce includes Billing. After: Orders depends only on the event contract and the broker; it does not know Billing exists.
Temporal couplingBefore: an order fails if Billing is down. After: the event waits in the broker until Billing recovers.
Deployment couplingBefore: a change to the orders table needs a joint release. After: Orders can change its table freely; only changes to the event need care, and versioning handles them.
New costsA broker to run; invoices appear seconds later rather than instantly; the event schema is now a public contract to version; the process is harder to trace without good observability.

The event carries the totals that Orders computed, so VAT is calculated in one place. Billing no longer needs to know the tax rules or the order line layout.

The refactoring removed a choice, not all coupling: Billing still depends on the meaning of OrderPlaced. That dependency is now explicit, narrow and versioned, which is what loose coupling means in practice.

How professionals use these ideas

Experienced designers rarely quote the names of the levels in reviews, but they apply them constantly: they question every flag parameter, look for shared tables and global state, ask what each module's one purpose is, and watch which modules change together in version control. At the boundaries between teams and services they are strict, because coupling there is expensive to unwind; inside one small module owned by one person they are relaxed, because there it is cheap.

They also make coupling visible. A dependency graph generated from the code, a count of Ca and Ce per component and a list of database tables accessed by more than one service are among the most informative views of an architecture, and all can be produced automatically and tracked in the build. The next section turns to the principle that keeps coupling weak in the first place: hiding decisions behind interfaces.

Check your understanding

Why is a function with a mode parameter that selects completely different behaviours an example of control coupling?

Because the caller has to know how the callee is organised inside and choose its internal branch; changing the callee's logic can force changes in callers.

A module has Ca = 12 and Ce = 1. What does that suggest?

Many modules depend on it and it depends on little, so changes to it are risky and should be rare and careful; its interface deserves particular stability.

Two services communicate only through events but must always be released together. Which kind of coupling remains?

Deployment coupling, usually caused by non-backward-compatible changes to the shared event or by hidden semantic agreements.

Give one cost of replacing a direct call with an event.

Results become eventually rather than immediately consistent; you also gain a broker to operate, an event schema to version and a flow that is harder to trace.

Next · 3.3Information hidingWhy modules should hide decisions, how information hiding differs from encapsulation, interfaces as contracts and design by contract, leaky abstractions, deep modules, Hyrum's law and how to keep APIs between components stable.Continue →

Sources and further reading

Have a question about this section? ArchitectBot, the course’s AI study assistant, answers from the course first.

Ask: “Our two services share one database table. How do I separate them step by step without a big-bang migration?”