Grokking Modern Software Architecture Styles
Vote

0% completed

​

Coupling, Cohesion, and Dependencies

Know the Vocabulary

Part 1: What Coupling Measures

Part 2: Four Kinds of Code Coupling and Temporal Coupling

  1. Data Passed Through a Narrow Interface
  1. Shared Data Structures
  1. Shared Database Tables
  1. Access to Another Part's Internals
  1. Temporal Coupling

The Five Kinds Compared

Part 3: Cohesion

Part 4: Low Coupling, High Cohesion, and the Cost of Overdoing Either

Part 5: Dependency Direction, Cycles, and Stability

Dependency Cycles

Stable and Volatile Parts

Part 6: Dependency Inversion and Who Owns the Interface

FAQs

Key Takeaways

Know the Vocabulary

This lesson uses the terms below repeatedly, so read them once now and return here when a later section needs one.

  • Coupling: The degree to which a change in one part of a system forces a change in another part.
  • Cohesion: The degree to which the code inside one part belongs to the same job, so that it changes for the same reasons.
  • Module: A group of related code, such as everything about orders, with a defined interface that other code calls.
  • Interface: A declaration of the operations a component provides to other components, with their inputs, outputs and errors, and no implementation. For a class, it lists the methods the class must provide; in Python, an abstract base class plays this role.
  • Temporal coupling: A runtime dependency in which one part works only while another part is running and responding at the same moment.
  • Dependency: A relationship in which one part uses another part's code, data or running instance and cannot work correctly without it.
  • Dependency direction: Which part knows about which, drawn as an arrow from the part that imports or calls to the part it uses.
  • Dependency cycle: A chain of dependencies that leads back to its starting part, such as Orders and Payments each importing the other.
  • Stable part: A part that changes rarely, such as the rules for what an order is. A volatile part instead changes often, such as a client for an external API.
  • Higher-level part: A part that holds business decisions, such as Orders. A lower-level part handles a technical detail, such as calling the payment provider's API.
  • Dependency inversion principle: A rule where the higher-level module owns the abstraction and the lower-level module implements it. Here, Orders defines the PaymentGateway interface and the payment code implements it.
  • Dependency injection: Giving a class the objects it depends on, usually through its constructor, instead of letting the class create them.
  • Distributed monolith: A system split into services that must still be changed and deployed together, because they share data or depend on chains of synchronous calls.

A developer on the food-delivery team renames one column in the PostgreSQL table that Orders owns, from total to total_cents. The change is small, and every Orders test passes. After the deploy, Dispatch can no longer read order totals, the Payments refund job fails, and a nightly revenue report shows zero. Three parts failed because of a change in a fourth part that their developers did not know they used.

This lesson explains why that happens and how to design a system so that it happens less often. It covers coupling and cohesion, and then dependencies: which part uses which, in what direction, and who owns the interface between them.

These ideas explain most of the differences between the styles in the Architecture Styles chapter. When a later lesson says that a set of microservices has become a distributed monolith, it uses the terms defined here.

Part 1: What Coupling Measures

Coupling is the degree to which a change in one part of a system forces a change in another part. It describes a pair of parts, and it is not always symmetric. Dispatch can be tightly coupled to Orders while Orders does not depend on Dispatch at all.

Coupling is never zero between two parts that work together. If Orders asks Dispatch to find a courier, Orders must at least know the name of the operation and the data it takes. The useful question is how much more than that each part relies on.

A practical way to measure coupling is to list what one part assumes about another. Each assumption forces an edit if the assumed thing changes. Dispatch might assume a function name, the fields of an object, the columns of a table, or the fact that Orders is running right now. The more assumptions a part makes, and the more often the assumed things change, the tighter the coupling.

In the opening example, Dispatch, Payments and the report each assumed the name of one column. None of the three declared that assumption anywhere, so nobody could see it before the deploy. This shows a second problem with tight coupling, beyond the extra edits: it is often invisible until something fails.

