Coupling and Cohesion: Designing Components That Change Cleanly
📑 On this page
- A concrete example: an invoice module
- Cohesion is about one meaningful purpose
- Functional cohesion
- Information cohesion
- Coupling is unavoidable
- Data coupling
- Control coupling
- Temporal coupling
- Schema and database coupling
- Deployment coupling
- Coupling through shared state
- Coupling through knowledge
- Dependency direction
- Events reduce some coupling and add others
- Test coupling
- Measure change patterns
- Refactoring toward better balance
- Knowledge check
- The one idea to remember
Two questions reveal much about a software component:
- Do its responsibilities belong together?
- How much does it need to know about other components?
Cohesion describes how strongly a component's responsibilities relate to one purpose. Coupling describes how strongly it depends on other components and their details.
High cohesion and controlled coupling usually make change safer.
A concrete example: an invoice module
A cohesive invoice module might own:
- Invoice creation
- Line-item rules
- Tax and total calculation
- Payment status transition
An incohesive module might also:
- Render website navigation
- Reset user passwords
- Resize product images
- Configure network retries
The invoice module becomes tightly coupled if it directly reaches into shipping tables, email internals, and UI state.
Its own purpose is clear only when related rules stay together and dependencies pass through narrow contracts.
Cohesion is about one meaningful purpose
High cohesion does not mean one function per file.
A component can contain several operations when they serve one concept.
For example, a shopping cart module can add items, remove items, calculate totals, and validate quantity. These operations share state and rules.
A generic helpers module containing date formatting, authorization, invoice math, and network retry logic has low cohesion because no single reason explains why those functions belong together.
Names such as utils, common, and misc can become warning signs.
Functional cohesion
Functional cohesion means parts cooperate to perform one defined operation.
validate order
calculate total
reserve inventory
create orderThese steps might form a cohesive checkout workflow if the component coordinates one business action.
Be careful: coordination and ownership differ.
Checkout can orchestrate payment and inventory through interfaces without owning all internal rules of both domains.
One component can be cohesive as a workflow while respecting boundaries of its collaborators.
Information cohesion
A module can be cohesive because operations work on one data abstraction.
A CustomerRepository may:
- Find customer
- Save customer
- Check email uniqueness
The operations belong together around customer persistence.
However, adding unrelated report generation because it also queries customer tables weakens the abstraction.
Database location alone is not always the right reason to group business responsibilities.
Coupling is unavoidable
Software components must cooperate, so zero coupling is neither possible nor desirable.
The goal is appropriate coupling:
- Explicit
- Narrow
- Stable
- Directed
- Matched to actual dependency
A checkout service must depend on some way to request payment. The design question is whether it depends on a clear payment contract or on one provider's internal classes, database schema, retry behavior, and deployment schedule.
Coupling quality matters more than raw dependency count.
Data coupling
Components are data-coupled when they exchange values through an interface.
Narrow data coupling can be healthy:
charge({
orderId,
amount,
currency,
idempotencyKey,
});Passing an enormous shared object exposes unrelated fields and encourages callers to depend on internals.
Use purpose-specific contracts containing what the operation needs.
Do not omit meaningful context merely to minimize fields; the contract must remain correct.
Control coupling
Control coupling occurs when one component tells another how to run internally through flags or command modes:
processOrder(order, mode = 7, skipTax = true, sendEmail = false)The caller must know internal control flow.
Prefer intention-revealing operations:
quoteOrder
placeOrder
cancelOrderSome options are legitimate, but a growing collection of Boolean switches often indicates several responsibilities hidden behind one interface.
Temporal coupling
Components are temporally coupled when operations must happen in a particular time or order.
Examples:
- Call
initializebeforesend. - Create a session before every query.
- Deploy schema version B before application version B.
Some ordering is fundamental. Problems arise when it is undocumented or fragile.
Reduce temporal coupling through:
- Constructors or factories that return valid objects
- Idempotent initialization
- Backward-compatible migrations
- Explicit state machines
- Durable queues
Make unavoidable order visible and test it.
Schema and database coupling
When several applications directly read and write the same tables, they depend on:
- Column names
- Constraints
- Migration timing
- Data meaning
- Each other's write behavior
This can be efficient inside one application or tightly coordinated team.
Across independent services, a shared database weakens ownership and deployment autonomy.
An API or event contract can create a clearer boundary, but adds network and consistency complexity.
Choose deliberately rather than assuming shared storage is free.
Deployment coupling
Two components are deployment-coupled when changing one requires releasing another simultaneously.
Causes include:
- Breaking API changes
- Shared libraries released in lockstep
- One schema understood by only one version
- Coordinated configuration
Backward-compatible contracts and additive migrations allow independent rollout.
Independence has cost. If one team owns a small monolith, coordinated deployment may be simpler and entirely acceptable.
Do not pursue independent deployment where no organizational or reliability need exists.
Coupling through shared state
Global variables, shared caches, common files, and mutable singletons create hidden dependencies.
One component changes state; another behaves differently without an explicit call.
This complicates:
- Testing
- Concurrency
- Debugging
- Ownership
- Recovery
Encapsulate state behind operations and make lifecycle clear.
Shared state is sometimes necessary, but its consistency, synchronization, and source of truth should be explicit.
Coupling through knowledge
A component may depend on another's internal assumptions even without importing its code.
Examples:
- Assuming IDs always increase
- Parsing an undocumented error message
- Depending on event order not guaranteed by the broker
- Knowing another service's retry schedule
This is semantic coupling.
Contracts should document behavior, errors, timing, and compatibility, not only field shapes.
Many production failures come from assumptions outside the formal interface.
Dependency direction
Stable, important policy should not depend directly on volatile infrastructure.
Instead of domain logic importing a vendor client:
Domain -> PaymentProviderSDKdefine a domain-facing port:
Domain -> PaymentGateway interface <- Provider adapterThe code-level dependencies point toward the stable abstraction.
This does not make the provider replaceable with zero effort. Different providers have different semantics, but the core no longer spreads one SDK throughout the codebase.
Events reduce some coupling and add others
Publishing an OrderPlaced event can let email, analytics, and fulfillment react without checkout calling each directly.
This reduces direct knowledge of consumers.
It introduces coupling to:
- Event schema
- Meaning
- Delivery guarantees
- Ordering
- Versioning
- Eventual consistency
Loose coupling is not no coupling.
Events are useful when independent reactions and timing justify the operational complexity.
Test coupling
Tests can become tightly coupled to implementation.
A brittle test may assert:
- Private method calls
- Exact internal query sequence
- Temporary object structure
- Incidental log wording
Such tests fail during harmless refactoring.
Prefer testing observable contracts and important interactions.
Some internal tests are valuable for complex algorithms, but the suite should allow implementation to improve without rewriting every assertion.
Measure change patterns
Code history can reveal design:
- Which files change together?
- Which module causes widespread edits?
- Where do defects cluster?
- Which team waits on another?
- Which service requires coordinated deployment?
High co-change across supposedly independent modules suggests hidden coupling.
A module changed for many unrelated reasons may lack cohesion.
Metrics guide investigation; they do not replace understanding of domain and team context.
Refactoring toward better balance
To improve cohesion and coupling:
- Name the responsibility.
- Move related rules together.
- Identify external dependencies.
- Define narrow behavior-based interfaces.
- Remove hidden shared state.
- Make timing and errors explicit.
- Add contract-focused tests.
- Migrate incrementally.
Avoid a sweeping rewrite when boundaries can be improved one dependency at a time.
The goal is easier expected change, not abstract purity.
Knowledge check
- How does cohesion differ from coupling?
- Why is a generic helpers module often low in cohesion?
- What is temporal coupling?
- How can an event reduce direct coupling while adding contract coupling?
- What can version-control co-change reveal?
The one idea to remember
Keep responsibilities that change together in cohesive components, and connect components through explicit, narrow contracts. Coupling cannot disappear, but it can be made visible, stable, and proportionate to the collaboration required.