Code smellBloaters
Data Clumps
The same group of variables travels together through fields and parameters, signaling a missing object that should own them.
Signs
- Three or more parameters appear side by side in several signatures:
(street, city, postalCode, country). - Several classes have the same set of fields with the same names, often copy-pasted.
- Validation or formatting for the group is repeated: building a one-line address string happens in four places.
- Deleting one member of the group would make the others meaningless. That is the test: if
latwithoutlngis nonsense, they are a clump.
Why it happens
The first time two values travel together, adding a type for them seems unnecessary. The second and third times, copying the existing signature is faster than designing one. Database tables and form layouts also push the group apart into separate columns and inputs, and code that mirrors them inherits the split.
Example
A delivery scheduling module where a geographic point and a time window are always passed as loose values.
function distanceKm(lat1: number, lng1: number, lat2: number, lng2: number): number { /* haversine */ return 0; }
function canDeliver(
depotLat: number, depotLng: number,
stopLat: number, stopLng: number,
windowStart: Date, windowEnd: Date,
now: Date,
): boolean {
if (now < windowStart || now > windowEnd) return false;
return distanceKm(depotLat, depotLng, stopLat, stopLng) < 40;
}
class Stop {
lat = 0;
lng = 0;
windowStart = new Date();
windowEnd = new Date();
}Extracting GeoPoint and TimeWindow shortens every signature and gives the behavior a home.
class GeoPoint {
constructor(readonly lat: number, readonly lng: number) {}
distanceKmTo(other: GeoPoint): number { /* haversine */ return 0; }
}
class TimeWindow {
constructor(readonly start: Date, readonly end: Date) {
if (end < start) throw new Error("window ends before it starts");
}
contains(t: Date): boolean { return t >= this.start && t <= this.end; }
}
class Stop {
constructor(readonly location: GeoPoint, readonly window: TimeWindow) {}
}
function canDeliver(depot: GeoPoint, stop: Stop, now: Date): boolean {
return stop.window.contains(now) && depot.distanceKmTo(stop.location) < 40;
}Treatment
- If the clump appears as fields in a class, use Extract Class to move them into their own type.
- If the clump appears in parameter lists, use Introduce Parameter Object.
- If the caller already holds an object containing the clump, use Preserve Whole Object and pass that object.
- After the type exists, look for functions that operate only on the clump (formatting, distance, overlap checks) and move them onto it with Move Method.
Payoff
- Signatures shrink and the concept gets a name the team can use.
- Invariants (end after start, latitude in range) are enforced once.
- Duplicate helper code collapses into methods on the new type.
- Future changes, such as adding altitude or a time zone, touch one type instead of every signature.
When to ignore
- The values coincide but do not form a concept:
widthandretryCountpassed together to one function by accident. - Across a module boundary where you intentionally keep the API to primitives, such as a public HTTP or FFI interface. Introduce the type on each side of the boundary instead.
- Performance-critical numeric code that keeps parallel arrays for cache locality.
Agent checklist
- You confirmed the members belong together by applying the "delete one, do the others still make sense" test.
- The new type is immutable unless the domain requires mutation.
- Every occurrence of the clump is replaced, not only the one you happened to notice; search for the field names across the codebase.
- Behavior that operated only on the clump moved onto the new type.
- Persistence mapping (ORM columns, JSON keys) is unchanged or migrated explicitly, and tests confirm the stored shape.
Treatments
- Extract ClassSplit a class that does two jobs by moving one job's fields and methods into a new class it holds a reference to.
- Introduce Parameter ObjectReplace a group of parameters that always travel together with one object type, then move behavior on that group into it.
- Preserve Whole ObjectPass the object itself instead of several values pulled out of it, so the method signature stays stable as needs change.
See also
- Primitive ObsessionDomain concepts like money, emails, IDs or statuses are passed around as raw strings and numbers, so their rules get re-checked everywhere.
- Long Parameter ListA function takes so many parameters that callers mix up their order and every new option changes every call site.
- Large ClassA class with too many fields, methods or responsibilities, so every change risks breaking something unrelated inside it.
- Duplicate CodeThe same logic appears in two or more places, so every fix and change must be made several times and the copies drift apart.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.