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.
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.
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.
| Question | What it decides | If the interviewer shrugs, I assume |
|---|---|---|
| Percent, flat, free delivery, first order only? | The size of the Coupon interface | Percent with a cap, flat with a minimum |
| Can two discounts apply to one order? | One coupon, or a list with an order | One per order; stacking is the follow-up |
| Rupees with decimals, or whole paise? | The type of every amount, rounding | Whole paise in a long, percent floors |
| One process or several servers? | Whether a lock is enough | One 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)
| Letter | The question | In the parking lot |
|---|---|---|
| S, single responsibility | How many reasons does this class have to change? | A lot that also prints receipts has two. Printing goes to the driver. |
| O, open-closed | What must I edit to add this? | A new fee rule is a new class behind PricingStrategy. |
| L, Liskov substitution | Swap 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 segregation | Does the caller see methods it never uses? | The repository offers add, get, remove and size, not a bulk export. |
| D, dependency inversion | Does 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)
| Pattern | Use when | Java shape | Interview trap |
|---|---|---|---|
| Singleton | Exactly one instance is a real rule | enum 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 change | static Notifier of(Channel c) with a switch over a sealed type or enum | A factory with one product. |
| Abstract factory | Families of objects that must match (a UI kit) | An interface with one method per product; one implementation per family | One family. Say "factory" and stop. |
| Builder | Many optional parts; telescoping constructors | Nested static class Builder with chained setters and build() that validates | Four fields do not need it; a record is shorter. |
| Adapter | A class you cannot change has the wrong interface | A small class implementing your interface and holding the foreign one | Adapting a class you own. Change it instead. |
| Decorator | Combinable 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. |
| Facade | A simple entry point to a messy subsystem | One class with a few methods calling several collaborators | It grows into a god object holding rules. |
| Proxy | Same interface, controlled access: lazy load, cache, access check, logging | Wraps the real object, decides whether and when to call it | Confusing it with decorator: proxy controls access, decorator adds behaviour. |
| Observer | Unknown, changing listeners must react to an event | A list of Consumer<Event>; the subject calls each | One listener; or a slow or throwing listener that stalls the rest. |
| Strategy | A rule varies: fee, discount, assignment | An interface with one method; a lambda is a strategy. | A single fixed rule behind an interface. |
| Command | Undo, queue or log of operations | Interface with execute() (and undo()); a Deque as the history | Undo needing an inverse you cannot write; store a snapshot. |
| State | Many states, each with its own rule per action | An interface per state, the object delegates to the current one | Two states need a field; four with their own rules earn classes. |
| Composition over inheritance | Behaviour must vary or combine | Hold a collaborator behind an interface; use a sealed interface plus records for fixed variants | A 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.
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.
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)
| State | Event | Guard | Next | Else refuse |
|---|---|---|---|---|
| FREE | hold | none | HELD, 300 s | none |
| HELD | confirm | same user, not lapsed | BOOKED | NOT_HOLDER (lapsed: NOT_HELD) |
| HELD | clock reaches expiry | checked on every read | FREE | none |
| HELD, BOOKED | hold | none | unchanged | TAKEN |
| FREE, BOOKED | confirm | none | unchanged | NOT_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.
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.
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.
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)
| Tool | Use when | Trap |
|---|---|---|
synchronized or one ReentrantLock | A check and an act on shared state must be one step; the default for a machine-coding build | One lock makes every gate wait; the next section removes it |
ConcurrentHashMap | Independent keys; putIfAbsent or compute for check-then-act | Two separate calls (get then put) are still a race |
AtomicInteger, compareAndSet | One counter or one slot | Two atomics are not atomic together |
| Immutable records | Objects read by many threads: final fields, handed over through a concurrent map or a lock | A 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.
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,
SpotandTicket, 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), andparkwith 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)
| Dimension | What to check in your own build | Done |
|---|---|---|
| Core flow runs | The driver calls every must-have and prints a result; the reviewer's input runs too | |
| Extensibility | The likely new rule lands as one class and one line of wiring | |
| Tests | Each rule has a named check run from main; at least one error path | |
| Correctness | No mutation before every check has passed | |
| Errors and edge cases | One error type for refused requests (bad id, full lot), caught in the driver; a bad id is refused and nothing changes | |
| Scope fits the time | Every must-have works; extras are cut, not half-built | |
| Ceilings stated | You said aloud where the design stops, threads included, and handled them if asked | |
| Patterns, only where earned | Each 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
- refactoring.guru, each pattern's page, https://refactoring.guru/design-patterns, secondary, read 31 Oct and 2 Nov 2026.
- Wikipedia, "SOLID", https://en.wikipedia.org/wiki/SOLID, secondary, read 31 Oct 2026.
- Joshua Bloch, Effective Java, 3rd edition, Addison-Wesley, 2018: Item 3 (enforce the singleton property with a private constructor or an enum type).
- OpenJDK 25, javadoc in the shipped source (src.zip) of ConcurrentHashMap, CyclicBarrier, AtomicReference and java.lang.Record, primary, read 1 and 2 Nov 2026.