skip to content

A codebase has a class holding unrelated helpers - string padding, date arithmetic, a retry loop - grouped only because they had nowhere else to live. Classify that grouping, and explain why the same three functions in Python, Go or Rust would usually not produce a class at all.

level: middleimportance: should knowfreq 45%

answer

  1. coincidental = "no other home"
  2. Java forces namespace into class shape
  3. Go util package = same smell, package level
  4. extension methods move discoverability, not ownership
  5. no shared state between any pair

basics

~20 s

Coincidental cohesion: the members share nothing but a file. Java and C# have no free functions, so the dump must take the shape of a class; Python, Go and Rust let functions live directly in a module, so the same dump is a namespace, not a type.

solid answer

~60 s

The grouping is **coincidental** - membership was decided by "it had no other home", not by shared state or a shared job. - **Java** has no top-level functions, so the only container available is a class: a final type with a private constructor and static members, no instances, no invariants. The dump is forced into type shape. - **Kotlin** allows top-level functions; they still compile to a synthetic `FileKt` class, but the source never asks the reader to model a type. - **Go** has no per-type home either - functions live in a package, and the idiom is a small package named for a concept (`strings`, `time`). A `util` package draws the same criticism, one level up. - **C#** adds extension methods, so the helper attaches to the receiver at the call site (`s.PadTo(10)`); the grouping stays coincidental but discoverability moves onto the receiver's type. The smell is a grouping decision, not a language artifact. What varies is whether the language forces the group to be a *type* - which then advertises identity, instances and subtyping that a bag of helpers cannot honour.

code

java · 7 lines
java
public final class Utils {
    private Utils() {}
    public static String pad(String s, int n) { ... }
    public static LocalDate addBusinessDays(LocalDate d, int n) { ... }
    public static <T> T retry(Supplier<T> op, int times) { ... }
}
// callers: Utils.pad(...), Utils.addBusinessDays(...)

go deeper

for a junior

Name the kind (coincidental) and give the test: no two members share state or a reason to change.

for a middle

Add the language dimension - Java and C# have no free functions so the group is forced into a class, while Go, Rust and Python put it in a module and get the same smell at package granularity.

for a senior

Discuss what each shape costs: false type expectations versus lost discoverability, and why extension methods trade one for the other while binding statically.

for a principal

Frame it as where the codebase draws its unit of naming and ownership, and set a rule the team can apply - a group needs either shared state or a single subject, or it does not get a name.

## What is being classified Cohesion asks one question about a set of members: *why are these together?* Coincidental cohesion is the weakest answer - "because someone needed a place to put them". Nothing about string padding constrains date arithmetic; changing one can never change the other; and no reader can predict from the name what else is inside. The diagnosis is a property of the *grouping*, and it is language-independent. What is language-dependent, and what interviewers are usually probing, is the **shape** the grouping is forced to take. ## Why the class exists at all in some languages Java requires every declaration to be a member of a type. If you want a reusable function and there is no natural owner for it, the language gives you exactly one move: a class with static members, conventionally made final with a private constructor so nobody instantiates it. That class is a namespace wearing the costume of a type. It has no state, so no invariants; no instances, so no identity; no meaningful subtype. Every OO expectation the keyword `class` sets up is false for it. C# is in the same family and formalises the workaround: `static class` is a compiler-enforced namespace-only type. It then goes further with **extension methods**, which are static methods the compiler lets you call in member position. `s.PadTo(10)` reads as though `String` grew a member. Crucially this is still static dispatch resolved at compile time and scoped by the `using` in effect - it changes where a reader looks for the code, not who owns it. Kotlin permits top-level functions outright. On the JVM the compiler still emits a synthetic file class, so the runtime artefact looks the same as Java's - but the *source* stops pretending there is a type, which is the part that matters for a human classifying the design. ## The same code where free functions are native Python, Go, Rust and C++ never needed the workaround. In Python the natural home is a module: `import dateutils` then `dateutils.add_business_days(...)`. In Go the unit is the package, and the standard library is a lesson in naming those packages for concepts - `strings`, `time`, `errors` - rather than for their role in your build. Rust has free functions in modules with `pub` controlling reach. This is why a Go `util` package is criticised in exactly the language Java reviewers use for a `Utils` class: the failure is the same coincidental grouping, just measured at package granularity. The languages differ in the *unit* the smell attaches to, not in whether it is a smell. ## What each choice actually costs - A static-only class costs conceptual honesty: readers must know it is not a real type, and tooling (mocking, subclassing, dependency injection) that works on types does not usefully apply. - A module of free functions costs discoverability: there is no receiver to autocomplete from, so the function is only findable if you already know the module's name. - Extension methods buy discoverability back but bind statically. If the owning type later adds a real member with the same name, the member wins on recompile and behaviour changes silently. ## How to classify a concrete dump Ask, member by member: does this member read or write any state the group shares, and does it change for the same reason as its neighbours? For a helper dump the answer is no on both counts for every pair. Compare that with a class whose methods all touch the same two fields - that group is held together by state, and file layout is irrelevant to the verdict. ## What actually fixes it Moving the helper to the type it is really about (date arithmetic belongs to a date type, or to a small module named for dates), or promoting a genuinely reusable concept into a small named module. The retry loop is not a helper at all - it is a policy object with its own configuration, which is the tell that it was in the wrong bag. Renaming `Utils` to `StringUtils` narrows the bag but does not change the kind of cohesion; splitting by concept does.

  • Does renaming the class from Utils to StringUtils change its cohesion?
    Only if the members change with it. Narrowing the name while leaving date and retry helpers inside makes the name a lie without altering the grouping rule. If the rename is accompanied by moving out everything that is not about strings, the remaining group is held together by a single subject - that is the improvement, and the rename merely records it.
  • A C# team replaces a static helper class with extension methods on the target type. What kind of coupling remains?
    Compile-time, import-scoped coupling: extension methods are static methods resolved by the compiler from the usings in scope, so nothing is added to the type at runtime and no dispatch changes. The remaining hazard is precedence - if the owning library later adds a real instance member with that signature, it wins over the extension on the next recompile and behaviour changes with no source edit.

A kitchen drawer that holds batteries, tape and takeaway menus. The drawer is not a bad drawer; the grouping rule - "things without a drawer" - is what guarantees you can never predict its contents.

saying these in an interview costs you the question

  • Calling any class with static members coincidental - a well-named factory or a pure-function module can be perfectly cohesive
  • Believing a Utils class is a Java-only pathology; Go util packages and Python misc modules are the same grouping
  • Treating a rename as a cohesion fix
  • Claiming extension methods add the method to the type at runtime
  • Assuming free functions are always better - they lose the receiver-driven discoverability a member gives you

context