Sit the verb on the facts
Put behavior where the data lives
Modeling behavior means putting operations next to the data they protect — not only in static helper classes. Example: occupy lives in ParkingHelper, calculateFee in FeeUtils, isValidEmail in StringUtil, while Ticket, Spot, and Money sit still like empty forms. When the hourly rule changes, you hunt three helpers. The data never enforced its own rules.
Which object already has the facts?
Put the method next to the data. Spot knows if it is free. That is occupy and release. Ticket knows entry time and status. That is duration and pay. Money knows paise and currency. That is plus and times. This is in plain clothes. You do not need the GRASP name to use it. Ask: if I put this method here, does this object already know enough? If not, you are about to pass five arguments into a util.
Some behavior needs a the entity should not swallow. Hourly versus airport pricing does not belong as a switch on Ticket. The ticket still knows its times. It can answer duration. A pricing object turns duration into Money. Each piece has the facts it needs.
| The data | The verb | The home |
|---|---|---|
| Free flag + occupant | occupy / release | ParkingSpot |
| Entry time + status | duration / pay / close | Ticket |
| Paise + currency | plus / times | Money |
| Rate table + rounding | feeFor | PricingStrategy |
| Free spots + vehicle | choose | SpotAllocationStrategy |
| Email string | validate / normalize |
Name the data
Free flag, times, paise, email string.
Name the verb
Occupy, duration, plus, validate.
Sit them together
If one object has the data, it gets the method.
Split only when two owners appear
Then a policy or a domain service, not a Helper.
The spot already knows if it is free
class ParkingSpot {
private SpotStatus status = SpotStatus.FREE;
private Vehicle occupant;
boolean tryOccupy(Vehicle vehicle) {
if (status != SpotStatus.FREE || !fits(vehicle)) return false;
occupant = vehicle;
status = SpotStatus.OCCUPIED;
return true;
}
void release() {
occupant = null;
status = SpotStatus.FREE;
}
boolean fits(Vehicle vehicle) {
return type.fits(vehicle.size());
}
}
// Avoid: ParkingHelper.occupy(spot, car)Helper and Util dumping grounds
A means the next intern will add another helper. Occupancy is not a . It is the spot’s life. FeeUtils.calculate that takes eight primitives is the same smell with a calculator name.
Thin entities on purpose
“Keep the model free of logic” pushes every rule into services. Then two services disagree. That is how you get an anemic model. Behavior belongs with the data.
When the facts are split
Sometimes no single object has everything. Pricing needs duration (ticket) and a rate table (policy). Transfer needs two wallets. Matching needs a rider and many drivers. That is when you introduce a small domain service or a strategy — not ParkingHelper2. The test is still the same: after the split, does each method sit on an object that has its facts? Ticket.duration stays on Ticket. feeFor stays on the policy.
Read domain services for the coordinator case, and Information Expert when you want the principle named. The move is older than the acronym.
Ticket answers duration. Policy answers money.
class Ticket {
private final Instant entry;
StayDuration duration(Instant now) {
return StayDuration.between(entry, now);
}
}
interface PricingStrategy {
Money feeFor(StayDuration parked, Vehicle vehicle);
}
final class HourlyPricing implements PricingStrategy {
private final Money perHour;
public Money feeFor(StayDuration parked, Vehicle vehicle) {
long hours = parked.billableHours();
return perHour.times((int) hours);
}
}Utils that are allowed to stay small
Parsing a plate format, converting a wire DTO, formatting a receipt string — these can live at the edge. They do not own occupancy. They do not decide legal status transitions. If a “util” method needs a Spot and a Vehicle and changes both, it is not a util. It is a missing method on Spot.
The interview habit
After you name a noun, name one verb it already has the data for. Spot → occupy. Ticket → pay. Money → plus. If you cannot name that verb, the noun is a DTO and the verb is about to hide in a helper. Fix it before you draw Strategy.
Email validation is not StringUtil’s job
Three services will write three different contains("@") checks. One will forget to lowercase. One will accept a blank. The Email constructor is the method. After that, User does not validate. Notification does not validate. The data already did.
class User {
private final UserId id;
private Email email;
void changeEmail(Email next) {
this.email = Objects.requireNonNull(next);
}
}
// Avoid: StringUtil.isValidEmail(user.getEmailString())Board.winner belongs on the board
TicTacToeHelper.checkWinner(cells) is a for-loop that the board already has the data for. Game can ask board.winner() after place(). The helper will grow diagonal rules in a second file while the board’s snapshot drifts. Sit the verb on the grid. The game object then only sequences turns — which is data it actually has.
Five arguments is a clue
FeeUtils.calculate(entry, exit, vehicleType, isEventDay, coupon, lotId, gstCode) is a method that is screaming that the facts are scattered. Some of those facts belong on Ticket. Some belong on a PricingContext value. Some belong on a policy. After you group them, the util disappears. The remaining feeFor(duration, vehicle) is small because the objects already know their parts.
This is also how you avoid “pass the whole ParkingLot into a helper.” If the helper needed the lot, the method probably belonged on the lot — or on a strategy the lot already holds.
Vending machine stock is not VendingUtils
decrementStock(slot, qty) on a helper means Slot did not own count. Slot.dispense() already has the count and the product. The machine sequences payment and then asks the slot. Payment is Money on a CoinBox that knows its contents. Three objects, three verbs, no util. The helper version will grow refund and change in the same file until nobody can test a slot alone.
When you feel the urge to open a *Utils class, name the data the method needs. If one type has it, put the method there. If two types have it and should not meet, you want a domain service. Utils is the third option only for true conversions at the edge.
If a method’s first three lines extract fields from an argument to do work, the work belongs on that argument. ticket.getEntry(), ticket.getExit(), then math — that math is Ticket.duration. Stop unpacking.
A habit for the first ten minutes
After each noun, write one verb in the same box. If a box has no verb, it is a DTO so far. If a verb has no box, it is about to become a helper. Fill both before you reach for Strategy. Strategy is a verb that already had a home and then needed to vary.