Part 2: Four Kinds of Code Coupling and Temporal Coupling

The five kinds below appear in almost every codebase. The first four are kinds of code coupling, ordered from the loosest, which lets each part change freely, to the tightest. The fifth, temporal coupling, is a runtime kind that sits outside that order. All five examples use the same request: Orders needs Dispatch to assign a courier to a new order.

The diagram below places the four kinds of code coupling on one scale, puts temporal coupling in its own band, and shows what Orders and Dispatch share in each case.

The Coupling Spectrum
The Coupling Spectrum

1. Data Passed Through a Narrow Interface

Orders calls one function that Dispatch publishes, request_courier(order_id, pickup_address, ready_at), and passes three plain values. Dispatch can change its tables, its assignment algorithm and its internal classes without telling Orders. Only a change to that one function's name, parameters or meaning forces Orders to change.

An interface is narrow when it exposes few operations and each one takes only the data the receiver needs. This is the target for most boundaries in a system.

2. Shared Data Structures

Orders passes its whole Order object, which has 25 fields, and Dispatch reads three of them. Dispatch now depends on the layout of a class that Orders changes for its own reasons. If Orders renames restaurant_address to pickup_location, Dispatch fails, even though Orders never meant to change anything Dispatch uses. A shared library of model classes that every part imports has the same effect at a larger scale.

3. Shared Database Tables

Dispatch skips Orders entirely and runs SELECT ... FROM orders WHERE status = 'ready' against the table that Orders writes. Every column name, column type and status value is now part of the contract between the two parts, but no code declares it. A type checker cannot find this dependency, because it lives inside a SQL string.

This is the coupling from the opening example. The FAQs explain why sharing one PostgreSQL server is not the problem.

4. Access to Another Part's Internals

Dispatch imports a private helper from Orders, such as orders.pricing._prep_time_minutes(), or writes directly into an in-memory cache that Orders maintains. In Python, a leading underscore marks a name as internal, but nothing stops another module from importing it. Dispatch now depends on how Orders is built, not on what Orders offers.

Any internal refactoring of Orders can now make Dispatch fail. This is the tightest form of code coupling. A codebase full of it is called a big ball of mud: a codebase with no clear structure, where any code can access any other code's internal details.

5. Temporal Coupling

Temporal coupling means one part works only while another part is running and responding at the same moment. It differs from the four kinds above: those describe what a code change forces, while this one describes what an outage or a slowdown forces. It can therefore combine with any of them, so Orders can call Dispatch through a narrow interface and still be temporally coupled to it.

Suppose Orders calls Dispatch synchronously and waits for a courier before it confirms the order to the customer. If Dispatch is down for ten minutes, no customer can place an order for ten minutes, even though nothing in Orders is broken. If Dispatch takes 4 seconds to answer during the dinner peak, every checkout takes at least 4 seconds.

Temporal coupling also lowers availability, because the availabilities of the parts multiply when they fail independently. If Orders and Dispatch are each available 99.9 percent of the time, a request that needs both is available about 99.8 percent of the time. That is roughly 86 minutes of downtime in a 30-day month instead of 43 (illustrative figures; the Quality Attributes lesson in this chapter explains the arithmetic).

The looser alternative is for Orders to save the order and put an OrderPlaced message on a message queue. A message queue is a store that holds messages until the receiving part reads them. Dispatch reads the message when it is running, so a Dispatch outage delays courier assignment but does not stop checkout.

The Five Kinds Compared

The table below compares the five kinds side by side. The first four rows run from looser to tighter code coupling, and the last row is runtime coupling.

