📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What is high cohesion, how do you recognise low cohesion in C# code, and how do you improve it?

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.

Mid-Level Theory Intermediate cohesion design-principles refactoring

🎯 Key points

  • Cohesion measures how closely the elements of a module relate to one purpose
  • High cohesion: methods use the same data and change together, which makes code easier to test and change safely
  • Symptoms of low cohesion: grab-bag Utils/Helper classes of unrelated methods, fields used by few methods, feature envy
  • Functional cohesion is the strongest; coincidental is the weakest
  • Organise by feature or concept rather than by technical type
  • LCOM metrics and change history help find low cohesion

⏱ 30-second answer

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.

🗣 Full answer (senior)

How I would answer this in an interview

"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, CommonHelpers or Manager with 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 across Controllers/, Services/, Dtos/ and Validators/. To improve it, I move behaviour next to the data it uses (often onto the entity or a value object like Money or Abn), 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."


1. Types of cohesion (weakest → strongest)

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.

2. Symptoms of low cohesion

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

3. Improving cohesion

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

4. Why it matters

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

Production example

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.

🔍 What the interviewer is checking

Checks whether the candidate can diagnose structural problems.

Green flags

  • Defines cohesion in terms of shared purpose and data.
  • Names concrete symptoms such as Utils classes and feature envy.
  • Mentions feature folders or value objects.
  • Connects cohesion to coupling.

Red flags

  • Confuses cohesion with coupling.
  • Suggests more helper classes as the fix.

📚 Learn in depth

1. Origin

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.

2. Measuring cohesion

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.

3. Field clusters, concretely

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)

4. Feature envy → rich model

// 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);
}

5. Feature folders

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.

6. Cohesion at architecture scale

  • Modules/services should own a business capability and its data; a "shared" service everyone changes has low cohesion.
  • Shared libraries should be small and focused; a Company.Common package is coincidental cohesion at package level and forces everyone to upgrade together.
  • Cohesive modules reduce chatty cross-module calls, which matters for performance in distributed systems.

7. Try it yourself

  1. Find the largest *Helper/*Utils class; group its methods by caller and concept.
  2. Draw a field-usage matrix for a large service class; look for clusters.
  3. Move one feature-envious method onto the entity it uses.
  4. Reorganise one feature into a feature folder and compare the diff of a typical change.
  5. Run an LCOM-style analysis (NDepend trial) and review the worst classes.

💻 Code

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));
    }
}

⚠ Common mistakes

1. Utils/Helpers as a default home

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

2. Organising everything by technical type

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.

3. Feature envy in services

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.

4. Over-splitting cohesive code

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.

5. Flag-driven methods

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

6. Primitive obsession

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.

7. A shared "Common" package across services

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.

8. Confusing cohesion with coupling

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.

9. Judging by class names alone

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.

10. Keeping dead code during refactoring

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.

🏛 Senior insight: trade-offs

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

🔗 Follow-ups the interviewer may ask

  1. When: Candidate says cohesive modules need fewer cross-calls Why: Moves to coupling
    How to answer

    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.

  2. When: Candidate splits a class along field clusters Why: Connects cohesion to SRP
    How to answer

    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.

  3. When: Candidate moves behaviour onto entities Why: Probes rich versus anaemic models
    How to answer

    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.

🛠 Real-world problems: how you handle them

CommonHelpers referenced by every project
Situation
A [2,000-line] `CommonHelpers` class was referenced by [every] project in a [30-project] solution.
Signals
Any edit caused a full rebuild and full regression run; merge conflicts in the file weekly.
Investigation
Grouped methods by caller and found [twelve] unrelated concepts and [half] the methods unused.
Resolution
Split into cohesive types (Money, Abn, date rules) placed in the features that used them; deleted unused code.
Prevention
Analyser rule and review guideline rejecting new `*Helper` classes without a domain name.

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.

↩ Usually asked after

More in Additional Principles

Last updated 31 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me