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