KindFood-delivery exampleWhat forces an edit or an outageA looser alternative
Narrow interfaceOrders calls request_courier with 3 valuesA change to that function's parameters or meaningAlready the loosest form
Shared data structureDispatch receives the full 25-field Order objectRenaming any field that Dispatch readsPass only the 3 values Dispatch needs
Shared tableDispatch queries the orders tableRenaming a column or a status valueAsk Orders through its interface
InternalsDispatch imports orders.pricing._prep_time_minutesAny refactoring inside OrdersOrders offers the value through its interface
Temporal (runtime)Orders waits on a synchronous call to DispatchDispatch being down or slowOrders queues a message and Dispatch reads it later

Part 3: Cohesion

Cohesion is the degree to which the code inside one part belongs to the same job. In a part with high cohesion, the pieces change for the same reason and are used together. In a part with low cohesion, they share little except a folder.

A high-cohesion example. The Dispatch module holds the algorithm that picks a courier, the code that tracks courier locations, and the estimated delivery time. All three answer one question: which courier delivers this order, and when. A change to the assignment rules, such as preferring cyclists for orders under 2 km, touches only Dispatch.

A low-cohesion example. Many codebases grow a module named common or utils. Suppose the food-delivery app has one that holds the refund calculation, the SMS templates, the menu photo resizer and the distance formula. These four pieces change for four unrelated reasons and belong to all three of the app's teams.

All three teams edit the same module, and every part of the app imports it. Low cohesion inside one part therefore creates high coupling between parts.

The diagram below contrasts the cohesive Dispatch module with the common module.

High and Low Cohesion
High and Low Cohesion

Low cohesion also appears in the opposite form, where one job is split across several parts. Suppose the delivery fee is computed partly in Orders, partly in Menu (a surcharge for large items) and partly in the Customer app (an estimate before checkout). A change to the fee formula then needs edits in three parts, released together. Moving the whole rule into a pricing module inside Orders, which the Customer app asks for its estimate, turns three edits into one.

A simple test of cohesion is to list why a part changed recently. If the last 20 changes to a module came from one business area, its cohesion is high. If they came from five unrelated areas, the module is really five modules in one folder.

Part 4: Low Coupling, High Cohesion, and the Cost of Overdoing Either

The goal is low coupling between parts and high cohesion within them. The two are connected. When code that changes together is kept together, most changes stay inside one part, and the parts need to know less about each other.

Overdoing loose coupling has a cost. Each layer of indirection, such as an interface, a queue or an event, adds code to write and a step to follow when debugging. Suppose Orders charges a card by publishing an event and waiting for a reply event. The customer needs an immediate yes or no at checkout, so checkout still waits for Payments. The events add a queue, a timeout and a retry path, and the temporal coupling remains.

A direct call through a narrow interface is the right amount of coupling there. An interface with one implementation, no expected second one and no test that replaces it is the same mistake on a smaller scale.

Splitting parts too finely, with cohesion as the reason, also has a cost. If every part does only one tiny thing, business jobs no longer fit inside single parts. Suppose Orders becomes three services: OrderValidation, OrderPricing and OrderStorage. Adding one field to an order now touches all three, so the job is split across parts again.

When the parts are separate services, this failure has a name. A distributed monolith is a system split into services that must still be changed and deployed together, because they share data or depend on chains of synchronous calls. It pays the network costs of services and keeps the release coupling of a monolith. The Microservices Architecture lesson shows how it forms and why each service must own its data, which removes shared data as a cause but leaves chains of synchronous calls.

Part 5: Dependency Direction, Cycles, and Stability

A dependency exists when one part uses another part's code, data or running instance and cannot work correctly without it. Every kind of coupling in Part 2 is a dependency. A part knows about another part when its code names it, by importing it, calling it or reading its data.

The direction of a dependency states which part knows about which. If Orders imports Dispatch and Dispatch contains no reference to Orders, then Orders depends on Dispatch and Dispatch does not depend on Orders. In diagrams, the arrow is drawn from the part that depends to the part it depends on.

Direction matters because a change can force edits only in the parts that depend on the changed part. A change in Dispatch can force an edit in Orders. A change in Orders cannot force an edit in Dispatch, because Dispatch names nothing in Orders.

Dependency Cycles

