Behavioral modeling
Assigning responsibilities
Responsibility assignment answers “which object should own this verb?” Put knowledge and action together. A type that only holds data while a god service does everything is usually the wrong split.
Example: who calculates the parking fee? The ticket knows entry time. The spot knows occupancy. The lot knows floors. A knows the pricing rule. Answering “the service” for every verb hides the design.
Six pockets
A sorting hat, not a junk drawer
Use six pockets. If you cannot pick one, you probably have two verbs glued together. Split the verb, then sort again. This is GRASP applied to the parking-lot cast — see GRASP and Controller for the principle names.
entity
occupy / refuse CLOSED→ACTIVE
Spot, Ticket
service
span two entities
only if neither owns the facts
repository
save / load ticket
TicketRepository
strategy
choose spot / compute fee
Allocation, Pricing
controller
park / exit
ParkingService
factory
new CompactSpot()
SpotFactory
What each pocket is allowed to know
An protects its own data. Ticket will not let you close it twice. Spot is occupied by at most one vehicle. A coordinates more than one entity when no single entity has all the facts. A loads and saves; it does not decide fees. A holds an algorithm that will change. A is the front door for one use case. A creates.
Fee calculation is the classic split. PricingStrategy has the policy. Ticket has entry time and vehicle type. The controller asks them at exit. Spot does not know about money. ParkingLot does not know about GST. TicketRepository does not compute anything; it remembers.
Verb → home
occupy / release
Entity (Spot)
reject CLOSED → ACTIVE
Entity (Ticket)
choose a free spot
Strategy
compute the fee
Strategy + Ticket data
save / load ticket
Repository
park / exit
Controller
new CompactSpot()
Factory
God service: all of the above in ParkingService.
Healthy
Thin controller
ParkingService calls the lot, the strategy, and the repository. Ticket.pay and Ticket.close still live on the entity. The service is a door, not a warehouse.
Smell
God service
One class allocates spots, calculates tax, writes SQL, and sends SMS. The name is polite. The file is the application. Every follow-up — airport pricing, EV spots — edits that file.
A thin controller is not a God service
Read this as a walk across pockets, not as “the service does exit.” Repository remembers. Strategy decides. Entity guards state. Lot coordinates spots. Controller sequences the use case.
class ParkingService { // controller — use-case door
private final ParkingLot lot;
private final PricingStrategy pricing;
private final TicketRepository tickets;
Receipt exit(TicketId id, Instant now) {
Ticket ticket = tickets.get(id); // repository remembers
Money fee = pricing.fee(ticket, now); // strategy decides
ticket.pay(fee); // entity guards CREATED→…→PAID
tickets.save(ticket);
lot.release(ticket.spotId()); // lot coordinates spots
ticket.close(); // PAID → CLOSED; CLOSED→ACTIVE impossible
tickets.save(ticket);
return new Receipt(id, fee);
}
}
// Smell: one class that also sends SMS, writes CSV, and switches on VehicleType.Ticket owns the illegal arrow
A webhook retry is not a reason to put setStatus on the repository or the controller. CLOSED → ACTIVE is an invariant of Ticket. The entity throws. The controller reports the error. The repository stores whatever legal status the entity already has. If you “fix” a closed stay by assigning ACTIVE in the service, you hid the machine in the wrong pocket.
class Ticket {
private TicketStatus status = TicketStatus.CREATED;
void activate() { require(TicketStatus.CREATED); status = TicketStatus.ACTIVE; }
void pay() { require(TicketStatus.ACTIVE); status = TicketStatus.PAID; }
void close() { require(TicketStatus.PAID); status = TicketStatus.CLOSED; }
private void require(TicketStatus expected) {
if (status != expected) {
throw new IllegalStateException(status + " cannot move from here");
}
}
}How to sort in the room
- 01
Start from the operation
exit(TicketId) is the controller’s method. Do not start from “I need a FeeCalculatorUtil.”
- 02
Ask who has the data
Entry time lives on Ticket. Occupancy lives on Spot. The fee formula lives on neither — that is Strategy.
- 03
Ask who must not know
Repository must not price. Strategy must not occupy. Factory must not close. Entity must not talk SQL.
- 04
Name the leftover pile
If three verbs still sit in ParkingService after the sort, you still have a God service. Split again.
Common mistakes
ParkingService that does everything
If the class allocates, prices, persists, and notifies, it is a God service wearing a polite name. Split by the six pockets. Keep the service as the use-case door. See design smells.
Putting the fee on the spot — or on nobody
Spot knows occupancy, not GST. Ticket knows time, not the airport surcharge. If you cannot name the pocket, do not dump the verb on “the service” by default.
Factory that also closes tickets
Creation is one job. Closing is another. A factory that knows about payment and gates stole the controller’s door and the entity’s lifecycle.
Walk through one fee calculation
“Ticket has entry and vehicle. PricingStrategy has the formula. Controller asks both at exit. Repository only saves the PAID ticket. Spot never hears about money.” That paragraph is the design. Pattern names can wait until extensibility points.
Read next
Takeaway
Entity, Service, Repository, Strategy, Controller, Factory. Sort every verb into one pocket. A thin ParkingService is a controller. A ParkingService that also prices, persists, and texts is a God service. Illegal transitions stay on the entity.