SwadhyAI Notes

All Low-Level Design notes

Extra pattern · not GoF

Dependency injection

means collaborators arrive from outside — usually through the constructor. The class uses Clock and TicketRepository; it does not construct them. That creates a tests can replace.

Without injection, ParkingLot calls Instant.now() inside park() and new TicketRepository() inside the constructor. Tests cannot freeze time or use a HashMap store. Pass Clock and TicketRepository in instead: production wires real ones; tests wire fakes.

Who creates the clock, the store, and the strategy — the class, or the caller?

Hidden new

Instant.now() and a concrete store inside the domain. Tests cannot freeze time or swap the store. The class created its own world.

Constructor arguments

Clock, TicketRepository, SpotAllocationStrategy arrive from the . No framework. No annotations.

Hidden new — tests cannot freeze time

java
class ParkingLot {
  Ticket park(Vehicle vehicle) {
    Instant now = Instant.now();           // cannot fake
    Ticket ticket = new Ticket(vehicle, now);
    tickets.put(ticket.id(), ticket);      // cannot swap store
    return ticket;
  }
}

Clock and repository injected

java
class ParkingLot {
  private final Clock clock;
  private final TicketRepository tickets;
  private final SpotAllocationStrategy allocation;

  ParkingLot(Clock clock, TicketRepository tickets, SpotAllocationStrategy allocation) {
    this.clock = clock;
    this.tickets = tickets;
    this.allocation = allocation;
  }

  Ticket park(Vehicle vehicle) {
    ParkingSpot spot = allocation.choose(freeSpots(), vehicle).orElseThrow();
    Ticket ticket = new Ticket(TicketId.newId(), vehicle, spot.id(), clock.instant());
    tickets.save(ticket);
    return ticket;
  }
}

new Ticket(...) is fine — the lot owns that value. Instant.now() is not. The store is not. Allocation is a Strategy you inject so tests can force spot 101.

What to inject

Talks to the world vs values you own

Inject these seams

  • Clock / time
  • Repository / store
  • Random / id generator
  • Network, mailer, payment port
  • Strategies, policies, registries

new these yourself

  • Ticket the lot just created
  • Money, TicketId, other value objects
  • A local ArrayList you own
  • A DTO you are about to return

This is DIP in the constructor

Dependency inversion says depend on abstractions. Injection is how the abstraction arrives. ParkingLot depends on Clock and TicketRepository, not on Instant.now() and a concrete JDBC class. The chooses the implementations. That is also deterministic design: given the same clock and store, park() is repeatable.

new Date() / Instant.now() inside the domain

Fees depend on time. If park() reads the system clock, a two-hour stay is not a unit test. Inject Clock. Tests pass Clock.fixed(...).

Service locator instead of a constructor

A hides the dependency and shares globals across tests. Pass the repository in. The wiring lives in one main() or a tiny factory — not getInstance().

What a test looks like

java
@Test
void twoHourStayChargesHourlyRate() {
  Clock clock = Clock.fixed(Instant.parse("2026-08-13T10:00:00Z"), ZoneOffset.UTC);
  TicketRepository tickets = new InMemoryTickets();
  ParkingLot lot = new ParkingLot(clock, tickets, new FirstFreeSpot());

  Ticket t = lot.park(new Car("KA-01-AA-0001"));
  // advance time in a mutable test clock, or inject a second Instant at exit
  Receipt r = lot.exit(t.id(), Instant.parse("2026-08-13T12:00:00Z"));

  assertEquals(Money.of(80, "INR"), r.fee());
}

Related seams on this track

Test doubles are what you inject. Registry and policy objects are collaborators too — construct them at the edge, pass them in. Injection is the wiring, not a fourth GoF family.

Id generators are a seam too

UUID.randomUUID() inside park() makes assertions on ticket ids painful. Inject an IdGenerator. Tests return “T-1”. Production returns a UUID. Same move as Clock.

java
interface IdGenerator {
  TicketId nextTicketId();
}

class ParkingLot {
  private final IdGenerator ids;
  Ticket park(Vehicle vehicle) {
    Ticket ticket = new Ticket(ids.nextTicketId(), vehicle, spot.id(), clock.instant());
    tickets.save(ticket);
    return ticket;
  }
}

main() is the composition root

You do not need Spring. A thirty-line main that news the clock, the store, the strategies, and the lot is the whole framework. Tests skip main and news a smaller graph.

java
public final class ParkingApp {
  public static void main(String[] args) {
    Clock clock = Clock.systemUTC();
    TicketRepository tickets = new InMemoryTickets();
    SpotAllocationStrategy allocation = new FirstFreeSpot();
    PricingRegistry pricing = new PricingRegistry();
    pricing.register(VehicleType.CAR, new HourlyPricing());
    ParkingLot lot = new ParkingLot(clock, tickets, allocation);
    new EntryController(lot, new RegistryVehicleFactory()).listen();
  }
}

Constructor injection vs setters

Prefer constructor arguments. A lot that is half-wired — clock set, store still null — will throw in park() instead of at construction. Required collaborators belong in the constructor. Optional ones are rare in LLD; if you have one, a Null Object logger is cleaner than a nullable setter.

Related pages