SwadhyAI Notes

All Low-Level Design notes

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.

java
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.

java
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

  1. 01

    Start from the operation

    exit(TicketId) is the controller’s method. Do not start from “I need a FeeCalculatorUtil.”

  2. 02

    Ask who has the data

    Entry time lives on Ticket. Occupancy lives on Spot. The fee formula lives on neither — that is Strategy.

  3. 03

    Ask who must not know

    Repository must not price. Strategy must not occupy. Factory must not close. Entity must not talk SQL.

  4. 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.