SwadhyAI Notes

All Low-Level Design notes

Extend the contract

Evolving an API without breaking callers

API evolution means adding behavior without breaking existing callers. Example: last week park took a Vehicle. This week EV cars need a charger flag. If you change park(Vehicle) to park(Vehicle, boolean ev), every caller breaks. If you add park(Vehicle, ParkOptions) and keep the old method as a default, callers stay green.

Version timeline

Add a new method; keep the old one

is source plus meaning. The original park(Vehicle) should keep doing what it did — allocate a normal spot.

  1. v1

    park(Vehicle)

    Allocate a normal spot. Return Ticket. This meaning never changes.

  2. v2

    park(Vehicle, ParkOptions)

    Charger and valet live on a type. v1 delegates to defaults.

  3. v3

    reserve(Vehicle, Window)

    A new verb. Do not overload park into “maybe later.”

The OCP habit in one API

Open for extension, closed for modification — the same idea as Open/Closed. You extend with an or an . You do not redefine park. At HTTP scale this is API compatibility and consumer contracts. In a machine-coding round you “break” only the code on the whiteboard, but saying “existing callers keep working” is the habit interviewers want.

Java: defaults then options

java
class ParkingLot {
  Ticket park(Vehicle vehicle) {
    return park(vehicle, ParkOptions.defaults());
  }

  Ticket park(Vehicle vehicle, ParkOptions options) {
    SpotSpec spec = options.needsCharger() ? SpotSpec.CHARGER : SpotSpec.ANY;
    return allocation.choose(freeSpots(), vehicle, spec)
        .map(spot -> openTicket(vehicle, spot))
        .orElseThrow(SpotUnavailable::new);
  }
}

final class ParkOptions {
  private final boolean needsCharger;
  private final boolean valet;

  static ParkOptions defaults() {
    return new ParkOptions(false, false);
  }

  ParkOptions(boolean needsCharger, boolean valet) {
    this.needsCharger = needsCharger;
    this.valet = valet;
  }

  boolean needsCharger() { return needsCharger; }
  boolean valet() { return valet; }
}

Compatible moves

  • Add an overload that old code never has to call.
  • Add a field on ParkOptions with a default.
  • Add a new verb: reserve, pay, reprint.
  • Widen a return with extra methods on Ticket, not a new unrelated type from park.

Breaking moves

  • Add a required boolean to park(Vehicle).
  • Change Ticket to Optional<Ticket> without a new name.
  • Make find also expire tickets (query → command).
  • Return null on full when callers used to get Ticket or an exception.

When you must rename

A that changes meaning should be a . pay() that used to take cash and now also talks to a card network should become pay(PaymentMethod), not a boolean old callers pass wrong. If park used to mean “now” and you add “hold for two hours,” that is reserve — a new use case, not a flag on park.

Why is a boolean flag not an API?

park(vehicle, true) — true what? Charger? Handicap? Valet? The compiler cannot tell. A type can. ParkOptions or NeedsCharger makes the call site read like the story. The original park(Vehicle) stays the default method.

Silent change of return value

park used to return Ticket and now returns null when the lot is full. Old callers NPE. Throw SpotUnavailable or return Optional — and pick one, then keep it. Switching from exception to null is a break even if the method still “compiles” for people who ignore the value.

What about HTTP versioning?

URL versions, header versions, and additive JSON fields are the HLD version of this page. See API compatibility. In LLD, the analog is: add methods and types; do not redefine park. Do not invent /v2/park on a class.

Java: a new verb, not a lying old one

java
class ParkingLot {
  Ticket park(Vehicle vehicle) { /* now, normal spot */ }

  Reservation reserve(Vehicle vehicle, TimeWindow window) {
    // hold a spot; do not call this park
    return reservations.hold(vehicle, window, clock.instant());
  }
}

Input models evolve at the edge

HTTP bodies grow fields. Domain Vehicle should not. Put new wire fields on ParkRequest, map them in the controller, and pass ParkOptions into the lot. Keeping wire models and domain types apart is request/response vs domain. Evolving JSON is not a reason to put uiColor on Ticket.

Return types are part of the contract

park returning Ticket is a promise: callers will call ticket.id() without a null check. Switching to Optional<Ticket> is a compile break — honest, and you can discuss it. Switching to null on full is a runtime break — dishonest. Switching find from Optional to a throwing Ticket is also a break for callers who treated empty as “not found” rather than “crash.” Pick Optional for lookups and named exceptions for commands, then keep that pairing when the lot grows EV spots and valet.

Adding a method on Ticket (spotFloor()) is usually compatible: old callers ignore it. Changing Ticket from a class to a record that drops close() is not. The object API and the HTTP API share this rule: additive is easy; redefining meaning is a new version, which in LLD means a new name.

Java: ParkOptions that grows without a flag pile

java
final class ParkOptions {
  private final boolean needsCharger;
  private final boolean valet;
  private final Optional<ReservationId> reservation;

  static ParkOptions defaults() {
    return new ParkOptions(false, false, Optional.empty());
  }

  ParkOptions withCharger() {
    return new ParkOptions(true, valet, reservation);
  }

  ParkOptions withReservation(ReservationId id) {
    return new ParkOptions(needsCharger, valet, Optional.of(id));
  }
}

Whiteboard script

“I will keep park(Vehicle) as the default method. New requirements go on ParkOptions or a new verb. I will not add park(Vehicle, boolean). If pay changes meaning, I will rename it.” That is enough. You can point at class API design for why an untyped process(Object) method cannot evolve: there is no second overload, only another map key.

Exercises

  1. EV charging arrives. Show two signatures that keep park(Vehicle) compiling and meaning “normal spot.”
  2. A teammate changes find to return Ticket instead of Optional and throws if missing. Who breaks, and is that a rename or a silent change?
  3. You need “park now” and “hold until 6pm.” Why is a boolean on park the wrong extension?
Class APIOpen/ClosedCompatibility (HLD)

Deprecate in words, not in silence

If park(Vehicle, boolean) already shipped in last week’s code, keep it as a wrapper that calls ParkOptions, and say so. Do not change the boolean’s meaning from charger to valet. Callers who passed true for charger would silently request valet. That is the worst evolution: compiles, lies. Prefer a new method and a comment that the boolean overload is charger-only until deleted.

An untyped process(Object) method cannot deprecate a method: there is only one funnel. park(Vehicle) can stay forever as the default while park(Vehicle, ParkOptions) takes the new story. That is why typed APIs evolve and blob APIs only grow keys.

Takeaway

Extend the contract. Do not redefine it. Add an overload, a type, or a new verb. Keep park(Vehicle) doing what it did. When meaning changes, rename. A boolean is not an evolution strategy.