SwadhyAI Notes

All Low-Level Design notes

GRASP · Creator

Creator

Creator answers one question: which object should construct another? Prefer the object that contains the new object, records it, closely uses it, or already has the data to build it. In a parking lot, a car enters and someone must create a Ticket. A static TicketUtil.create(...) that gathers the lot, spot, vehicle, and clock hides that ownership — six months later nobody knows who is allowed to mint a stay. ParkingLot records tickets, so ParkingLot creates Ticket.

Who naturally contains or records this thing?

Prefer the object that contains the new object, records it, closely uses it, or has the data needed to build it. ParkingLot records tickets, so ParkingLot creates Ticket. A Floor creates its spots. A Controller may create a command that it will push onto a history stack. A random Util class owns none of those collections, so it is a poor home for new.

This is not the Factory pattern yet. Factory appears when the concrete type itself varies — CompactSpot versus EvSpot, email versus SMS. Creator still picks who is allowed to call the factory. The lot may ask SpotFactory.make(type) because the lot owns the floor plan. See Factory Method.

new in a UI click handler, or in a DTO mapper, usually means the wrong object is creating. Creation is a domain decision: a stay starts, a spot is born with a floor. Keep that decision next to the owner. If two owners both feel right, pick the one that will hold the reference after construction. The object that forgets the child immediately was a poor creator.

Enter sequence

The lot records the stay, so the lot creates the ticket

  1. 1

    Vehicle arrives

    The gate (or API) has a Vehicle and a Clock. It does not new Ticket.

  2. 2

    Lot allocates a spot

    Allocation strategy chooses. The lot still owns the floor plan.

  3. 3

    Lot creates the ticket

    The lot records stays, so the lot constructs Ticket with id, vehicle, spot, time.

  4. 4

    Lot stores the reference

    tickets.add(ticket). The creator is the object that still holds the child.

Java: creation next to ownership

java
class ParkingLot {
  private final List<Ticket> tickets = new ArrayList<>();
  private final List<ParkingSpot> spots;
  private final TicketIdGenerator ids;
  private final SpotAllocationStrategy allocation;

  Ticket enter(Vehicle vehicle, Clock clock) {
    ParkingSpot spot = allocation.choose(freeSpots(), vehicle)
        .orElseThrow(() -> new IllegalStateException("lot full"));
    Ticket ticket = new Ticket(ids.next(), vehicle, spot.id(), clock.instant());
    spot.occupy(vehicle);
    tickets.add(ticket);
    return ticket;
  }
}

class Floor {
  private final List<ParkingSpot> spots = new ArrayList<>();

  Floor(int number, int compact, int large, SpotFactory spotsFactory) {
    for (int i = 0; i < compact; i++) {
      spots.add(spotsFactory.create(SpotType.COMPACT, SpotId.of(number, i)));
    }
    for (int i = 0; i < large; i++) {
      spots.add(spotsFactory.create(SpotType.LARGE, SpotId.of(number, compact + i)));
    }
  }
}

When Factory is Creator’s cousin

Spot type varies: compact, large, EV with charger. The floor still creates spots — it owns them — but it should not switch on enums in three places. SpotFactory.make(type, id) is the varying construction. Floor remains the Creator because Floor holds the list after construction.

When a factory is premature

Ticket has one class. ParkingLot new Ticket(...) is enough. A factory is for spots, notifiers, or handlers whose concrete class changes. YAGNI applies: do not invent TicketFactory until a second ticket type exists. That extra abstraction adds nothing until the type actually varies.

Java: a factory the owner still calls

java
interface SpotFactory {
  ParkingSpot create(SpotType type, SpotId id);
}

class DefaultSpotFactory implements SpotFactory {
  public ParkingSpot create(SpotType type, SpotId id) {
    return switch (type) {
      case COMPACT -> new ParkingSpot(id, SpotSize.COMPACT, false);
      case LARGE -> new ParkingSpot(id, SpotSize.LARGE, false);
      case EV -> new ParkingSpot(id, SpotSize.LARGE, true);
    };
  }
}

// Floor still creates (records) spots; the factory only picks the class.

TicketUtil as a dumping ground

Static createTicket methods gather arguments from everywhere and hide ownership. Six months later nobody knows whether the lot, the gate, or the API is allowed to mint a stay. Put new Ticket next to the object that will store it. If tests need tickets, they call lot.enter — the same path production uses — or they use a test fixture that still goes through the lot.

The UI creating tickets is how you end up with stays that were never added to the lot’s list. The mapper creating tickets is how you end up with two Ticket classes, one for JSON and one for the domain. Creation is not mapping. Mapping copies; Creator starts a lifecycle. See ownership and lifecycle.

Controllers create use-case results, not entities they will not keep

