Two services at a fintech company deploy independently — or so the
team believes. Service A handles account balances. Service B handles
transactions. A database table, account_snapshots, is read
by both. When Service A’s team changes the schema of that table to add a
pending_balance column and restructures how
current_balance is computed, Service B’s queries begin
returning wrong results. Neither team changed Service B. Neither team
broke a test. Production transactions reported incorrect available
balances for six hours before anyone traced the cause.
The services were deployed independently. They were not developed independently. They shared state. The coupling was real; it was just invisible.
You have already seen tradeoffs (T12) examined at infrastructure and system design levels in Books 3 and 4, where every architectural decision involves competing forces. Here the same lens applies at code level, and the central code-level tradeoff is coupling versus cohesion. The shape is the same: no decision is free; every choice to gain one property costs another. Understanding which property to optimise at each boundary is the engineering judgment this chapter develops.
Coupling measures how much one component depends on another. Two components are tightly coupled when a change in one requires a change in the other. They are loosely coupled when each can change independently. Low coupling is the goal at component boundaries.
Cohesion measures how well the elements within a component belong together. A component with high cohesion performs one clearly-defined function. Its elements are related by purpose. Low cohesion — a class that manages users, sends emails, and writes to the audit log — signals that the component covers too many responsibilities and will be modified for too many different reasons.
The target state is always: high cohesion within components, low coupling between components. These goals are complementary but not free. Achieving low coupling requires deciding what belongs together (cohesion) and enforcing boundaries around those clusters.
Coupling types, ordered from weakest to strongest:
Data coupling — components communicate by passing
only the data they need. A function receives amount: Money
and returns Receipt. The caller and callee are minimally
entangled.
Stamp coupling — components pass whole objects when
they only need a field. processPayment(order: Order) when
the function only uses order.amount. The callee now depends
on the entire Order type.
Control coupling — one component tells another what
to do via a flag. process(data, mode="strict"). The caller
has knowledge of the callee’s internal branching logic.
Common coupling — multiple components share global state. A global configuration dictionary, a module-level registry, a class variable. All components reading that state are coupled to each other through it.
Content coupling — one component directly accesses the internals of another. Service B reads Service A’s database table. One class modifies another’s private fields. This is the strongest and most damaging form.
The fintech example in the introduction is content coupling: Service B accessed Service A’s database directly, giving Service B knowledge of Service A’s internal storage format.
Two code smells signal coupling problems in practice. Feature envy
appears when a method in class A spends most of its logic reading and
transforming fields from class B. The method belongs in B; it was placed
in A by accident or convenience. Shotgun surgery appears when a single
change — a new field on Order, a new payment status —
requires modifications in ten different places. The change propagates
because the concept is spread across many components instead of being
owned by one.
A function that exhibits control coupling:
function processOrder(order, mode):
if mode == "strict":
validate_aggressively(order)
charge_immediately(order)
elif mode == "deferred":
validate_minimally(order)
queue_charge(order)
elif mode == "test":
skip_validation(order)
log_only(order)
The caller must know which modes exist and what they mean. When a new
mode is needed, the caller learns internal logic of
processOrder. This is the failure signature of control
coupling: the interface exports branching knowledge.
The correction replaces the flag with a strategy (covered in Chapter 8):
interface OrderProcessor:
method process(order: Order) -> Result
class StrictOrderProcessor implements OrderProcessor:
method process(order):
validate_aggressively(order)
charge_immediately(order)
class DeferredOrderProcessor implements OrderProcessor:
method process(order):
validate_minimally(order)
queue_charge(order)
Callers depend on the interface. The choice of processor is a deployment decision, not a runtime flag. No caller needs to know the branching logic.
Common coupling example — the global config anti-pattern:
# Module level
PAYMENT_CONFIG = {
"timeout": 30,
"retry_count": 3,
"processor": "stripe"
}
# In payment_service.py
function charge(amount):
timeout = PAYMENT_CONFIG["timeout"]
...
# In notification_service.py
function notify_payment(result):
processor = PAYMENT_CONFIG["processor"]
...
Payment and notification services are now coupled through
PAYMENT_CONFIG. A test that modifies
PAYMENT_CONFIG["timeout"] affects both services.
Parallelising tests that touch this config produces race conditions.
AT8 — Coupling/Cohesion: Every boundary decision trades off these two properties. Grouping more functionality into fewer components raises cohesion within those components but risks making components too large and too responsible. Splitting functionality across many small components reduces responsibility per component but increases the number of boundaries, each of which is a coupling point. The right granularity depends on how independently the pieces need to change.
AT5 — Centralisation/Distribution: Shared state (common coupling) is centralised. It is easy to read and write from many places. The cost is that every reader is coupled to the state structure and to every writer. Distributing state — each component owns its own data — eliminates common coupling at the cost of coordination overhead when state must be reconciled.
FM4 — Data Consistency Failure: Content coupling through shared database tables is the most common path to data consistency failures in service-oriented architectures. Two services writing the same rows with different assumptions about invariants corrupt data silently. Neither service detects the inconsistency because each sees only its own writes.
FM9 — Silent Data Corruption: Control coupling with
flags that carry implicit meaning creates silent corruption risk. When a
flag value is misinterpreted — mode="test" applied to a
production workflow — behaviour changes without an error. The corruption
is undetected until downstream effects surface.
Unix pipe model: Each command is highly cohesive
(does one thing) and minimally coupled (communicates only through
stdin/stdout byte streams — data coupling).
cat file | grep pattern | sort | uniq chains four tools
with no shared state, no control flags passed between them, no knowledge
of each other’s internals. The result is a composable system that has
survived fifty years of change.
React component model: Components receive props (data coupling) and emit events (data coupling). A well-designed React component tree has components that do not directly read each other’s state. Violations — components that reach into sibling or cousin component state — produce the shotgun surgery smell when a UI change cascades across the component tree.
Microservices and database-per-service: The database-per-service pattern directly addresses content coupling. If each service owns its database schema, no other service can read or write it directly. Services communicate through published interfaces (APIs, events). The constraint is deliberate: it prevents content coupling from forming at the infrastructure level, forcing API design.
Shared kernel in enterprise systems: Many legacy enterprise systems have a shared database schema accessed by dozens of applications — an extreme case of common coupling. Every application has knowledge of every other’s storage format. Schema migrations require coordinating all applications simultaneously. This is the architectural form of the fintech failure described in the introduction.
Concept: Coupling and Cohesion
Thread: T12 (Tradeoffs) ← Book 4, Ch 1 (System design tradeoffs) → Ch 13 (Domain-Driven Design boundaries)
Core Idea: Coupling measures inter-component dependency; cohesion measures intra-component unity; the target state is high cohesion within components and low coupling between them, and the coupling type (data through content) determines the cost of a wrong boundary.
Tradeoff: AT8 — Coupling/Cohesion: every boundary decision trades component responsibility against the number of coupling points; finer granularity means lower coupling but more interfaces to maintain.
Failure Mode: FM4 — Data Consistency Failure: content coupling through shared state (especially shared database tables across services) allows one component to corrupt another’s invariants without detection.
Signal: When a single logical change requires modifications in five or more separate files or classes, shotgun surgery is present and common or content coupling is the cause.
Maps to: Book 0, Framework 8 (Patterns); P8 (Coupling/Cohesion)
1. What is the difference between coupling and cohesion? What is the target state for each at component boundaries?
2. Name the five coupling types described in this chapter, from weakest to strongest. Give a one-sentence description of each.
3. What are the two code smells that signal coupling problems in practice? Describe what each looks like.
Classify the coupling type in each of the following scenarios:
(a) sendEmail(recipient, subject, body) — only these fields
are passed; (b) generateReport(user: User) where the
function only uses user.email and user.name;
(c) a class reads from a global FEATURE_FLAGS dictionary;
(d) process(data, skipValidation=True). For each, identify
the coupling type and describe the minimum refactoring to weaken
it.
A UserService class contains:
createUser, sendWelcomeEmail,
updatePassword, writeUserCreationToAuditLog,
calculateUserRiskScore. Which methods belong in
UserService? Which belong elsewhere? Organise them by
cohesion and describe what new components you would create.
Identify the code smell in this design:
OrderService.calculateTotal(order) reads
order.customer.taxRegion,
order.customer.discountTier, and
order.customer.memberSince. Name the smell and describe the
fix.
A complete answer will: (1) name the coupling type for each approach: (a) direct calls create connascence of execution and connascence of identity — the notification service must know the API shape of all five source services and call them in a specific order; (b) event subscription creates connascence of meaning only (shared event schema) — the notification service depends on the event structure, not on the source services’ internal APIs, (2) name AT8 (Coupling/Cohesion) as the governing tradeoff: direct calls are tighter coupling (easier to trace, lower latency) but higher change impact (if any source service changes its API, notification service must change too); event subscription is looser coupling (each service evolves independently) but harder to trace and adds event broker complexity, (3) name FM2 (cascading failure) for approach (a): if any of the five source services is down, the notification service cannot issue notifications for that event type — a failure in the payments service silences payment notifications; for approach (b), name FM4 (stale data): if the event broker is slow, notifications are delayed but the notification service remains operational, and (4) recommend approach (b) for this specific design and justify: five source services with independent release cycles create too many synchronous coupling points for approach (a) to maintain safely — the number of breaking changes scales with the number of source services, making event subscription the correct choice at this fan-in count.
Customer object used by billing,
support, marketing, and logistics. Each team needs slightly different
fields and methods. Propose a design that gives each team what it needs
without creating a single 500-line Customer class that
changes for every team’s reasons. Name the principle you are applying,
the tradeoffs, and where coupling is unavoidable.A complete answer will: (1) apply the Interface Segregation
Principle (or DDD bounded context decomposition): define separate
BillingCustomer, SupportCustomer,
MarketingCustomer, and LogisticsCustomer
types, each containing only the fields and methods relevant to that
team’s domain — each type is owned by the team that uses it and changes
only when that team’s requirements change, (2) name AT8
(Coupling/Cohesion): segregated types reduce the blast radius of changes
(billing changes do not affect support) but increase the total amount of
code and require a mapping layer when data must cross boundaries — state
the conditions under which the added mapping complexity is worth the
reduced coupling, (3) identify where coupling is unavoidable: all four
types share the same customer identity (customer ID, name, contact
information) — this shared identity creates connascence of meaning that
cannot be eliminated without duplicating the identity concept; propose a
CustomerIdentity type owned by a shared domain layer that
all four bounded contexts import, and (4) name FM8 (silent semantic
drift) as the failure mode when the four types diverge: if billing’s
Customer.address and logistics’
Customer.address are separately maintained and drift apart
(different validation rules, different field names), data
inconsistencies accumulate silently — state the synchronisation
mechanism (e.g., shared identity events or a read-model projection) that
keeps identity data consistent across bounded contexts.