The Computing Series

Coupling and Cohesion

Introduction

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.

Thread Activation

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.

The Concept

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.

How It Works

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.

Tradeoffs

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.

Where It Fails

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.

Real Systems

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)

Exercises

Level 1 — Understand

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.

Level 2 — Apply

  1. 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.

  2. 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.

  3. 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.

Level 3 — Design

  1. A team is building a notification system. Notifications are triggered by events from five other services: orders, payments, shipments, returns, and accounts. Design the coupling structure between the notification service and the five source services. Compare two approaches: (a) notification service imports and calls each service directly, (b) notification service subscribes to events published by each service. Name the coupling type in each approach, the tradeoffs using AT codes, and the failure modes using FM codes.

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.

  1. A company has a 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.

Read in the book →