A dependency cycle is a chain of dependencies that leads back to its starting part. In the food-delivery app, Orders imports Payments to charge the customer's card at checkout. Later, a developer changes Payments so that it reads the order total from Orders and calls orders.mark_paid(order_id) after a successful charge. Payments now imports Orders, and the two modules form a cycle.

Cycles are harmful for four concrete reasons:

  1. Neither part can be understood or tested alone. A test of Payments must set up Orders, and a test of Orders must set up Payments.
  2. A change in either part can force a change in the other, so the two behave as one module with two names.
  3. In Python, two modules that import each other can fail at startup with an ImportError, because one is only partly loaded when the other needs it.
  4. Neither part can easily become a separate service. A split turns the imports into network calls in both directions, and the two services still have to change and deploy together.

There are three common ways to break the cycle. First, make the call one-way: Orders passes the amount to Payments, Payments returns the result of the charge, and Orders marks its own order as paid. Second, when the confirmation arrives later, Payments can publish a PaymentSucceeded event that Orders listens for, so Payments never names Orders. Third, Orders can own an interface that Payments implements, which is dependency inversion and the subject of Part 6.

The diagram below shows the cycle and the same two parts after the third fix. On the right, the only import between the two modules points into the orders module, so Orders names nothing inside Payments.

Breaking a Dependency Cycle
Breaking a Dependency Cycle

Stable and Volatile Parts

A stable part changes rarely, usually because it captures business rules that the company has used for a long time. A volatile part changes often. In the food-delivery app, the rules for what an order is (items, prices, a restaurant and a delivery address) are stable. The code that calls the external payment provider is volatile. The provider releases new API versions and adds required fields, and the company may switch to a cheaper provider.

Illustrative numbers show the difference. Suppose that over six months the payment provider client changed 12 times and the core order rules changed twice. If Orders imports the provider client directly, each of those 12 changes can force an edit or at least a retest of Orders.

This gives a guideline: a dependency should point toward a part that changes less often than its user. The reason is that each change to a part can force work in every part that depends on it. A part with many dependents is therefore expensive to change, so it should be one that rarely needs to.

The guideline is not an absolute rule. A small app that calls the payment provider from one place may keep the direct dependency, because an occasional edit costs less than an extra interface. Some styles make the guideline strict, as the end of Part 6 shows.

Orders still has to charge cards through the provider, so at first its dependency seems to point toward a volatile part. Dependency inversion removes that conflict.

Part 6: Dependency Inversion and Who Owns the Interface

A higher-level part holds business decisions, such as Orders deciding when an order counts as paid. A lower-level part handles a technical detail, such as formatting a request for the payment provider's HTTP API. Because the higher-level part makes the call, it usually imports the lower-level part.

The dependency inversion principle is a rule where the higher-level module owns the abstraction and the lower-level module implements it. Here, Orders defines a PaymentGateway interface in its own terms, and the payment code implements that interface. At runtime the call still goes from Orders to the payment code. The source code dependency, however, now goes the other way, which is why the principle is called an inversion.

Before the inversion, Orders creates the provider's client itself and works with the provider's own field names and response format. The code below shows that version of the checkout operation.

Python3
Python3
. . . .

Three things in that code tie Orders to the provider. They are the import of PaymentProviderClient, the parameter names such as source, and the response keys status and failure_code. A provider change to any of them forces an edit in Orders. A unit test of checkout also cannot run without the real provider or a patched copy of its client.

After the inversion, Orders declares what it needs in its own words and receives an object that provides it. The code below shows the interface that Orders owns and the new OrderService, which no longer imports anything from the payments module.

Python3
Python3
. . . .

The payments module now depends on Orders, not the other way around. The code below shows the payments class that implements PaymentGateway, the only place that knows the provider's field names.

Python3
Python3
. . . .

