Cohesion as how strongly the parts of a module belong together; types of cohesion; symptoms such as utility classes and feature envy; measuring with LCOM; refactoring by feature.
Cohesion is how strongly the parts of a module belong together. A highly cohesive class has methods that work on the same data towards one purpose, so a change to that concept happens in one place. Low cohesion shows up as Utils or Manager classes with unrelated methods, fields that only one or two methods use, and classes that change for many different reasons. I improve it by grouping code around domain concepts and features, moving methods to the data they use, and splitting classes whose fields form separate clusters.
"Cohesion is how strongly the parts of a module belong together. In a highly cohesive class the methods work on the same data towards one purpose, so when that concept changes, the change happens in one place, and the class is easy to name, understand and test. Low cohesion is the opposite: a grab-bag. In C# I recognise it from a few signals. Classes named
Utils,CommonHelpersorManagerwith no domain noun, holding unrelated methods. Fields used by only a subset of methods, which means two classes are sharing a file. Feature envy: a service method that only manipulates another object's data. And at folder level, one feature scattered acrossControllers/,Services/,Dtos/andValidators/. To improve it, I move behaviour next to the data it uses (often onto the entity or a value object likeMoneyorAbn), split classes along their field clusters, organise code by feature, and replace helper grab-bags with small named types. High cohesion inside modules is also what makes low coupling between them possible."
| Type | Elements are grouped because... | C# example |
|---|---|---|
| Coincidental | No real reason | Utils with FormatDate, HashPassword, SendEmail |
| Logical | They're the same kind of thing, selected by a flag | ExportManager.Export(type) doing CSV, PDF and email |
| Temporal | They happen at the same time | A startup class doing every boot task |
| Procedural | They follow a sequence of control | Steps of a script grouped by order only |
| Communicational | They use the same input data | Methods all operating on one customer record |
| Sequential | Output of one feeds the next | Parse → validate → transform pipeline |
| Functional | All contribute to one well-defined task | TaxCalculator, Money, InvoicePolicy |
The goal is functional cohesion; coincidental and logical cohesion are the ones to remove.
| Symptom | What it suggests |
|---|---|
*Helper, *Utils, *Manager, Common* with unrelated methods |
Coincidental cohesion |
| Field clusters used by disjoint method sets | Two classes in one |
| Feature envy (method uses another object's data more than its own) | Behaviour in the wrong place; anaemic model |
Methods with a type/mode flag switching behaviour |
Logical cohesion |
| A feature spread across many technical folders | Low cohesion at module level |
| Class changes for many unrelated reasons | Mixed responsibilities (SRP) |
| A project referenced by everything | Grab-bag shared library |
| Refactoring | When |
|---|---|
| Move method to the data's owner | Feature envy |
| Extract class along a field cluster | Disjoint field usage |
Introduce value objects (Money, Abn, DateRange) |
Primitive obsession + helper functions |
| Feature folders / vertical slices | Features scattered by technical type |
| Replace flag-driven methods with separate types | Logical cohesion |
| Delete unused members found during the split | Always |
| Benefit | Because |
|---|---|
| Cheaper change | A concept changes in one place |
| Easier tests | Small purpose = small fixture |
| Easier naming and discovery | The class is the concept |
| Lower coupling | Related code calls itself, not other modules |
| Faster builds/less retesting | Fewer projects depend on grab-bag libraries |
In a [30-project] solution, a [2,000-line] CommonHelpers class was referenced by every project, so any edit triggered a full rebuild and a full regression run, and it had merge conflicts weekly. I grouped its methods by caller and by concept and found [twelve] unrelated concepts (currency formatting, ABN validation, GST rules, date-range logic and more) and that [about half] of the methods were unused. I turned each concept into a cohesive type (Money, Abn, GstPolicy, DateRange), placed next to the features that used them, and deleted the dead code. After that, most changes affected a single module, builds got faster, and we added a review guideline rejecting new *Helper classes without a domain name.
Checks whether the candidate can diagnose structural problems.
Green flags
Red flags
Larry Constantine and Ed Yourdon introduced cohesion and coupling in Structured Design (1970s), including the cohesion scale above. The ideas carried over directly to object-oriented design: GRASP lists High Cohesion and Low Coupling as core patterns.
| Measure | What it looks at | Tool |
|---|---|---|
| LCOM (Lack of Cohesion of Methods) | Pairs of methods sharing (or not) instance fields | NDepend (LCOM, LCOM HS), some IDE plugins |
| Field-usage matrix | Which methods touch which fields | Manual or tool-assisted |
| Change coupling | Files that change together in Git history | CodeScene, Git log scripts |
| Dependency fan-in | How many projects depend on a module | Dependency graphs (dotnet list reference, NDepend) |
Use metrics to find candidates; decide with judgement.
class CustomerManager
fields: _db, _smtp, _templates, _pdf, _taxRates
methods:
GetCustomer() uses _db
UpdateCustomer() uses _db
SendWelcomeEmail() uses _smtp, _templates
SendStatement() uses _smtp, _templates, _pdf
CalculateTax() uses _taxRates
Clusters → CustomerRepository(_db), CustomerEmails(_smtp,_templates,_pdf), TaxPolicy(_taxRates)
// Feature envy: the service reaches into the order's data
public decimal TotalWithDiscount(Order o) =>
o.Lines.Sum(l => l.Price * l.Quantity) * (o.Customer.IsGold ? 0.9m : 1m);
// Cohesive: the order owns its total
public sealed class Order
{
public decimal Total(DiscountPolicy policy) => policy.Apply(Lines.Sum(l => l.Subtotal), Customer);
}
| By technical type (low cohesion of change) | By feature (high cohesion of change) |
|---|---|
Controllers/OrdersController.cs |
Orders/PlaceOrder.cs (endpoint + handler) |
Services/OrderService.cs |
Orders/OrderPolicy.cs |
Validators/OrderValidator.cs |
Orders/PlaceOrderValidator.cs |
Dtos/OrderDto.cs |
Orders/OrderResponse.cs |
A feature change then touches one folder.
Company.Common package is coincidental cohesion at package level and forces everyone to upgrade together.*Helper/*Utils class; group its methods by caller and concept.using System.Globalization;
// Low cohesion: unrelated concepts in one grab-bag that every project references.
public static class CommonHelpers
{
public static string FormatAud(decimal amount) => amount.ToString("C", new CultureInfo("en-AU"));
public static bool IsValidAbn(string abn) => abn.Length == 11 && abn.All(char.IsDigit);
public static decimal AddGst(decimal amount) => amount * 1.10m;
public static bool Overlaps(DateOnly s1, DateOnly e1, DateOnly s2, DateOnly e2) => s1 <= e2 && s2 <= e1;
}
// High cohesion: each type owns one concept, its data and its behaviour.
public readonly record struct Money(decimal Amount)
{
private static readonly CultureInfo AuCulture = new("en-AU");
public Money WithGst() => new(Amount * 1.10m);
public override string ToString() => Amount.ToString("C", AuCulture);
}
public readonly record struct Abn
{
public string Value { get; }
private Abn(string value) => Value = value;
public static bool TryCreate(string input, out Abn abn)
{
var digits = new string(input.Where(char.IsDigit).ToArray());
abn = digits.Length == 11 ? new Abn(digits) : default;
return digits.Length == 11; // format check only; the ABN checksum rule would live here too
}
}
public readonly record struct DateRange(DateOnly Start, DateOnly End)
{
public bool Overlaps(DateRange other) => Start <= other.End && other.Start <= End;
}
public static class Demo
{
public static void Main()
{
Console.WriteLine(new Money(100m).WithGst());
Console.WriteLine(Abn.TryCreate("51 824 753 556", out var abn) ? abn.Value : "invalid");
var a = new DateRange(new(2026, 11, 1), new(2026, 11, 10));
var b = new DateRange(new(2026, 11, 8), new(2026, 11, 20));
Console.WriteLine(a.Overlaps(b));
}
}
Utils/Helpers as a default homeWhat happens: any code without an obvious place goes into CommonHelpers.
Why it is wrong: coincidental cohesion; becomes a dependency of everything.
Do instead: give code a home named after the concept it serves.
What happens: Controllers/, Services/, Dtos/, Validators/ folders.
Why it is wrong: one feature is scattered; changes touch many folders.
Do instead: feature folders; shared domain types in the domain.
What happens: services compute things purely from an entity's fields. Why it is wrong: anaemic models; logic duplicated across services. Do instead: move behaviour onto the type that owns the data.
What happens: one concept spread over many tiny classes. Why it is wrong: low cohesion at module level; hard to see the whole concept. Do instead: split along real field clusters and reasons to change.
What happens: Export(report, ExportType type) with a big switch.
Why it is wrong: logical cohesion; each variant changes for different reasons.
Do instead: separate types per variant (or a strategy).
What happens: ABNs, money and date ranges passed as string/decimal/two DateTimes plus helpers.
Why it is wrong: rules scatter into helpers; invalid values flow everywhere.
Do instead: value objects that own their validation and behaviour.
What happens: one NuGet package holds everyone's utilities. Why it is wrong: coupled upgrades; unrelated changes force releases. Do instead: small focused packages, or keep code in the owning service.
What happens: "this class is too coupled" when it's actually a grab-bag. Why it is wrong: different problems need different fixes. Do instead: cohesion = inside a module; coupling = between modules.
What happens: every class named Manager is condemned.
Why it is wrong: the name is a hint; the real test is whether members share a purpose.
Do instead: check field usage and reasons to change.
What happens: unused helper methods are moved along with the rest. Why it is wrong: carry cost with no value. Do instead: delete unused members as you split.
Cohesion is the best predictor I know of how cheap a change will be. I look at it at two levels: within a class (do methods share data?) and within a folder or project (does a feature live together?).
| Signal | Action |
|---|---|
Helper class used everywhere |
Split into concept types near their features |
| Fields used by disjoint method sets | Split the class |
| Service method only touches entity data | Move it onto the entity |
| Files always changed together | Put them in the same feature folder |
Coupling is the degree to which one module depends on another: how much it must know about it, and how likely it is to change when the other changes. Some coupling is unavoidable; the goal is coupling that is explicit, narrow and stable. In code the harmful kinds are reaching into another type's internals, shared mutable global state and passing control flags. In distributed .NET systems the expensive kinds are temporal coupling (synchronous call chains), shared databases and lock-step deployments. I reduce them with narrow interfaces, published contracts, asynchronous messaging where it fits, and each service owning its data.
SRP says a module should have one reason to change, which Robert C. Martin later phrased as "responsible to one actor": one group of people whose requests drive its changes. It is not "a class does one thing" or "one method per class". In .NET I look for classes that change for unrelated reasons, such as an OrderService that holds pricing rules (finance), email templates (marketing) and SQL (DBA), and split along those change drivers. I stop splitting when the pieces would always change together, because that is just scattering one responsibility across files.
In an anemic domain model, entities are property bags with public getters and setters, and all business rules live in service classes that read and set those properties. In a rich domain model, entities own their behaviour: state is private, changes happen through methods like order.Pay(amount) that enforce invariants, and services only orchestrate (load, call, save). Anemic models scatter and duplicate rules, let any code put objects into invalid states, and are hard to test without a database. They are acceptable for simple CRUD or reporting with few rules. Move towards a richer model incrementally: make setters private, move one rule at a time into the entity, protect collections, and use value objects, guided by where bugs actually occur.
How to tell it (STAR)
We had a giant helper class that every project depended on, so any edit meant rebuilding and retesting everything. I grouped its methods by who used them, moved each group next to its feature as a proper type, and deleted the half that nobody called. Most changes after that touched a single module.
Last updated 31 Oct 2026.