SwadhyAI Notes

All Low-Level Design notes

Translate at the boundary

Request and response vs domain objects

Input and output models sit at the edge: they parse HTTP or UI payloads into domain types, and shape responses back out. They are not the domain itself. Example: the HTTP body looks like { "plate": "MH12", "type": "car" }. If Ticket is that JSON class, your entity now has annotations, setters for every field, and no place for close().

Layer sandwich

The lot never sees the body

ParkRequest (wire)
Controller maps
Vehicle → park()
ParkResponse (wire)

Request and response models are . Domain objects are shaped like the problem: identity, invariants, behavior. A ParkRequest becomes a Vehicle at the controller. A Receipt is what exit returns; ParkResponse is what the kiosk prints.

Java: translate at the boundary

java
final class ParkRequest {
  final String plate;
  final String vehicleType;
}

final class ParkResponse {
  final String ticketId;
  final String spotId;
}

class ParkingController {
  private final ParkingLot lot;
  private final VehicleFactory vehicles;

  ParkResponse park(ParkRequest request) {
    Vehicle vehicle = vehicles.from(request.plate, request.vehicleType);
    Ticket ticket = lot.park(vehicle);
    return new ParkResponse(ticket.id().value(), ticket.spotId().value());
  }
}

They may look similar

Plate strings appear on both sides. That is allowed. They are not allowed to be the same class once the JSON grows a uiColor. is the tax you pay so Ticket.close() never learns Jackson.

In-process interviews still have a boundary

You may not have HTTP. The method argument is still a : Vehicle, not a Map. The entity is what you store. Tests construct Vehicle, not ParkRequest, unless they are testing the controller.

Do not let the lot take the DTO

lot.park(ParkRequest) pulls HTTP into the lot. Parse at the controller. Validate there too — see validation at the boundary. The deeper split between bags of fields and objects with rules is DTO vs domain. REST field names and status codes belong to REST API design; they should not become TicketStatus ordinals by accident.

Java: responses are snapshots

java
final class TicketView {
  final String id;
  final String status;
  final String plate;
  final String enteredAt;

  static TicketView from(Ticket ticket) {
    return new TicketView(
        ticket.id().value(),
        ticket.status().name(),
        ticket.vehicle().plate().value(),
        ticket.entry().toString());
  }
}

class ParkingLot {
  Ticket park(Vehicle vehicle) { /* domain in, domain out */ }
}

Two leaks

Ticket is the JSON class

Once Ticket has setters for every wire field, anyone can set status to CLOSED. Keep the wire type dumb. Keep the entity strict.

Domain method taking a request DTO

The lot now “knows” plate as a raw string and vehicleType as a magic token. Factory + Vehicle is the boundary. park(Vehicle) is the API.

What belongs on each type

TypeOwnsMust not own
ParkRequestRaw strings from the kioskSpot allocation, fees
VehiclePlate, type, fit rulesJSON keys, HTTP status
TicketIdentity, status, close()uiColor, toJson for the app
ParkResponseIds the kiosk printsLive Ticket reference

Tests tell you which type you meant

Controller tests feed ParkRequest with a blank plate and expect an input error. Lot tests construct Vehicle and expect SpotUnavailable. If both tests construct the same class, you have collapsed the boundary. Keep ParkRequest out of ParkingLotTest. Keep Vehicle out of JSON fixtures unless you are testing the mapper.

An untyped process(Object) method makes this impossible: every test is a map of strings, and you cannot tell whether you are testing parsing or allocation. park(Vehicle) vs ParkResponse park(ParkRequest) is two tests, two layers, two failures.

Java: exit maps Receipt to a wire type

java
final class ExitResponse {
  final String ticketId;
  final String amount;
  final String currency;

  static ExitResponse from(Receipt receipt) {
    return new ExitResponse(
        receipt.ticketId().value(),
        receipt.fee().toDisplay(),
        receipt.fee().currency());
  }
}

class ParkingController {
  ExitResponse exit(String ticketId) {
    Receipt receipt = lot.exit(new TicketId(ticketId));
    return ExitResponse.from(receipt);
  }
}

What belongs in the mapper

String to Plate, string to VehicleType, Ticket to ids and display times. Not allocation. Not pricing. Not SQL. If the mapper calls tickets.save, it is no longer a boundary mapper — it is a hidden lot. Keep mapping boring. That boredom is the design.

Exercises

  1. The UI wants a hex color per vehicle type. Where does uiColor live, and what does park still return?
  2. Write the one-line mapping from ParkRequest to Vehicle. Where do blank plates get rejected?
  3. Why is returning Ticket from the controller a leak even if it “saves a DTO class”?
DTO vs domainValidationClass API

Worked scene: uiColor arrives

Product wants cars drawn blue and bikes green. That field is on ParkResponse or a VehicleTypeView, not on Ticket. park still returns Ticket to the lot, ParkResponse to the kiosk. If Ticket grew uiColor, close() and JSON annotations would share a class. The mapper is allowed to look up a color table. The lot is not.

In-process tests construct Vehicle directly. HTTP tests construct ParkRequest. Crossing those tests is how the boundary disappears. An untyped process(Object) method has only one test, and it cannot tell parsing from allocation.

How to explain this in an interview

ParkRequest vs Vehicle

Strings at the controller. Vehicle in the lot. Never lot.park(request).

Ticket vs ParkResponse

Ticket stays inside. Response carries ids the kiosk prints.

uiColor

View or a lookup table. Not a field on Ticket.close’s class.

In-process round

Still pass Vehicle, not Map. The boundary exists without HTTP.

Takeaway

Wire types are dumb on purpose. Domain types are strict on purpose. Map at the controller. ParkingLot.park takes a Vehicle, never a JSON bag. Tickets do not grow toJson because the UI asked for a color.