A ParkVehicle controller may construct its collaborators in a composition root, but it should not new Ticket if the lot is going to store Ticket. Let the lot enter(). The controller receives the system operation and returns the ticket the lot created. That keeps Creator and Controller from fighting over the same object.

Commands are different. A controller may new PlayMove in a game because the controller (or the game) will push that command onto a history stack — it records the command. The rule is consistent: whoever will hold it may create it.

Ids belong next to the creator too

TicketIdGenerator is data the lot needs to mint a stay. The lot may hold the generator as a collaborator and still be the Creator of Ticket. A static UUID.randomUUID() inside TicketUtil hides the policy for ids (sequential for receipts, UUID for distributed gates) in a place that does not record tickets. Pass the generator into the lot; the lot still writes new Ticket(ids.next(), ...).

java
interface TicketIdGenerator { TicketId next(); }

class SequentialIds implements TicketIdGenerator {
  private long n = 1;
  public synchronized TicketId next() { return new TicketId(n++); }
}

class ParkingLot {
  private final TicketIdGenerator ids;
  Ticket enter(Vehicle vehicle, ParkingSpot spot, Clock clock) {
    Ticket ticket = new Ticket(ids.next(), vehicle, spot.id(), clock.instant());
    tickets.add(ticket);
    return ticket;
  }
}

Spots are born with a floor, not in a mapper

JSON that says floor 2, compact, id A12 should not become a ParkingSpot in the HTTP layer. The floor (or a bootstrap that owns the floor plan) creates spots when the lot is built. Runtime requests occupy existing spots; they do not birth them. If a test needs a spot, it builds a Floor or uses a lot fixture, not new ParkingSpot in the assertion file — unless the test is specifically about Spot itself.

That rule keeps lifecycle honest. A spot that exists only in a DTO never got occupy/vacate invariants. Creator is how those invariants get a home: the object that will police the collection is the object that filled it.

Related

Creator versus the HTTP mapper

A JSON body may contain vehicle plate, type, and driver phone. The mapper’s job is to build a Vehicle value — data that already existed in the world — not a Ticket that starts a lifecycle. Tickets are born when the lot accepts the car. If the mapper news a Ticket, you will save stays that never occupied a spot, and occupy spots that never got a ticket. Those two objects have different birthdays. Creator keeps the birthdays next to the owners.

java
class VehicleRequest {
  String plate;
  String type;
  String phone;
  Vehicle toVehicle() {
    return new Vehicle(Plate.of(plate), VehicleType.valueOf(type), phone);
  }
}

class ParkingHttp {
  private final ParkVehicle park;
  TicketResponse postEntry(VehicleRequest body) {
    Ticket ticket = park.handle(body.toVehicle(), Clock.systemUTC());
    return TicketResponse.from(ticket);
  }
}
// Mapper creates Vehicle (a value). Lot creates Ticket (a stay).

Who creates Money? Usually nobody interesting

Money.zero() and Money.of(rupees) are value constructors. Any expert may call them. Creator is about lifecycle objects — tickets, spots, payments with ids — not about every new in the file. Do not invent MoneyFactory. Do invent a home for Payment if payment is a recorded entity the ledger will hold.

java
class Money {
  static Money zero() { return new Money(BigDecimal.ZERO); }
  static Money of(BigDecimal amount) { return new Money(amount); }
  Money times(long n) { return new Money(amount.multiply(BigDecimal.valueOf(n))); }
  Money plus(Money other) { return new Money(amount.add(other.amount)); }
}

Tests create through the owner

lot.enter in a test is not slower than new Ticket; it is honest. You also occupy the spot and record the stay, so assertions about free count stay true. A test that news Ticket and never adds it to the lot will pass until someone asks how many open stays exist. Creator in tests is the same rule as Creator in production: the object that holds the collection fills the collection.

Payment records have an owner too

If you persist payments, the ledger or PayTicket controller that will hold Payment creates it after the gateway returns an id. Ticket should not new Payment just because it knows the amount. Ticket may store paymentId as a value. The recorded Payment lives with the ledger. Same Creator rule, different noun.

new in a static helper hides the owner

TicketFactory.create static methods feel neat until two call sites mint stays the lot never stored. Move new Ticket into ParkingLot.enter. If you still want a factory, the lot calls it. Static helpers are not Creators; they are missing owners. Prefer a method on the object that will hold the list. If the helper also occupies the spot, it has stolen the lot’s job as well as its constructor.

Takeaway

The object that contains, records, or closely uses another object should create it. ParkingLot creates Ticket because it stores tickets. Floor creates spots, optionally through a factory when the type varies. TicketUtil and UI click handlers are not owners. Factory is a cousin, not a replacement for this question — and a factory for a single concrete type is a premature abstraction.