Coupling and Cohesion: Foundations That Affect Your Entire Codebase
Whether you are designing classes, structuring modules within a modular monolith, or composing microservices, regardless of the scale, you must always group functionality into components and establish clear dependency relationships between them.
That's where you encounter terms like coupling and cohesion, as they describe relationships and grouping, respectively. That is why it is important to understand these metrics, their types, and how they influence system design.
Let’s discover those concepts in more detail.
Coupling
Coupling describes how one component depends on another.
When a component is coupled to another:
- changes in one component require cascading changes in others
- components are harder to modify, test, and reuse
For this reason, well-designed systems aim to limit the number of dependencies and enforce clear dependency directions.
Types Of Coupling
Coupling ranges from tight to loose, and not all types of dependencies are equal.
We’ll explore these types, from the worst to the most desirable.
— content coupling (the worst)
Content coupling occurs when one component directly accesses or modifies the internal details of another, such as relying on its state or content.
As a result, any change in how the second component processes or stores data forces changes in the dependent component as well.
class OrderService : IOrderService
{
private readonly SqlRepository _repo = new SqlRepository();
public void CreateOrder(Order order)
{
// OrderService directly access SqlRepository private content
_repo._orderTable.Insert(order);
}
}Coupling can be lowered by applying OOP practices like encapsulation, abstraction, and so on.
— common coupling
Common coupling occurs when multiple components share the same global data, such as global variables or configurations.
Any change to the shared resource requires changes in or affects all components that depend on it.
static class AppState
{
// Shared global data
public static bool IsDebugEnabled = true;
}— external coupling
External coupling occurs when two components depend on an externally imposed data format, communication protocol, or device interface:
- file formats
- serialization type
- database schemas
- HTTP contracts
This type of coupling typically arises from interaction with external systems, tools, or hardware, rather than from internal design decisions.
// depends on exact JSON shape
var dto = JsonSerializer.Deserialize<OrderDto>(json);This is sometimes unavoidable, but can be isolated with a wrapper, the adapter pattern, or the anti-corruption layer.
— control coupling
Control coupling occurs when one component controls the behavior of another by passing information about what to do and how to do it.
This happens when a component dictates the execution flow of another component instead of letting it operate independently.
Control coupling is safer than content coupling because it respects component boundaries. However, in the example below, you can see that:
- the behavior of
ReportGeneratordepends on input from another component, rather than being self-contained - changing report types or logic force changes in both classes
class ReportService
{
private readonly ReportGenerator _generator = new ReportGenerator();
public void CreateReport()
{
// ReportService decides what ReportGenerator should do
// via the reportType flag
_generator.GenerateReport(reportType: "PDF");
}
}The same principle applies to methods that use flag parameters, such as .Enable(false), instead of providing two distinct methods like .Enable() and .Disable().
In general, use polymorphism and design patterns such as strategy, state, chain of responsibility, or other structured paradigm tricks to avoid flags and minimize control coupling.
— data-structured coupling (stamp coupling)
Data-structure coupling occurs when components share the same complex data structure, DTO, or model, but each component uses only certain parts of it.
This type of coupling is unwanted since a component gets knowledge of the internal state of another component, even on parts it does not actually use.
// only Id is needed, but the whole object is passed
public void PrintInvoice(Order order)
{
Console.WriteLine(order.Id);
}Another problem is that changes to the shared structure can force updates in components that don’t even use the modified parts.
— data coupling
Data coupling occurs when components share data, typically through parameters.
The shared data is elementary and minimal, containing only what the receiving component actually needs.
// each component receives just the information required for its operation
public void PrintInvoice(int orderId)
{
Console.WriteLine(orderId);
}— message coupling
Message coupling occurs when components interact only by exchanging messages, without sharing state.
This is the loosest and most desirable form of coupling, achievable through careful decentralization and communication via messages.
public class OrderPlacedMessage
{
public Guid OrderId { get; }
}
class OrderService : IOrderService
{
public void PlaceOrder(Order order)
{
. . .
// OrderService is not coupled to consumers of the message
_messageBroker.Publish(new OrderPlacedMessage(order.Id));
}
}The state is fully decentralized, and each component manages its own message contract, data, and behaviour independently.
— no coupling (the best)
The best coupling between components when you don’t have it 😅.
No coupling occurs when components do not interact at all.
Each component operates completely independently, with no shared data, calls, or dependencies.
Coupling Principle
Of course, having no dependencies between components seems great, but this does not mean you should avoid dependencies at all costs 😅.
Dependencies are inevitable in any system.
Therefore, you should learn principles that will help you to structure, design, and understand the codebase better.
The Acyclic Dependencies Principle
This recommendation says that dependencies between components should be in the form of an acyclic graph or a tree, but not cyclic.
Cyclic dependencies make the system hard to understand and maintain, and often lead to ripple effects, where a change in one component forces unexpected changes in others.
Principle of Least Knowledge (Law of Demeter)
When a component depends on another, it may indirectly reference additional components.
This often indicates content coupling and weak encapsulation.
❌ violation
var users = _userService.GetUserRepository().GetUser("john.doe@mail.com");Principle of least knowledge says that a component should know as little as possible about other components.
You should depend only on immediate collaborators, not on the internal structure of other objects.
✅ better
var users = _userService.GetUser("john.doe@mail.com");This means that when a lower-level module changes, the impact doesn’t ripple through the system; only one module has to adapt, keeping components independent and reducing overall coupling.
A drawback of the Law of Demeter is that it sometimes requires creating many small adapter methods to delegate calls to internal components.
Stable Dependency Principle
Coupling can be afferent, when other modules depend on a given module, and efferent, when the given module depends on other modules.
Based on the number of dependencies, you can classify components as:
- unstable — when no one depends on a component, it can be easily changed
- flexible — have a few dependencies and the ability to change without major impact
- stable — heavily depended upon; changes are harder because they affect other components
You might think that having only stable components is ideal, but that’s a misconception. If every component in a system were stable, the system would become practically unchangeable and extremely rigid.
So maybe we need unstable components 🤔? That would make the application too volatile and easy to break.
All degrees of stability are good when used properly😅:
- you want components that change frequently (like UI), to be unstable, have very little dependency, and be easy to replace
- flexible components designed to be easy to change and serve as “glue” connecting other modules
- business logic should be a stable component because it represents the permanent rules of your system
- abstractions should remain stable, allowing you to change implementations without affecting the clients
This rule says, that a component should depend only on components that are more stable than itself.
Think carefully:
- a stable component should not depend on an unstable one, because frequent changes in the volatile component would propagate and disrupt the stable component
- conversely, an unstable component that depends on a stable one may become difficult to evolve, as it is constrained by the stable component’s structure and rules
Stable Abstractions Principle
In the previous section, I noted that a component containing abstractions should be stable and widely depended upon. This might not be immediately obvious🤔. Therefore, let’s explore it in more detail.
I guess it is not a surprise for anyone that coupling is lowered when you depend on abstraction rather than implementation, because inversion of control, modularity, unit tests, you can substitute implementation, and bla bla bla...
class UserService : IUserService
{
// coupled to the abstraction, not implementation
private readonly IUserRepository _userRepository;
public UserService(IUserRepository userRepository)
{
_userRepository= userRepository;
}
. . .
}However, why would it also work for modules?
When you depend on a concrete implementation, it becomes stable — or difficult to change. This is problematic because implementations are meant to evolve.
However, as soon as you introduce abstraction, changes in concrete implementations do not require adjustments to the interfaces they implement.
Even though any modification to an abstraction would require changes in all of its concrete implementations, this rarely happens since it would cause changes in all implementations. By nature, interfaces are supposed to change infrequently, be less volatile than implementations, and provide a stable foundation for dependent code.
Stable components, stable abstractions — doesn’t that sound like a match made in heaven😅?
There you have it:
Don’t reference to volatile concrete components. Refer to stable, abstract one instead.
In other words, if a package is difficult to change because many other packages depend on it, it should consist of abstractions, allowing its behavior to be extended without modifying its clients.
Conversely, an unstable component can be concrete, since its instability allows the contained code to be changed easily.
Therefore, it’s recommended to separate abstraction from implementation. This not only allows modules to have different levels of stability, but also lets you replace implementations when needed without modifying all dependent packages — only the composition root needs to change.
That was a big discovery in ports and adapters architecture, you can check it out in your spare time 🙃.
Cohesion
Enough for coupling. Let’s talk about cohesion 😁.
Cohesion is about how well the responsibilities inside a class / module / component belong together.
When functionality is poorly organized by components, it leads to the following issues:
- changes in one functionality require changes in multiple components
- components are harder to understand, maintain, test, and reuse
For this reason, well-designed systems strive to group related functionality together, ensuring that each component has a clear, focused purpose.
Types Of Cohesion
Cohesion ranges from low to high. Let’s explore these types from the worst to the best.
Get iamprovidence’s stories in your inbox
Join Medium for free to get updates from this writer.
— сoincidental сohesion (the worst)
Coincidental cohesion occurs when a functionality is grouped randomly.
As a result, the component is hard to understand, hard to reuse, and extremely fragile — any change risks breaking unrelated behavior.
class Utility
{
public void Log(string message) { /* logging logic */ }
public void SendEmail(string email) { /* email logic */ }
public void ProcessOrder() { /* order logic */ }
}Modules are often given generic names like shared, common, classes, abstractions, or utils, and end up collecting everything developers didn’t know where to place 😅.
— logical сohesion
Logical cohesion happens when responsibilities are grouped by category or operation type.
This often leads to conditional logic and hidden coupling inside the component.
class GetDataRepository
{
public Order GetOrder(int id) { . . . }
public Product GetProduct(int id) { . . . }
}
class UpdateDataRepository
{
public void UpdateOrder(Order order) { . . . }
public void UpdateProduct(Product product) { . . . }
}— temporal cohesion
Temporal cohesion groups operations that are related only by time, such as startup/shutdown logic, the occurrence of exceptions, etc.
While sometimes acceptable, it still mixes responsibilities that may evolve independently:
- they serve different concerns: persistence, logging, or infrastructure
- they don’t share data or a single responsibility
class ApplicationShutdownService
{
. . .
public void Shutdown()
{
// the methods are executed at the same time (shutdown)
SaveUserSession();
FlushLogs();
CloseDatabaseConnections();
SendMetrics();
}
private void SaveUserSession() { . . . }
private void FlushLogs() { . . . }
private void CloseDatabaseConnections() { . . . }
private void SendMetrics() { . . . }
}A way to improve this is to extract each responsibility into its own service, keeping only the coordination logic at the orchestration level.
class ApplicationShutdownService
{
. . .
public void Shutdown()
{
_sessionService.Save();
_logger.Flush();
_db.Close();
_metrics.Send();
}
}— procedural cohesion
Procedural cohesion exists when elements are grouped because they follow a specific execution order.
Procedural cohesion appears when:
- the class exists to execute a procedure, not a single purpose
- steps depend on each other and need to be executed in a specific order
- each step represents a different concern
class RegisterUserCommand : ICommand<RegisterUserRequest, void>
{
public void Handle(RegisterUserRequest request)
{
// grouped by execution order
ValidateInput(request.User); // validation concern
SaveUser(request.User); // persistence concern
}
private void ValidateInput(UserDto user) { . . . }
private void SaveUser(UserDto user) { . . . }
}This is often acceptable and common in:
- workflow coordinators
- sagas
- application services
Similarly to what we did before, the cohesion can be improved when real logic lives in cohesive services.
— communicational cohesion
Communicational cohesion appears when operations are grouped because they work on the same data.
This level of cohesion is generally acceptable and common, especially in data access layers, where operations naturally revolve around the same data model.
class OrderRepository : IOrderRepository
{
public Order Get(int id) { . . . }
public void Update(Order order) { . . . }
}However, if the class starts accumulating too many behaviors, it can gradually become a “God object”.
Communicational cohesion becomes a problem when “working on the same data” is the only thing holding a component together.
When responsibilities multiply, cohesion drops — even if the data stays the same.
— sequential cohesion
Sequential cohesion appears when functionality depends on each other.
This type of cohesion is commonly seen in pipeline processing, where each method contributes to a single logical workflow. Changes to any step may require adjustments to the entire sequence, yet the overall cohesion remains logical and predictable.
class TextProcessor : ITextProcessor
{
public string ProcessText(string input)
{
// each method depends on the output of the previous method
var cleaned = RemovePunctuation(input);
var normalized = NormalizeCase(cleaned);
var words = SplitWords(normalized);
return string.Join(" ", words);
}
private CleanedString RemovePunctuation(string text) { . . . }
private NormalizedString NormalizeCase(CleanedString text) { . . . }
private string[] SplitWords(NormalizedString text) { . . . }
}It’s stronger than procedural cohesion, because the steps are not just executed in order — the output of one operation is the input of the next one.
— functional cohesion (the best)
Functional cohesion occurs when everything in the component serves one purpose.
You can spot it by the following features:
- the class has one reason to change (e.g. the rules for calculating an invoice total)
- all methods directly contribute to the single purpose
- no unrelated responsibilities like persistence, notifications, or logging are mixed in
class InvoiceCalculator : IInvoiceCalculator
{
public decimal CalculateTotal(Invoice invoice)
{
decimal subtotal = invoice.Items.Sum(i => i.Price * i.Quantity);
decimal tax = subtotal * invoice.TaxRate;
decimal discount = subtotal * invoice.DiscountRate;
return subtotal + tax - discount;
}
}Functional cohesion is the ideal, but it’s rarely possible to achieve 100% of the time in real-world systems, as components often need to interact with multiple concerns.
Cohesion Principles
There are multiple principles that are often closely associated with cohesion, like separation of concerns, encapsulation, the single responsibility principle, the divide-and-conquer approach, and so on.
You may already be familiar with these concepts. If needed, you can revisit them in dedicated articles, as I won’t repeat them here 🙃.
Long story short, they all aim to reduce complexity by grouping related behavior together and isolating what changes for different reasons.
Module Cohesion Principles
One of the crucial design decisions that defines module structure and responsibilities is to determine which classes belong to which module.
To help us decide, we have three module cohesion principles:
- The Release Equivalence Principle (REP) says that if you reuse classes together, you should release them as a whole
(all reused classes should be versioned and deployed together)
- The Common Closure Principle (CCP) says that classes that change together should be packaged together (single responsibility principle)
(meaning that changes to functionality should be applied in one component, not scattered across many)
- The Common Reuse Principle (CRP) says that if your classes depend on each other, they must form a package (interface segregation principle)
(this also means you should not depend on components containing classes you don’t actually use)
As with other trilemmas, you have to choose two principles and sacrifice the third one 😒.
Place your fingers on two nodes, and the line between them shows what you had to deal with.
There is no one good answer — decide what works best in your context. In practice, you would mix those or have something in between.
Two Sides of the Same Design Decision
Coupling and cohesion are usually discussed together. Not a surprise. Both concepts describe complementary aspects of how a system is structured.
When components are poorly grouped, they tend to require more dependencies, and vice versa, when responsibilities are well defined, components naturally reference fewer external dependencies.
Let’s review a few possible combinations of coupling and cohesion:
Top-Left Corner (Low Coupling/High Cohesion)
The ideal combination in a system that you should aim for is high cohesion and low coupling.
High cohesion means that functionality in a component focuses on a single, well-defined responsibility.
Low coupling means the component is self-contained and independent of others.
This is a perfect combination, because in essence, high cohesion means keeping parts of a code base that are related to each other in a single place, while low coupling is about separating unrelated parts of the code base.
Top-Right Corner (High Coupling/High Cohesion)
Decomposition is great, but overusing it can do more harm than good.
Destructive decoupling occurs when a developer splits code into many small components or abstractions in the name of decoupling, but without preserving clear responsibilities.
As a result, it harms the structure of the system, and the codebase loses focus: behavior that logically belongs together is scattered across multiple layers, interfaces, and components.
Tight coupling and high cohesion result in a complex system with multiple components that don’t have a clear task, with many dependencies, and poor maintainability.
Bottom-Right Corner (High Coupling/Low Cohesion)
Sometimes your code could be split into modules, but responsibilities are poorly defined.
The issue is not the number of components, but that the boundaries for functionality are chosen poorly.
You can observe this at many levels — classes, projects, or even microservices — but it is especially common in traditional folder structures where code is grouped by technical concerns.
At first glance, the project on the left appears well organized, with separate folders (components) for services, mappers, validation, and so on.
However, each folder is coupled to another and cannot function on its own (services do not work without validators and mappers that depend on dtos).
Additionaly, this structure lacks cohesion (functionality related to apartments, bookings, payments, and so on is scattered across multiple directories). It follows logical cohesion (level 2) and not communicational (level 6) one.
A more cohesive, with less coupling approach is to follow Screaming Architecture. This keeps related classes together, makes directories independent from each other, and organizes the project around domain concepts rather than technical roles, reflecting project semantics.
Tight coupling and loose cohesion result in a dependent system where components depend on each other and can’t function on its own.
Bottom-Right Corner (Low Coupling/Low Cohesion)
When both cohesion and coupling are low, all functionality is concentrated in a single component that does not depend on others.
This often results in a God Object, aka Big Ball of Mud, aka Spaghetti Code— a well-known anti-pattern where one large piece of code tries to do everything.
Such components are hard to understand, test, and evolve, despite having few external dependencies.
Loose coupling and low cohesion result in a monolithic system with one components that serve various goals.
Summary
If you get to this point without a headache, I don't know what kind of monster you are 😅. Not to overwhelm you with information, let’s just briefly review what we learned today:
- Coupling describes dependencies on elements outside a component.
- Cohesion describes dependencies between elements inside a component.
- Classes with high coupling are often overly abstract, orchestrate responsibilities that could be easily split or delegate to other objects.
- Classes with low cohesion perform many functions or handle responsibilities that are not closely related.
- Your design should exhibit low coupling and high cohesion. This means a component has few external dependencies and focuses on a single responsibility inside.
- Aim for “high cohesion and low coupling” across different levels of your codebase (classes, folders, projects, microservices, and so on).
- Low coupling cannot be achieved without reducing cohesion, and the reverse is also true. Find a balance.
- While no coupling would be ideal, some degree of coupling is inevitable. Even a common coupling could be fine. Just ensure that you depend on abstractions rather than implementations.
- Functional cohesion is the ideal goal for a component, but in practice, some degree of procedural, temporal, or communicational cohesion is unavoidable.
Give this article a clap or a few if you like it 👏
You can support me with a link below ☕️
And don’t forget to follow ✅
See U😉









