The Class That Resists a Noun
The Class That Resists a Noun
The Smell
The logistics platform has a class called ShipmentContext. A developer on the team is asked: “What is a ShipmentContext?” Their answer takes two minutes. It holds the shipment data, the carrier configuration, the customer preferences, the rate calculation result, the current tracking status, and the billing state. It is passed through six method calls as a parameter, accumulating data at each stage. No method uses all of its fields. Most methods use two or three fields and ignore the rest.
The word “Context” is a naming surrender. It means “a bag of stuff related to the general topic.” The developer could not name this class ShipmentWithCarrierAndRateAndCustomerAndBilling because that would make the design problem too visible. So they named it Context, which hides the problem behind a respectable-sounding word.
The Cognitive Cost
When a method takes a ShipmentContext parameter, the reader must determine which of its fifteen fields the method actually uses. The method signature says: “I need everything.” The implementation says: “I need three fields.” The gap between the declared dependency and the actual dependency is pure extraneous cognitive load.
A method signature with ShipmentContext context communicates nothing about what the method needs. A method signature with CarrierConfig carrier, ShipmentDimensions dimensions communicates exactly what the method needs. The reader does not need to open the method to understand its dependencies. The parameter names are the documentation.
In the logistics codebase, ShipmentContext is passed to 23 methods across 8 classes. At each call site, the reader pays the cost of uncertainty: “Which parts of the context does this method read? Which parts does it modify? Can I safely change a field without breaking this method?” Those questions are unanswerable without reading each of the 23 method implementations.
The Before
// HARD TO READ: ShipmentContext holds data from five different subdomains.
// No method uses all fifteen fields. Every method receives data it doesn't need.
public class ShipmentContext {
// Shipment data
private Long shipmentId;
private Address origin;
private Address destination;
private List<ShipmentItem> items;
private BigDecimal totalWeight;
// Carrier data
private Long carrierId;
private String carrierName;
private CarrierConfig carrierConfig;
// Rate data
private BigDecimal calculatedRate;
private String rateType;
private List<Surcharge> appliedSurcharges;
// Customer data
private Long customerId;
private NotificationPreference notificationPref;
// Billing data
private InvoiceStatus invoiceStatus;
// 15 getters and 15 setters omitted
}
// Usage: every method receives the entire context
public class RateCalculationService {
public void calculateRate(ShipmentContext context) {
// Uses: origin, destination, items, totalWeight, carrierConfig
// Ignores: shipmentId, carrierId, carrierName, calculatedRate,
// rateType, appliedSurcharges, customerId,
// notificationPref, invoiceStatus
BigDecimal rate = /* calculation using 5 of 15 fields */;
context.setCalculatedRate(rate); // Mutates the context
context.setRateType("STANDARD"); // Mutates the context again
}
}
The Fix
// READABLE: Each concept is a separate type.
// Method signatures declare exactly what they need.
// No method receives data it does not use.
public record ShipmentRoute(Address origin, Address destination) {}
public record ShipmentCargo(List<ShipmentItem> items, BigDecimal totalWeight) {}
public record ShippingQuote(BigDecimal rate, String rateType,
List<Surcharge> appliedSurcharges) {}
// The rate calculator declares its actual dependencies.
// A reader knows what it needs without opening the implementation.
public class RateCalculationService {
public ShippingQuote calculateRate(ShipmentRoute route,
ShipmentCargo cargo,
CarrierConfig carrier) {
BigDecimal rate = /* calculation using route, cargo, carrier */;
List<Surcharge> surcharges = /* surcharge logic */;
return new ShippingQuote(rate, "STANDARD", surcharges);
}
}
The refactoring replaces one fifteen-field mutable class with three immutable records, each holding a coherent concept. ShipmentRoute is the origin and destination. ShipmentCargo is what is being shipped. ShippingQuote is the rate calculation result. Each name was obvious once the concepts were separated.
The ShipmentContext class resisted a noun because it was not one thing. It was five things taped together. The name “Context” was the only word vague enough to cover all five. Once split into concepts that each have a clear name, the need for a “Context” class disappears.
The method signature transformation is the key improvement. calculateRate(ShipmentContext context) tells the reader nothing. calculateRate(ShipmentRoute route, ShipmentCargo cargo, CarrierConfig carrier) tells the reader everything. The signature is the documentation. No reader needs to open the implementation to understand the dependency surface.
The Rule
If a class name uses “Context,” “Info,” “Data,” or “Details” as a suffix, and the class has more than five fields, it is a bag of unrelated concepts that should be split into records grouped by cohesion. The test: describe the class in one sentence without using “and.” If you cannot, the class contains multiple concepts that deserve separate types. Split until each type can be described in one sentence, and the name of each type will be obvious.