SwadhyAI Notes

All Low-Level Design notes

Two gates

Validation at the boundary

Validation means rejecting bad input at a clear boundary, then trusting typed objects inside the domain. Example: VehicleType arrives as a string from the kiosk. Three methods each check null plate, empty plate, unknown type. One of them forgets. Validate at the controller or , then let Ticket assume it was given a real Vehicle.

Gate 1 — the edge

: missing fields, bad enums, plates that are blank. After that you have a Vehicle. This is the last place a bad string is allowed to exist. .

  • Blank plate → IllegalArgumentException (or a named input error)
  • Unknown type token → VehicleType.parse throws
  • Controller maps ParkRequest → Vehicle

Gate 2 — the entity

requireNonNull on ids. A spot holds at most one car. close() checks status. These are invariants, not regex. close() does not also check that the plate matches a pattern.

  • Ticket constructor rejects null Vehicle
  • Spot.tryOccupy fails if occupied
  • InvalidTransition on CLOSED→ACTIVE

Where the same null check dies

Do not sprinkle the same null checks through ParkingLot, Pricing, and the repository. If the factory throws on unknown type, the lot never sees a raw string. Pair this with input/output models and named errors. Value objects like Plate belong in entity vs value object.

Java: parse once, then trust the type

java
class VehicleFactory {
  Vehicle from(String plate, String rawType) {
    if (plate == null || plate.isBlank()) {
      throw new IllegalArgumentException("plate");
    }
    VehicleType type = VehicleType.parse(rawType); // throws if unknown
    return new Vehicle(new Plate(plate.trim()), type);
  }
}

class Ticket {
  Ticket(TicketId id, Vehicle vehicle, SpotId spot, Instant entry) {
    this.id = Objects.requireNonNull(id);
    this.vehicle = Objects.requireNonNull(vehicle);
    this.spotId = Objects.requireNonNull(spot);
    this.entry = Objects.requireNonNull(entry);
  }
}

The factory is the last string

After Vehicle exists, ParkingLot.park(Vehicle) does not check plate != null. That check already happened. If a test constructs Vehicle directly, it uses a legal Plate. Tests of the factory use the dirty strings. Tests of the lot use clean types. Validating once at construction means you never copy the same plate check into every method.

Java: VehicleType.parse

java
enum VehicleType {
  CAR, BIKE, BUS, EV;

  static VehicleType parse(String raw) {
    if (raw == null) throw new IllegalArgumentException("type");
    try {
      return VehicleType.valueOf(raw.trim().toUpperCase());
    } catch (IllegalArgumentException ex) {
      throw new UnknownVehicleType(raw);
    }
  }
}

Two mistakes

The same null check in every method

If park, exit, and fee all test plate != null, the real type is still String. Introduce Plate or Vehicle at the factory.

Entity parsing HTTP

Ticket.fromJson(map) puts the boundary in the middle of the domain. Validate outside. Construct inside with real types.

Security cousin, not a substitute

Authn, authz, and injection defenses live in API security. LLD validation is still: invalid strings are rejected at the boundary; invariants live on Ticket. Do not skip Plate because “the gateway already validated.” The lot must stay honest if a test or another process constructs objects.

What each layer still does

Kiosk / HTTP
  missing JSON fields
        │
        ▼
VehicleFactory / controller     ← last raw String
  blank plate, unknown type
        │  Vehicle
        ▼
ParkingLot.park(Vehicle)        ← allocation, capacity
        │
        ▼
Ticket / Spot                   ← occupy once, close once
        │
        ▼
TicketRepository                ← persist; no plate regex

Invariants are not regex

Plate format is input. “A spot holds one car” is an invariant. “Fee is never negative” is an invariant if that is the rule. Putting the regex on Ticket.close() means every close re-validates a plate that was already a Plate. Putting occupy rules only on the factory means a test that constructs Spot directly can double-park. Edge validation is for strings. Entity guards are for the life of the object.

An untyped process(Object) method usually validates inside the blob with a pile of ifs. You cannot tell which ifs are parsing and which are “lot is full.” Split them: factory for strings, park for capacity, Ticket.close for status. Three places, three kinds of failure, three tests.

Java: Plate rejects blank once

java
final class Plate {
  private final String value;

  Plate(String raw) {
    if (raw == null || raw.isBlank()) throw new IllegalArgumentException("plate");
    this.value = raw.trim().toUpperCase();
  }

  String value() { return value; }

  @Override public boolean equals(Object o) {
    return o instanceof Plate p && value.equals(p.value);
  }

  @Override public int hashCode() { return value.hashCode(); }
}

Interview split

“Invalid strings are rejected in VehicleFactory. ParkingLot.park assumes a Vehicle. Ticket still refuses to close twice.” If they ask about SQL CHECK constraints, agree they can exist, and still keep Plate. Constraints in the database are a second gate, not a reason to skip the type.

Exercises

  1. Where is the last place a bad vehicle-type string is allowed? Name the class and the method.
  2. Ticket.close() still checks status after the factory succeeded. Why is that not duplicate validation?
  3. A teammate adds plate.matches("[A-Z0-9]+") inside PricingStrategy. Where should that rule move?
ErrorsInput / outputValue objects

Worked scene: three methods, one forgotten check

park, fee, and notifySms each test plate != null. notifySms is written last and skips the test. A blank plate from a buggy kiosk sends an SMS with no plate and parks anyway. VehicleFactory.from would have thrown before park ran. The forgotten check cannot exist if the type does not exist. That is the whole argument for parse-once.

Ticket.close still checks ACTIVE. That is not a forgotten plate check; it is an invariant that remains true after a legal Vehicle was built. Mixing those two in one validate() on ParkingLot is how the lot becomes a god object that re-parses JSON and also runs the state machine.

java
class ParkingLot {
  Ticket park(Vehicle vehicle) {
    // vehicle is already legal — no plate regex here
    return allocation.open(vehicle, clock.instant());
  }
}

How to explain this in an interview

Blank plate

Plate / VehicleFactory. Last raw string. Not PricingStrategy.

Unknown type token

VehicleType.parse. The lot never sees "carx".

Close twice

Ticket.close invariant. Not a regex. Not JSON.

Same null check thrice

You are missing a type. Introduce Plate or Vehicle.

In about a minute, say: “Invalid strings are rejected in VehicleFactory. park(Vehicle) assumes a Vehicle. Ticket.close still rejects CLOSED→ACTIVE. That is not duplicate validation — it is a different gate.”

Plate regex does not belong on PricingStrategy. SQL CHECK can exist and still not replace Plate. An untyped process(Object) method piles parsing and capacity into one blob of ifs; split them.

Java: close still guards after a legal Vehicle

java
Receipt close(Instant exit, PricingStrategy pricing) {
  if (status != TicketStatus.ACTIVE) {
    throw new InvalidTransition(status, TicketStatus.CLOSED);
  }
  status = TicketStatus.CLOSED;
  return new Receipt(id, pricing.fee(this, exit));
}

Takeaway

Parse once at the factory. Trust Vehicle inside the lot. Entities still reject illegal transitions — they do not re-parse JSON. If the same null check appears in three methods, you are missing a type.