At startup, one function creates a ProviderPaymentGateway and passes it to OrderService through the constructor. This is dependency injection: OrderService receives the object it depends on instead of creating it. The result is three concrete improvements:

  • Switching to a cheaper provider means writing a second class that implements PaymentGateway. OrderService does not change.
  • A unit test passes a fake gateway that returns a failed PaymentResult, and the test runs in memory with no network.
  • In the illustrative six months above, the 12 provider changes would stay inside the payments module, unless one of them needed data that the PaymentGateway interface does not pass.

Who owns the interface matters as much as the interface itself. If the payments module had defined the interface as create_charge(amount, currency, source), Orders would still depend on the provider's concepts and words. The owner is the part that needs the service, and the interface uses that part's vocabulary.

Several lessons in the Architecture Styles chapter build on this principle. In Layered (N-Layer) Architecture, classic layering points every source code dependency down toward the database. Dependency inversion turns two of them around. The business layer defines its own interfaces, such as the repositories and the payment gateway, and the data access and integration layers implement them. Hexagonal (Ports and Adapters) calls an interface like PaymentGateway a port and a class like ProviderPaymentGateway an adapter. Clean Architecture/Onion Architecture turns the idea into a dependency rule, under which every source code import points inward, toward the business rules that change least often.

FAQs

Q: Is a shared database always tight coupling?

Sharing a database server is not the problem. In a monolith, Orders, Payments and Dispatch can all use one PostgreSQL database, as long as each module reads and writes only its own tables. The coupling appears when one module queries or updates tables that another module owns.

Q: Does dependency inversion mean every class needs an interface?

No. An interface is worth its cost where a higher-level part calls something volatile or external, such as a payment provider, a database or an email service. Between two stable modules in the same codebase, a plain call through a narrow interface is usually enough. An extra abstract base class there only adds files to read.

Q: How can I find coupling problems in an existing codebase?

Three checks find most of them. First, list the imports between modules; a tool such as import-linter for Python can fail the build when a forbidden import appears. Second, look in version control history for files that are changed together in many commits. Third, draw the import graph with a tool such as pydeps and look for cycles.

Q: How is cohesion related to the single responsibility principle?

The single responsibility principle states that a class should have one reason to change. That is cohesion applied to one class. Cohesion applies at every size, from a function to a whole service.

Key Takeaways

  • Coupling is the degree to which a change in one part forces a change in another. It is never zero between parts that work together, so aim for the loosest form that does the job.
  • From looser to tighter, code coupling comes as data through a narrow interface, shared data structures, shared tables, and access to internals. Temporal coupling is separate: one part fails or slows down whenever another does.
  • Cohesion is the degree to which the code inside one part belongs to the same job. A common module with four unrelated jobs and a fee rule split across three parts are both low cohesion.
  • Aim for low coupling between parts and high cohesion within them, without overdoing either. Needless indirection and very small parts both cost more than they return.
  • A distributed monolith is tight coupling between services, which then must be changed and deployed together.
  • A dependency cycle makes two parts behave as one. Break it with a one-way call, an event, or an interface owned by one side.
  • A change can force edits only in the parts that depend on it, so as a guideline, dependencies should point toward parts that change less often than their users. Dependency inversion keeps that direction when a higher-level part calls a volatile one: the higher-level part owns the interface, and the lower-level part implements it.

Reading Progress

0%


Vote for new content

On This Page

Know the Vocabulary

Part 1: What Coupling Measures

Part 2: Four Kinds of Code Coupling and Temporal Coupling

  1. Data Passed Through a Narrow Interface
  1. Shared Data Structures
  1. Shared Database Tables
  1. Access to Another Part's Internals
  1. Temporal Coupling

The Five Kinds Compared

Part 3: Cohesion

Part 4: Low Coupling, High Cohesion, and the Cost of Overdoing Either

Part 5: Dependency Direction, Cycles, and Stability

Dependency Cycles

Stable and Volatile Parts

Part 6: Dependency Inversion and Who Owns the Interface

FAQs

Key Takeaways