SwadhyAI Notes

All Low-Level Design notes

Bag vs object

DTO vs domain model

A DTO is a data bag for transport. A domain object owns behavior and invariants. Do not use one class for both jobs. Example: TicketDto has public fields, getters, setters, and no methods. Someone uses it as the ticket. Fees are computed in ParkingService by reading dto.entry and dto.exit. That is an dressed as a DTO.

DTO — museum replica

A : fields the UI needs. You can print it, serialize it, send it across a process. You cannot close it. Changing a field on the replica does not change the lot.

  • Public or simple accessors
  • No InvalidTransition
  • Allowed to look like JSON

Domain — the real ticket

Identity, invariants, . Private status. close() is the only way to CLOSED. This object never leaks to the kiosk as a live reference.

  • Ticket.close(), Spot.tryOccupy()
  • Money as a value object
  • No Jackson annotations required

Java: bag vs object

java
// DTO — snapshot for the edge
final class TicketView {
  public final String id;
  public final String status;
  public final String plate;

  TicketView(String id, String status, String plate) {
    this.id = id;
    this.status = status;
    this.plate = plate;
  }
}

// Domain — behavior and invariants
class Ticket {
  private TicketStatus status;

  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));
  }
}

One class, three jobs

Ticket with @Column, @JsonProperty, and close() will change for three reasons: schema, wire, and rules. Split the snapshots. Keep the behavior. The persistence twin of this page is domain vs persistence model. The edge twin is request/response models. Rich vs anemic is rich vs anemic.

Services that only call getters

ParkingService reading ticket.getEntry() and writing ticket.setFee() is the anemic model. Ask ticket.close(clock, pricing). Application services still exist: they load the aggregate, call the command, save. They do not steal close().

Java: map out, command in

java
class ParkingService {
  private final ParkingLot lot;

  TicketView view(TicketId id) {
    Ticket ticket = lot.find(id).orElseThrow(() -> new UnknownTicket(id));
    return new TicketView(ticket.id().value(), ticket.status().name(), ticket.plate().value());
  }

  Receipt exit(TicketId id) {
    return lot.exit(id); // domain command, not dto.setStatus("CLOSED")
  }
}

Mistakes

  • One class for JSON, SQL, and rules — three reasons to change. Split the snapshots.
  • Service does all the work on getters — that is anemic. Put close() on Ticket.
  • Returning the live Ticket as a “DTO” — the kiosk can call close() twice or skip payment. Return TicketView.

HLD mapping

Across services you will version JSON contracts — API compatibility. That is DTO evolution. Domain Ticket still closes the same way. Do not version close() because a mobile app added a field.

Fewer classes is not cheaper

One Ticket class with JSON, SQL, and close() looks small. It changes when the mobile app wants a color, when finance wants a new column, and when CLOSED→ACTIVE must stay illegal. Three reasons, one type, every change is a fight. TicketView, Ticket, and a row mapper are more files and fewer fights. Interviewers who say “too many classes” still want close() on an object that cannot be a JSON bag.

An untyped process(Object) method is the extreme DTO: one bag, no behavior, every rule in a service. park(Vehicle) returning Ticket is the extreme domain: named command, typed result. The healthy design uses both, at different layers. The unhealthy design uses one class for both layers.

Java: anemic service vs rich command

java
// Anemic — service owns the rules
class ParkingService {
  Receipt exit(TicketDto dto, Instant now) {
    if (!"ACTIVE".equals(dto.status)) throw new IllegalStateException();
    dto.status = "CLOSED";
    dto.fee = pricing.calculate(dto.entry, now);
    return new Receipt(dto.id, dto.fee);
  }
}

// Rich — Ticket owns the rules
class ParkingLot {
  Receipt exit(TicketId id) {
    Ticket ticket = tickets.findById(id).orElseThrow(() -> new UnknownTicket(id));
    return ticket.close(clock.instant(), pricing);
  }
}

DTO may be mutable for mapping

Frameworks like setters. Fine at the edge. Do not let those setters become Ticket.setStatus.

Domain should not be a JavaBean

Private fields, commands, no public status setter. Equality by id for Ticket, by value for Money.

Exercises

  1. A reviewer says “just use Ticket everywhere, fewer classes.” Name two fields that would then appear on Ticket that do not belong.
  2. Rewrite a ParkingService that calls setStatus("CLOSED") into a single domain command.
  3. Is Receipt a DTO or a domain object? When would you split it into Receipt and ReceiptView?
Rich vs anemicPersistence modelWire models

Worked scene: the kiosk prints a view

park returns Ticket to the lot’s in-process caller. The controller maps Ticket → ParkResponse with id and spot. The kiosk never holds Ticket, so it cannot call close() when the driver is still shopping. If you return Ticket across that boundary “to save a class,” you have given the kiosk a loaded gun. TicketView is the safety. close() stays on the object that owns status.

Receipt can stay a domain object if it has fee math. ReceiptView is the printout: amount string, currency, ticket id. Split when the printer wants a barcode the domain should not store. Do not split when Receipt is already a frozen value with no setters. Frozen values can cross a boundary. Living entities cannot.

java
final class Receipt {
  private final TicketId ticketId;
  private final Money fee;
  Money fee() { return fee; }
}

final class ReceiptView {
  final String ticketId;
  final String amount;
  static ReceiptView from(Receipt receipt) {
    return new ReceiptView(receipt.ticketId().value(), receipt.fee().toDisplay());
  }
}

How to explain this in an interview

TicketView

Snapshot for UI. No close(). Changing view.status does nothing.

Ticket

close(), pay(), private status. Never a JSON bag.

Receipt

Domain value if it has Money. ReceiptView if the printer wants a barcode.

Service

Load, call ticket.close, save. Do not setStatus on a DTO.

In about a minute, say: “TicketView is a snapshot. Ticket.close owns the rules. I will not share one class for JSON, SQL, and close(). An untyped process(Object) method is the extreme DTO; park(Vehicle) is the domain API.”

Frozen values like Receipt can cross a boundary. Living entities cannot. If the kiosk holds Ticket, it can skip payment. Give it TicketView.

Fewer classes is not cheaper when one class changes for JSON, SQL, and close(). Split the snapshots.

Java: mapper owns JSON names

java
class TicketMapper {
  TicketView toView(Ticket ticket) {
    return new TicketView(
        ticket.id().value(),
        ticket.status().name(),
        ticket.vehicle().plate().value());
  }
}

Takeaway

A DTO is a snapshot for a boundary. A domain object has rules. Sharing one class for JSON, SQL, and close() is how Jackson annotations leak into the lot. Two types is cheaper than one type with two jobs.