LearnThe interviewsstep 7

Machine coding and low-level design

  • 37 min
  • 17 stops
  • 4 diagrams
Read it, skim it or skip it? Pick your level
Read this if you are

Skim means: the lede, the figures, the tables and the sections marked for your level. Staff: read it if your loop has this round. Skip: a Principal loop has no machine coding (author's estimate, not measured); lessons 8 and 12 cover its design review. The coding round is lesson 6.

A machine-coding round asks you to build a small program that runs, in your own editor, under a clock, from a one-page problem such as a parking lot. Someone then reads the code for three things: the core use cases run from a driver, each class has one job with its behaviour next to its data, and a new requirement lands without rewriting what works. Patterns serve the third; none is a goal. One parking lot runs through the lesson, from first run to threads; a seat hold and tic-tac-toe show two other shapes. All code is Java 21 and was run.

Not advice. General information, and every example is invented.
Not advice

General information, not career advice, and no guarantee of any result. No employer publishes its machine-coding rubric, so how rounds run and are marked is the author's judgement, labelled; every example is invented.

What you hand over, and the clock

You hand over a project folder: models, the one rule that varies behind an interface, exceptions, a driver that calls each use case and prints the result, and checks that fail loudly. Figure 7.1 is the plan for 90 minutes; for 45, shrink each stretch. Two rules always hold: run the code after every must-have, not once at the end; and in the first ten minutes write the scope in five lines, saying aloud what is out. The last eight minutes are five lines of notes: in, out, the one pattern and why, ceilings, how to run.

The 90 minutes, drawn to scale (every 6 pixels is one minute)0-1010-20Must-haves, run after eachError pathsExtension82-90Clarify, scopeModels, stubsErrorsExtendCleanMinute marks: 0, 10, 20, 55, 70, 82, 90. The lot is built through the extension; typing stops at 82.
Figure 7.1. The 90-minute plan (author's estimate, not measured; no company's rule).

Next stop: A vague prompt: questions before code, 1 min

A vague prompt: questions before code

Author's judgement (not measured): some rounds hand you one line, and early marks go to whoever turns it into questions that decide the design, with a default for each.

INVENTED example 1 min

The whole prompt: "Build the discount part of a food-ordering app." The candidate takes two minutes and asks before drawing anything.

QuestionWhat it decidesIf the interviewer shrugs, I assume
Percent, flat, free delivery, first order only?The size of the Coupon interfacePercent with a cap, flat with a minimum
Can two discounts apply to one order?One coupon, or a list with an orderOne per order; stacking is the follow-up
Rupees with decimals, or whole paise?The type of every amount, roundingWhole paise in a long, percent floors
One process or several servers?Whether a lock is enoughOne process; I say what changes with two

Then four spoken lines: "One coupon per order, whole paise, no per-user limits, one process. Out: tax, stock, payment. The discount rule varies, so that is the interface. Stop me if any of this is wrong." Signal: each question is tied to a class or a type, not asked for comfort; the defaults are written down, so an unanswered question costs nothing.

Next stop: SOLID and the patterns on one page, 3 min

SOLID and the patterns on one page

SOLID is five questions to put to a class you just wrote, not five laws. The wording paraphrases principles associated with Robert C. Martin.

Table: Letter, The question, In the parking lot (5 rows)
LetterThe questionIn the parking lot
S, single responsibilityHow many reasons does this class have to change?A lot that also prints receipts has two. Printing goes to the driver.
O, open-closedWhat must I edit to add this?A new fee rule is a new class behind PricingStrategy.
L, Liskov substitutionSwap in any subtype: does every promise of the base still hold?Every strategy returns a non-negative fee for any vehicle and stay; amounts may differ, the lot relies on the promise.
I, interface segregationDoes the caller see methods it never uses?The repository offers add, get, remove and size, not a bulk export.
D, dependency inversionDoes this class build its own collaborators?The lot receives its pricing in the constructor; a test passes a lambda.

Over-applying them is the common failure. Put an interface only on the list that will grow: in the lot, the fee rule; vehicle size is an enum, because a bike and a car differ in a number, not in behaviour.

Descriptions follow refactoring.guru (secondary); the traps are the author's judgement.

Table: Pattern, Use when, Java shape, Interview trap (13 rows)
PatternUse whenJava shapeInterview trap
SingletonExactly one instance is a real ruleenum Registry { INSTANCE; }, or a private static holder class whose field is built on first use. Double-checked locking needs a volatile field.Global state that makes tests hard: pass the one object in. Asked for thread-safe, give enum or holder, not if (x == null).
Factory (static factory; GoF Factory Method uses a creator subclass)The set of concrete types will grow and callers must not changestatic Notifier of(Channel c) with a switch over a sealed type or enumA factory with one product.
Abstract factoryFamilies of objects that must match (a UI kit)An interface with one method per product; one implementation per familyOne family. Say "factory" and stop.
BuilderMany optional parts; telescoping constructorsNested static class Builder with chained setters and build() that validatesFour fields do not need it; a record is shorter.
AdapterA class you cannot change has the wrong interfaceA small class implementing your interface and holding the foreign oneAdapting a class you own. Change it instead.
DecoratorCombinable add-ons at run time (pass over a base fee)Same interface, holds one inner instance, delegates and adds (built below)Fixed combinations; a flag is enough.
FacadeA simple entry point to a messy subsystemOne class with a few methods calling several collaboratorsIt grows into a god object holding rules.
ProxySame interface, controlled access: lazy load, cache, access check, loggingWraps the real object, decides whether and when to call itConfusing it with decorator: proxy controls access, decorator adds behaviour.
ObserverUnknown, changing listeners must react to an eventA list of Consumer<Event>; the subject calls eachOne listener; or a slow or throwing listener that stalls the rest.
StrategyA rule varies: fee, discount, assignmentAn interface with one method; a lambda is a strategy.A single fixed rule behind an interface.
CommandUndo, queue or log of operationsInterface with execute() (and undo()); a Deque as the historyUndo needing an inverse you cannot write; store a snapshot.
StateMany states, each with its own rule per actionAn interface per state, the object delegates to the current oneTwo states need a field; four with their own rules earn classes.
Composition over inheritanceBehaviour must vary or combineHold a collaborator behind an interface; use a sealed interface plus records for fixed variantsA subclass per combination. Inheritance for reuse breaks Liskov later.

Next stop: One complete design: a parking lot, 7 min

One complete design: a parking lot

For: Intern to Staff.

The problem is invented. A lot has small, medium and large spots (the code also declares XL, to show what adding a size costs). A bike fits any spot, a car needs medium or large, a truck needs large. On exit it charges Rs 10, 20 or 40 per started hour for a bike, car or truck, minimum one hour. Money is integer paise; time is passed in as whole minutes, so tests are deterministic. In scope: park, leave, free count, swappable pricing, refusal of a full lot and of an unknown ticket. Said aloud as out of scope: payment methods, persistence, cameras, several floors and gates, and threads.

interfacePricingStrategyfee(vehicle, mins) -> paiseHourlyRateswitch: paise per hourfee(vehicle, minutes)ParkingLotspots: List<Spot>pricing: PricingStrategytickets: TicketRepositoryrule: sizes per vehiclepark, leave, freeCountSpotspotIdsize: SpotSizeoccupiedBy: id or nullisFree, take, freeenumSpotSizeSMALL MEDIUM LARGE XLTicketRepositoryrows: id -> Ticketadd, get, remove, sizeTicketticketIdvehicle: VehicleTypespotIdentryMin (record)enumVehicleTypeBIKE, CAR, TRUCKParkingError(reason)realisesinjected1..*1creates0..*by idthrowsLegend (UML): hollow diamond = aggregation, partsmade outside and passed in; 1, 1..*, 0..* = how many;dashed = depends on, or realises (hollow head);solid hollow head = is-a; open head = refers to.
Figure 7.2. The design (version one). A spot holds its ticket by id, so spot and ticket never point at each other. One error type carries a reason (enum Reason: LOT_FULL, UNKNOWN_TICKET, BAD_EXIT, SPOT_MISSING).

Which sizes a vehicle may use is a table, smallest first, not a comparison of enum positions, which breaks silently when a size is added; with the table, XL was one constant and one name per row.

The code

One file, run with java --source 21 ParkingLot.java. The checks throw AssertionError, so no flag is needed.

Code, 138 lines
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class ParkingLot {
  enum VehicleType { BIKE, CAR, TRUCK }
  enum SpotSize { SMALL, MEDIUM, LARGE, XL }
  enum Reason { LOT_FULL, UNKNOWN_TICKET, BAD_EXIT, SPOT_MISSING }

  /** The one error the driver catches; reason says which. */
  static class ParkingError extends RuntimeException {
    final Reason reason;
    ParkingError(Reason reason, String msg) {
      super(msg);
      this.reason = reason;
    }
  }

  interface PricingStrategy {
    long fee(VehicleType vehicle, int minutes); // paise
  }

  /** Rs 10 / 20 / 40 per started hour, minimum one hour. */
  static class HourlyRate implements PricingStrategy {
    public long fee(VehicleType vehicle, int minutes) {
      long hours = Math.max(1, (minutes + 59L) / 60); // long: no wrap
      // no default branch: a new VehicleType will not compile here
      long perHour = switch (vehicle) {
        case BIKE -> 1000;
        case CAR -> 2000;
        case TRUCK -> 4000;
      };
      return Math.multiplyExact(perHour, hours); // throws, never wraps
    }
  }

  /** Sizes a vehicle may use, smallest first. No ordinal compares. */
  static Map<VehicleType, List<SpotSize>> defaultRule() {
    var r = new EnumMap<VehicleType, List<SpotSize>>(VehicleType.class);
    r.put(VehicleType.BIKE, List.of(SpotSize.SMALL, SpotSize.MEDIUM,
        SpotSize.LARGE, SpotSize.XL));
    r.put(VehicleType.CAR, List.of(SpotSize.MEDIUM, SpotSize.LARGE,
        SpotSize.XL));
    r.put(VehicleType.TRUCK, List.of(SpotSize.LARGE, SpotSize.XL));
    return r;
  }

  static class Spot {
    final String spotId;
    final SpotSize size;
    private String occupiedBy; // ticket id, null when free
    Spot(String spotId, SpotSize size) {
      this.spotId = spotId;
      this.size = size;
    }
    boolean isFree() { return occupiedBy == null; }
    void take(String ticketId) { occupiedBy = ticketId; }
    void free() { occupiedBy = null; }
  }

  record Ticket(String ticketId, VehicleType vehicle, String spotId,
                int entryMin) {}

  static class TicketRepository {
    private final Map<String, Ticket> rows = new HashMap<>();
    void add(Ticket t) { rows.put(t.ticketId(), t); }
    Ticket get(String id) {
      Ticket t = rows.get(id);
      if (t == null) {
        throw new ParkingError(Reason.UNKNOWN_TICKET, id);
      }
      return t;
    }
    void remove(String id) { rows.remove(id); }
    int size() { return rows.size(); }
  }

  private final List<Spot> spots;
  private final PricingStrategy pricing;
  private final Map<VehicleType, List<SpotSize>> rule;
  private final TicketRepository tickets;
  private int nextId = 1;

  ParkingLot(List<Spot> spots, PricingStrategy pricing,
      Map<VehicleType, List<SpotSize>> rule, TicketRepository tickets) {
    if (List.of(VehicleType.values()).stream()   // every type, some size
        .anyMatch(v -> rule.getOrDefault(v, List.of()).isEmpty())) {
      throw new IllegalArgumentException("rule misses a vehicle type");
    }
    this.spots = spots;
    this.pricing = pricing;
    this.rule = rule;
    this.tickets = tickets;
  }

  private Spot findSpot(VehicleType vehicle) {
    for (SpotSize size : rule.get(vehicle)) { // smallest allowed first
      for (Spot s : spots) {
        if (s.isFree() && s.size == size) return s;
      }
    }
    throw new ParkingError(Reason.LOT_FULL, "no free spot for " + vehicle);
  }

  Ticket park(VehicleType vehicle, int nowMin) {
    if (nowMin < 0) throw new IllegalArgumentException("time < 0");
    Spot spot = findSpot(vehicle);          // every check first
    var ticket = new Ticket("T" + nextId++, vehicle, spot.spotId, nowMin);
    spot.take(ticket.ticketId());           // then the only mutations
    tickets.add(ticket);
    return ticket;
  }

  long leave(String ticketId, int nowMin) {
    Ticket t = tickets.get(ticketId);       // UNKNOWN_TICKET
    if (nowMin < t.entryMin()) {
      throw new ParkingError(Reason.BAD_EXIT, "exit before entry");
    }
    long fee = pricing.fee(t.vehicle(), nowMin - t.entryMin());
    Spot spot = spotById(t.spotId());       // SPOT_MISSING, still a check
    spot.free();                            // then the only mutations
    tickets.remove(ticketId);
    return fee;
  }

  private Spot spotById(String id) {
    for (Spot s : spots) if (s.spotId.equals(id)) return s;
    throw new ParkingError(Reason.SPOT_MISSING, id);
  }

  int freeCount() {
    int n = 0;
    for (Spot s : spots) if (s.isFree()) n++;
    return n;
  }

findSpot walks sizes smallest first, so a bike takes a large spot only when no smaller one is free. (minutes + 59L) / 60 counts started hours in integers, so no floating point touches money; the fee is whole paise in a long, because an int wraps: a truck leaving at minute 999,999,999 was charged -2,052,808,736 paise. Math.multiplyExact throws rather than wrap if a later rule multiplies further, and park refuses a negative time, so a stay never goes below zero.

Two guards cover a new vehicle type such as SCOOTER: the switch in HourlyRate has no default, so the file will not compile without a rate, and the constructor refuses a size table that misses a type or gives it no size, where park would throw NullPointerException, or refuse that vehicle as LOT_FULL with spots free.

leave(): every check first, then the only changesget ticketraises UNKNOWN_TICKETexit >= entry?raises BAD_EXITprice, find spotraises SPOT_MISSINGchecks: if any fails, nothing has changedfree, removethe only mutationsreturn fee last. A raise anywhere on the left leaves the lot exactly as it was.
Figure 7.3. The rule the tests pin down: nothing changes until every check has passed.

Figure 7.3 is "validate before mutate": mark each line of a method check or change; a check after a change is the bug. Easy to miss is the spot lookup in leave, which fails if the ticket store and the spot list disagree, say after a restore; the last check pins it.

Code, 120 lines
  // ---- driver and tests (same file, so checks can read private fields)
  static void check(boolean ok, String msg) {
    if (!ok) throw new AssertionError(msg);
  }

  static void expect(Reason why, Runnable r) {
    try {
      r.run();
    } catch (ParkingError e) {
      if (e.reason == why) return;
      throw new AssertionError("wrong reason " + e.reason);
    }
    throw new AssertionError("no error, wanted " + why);
  }

  static ParkingLot lot(SpotSize... sizes) {
    var spots = new ArrayList<Spot>();
    for (int i = 0; i < sizes.length; i++) {
      spots.add(new Spot("P" + i, sizes[i]));
    }
    return new ParkingLot(spots, new HourlyRate(), defaultRule(),
        new TicketRepository());
  }

  static void demo() {
    var lot = new ParkingLot(List.of(new Spot("S1", SpotSize.SMALL),
        new Spot("M1", SpotSize.MEDIUM), new Spot("L1", SpotSize.LARGE)),
        new HourlyRate(), defaultRule(), new TicketRepository());
    var bike = lot.park(VehicleType.BIKE, 0);
    var car = lot.park(VehicleType.CAR, 10);
    System.out.println("bike -> " + bike.spotId() + " | car -> "
        + car.spotId() + " | free: " + lot.freeCount());
    var truck = lot.park(VehicleType.TRUCK, 20);
    System.out.println("truck -> " + truck.spotId() + " | free: "
        + lot.freeCount());
    try {
      lot.park(VehicleType.CAR, 30);
    } catch (ParkingError e) {
      System.out.println("refused: " + e.getMessage());
    }
    long fee = lot.leave(car.ticketId(), 100);
    System.out.println("car left after 90 min, fee paise: " + fee
        + " | free: " + lot.freeCount());
    try {
      lot.leave("T99", 101);
    } catch (ParkingError e) {
      System.out.println("unknown ticket: " + e.getMessage());
    }
  }

  static void tests() {
    // bike takes the smallest fitting spot
    check(lot(SpotSize.LARGE, SpotSize.SMALL).park(VehicleType.BIKE, 0)
        .spotId().equals("P1"), "smallest fitting");
    // truck needs LARGE or XL
    expect(Reason.LOT_FULL, () -> lot(SpotSize.MEDIUM)
        .park(VehicleType.TRUCK, 0));
    // a full lot refuses and changes nothing
    var full = lot(SpotSize.MEDIUM);
    var first = full.park(VehicleType.CAR, 0);
    expect(Reason.LOT_FULL, () -> full.park(VehicleType.CAR, 5));
    check(full.tickets.size() == 1, "ticket count");
    check(!full.spots.get(0).isFree(), "spot still taken");
    // fee, free, and the one-hour minimum
    var l2 = lot(SpotSize.MEDIUM, SpotSize.SMALL);
    var t2 = l2.park(VehicleType.CAR, 0);
    check(l2.leave(t2.ticketId(), 90) == 4000, "90 min car");
    check(l2.freeCount() == 2, "freed");
    var t3 = l2.park(VehicleType.BIKE, 0);
    check(l2.leave(t3.ticketId(), 1) == 1000, "minimum hour");
    // a stay near the int limit: long paise, no wrap, never negative
    var l3 = lot(SpotSize.LARGE);
    var t5 = l3.park(VehicleType.TRUCK, 0);
    check(l3.leave(t5.ticketId(), Integer.MAX_VALUE) == 143_165_580_000L,
        "no overflow");
    try {                                   // and a negative time is input
      lot(SpotSize.SMALL).park(VehicleType.BIKE, -1);
      throw new AssertionError("negative time accepted");
    } catch (IllegalArgumentException expected) { }
    // unknown ticket and exit before entry keep state
    var l4 = lot(SpotSize.SMALL);
    var t4 = l4.park(VehicleType.BIKE, 50);
    expect(Reason.UNKNOWN_TICKET, () -> l4.leave("T404", 60));
    expect(Reason.BAD_EXIT, () -> l4.leave(t4.ticketId(), 10));
    check(l4.freeCount() == 0 && l4.tickets.size() == 1, "kept state");
    // pricing is swappable: a lambda is a strategy
    var l6 = new ParkingLot(List.of(new Spot("P0", SpotSize.SMALL)),
        (v, m) -> 500, defaultRule(), new TicketRepository());
    var t6 = l6.park(VehicleType.BIKE, 0);
    check(l6.leave(t6.ticketId(), 999) == 500, "flat fee");
    // XL works through the rule table; a bike still prefers SMALL
    check(lot(SpotSize.XL).park(VehicleType.TRUCK, 0)
        .spotId().equals("P0"), "xl");
    check(lot(SpotSize.XL, SpotSize.SMALL).park(VehicleType.BIKE, 0)
        .spotId().equals("P1"), "pref");
    // leave fails on a missing spot BEFORE mutating anything
    var repo = new TicketRepository();
    repo.add(new Ticket("T7", VehicleType.CAR, "GHOST", 0));
    var l9 = new ParkingLot(new ArrayList<>(List.of(
        new Spot("P0", SpotSize.MEDIUM))), new HourlyRate(),
        defaultRule(), repo);
    expect(Reason.SPOT_MISSING, () -> l9.leave("T7", 30));
    check(repo.size() == 1 && l9.freeCount() == 1, "failed leave");
    // a rule that gives a type no size is refused at start-up
    var partial = new EnumMap<VehicleType, List<SpotSize>>(defaultRule());
    partial.put(VehicleType.TRUCK, List.of());
    try {
      new ParkingLot(List.of(), new HourlyRate(), partial,
          new TicketRepository());
      throw new AssertionError("partial rule accepted");
    } catch (IllegalArgumentException expected) { }
  }

  public static void main(String[] args) {
    demo();
    tests();
    System.out.println("tests passed");
  }
}

The run

Exit status 0; the driver prints:

Code, 7 lines
bike -> S1 | car -> M1 | free: 1
truck -> L1 | free: 0
refused: no free spot for CAR
car left after 90 min, fee paise: 4000 | free: 1
unknown ticket: T99
tests passed

Ceilings to say aloud, unprompted: search and mutation are two steps, so this lot is not thread-safe; findSpot and spotById scan lists, wrong for thousands of spots; time is an argument, where a real system injects a clock like it injects pricing.

Next stop: "Now add X": a requirement that moves the design, 3 min

"Now add X": a requirement that moves the design

For: Intern to Staff.

The likeliest follow-up is "add a monthly pass for cars" (author's estimate, not measured). A pass belongs to a holder, and fee(vehicle, minutes) does not know who parked, so a second strategy alone cannot work: the ticket must carry a plate and the interface must take the ticket. The pass is a decorator over a base strategy; the invented rule is that it covers cars only, billed elsewhere. The design part of the diff, version one to two:

Code, 59 lines
@@ -5,2 +5,3 @@
 import java.util.Map;
+import java.util.Set;
 
@@ -21,3 +22,3 @@
   interface PricingStrategy {
-    long fee(VehicleType vehicle, int minutes); // paise
+    long fee(Ticket ticket, int minutes); // paise
   }
@@ -26,6 +27,6 @@
   static class HourlyRate implements PricingStrategy {
-    public long fee(VehicleType vehicle, int minutes) {
+    public long fee(Ticket ticket, int minutes) {
       long hours = Math.max(1, (minutes + 59L) / 60); // long: no wrap
       // no default branch: a new VehicleType will not compile here
-      long perHour = switch (vehicle) {
+      long perHour = switch (ticket.vehicle()) {
         case BIKE -> 1000;
@@ -38,2 +39,17 @@
 
+  /** Cars whose plate holds a pass pay nothing; others pay the base. */
+  static class MonthlyPass implements PricingStrategy {
+    private final Set<String> plates;
+    private final PricingStrategy base;
+    MonthlyPass(Set<String> plates, PricingStrategy base) {
+      this.plates = plates;
+      this.base = base;
+    }
+    public long fee(Ticket ticket, int minutes) {
+      boolean covered = ticket.vehicle() == VehicleType.CAR
+          && plates.contains(ticket.plate());
+      return covered ? 0 : base.fee(ticket, minutes);
+    }
+  }
+
   /** Sizes a vehicle may use, smallest first. No ordinal compares. */
@@ -62,4 +78,4 @@
 
-  record Ticket(String ticketId, VehicleType vehicle, String spotId,
-                int entryMin) {}
+  record Ticket(String ticketId, VehicleType vehicle, String plate,
+                String spotId, int entryMin) {}
 
@@ -106,6 +122,7 @@
 
-  Ticket park(VehicleType vehicle, int nowMin) {
+  Ticket park(VehicleType vehicle, String plate, int nowMin) {
     if (nowMin < 0) throw new IllegalArgumentException("time < 0");
     Spot spot = findSpot(vehicle);          // every check first
-    var ticket = new Ticket("T" + nextId++, vehicle, spot.spotId, nowMin);
+    var ticket = new Ticket("T" + nextId++, vehicle, plate,
+        spot.spotId, nowMin);
     spot.take(ticket.ticketId());           // then the only mutations
@@ -120,3 +137,3 @@
     }
-    long fee = pricing.fee(t.vehicle(), nowMin - t.entryMin());
+    long fee = pricing.fee(t, nowMin - t.entryMin());
     Spot spot = spotById(t.spotId());       // SPOT_MISSING, still a check

Every caller of park changes. One command adds an invented plate to all of them:

Code, 4 lines
sed -i.bak \
  -e 's/park(VehicleType\.[A-Z]*, /&"KA00", /' \
  -e 's/Ticket("T7", VehicleType\.CAR, /&"KA00", /' ParkingLot.java

One check goes at the end of tests(); the file then printed tests passed:

Code, 9 lines
    var pass = new MonthlyPass(Set.of("KA01"), new HourlyRate());
    var l10 = new ParkingLot(List.of(new Spot("P0", SpotSize.MEDIUM),
        new Spot("P1", SpotSize.SMALL)), pass, defaultRule(),
        new TicketRepository());
    var c = l10.park(VehicleType.CAR, "KA01", 0);
    var b = l10.park(VehicleType.BIKE, "KA01", 0);
    check(l10.leave(c.ticketId(), 500) == 0, "pass car free");
    check(l10.leave(b.ticketId(), 60) == 1000, "bike not covered");

Measured with diff: 25 lines added, 8 removed; findSpot, the size table and the checks in leave did not change, but every caller and fee rule did, so say the interface edit first: a pass request partly tests whether you notice that identity enters pricing (the author's reading).

Other follow-ups, placed rather than built: several floors and gates (a SpotAssigner strategy choosing among spots that already fit; the ticket records its gate); EV spots (a SpotSize constant and one name per row); dynamic pricing (another decorator).

Next stop: Commands on stdin, and the folder, 2 min

Commands on stdin, and the folder

For: Intern to Staff.

Some rounds run your program on input lines and expect exact output (author's judgement, not measured). The driver is one loop: split, call the lot, print one line, and turn every error into a line, so bad input does not end the run. Added to version two with imports of Arrays and Scanner, it serves stdin when spot sizes are passed as arguments:

Code, 34 lines
  /** One command per line, one output line per command. */
  static void serve(Scanner in, ParkingLot lot) {
    while (in.hasNextLine()) {
      String[] w = in.nextLine().trim().split("\\s+");
      if (w[0].isEmpty()) continue;           // blank line
      try {
        String out = switch (w[0]) {
          case "park" -> {
            var t = lot.park(VehicleType.valueOf(w[1]), w[2],
                Integer.parseInt(w[3]));
            yield t.ticketId() + " " + t.spotId();
          }
          case "leave" -> "" + lot.leave(w[1],
              Integer.parseInt(w[2]));
          case "free" -> "" + lot.freeCount();
          default -> "error BAD_COMMAND";
        };
        System.out.println(out);
      } catch (ParkingError e) {
        System.out.println("error " + e.reason);
      } catch (IllegalArgumentException | IndexOutOfBoundsException e) {
        System.out.println("error BAD_INPUT"); // bad name or number
      }
    }
  }

  public static void main(String[] args) {
    if (args.length > 0) {                    // spot sizes: serve stdin
      serve(new Scanner(System.in), lot(Arrays.stream(args)
          .map(SpotSize::valueOf).toArray(SpotSize[]::new)));
      return;
    }
    demo();

Run as java --source 21 ParkingLot.java SMALL MEDIUM LARGE < in.txt, each input (left) printed one line (right); a saved expected file under diff is the check to keep:

Code, 13 lines
park CAR KA01 0       T1 P1
park BIKE KA02 5      T2 P0
free                  1
leave T1 90           4000
park SCOOTER KA03 9   error BAD_INPUT
leave T9 10           error UNKNOWN_TICKET
park TRUCK KA04 20    T3 P2
park TRUCK KA05 21    error LOT_FULL
leave T3 999999999    66666668000
park BIKE KA06 -5     error BAD_INPUT
fly                   error BAD_COMMAND
park CAR              error BAD_INPUT

When the round wants a project rather than one file, split by reason to change:

Code, 10 lines
parking/
  Main.java            stdin driver
  ParkingLot.java      park, leave, freeCount
  model/               VehicleType, SpotSize, Spot, Ticket
  pricing/             PricingStrategy, HourlyRate, MonthlyPass
  store/               TicketRepository
  error/               ParkingError, Reason
  ParkingLotTest.java  the checks (JUnit if the project has it)
  NOTES.md             the five lines of notes

In packages, nested classes become public types and tests read state through methods (isFree, size); ask in minute one whether one file will do.

Next stop: A second shape: a seat hold as a state machine, 3 min

A second shape: a seat hold as a state machine

For: Intern, SDE-1, SDE-2, SDE-3.

The other common shape moves one object through states: a vending machine, an elevator, a booking (author's estimate, not measured). The prompt (INVENTED): a user holds a seat for five minutes and confirms it while the hold lasts; an unconfirmed hold lapses. Table first, then code.

Table: State, Event, Guard, Next, Else refuse (5 rows)
StateEventGuardNextElse refuse
FREEholdnoneHELD, 300 snone
HELDconfirmsame user, not lapsedBOOKEDNOT_HOLDER (lapsed: NOT_HELD)
HELDclock reaches expirychecked on every readFREEnone
HELD, BOOKEDholdnoneunchangedTAKEN
FREE, BOOKEDconfirmnoneunchangedNOT_HELD

Three states earn an enum and one guarded method per event. Expiry needs no timer thread: every read goes through Seat.state(now), which frees a lapsed hold first, and the injected clock lets a test move time instead of sleeping.

Code, 83 lines
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.LongSupplier;

public class SeatHold {
  enum State { FREE, HELD, BOOKED }
  enum Reason { NO_SEAT, TAKEN, NOT_HELD, NOT_HOLDER }

  static class BookingError extends RuntimeException {
    final Reason reason;
    BookingError(Reason reason, String seatId) {
      super(reason + " " + seatId);
      this.reason = reason;
    }
  }

  static void require(boolean ok, Reason why, String seatId) {
    if (!ok) throw new BookingError(why, seatId);
  }

  static class Seat {
    private State state = State.FREE;
    private String holder;            // the user, while HELD or BOOKED
    private long expiresAt;           // seconds, while HELD

    State state(long now) {
      if (state == State.HELD && now >= expiresAt) {
        state = State.FREE;           // HELD -> FREE: expiry, lazily
        holder = null;
      }
      return state;
    }
  }

  static final long HOLD_SECONDS = 300;
  private final Map<String, Seat> seats = new HashMap<>();
  private final LongSupplier clock;   // seconds; a test moves it

  SeatHold(List<String> seatIds, LongSupplier clock) {
    for (String id : seatIds) seats.put(id, new Seat());
    this.clock = clock;
  }

  private Seat seat(String id) {
    Seat s = seats.get(id);
    require(s != null, Reason.NO_SEAT, id);
    return s;
  }

  /** FREE -> HELD for five minutes. */
  synchronized void hold(String seatId, String user) {
    long now = clock.getAsLong();
    Seat s = seat(seatId);
    require(s.state(now) == State.FREE, Reason.TAKEN, seatId);
    s.state = State.HELD;
    s.holder = user;
    s.expiresAt = now + HOLD_SECONDS;
  }

  /** HELD -> BOOKED, only by the holder, only before expiry. */
  synchronized void confirm(String seatId, String user) {
    Seat s = seat(seatId);
    require(s.state(clock.getAsLong()) == State.HELD,
        Reason.NOT_HELD, seatId);
    require(s.holder.equals(user), Reason.NOT_HOLDER, seatId);
    s.state = State.BOOKED;
  }

  synchronized State stateOf(String seatId) {
    return seat(seatId).state(clock.getAsLong());
  }

  synchronized void holdAll(List<String> seatIds, String user) {
    var ids = Set.copyOf(seatIds);          // "A3, A3" is one seat
    long now = clock.getAsLong();
    for (String id : ids) {                 // every check first
      require(seat(id).state(now) == State.FREE, Reason.TAKEN, id);
    }
    for (String id : ids) hold(id, user);   // then the only changes
  }

The file printed tests passed; with the expiry line removed it failed at the first NOT_HELD. The checks begin as below; the rest pin each refusal, a booking that never lapses, the 299 and 300 second edge, and a duplicate seat.

Code, 10 lines
  // check and expect as in the parking lot, catching BookingError
    long[] now = {0};                       // the clock the test moves
    var show = new SeatHold(List.of("A1", "A2", "A3"), () -> now[0]);
    show.hold("A2", "ravi");
    now[0] = 300;                           // ravi's five minutes are up
    expect(Reason.NOT_HELD, () -> show.confirm("A2", "ravi"));
    show.hold("A2", "meera");               // the expired seat is free
    expect(Reason.TAKEN, () -> show.holdAll(List.of("A3", "A2"), "ravi"));
    check(show.stateOf("A3") == State.FREE, "all or none");

The extension the interviewer adds: "a family holds four seats, all or none". It is holdAll, Figure 7.3 over a list: every seat checked, then every hold. The trap is a duplicate: without Set.copyOf, "A3, A3" passes both checks, holds A3, then refuses it, a half-done group (the run showed it). Placed, not built: release by the holder (one more row) and two servers, where a conditional update sets HELD only where the seat is FREE or lapsed.

What the interviewer marks (author's judgement, not measured). Up: the table first; each refusal named, the seat left as it was; state changed only inside the class; synchronized on each event, since a hold is the lot's check-then-act. Down: two booleans (isHeld, isBooked) that can both be true; a timer thread per hold; a test that sleeps five minutes; a confirm that books a lapsed hold.

Next stop: A third shape for a campus round: tic-tac-toe, 3 min

A third shape for a campus round: tic-tac-toe

For: Intern, SDE-1.

Campus rounds often set a game (author's judgement, not measured). The prompt (INVENTED): two players take turns on an n by n board; report a win or draw, and refuse a bad move with its reason. No rule varies, so no interface; the lot's checks carry over.

Code, 84 lines
public class TicTacToe {
  enum Mark { X, O }
  enum Status { PLAYING, X_WON, O_WON, DRAW }
  enum Reason { OFF_BOARD, TAKEN, GAME_OVER }

  static class MoveError extends RuntimeException {
    final Reason reason;
    MoveError(Reason r) { super(r.name()); reason = r; }
  }

  private final int n;
  private final Mark[][] cells;         // null means empty
  private Mark turn = Mark.X;
  private Status status = Status.PLAYING;
  private int moves;

  TicTacToe(int n) {
    if (n < 3) throw new IllegalArgumentException("board below 3");
    this.n = n;
    cells = new Mark[n][n];
  }

  Status play(int r, int c) {
    if (status != Status.PLAYING) throw new MoveError(Reason.GAME_OVER);
    if (r < 0 || r >= n || c < 0 || c >= n) {
      throw new MoveError(Reason.OFF_BOARD);
    }
    if (cells[r][c] != null) throw new MoveError(Reason.TAKEN);
    cells[r][c] = turn;                 // every check passed: change
    moves++;
    if (wins(r, c)) status = turn == Mark.X ? Status.X_WON : Status.O_WON;
    else if (moves == n * n) status = Status.DRAW;
    turn = turn == Mark.X ? Mark.O : Mark.X;
    return status;
  }

  /** Only lines through the last move can change. */
  private boolean wins(int r, int c) {
    Mark m = cells[r][c];
    boolean row = true, col = true, dia = r == c, anti = r + c == n - 1;
    for (int i = 0; i < n; i++) {
      row &= cells[r][i] == m;
      col &= cells[i][c] == m;
      dia &= cells[i][i] == m;
      anti &= cells[i][n - 1 - i] == m;
    }
    return row || col || dia || anti;
  }

  // ---- tests
  static void check(boolean ok, String msg) {
    if (!ok) throw new AssertionError(msg);
  }

  static void expect(Reason why, Runnable r) {
    try { r.run(); } catch (MoveError e) { if (e.reason == why) return; }
    throw new AssertionError("wanted " + why);
  }

  static Status playAll(TicTacToe g, int... rc) {  // row, col, row, col
    Status s = null;
    for (int i = 0; i < rc.length; i += 2) s = g.play(rc[i], rc[i + 1]);
    return s;
  }

  public static void main(String[] args) {
    var g = new TicTacToe(3);
    check(playAll(g, 0,0, 1,0, 0,1, 1,1, 0,2) == Status.X_WON, "row");
    expect(Reason.GAME_OVER, () -> g.play(2, 2));
    var d = new TicTacToe(3);
    check(playAll(d, 0,0, 0,1, 0,2, 1,1, 2,1, 2,0, 1,0, 1,2, 2,2)
        == Status.DRAW, "draw");
    var big = new TicTacToe(4);         // O takes the anti-diagonal
    check(playAll(big, 0,0, 0,3, 1,0, 1,2, 2,2, 2,1, 1,1, 3,0)
        == Status.O_WON, "4 by 4");
    var t = new TicTacToe(3);
    t.play(1, 1);
    expect(Reason.TAKEN, () -> t.play(1, 1));
    expect(Reason.OFF_BOARD, () -> t.play(3, 0));
    check(t.turn == Mark.O && t.moves == 1, "a refusal changed nothing");
    System.out.println("tests passed");
  }
}

It printed tests passed; with the GAME_OVER line in play deleted, it failed with wanted GAME_OVER. Say aloud: n is an argument, so "make it 4 by 4" is free.

The Java this code uses

Enums compared with ==; static nested classes; an exception carrying a reason; a lambda or method reference (u::undo) as a Runnable; varargs (int... rc); a 2-D array; a record, equals and hashCode free; a Deque as a stack. The lot adds an interface, EnumMap and a switch expression.

Now add undo. A move has an exact inverse, so a stack of moves is the history (the Command row). Import ArrayDeque and Deque, add NO_MOVE to Reason, then:

Code, 22 lines
  // beside the other fields
  record Move(int r, int c) {}
  private final Deque<Move> history = new ArrayDeque<>();

  // in play, after moves++
    history.push(new Move(r, c));

  void undo() {
    if (history.isEmpty()) throw new MoveError(Reason.NO_MOVE);
    Move m = history.pop();
    cells[m.r()][m.c()] = null;
    moves--;
    turn = turn == Mark.X ? Mark.O : Mark.X;
    status = Status.PLAYING;
  }

  // in main, before the println
    var u = new TicTacToe(3);
    expect(Reason.NO_MOVE, u::undo);
    g.undo();                         // takes back X's win
    check(g.play(2, 2) == Status.PLAYING && g.turn == Mark.O, "undo");

It printed tests passed again.

Next stop: Concurrency, 3 min

Concurrency

For: SDE-2, SDE-3, Staff (SDE-1 if the recruiter says threads are in scope).

Two gates picking the same spot is a common senior follow-up (author's judgement, not measured). A race needs state two threads share, at least one writing, and two steps another thread can come between. It takes three shapes, each in Figure 7.4 with its fix. Check-then-act: a spot is free when checked and taken when you act. Read-modify-write: count++ reads, adds, writes, and erases a write made in between. Publish: an object becomes visible before it is fully built.

timeCheck-then-actRead-modify-writePublishthread Athread Bthread Athread Bthread Athread Bfree=1? yesfree=1? yestake spottake spotread n=0read n=0write n=1write n=1put(p)get(p)reads nullp.name=Ashatwo cars, one spottwo hits, n=1half-built seenfix: one lockfix: one atomic stepfix: final fields, safe put
Figure 7.4. The three race shapes, one bad order each (invented). For publish, field order is not enough in Java: B needs final fields or a safe hand-over.

Fixing the lot is the check-then-act case: findSpot and spot.take must run under one lock, so park, leave and freeCount become synchronized (unlocked, freeCount reads spots while a park writes them). A passing test proves little, since the bad interleaving may not occur, so the test forces it: a hook between check and act makes two threads meet there, timing out on the locked lot rather than hanging. The diff from version two with its driver to version three, then the test, placed after tests():

Code, 39 lines
@@ -8,2 +8,6 @@
 import java.util.Set;
+import java.util.concurrent.BrokenBarrierException;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 
@@ -102,2 +106,3 @@
   private int nextId = 1;
+  Runnable gap = () -> { };  // test hook between check and act
 
@@ -124,5 +129,6 @@
 
-  Ticket park(VehicleType vehicle, String plate, int nowMin) {
+  synchronized Ticket park(VehicleType vehicle, String plate, int nowMin) {
     if (nowMin < 0) throw new IllegalArgumentException("time < 0");
     Spot spot = findSpot(vehicle);          // every check first
+    gap.run();
     var ticket = new Ticket("T" + nextId++, vehicle, plate,
@@ -134,3 +140,3 @@
 
-  long leave(String ticketId, int nowMin) {
+  synchronized long leave(String ticketId, int nowMin) {
     Ticket t = tickets.get(ticketId);       // UNKNOWN_TICKET
@@ -151,3 +157,3 @@
 
-  int freeCount() {
+  synchronized int freeCount() {
     int n = 0;
@@ -303,3 +309,3 @@
 
-  public static void main(String[] args) {
+  public static void main(String[] args) throws Exception {
     if (args.length > 0) {                    // spot sizes: serve stdin
@@ -311,2 +317,3 @@
     tests();
+    twoGates();
     System.out.println("tests passed");
Code, 25 lines
  static void twoGates() throws Exception {
    var lot = lot(SpotSize.MEDIUM, SpotSize.MEDIUM);
    var meet = new CyclicBarrier(2);
    lot.gap = () -> {                // both threads reach the gap, if able
      try {
        meet.await(300, TimeUnit.MILLISECONDS);
      } catch (TimeoutException | BrokenBarrierException e) {
        // locked: the other thread cannot get in, so do not wait forever
      } catch (InterruptedException e) {
        throw new AssertionError(e);
      }
    };
    var spots = new String[2];
    var ts = new Thread[2];
    for (int i = 0; i < 2; i++) {
      int k = i;
      ts[i] = new Thread(() ->
          spots[k] = lot.park(VehicleType.CAR, "G" + k, 0).spotId());
      ts[i].start();
    }
    for (var t : ts) t.join();
    check(!spots[0].equals(spots[1]), "same spot twice: " + spots[0]);
    check(lot.freeCount() == 0 && lot.tickets.size() == 2, "state");
  }

With twoGates pasted in, the patched file printed tests passed in five runs of five; with synchronized removed it failed three of three with same spot twice: P0, both threads having checked before either acted (one 8-core laptop).

Table: Tool, Use when, Trap (4 rows)
ToolUse whenTrap
synchronized or one ReentrantLockA check and an act on shared state must be one step; the default for a machine-coding buildOne lock makes every gate wait; the next section removes it
ConcurrentHashMapIndependent keys; putIfAbsent or compute for check-then-actTwo separate calls (get then put) are still a race
AtomicInteger, compareAndSetOne counter or one slotTwo atomics are not atomic together
Immutable recordsObjects read by many threads: final fields, handed over through a concurrent map or a lockA mutable object put in a shared map before it is filled

Two locks bring deadlock: a transfer holding account A waits for B while another holding B waits for A; the fix is one global order, lower id first. A lock also ends at one process; with two servers the guard moves to the database.

Next stop: Beyond one lock: a compare-and-set per spot, 3 min

Beyond one lock: a compare-and-set per spot

For: SDE-3, Staff.

The senior follow-up to the lock is "a thousand gates at nine in the morning". Version four drops the lock: each spot claims itself with one compare-and-set, so gates collide only on the same spot, and the loser tries the next. Because Spot owned its field, the change is the spot's insides, the loop and park:

Code, 31 lines
  static class Spot {
    // spotId, size and the constructor as in version one
    private final AtomicReference<String> occupiedBy =
        new AtomicReference<>(); // ticket id, null when free
    boolean isFree() { return occupiedBy.get() == null; }
    boolean take(String ticketId) { // one atomic check-and-set
      return occupiedBy.compareAndSet(null, ticketId);
    }
    void free() { occupiedBy.set(null); }
  }

  private Spot findSpot(VehicleType vehicle, String ticketId) {
    for (SpotSize size : rule.get(vehicle)) { // smallest allowed first
      for (Spot s : spots) {
        if (!s.isFree() || s.size != size) continue;
        gap.run();
        if (s.take(ticketId)) return s; // lost the race: try the next
      }
    }
    throw new ParkingError(Reason.LOT_FULL, "no free spot for " + vehicle);
  }

  Ticket park(VehicleType vehicle, String plate, int nowMin) {
    if (nowMin < 0) throw new IllegalArgumentException("time < 0");
    String id = "T" + nextId.getAndIncrement(); // a refusal burns one
    Spot spot = findSpot(vehicle, id);      // the check and the take
    var ticket = new Ticket(id, vehicle, plate, spot.spotId, nowMin);
    tickets.add(ticket);
    return ticket;
  }

nextId becomes an AtomicInteger, and the synchronized keywords and the old hook line go. The ticket map becomes a ConcurrentHashMap, whose remove(id, t) succeeds for one caller only; it is the last check in leave, so two exits on one ticket charge once:

Code, 21 lines
  static class TicketRepository {
    private final Map<String, Ticket> rows = new ConcurrentHashMap<>();
    // add, get and size as in version one
    boolean remove(String id, Ticket t) { return rows.remove(id, t); }
  }

  long leave(String ticketId, int nowMin) {
    Ticket t = tickets.get(ticketId);       // UNKNOWN_TICKET
    if (nowMin < t.entryMin()) {
      throw new ParkingError(Reason.BAD_EXIT, "exit before entry");
    }
    long fee = pricing.fee(t, nowMin - t.entryMin());
    Spot spot = spotById(t.spotId());       // SPOT_MISSING, still a check
    gap.run();
    if (!tickets.remove(ticketId, t)) {     // the last check: one exit wins
      throw new ParkingError(Reason.UNKNOWN_TICKET, ticketId);
    }
    spot.free();                            // then the only mutation
    return fee;
  }

The same twoGates test, its hook now between isFree and take, passed five runs of five with no lock; with take as a plain set, it failed three of three with same spot twice: P0. A second test, two threads leaving one ticket and meeting at the hook in leave, charged once in five of five; with the result of remove ignored, it charged twice in three of three.

The costs, said aloud: park is no longer one step, so for an instant a spot is taken with no ticket, and freeCount is a snapshot (fine for a display board, wrong for an audit); a refused park burns an id; the scan is still linear. The middle option is one lock per spot size: a car in MEDIUM never waits for a truck, though a car falling through to LARGE takes the truck's lock; each park holds one lock at a time, so no lock order is needed. Author's judgement (not measured): in a 90-minute build, ship the single lock and offer this version.

Next stop: What a Staff candidate adds, 2 min

What a Staff candidate adds

For: SDE-3, Staff.

Author's judgement (not measured): Staff adds judgement about change, shown best by reviewing code you did not write. Read in order: does it run, does any method change state before its checks, what do threads share, where does the likely change land, what do the tests pin. Then write three comments, costliest first, each with the line and the fix.

INVENTED example 1 min

Other teams own fee rules behind PricingStrategy, so the pull request for the pass keeps the old method and adds the new one as a default:

  interface PricingStrategy {
    @Deprecated
    long fee(VehicleType vehicle, int minutes);     // other teams' rules
    default long fee(Ticket ticket, int minutes) {  // the lot calls this
      return fee(ticket.vehicle(), minutes);
    }
  }
  // in MonthlyPass, or the file does not compile:
    public long fee(VehicleType v, int minutes) { // bridge: no plate
      return base.fee(v, minutes);
    }

One, the cost: the bridge has no plate, so a caller still on the old method skips the pass. Run, the pass charged a 500-minute car stay 18,000 paise through the old method and a covered car 0 through the ticket. Two, the drag: every new rule must implement a method it cannot honour. Three, the fix: leave the old interface alone, deprecated, and give the lot a new one with a one-line adapter:

  @Deprecated                       // other teams' rules, unchanged
  interface PricingStrategy {
    long fee(VehicleType vehicle, int minutes); // paise
  }

  interface TicketPricing {         // new: the lot calls only this
    long fee(Ticket ticket, int minutes);
    static TicketPricing from(PricingStrategy old) {
      return (t, minutes) -> old.fee(t.vehicle(), minutes);
    }
  }

The pass implements only TicketPricing; the lot takes TicketPricing.from(new HourlyRate()). Run, an old lambda charged 500 through the lot, the old class 18,000 through the adapter, the pass 0. It ends with removal: javac -Xlint:deprecation names each file still on the old interface, the adapter's too, in a project with a file per type (one file prints nothing); when only the adapter's is left, delete the old interface and from.

Next stop: The Staff bar, and a review drill, 1 min

The Staff bar, and a review drill

For: Staff.

The bar (author's judgement, not measured): the timed drill at 8 of 8, the removal plan, and two things said unasked. Boundaries: other teams call the pricing interface and read the ticket; spots and the repository stay inside, free to change. Stored data: old tickets have no plate, so the reader that treats a missing plate as "no pass" ships before the writer; backfill old rows only if a report needs them.

The drill, 30 minutes, key covered: review this pull request (INVENTED) on version three. It counts exits per gate for a display board and passes the gate to pricing; every rule and caller gains the argument.

Code, 19 lines
  interface PricingStrategy {
    long fee(Ticket ticket, int minutes, String gate);
  }

  private final Map<String, Integer> byGate = new HashMap<>();
  int exits(String gate) { return byGate.getOrDefault(gate, 0); }

  synchronized long leave(String ticketId, int nowMin, String gate) {
    Ticket t = tickets.get(ticketId);       // UNKNOWN_TICKET
    tickets.remove(ticketId);               // "saves a lookup later"
    if (nowMin < t.entryMin()) {
      throw new ParkingError(Reason.BAD_EXIT, "exit before entry");
    }
    long fee = pricing.fee(t, nowMin - t.entryMin(), gate);
    spotById(t.spotId()).free();
    byGate.merge(gate, 1, Integer::sum);
    return fee;
  }

The key. One: tickets.remove runs before the time check, so a refused exit loses the ticket and strands the spot (run, version three's "kept state" check fails); move it below every check. Two: the new parameter breaks every team's rule, lambdas included; add it beside the old interface, with the adapter. Three: exits reads the HashMap without the lock its writes hold, so the board can read a stale count; make it synchronized. Time and pass line (author's estimate, not measured): all three, in this order, each with its fix, the first shown failing by a run.

Next stop: What the round looks like in India: author's judgement, 1 min

What the round looks like in India: author's judgement

The usual shape, as the author understands it (not measured, and not a rule about any company): a build of 60 to 120 minutes in your own editor, sometimes after a briefing, then a review of 15 to 30 minutes in which someone runs your code, tries an input you did not plan for, and asks for a change. The change request is the extension above: place it before you are asked.

Ask your recruiter

Will the code be run, and by whom? Which language? Is there a briefing or review, and how long? Will someone watch? Can you look up syntax? Is there an upload deadline? Is the input format fixed (commands on stdin, exact output)? Is concurrency in scope? Lesson 4 covers how to ask.

Next stop: The 45-minute design-only variant: a script, 1 min

The 45-minute design-only variant: a script

For: SDE-2, SDE-3, Staff.

Some senior loops ask for the design alone in 45 minutes, with little or no running code. On the lot (claims and minutes the author's estimate, not measured):

  • 0 to 5, scope. "Three spot sizes, a fee per started hour in whole paise, one process. Out: payment, cameras, floors. The fee rule varies, so it gets the interface."
  • 5 to 15, draw Figure 7.2 in this order, naming each box as it lands: the two enums, Spot and Ticket, the lot in the middle, the pricing interface with one rule, then the repository and the error.
  • 15 to 25, two signatures: long fee(VehicleType vehicle, int minutes), and park with its body as comments, every check then the only change (Figure 7.3).
  • 25 to 32, place the pass unasked: the ticket gains a plate, the interface takes the ticket, the pass wraps a base rule.
  • 32 to 45, follow-ups:

"Why an interface, not a subclass per rule?" "Rules stack: a pass over an hourly rate. Subclasses need one per combination; a decorator holds one inner rule."

"Where does the lock go?" "One lock over park, leave and the free count, since all touch spots; a compare-and-set per spot when gates queue."

"How do you persist it?" "A spots table with a nullable ticket column. Park is one conditional update, set the ticket where it is null, which worked only if one row changed; or a unique key on spot in an occupancy table. Ticket ids come from the store, so a restart cannot reissue T1."

Next stop: What a reviewer marks down: author's judgement, 1 min

What a reviewer marks down: author's judgement

The checklist is the author's, invented for this guide, rows heaviest first (author's estimate, not measured); the drill scores it.

Table: Dimension, What to check in your own build, Done (8 rows)
DimensionWhat to check in your own buildDone
Core flow runsThe driver calls every must-have and prints a result; the reviewer's input runs too
ExtensibilityThe likely new rule lands as one class and one line of wiring
TestsEach rule has a named check run from main; at least one error path
CorrectnessNo mutation before every check has passed
Errors and edge casesOne error type for refused requests (bad id, full lot), caught in the driver; a bad id is refused and nothing changes
Scope fits the timeEvery must-have works; extras are cut, not half-built
Ceilings statedYou said aloud where the design stops, threads included, and handled them if asked
Patterns, only where earnedEach pattern answers a named requirement

Next stop: A timed drill and its pass mark, 1 min

A timed drill and its pass mark

For: Intern to Staff.

Reading the lot is not the skill; rebuilding it is. Book closed, in a blank folder, on the Figure 7.1 plan: at minute 82 typing stops, with version one, the stdin driver and the monthly pass typed and every check passing; minutes 82 to 90 are the notes. Pass mark: 7 of the 8 checklist rows, "Core flow runs" and "Correctness" among them (Staff: 8 of 8); miss it and repeat another day. Then, on two other days, rebuild tic-tac-toe with undo and the seat hold with holdAll to the same mark. Minutes and mark are the author's estimate (not measured).

More practice: fifteen prompts with their follow-ups in the Practice banks, and 3 prompt-kit cards.

Next stop: What stays uncertain, 1 min

What stays uncertain

  • Martin's SOLID article and the Gang of Four book were not read; the wording follows secondary summaries, and the Liskov wording is the standard formulation.
  • All code ran with the source-level-21 launcher on OpenJDK 25, not a JDK 21 binary.
  • Nobody has timed the drill or the plan; every minute figure is the author's estimate (not measured).

Sources

4 sources
  1. refactoring.guru, each pattern's page, https://refactoring.guru/design-patterns, secondary, read 31 Oct and 2 Nov 2026.
  2. Wikipedia, "SOLID", https://en.wikipedia.org/wiki/SOLID, secondary, read 31 Oct 2026.
  3. Joshua Bloch, Effective Java, 3rd edition, Addison-Wesley, 2018: Item 3 (enforce the singleton property with a private constructor or an enum type).
  4. OpenJDK 25, javadoc in the shipped source (src.zip) of ConcurrentHashMap, CyclicBarrier, AtomicReference and java.lang.Record, primary, read 1 and 2 Nov 2026.

Use it in step 7: Machine coding and class design