Types of Coupling Between Software Modules

from Reliable Software Through Composite Design
22. 10. 2024 /
#code#theory

I found the classification of software cohesion and coupling types presented in Glenford J. Myers' 1975 book ""Reliable software through composite design" quite fascinating.

Rather than just knowing it in theory, I was able to conceptualize the types of highly coupled code that I had become familiar with through development.

I gathered related content from Wikipedia and recent books I've read, and organized it with examples from my field of web client development.

This post focuses solely on coupling, not cohesion.

Coupling

"Coupling measures the degree of interdependence between modules. Good software has low coupling."

Sorted from highest to lowest coupling: Data Coupling > Stamp Coupling > Control Coupling > External Coupling > Common Coupling > Content Coupling

Data Coupling

When the dependency between modules is based solely on data being passed, these modules are said to be data coupled. In data coupling, components communicate with each other independently through data.

It is the weakest form of coupling. It occurs when components exchange the minimum necessary data to function independently.

This reminds me of a React component that receives sharply defined immutable data types as props.

// React Component type UserProps = { name: string; age: number; }; const UserProfile: React.FC<UserProps> = ({ name, age }) => { return ( <div> <h1>{name}</h1> <p>Age: {age}</p> </div> ); }; // Usage example <UserProfile name="Alice" age={30} />;

Stamp Coupling

Stamp coupling occurs when a module passes an entire data structure to another module. It includes tramp data. It is also known as Data-Structured Coupling.

This reminds me of a React component that receives an object of a specific type from an API response as props.

// Data structure type User = { id: number; name: string; email: string; age: number; }; // React Component const UserDetail: React.FC<{ user: User }> = ({ user }) => { return ( <div> <h1>{user.name}</h1> <p>Email: {user.email}</p> <p>Age: {user.age}</p> </div> ); }; // Usage example const user = { id: 1, name: 'Alice', email: 'alice@example.com', age: 30 }; <UserDetail user={user} />;

Implementing it this way might lead to unnecessary data being passed to the component at certain points, but it allows you to reveal the meaning of the prop and impose type constraints.

Data coupling does not use "the structure of data," so it has a lower level of coupling than stamp coupling, but that doesn't necessarily mean data coupling is always better.

Control Coupling

When modules communicate by passing control information, they are said to be control coupled.

You can think of a function where branching is controlled through an argument. The function receives a flag-like argument, which is control information, to decide what to do.

// Function using a control flag function processOrder(orderId: number, isExpress: boolean) { if (isExpress) { console.log(`Processing express order: ${orderId}`); } else { console.log(`Processing standard order: ${orderId}`); } } // Usage example processOrder(123, true);

Alternatively, you can think of a function that receives and calls a callback function. It receives the control information itself from the outside.

// Function receiving a callback function fetchData(url: string, callback: (data: any) => void) { // Simulating an API call setTimeout(() => { const data = { id: 1, name: 'Sample Data' }; callback(data); }, 1000); } // Usage example fetchData('https://api.example.com/data', (data) => { console.log('Received data:', data); });

Control coupling is often unavoidable from the perspective of feature separation and reuse when writing code. The degree to which control coupling is harmful depends on whether you need to know the implementation of the module.

For example, if the code looks like this, you must know exactly what control_flag is. This is control coupling to avoid because the caller must know the implementation.

// Function using an ambiguous control flag function executeTask(taskId: number, controlFlag: string) { if (controlFlag === 'start') { console.log(`Starting task: ${taskId}`); } else if (controlFlag === 'stop') { console.log(`Stopping task: ${taskId}`); } else { console.log(`Unknown action for task: ${taskId}`); } } // Usage example executeTask(10, 'start');

On the other hand, the strategy pattern or a pattern using callback functions allows the control flow to be injected from the usage place, so less information about the implementation of the module to be called is needed.

Such control coupling can be considered less harmful than the previous example.

// Strategy pattern example interface PaymentStrategy { pay(amount: number): void; } class CreditCardPayment implements PaymentStrategy { pay(amount: number) { console.log(`Paid ${amount} using credit card.`); } } class PayPalPayment implements PaymentStrategy { pay(amount: number) { console.log(`Paid ${amount} using PayPal.`); } } function processPayment(strategy: PaymentStrategy, amount: number) { strategy.pay(amount); } // Usage example const paymentMethod = new CreditCardPayment(); processPayment(paymentMethod, 100);

However, even this way of receiving control flow from the outside can eventually increase coupling between modules if the implementation of the module becomes complex. Such coupling should only be used where necessary, where it is clear to receive and call the control flow, and where it can be fully utilized.

External Coupling

In external coupling, modules depend on other modules or specific types of hardware outside the software being developed. You can think of a function that accesses the file system.

import fs from 'fs'; // Function accessing the external file system function readConfigFile(filePath: string) { try { const data = fs.readFileSync(filePath, 'utf8'); console.log('Config data:', data); } catch (err) { console.error('Error reading config file:', err); } } // Usage example readConfigFile('./config.json');

External coupling opens up the possibility of being affected by the environment outside the software being developed, potentially impacting not only the coupled module but also other modules. Additionally, external systems can make it difficult to test the module.

Not all external couplings necessarily affect other modules in the implementation.

The common coupling explained below has a more definitive impact on one module affecting another due to the existence of shared data.

Common Coupling

When modules share a global state, they are said to be commonly coupled. You can think of several functions accessing a single global variable.

// Global variable let globalCounter = 0; function incrementCounter() { globalCounter++; console.log('Counter incremented:', globalCounter); } function resetCounter() { globalCounter = 0; console.log('Counter reset'); } // Usage example incrementCounter(); incrementCounter(); resetCounter();

When a change is made to global data, you must examine the behavior of all modules accessing this data to understand why the state changed.

In the previous control coupling, the data from the caller is coupled to the implementation of one module, but in common coupling, the data can be coupled to the implementation of multiple modules, creating a considerably complex situation.

This makes module reuse difficult, reduces data access control capabilities, and is obviously not good for maintenance.

Content Coupling

Content coupling occurs when one module modifies the data of another module or when control flow is passed from one module to another.

It is the worst form of coupling and should be avoided. Looking at the example below, you can intuitively see that it is far more problematic than other couplings.

When a specific class directly changes the public member variables of another class, control flow for specific data is transferred from one class to another. This makes it difficult to predict the operation of the module and has a significant negative impact on maintenance.

// Class A class ClassA { public data: number; constructor(data: number) { this.data = data; } public displayData() { console.log(`Data: ${this.data}`); } } // Class B class ClassB { private classAInstance: ClassA; constructor(classAInstance: ClassA) { this.classAInstance = classAInstance; } public modifyData(newData: number) { // ClassB directly accesses and modifies the public member variable of ClassA this.classAInstance.data = newData; console.log(`Data modified to: ${this.classAInstance.data}`); } } // Usage example const instanceA = new ClassA(10); const instanceB = new ClassB(instanceA); instanceA.displayData(); // "Data: 10" instanceB.modifyData(20); // "Data modified to: 20" instanceA.displayData(); // "Data: 20"

Content coupling can occur when encapsulation or the intention of encapsulation is ignored and direct access is made to data in another module.

class MyClass { private secret: string; constructor(secret: string) { this.secret = secret; } reveal() { return this.secret; } } const instance = new MyClass('mySecret'); // In TypeScript, this will cause an error, but it can be bypassed using type assertion const secretValue = (instance as any).secret; console.log(secretValue); // "mySecret"

TypeScript's private access modifier is valid during type checking to hide member variables. However, in the JavaScript runtime, which is transpiled, all member variables are public. Using this, you can forcibly access member variables hidden by private.

It may be a rather absurd and extreme example. In the world of microservice architecture, content coupling becomes a bit more likely.

Content coupling describes a situation where a higher-level service reaches into the internals of a lower-level service to change its internal state. The most frequent occurrence of this situation is when an external service accesses and directly changes the database of another microservice. - Sam Newman, Building Microservices

References


Written by Max Kim
Copyright © 2025 Jonghyuk Max Kim. All Right Reserved