Everyday habits
DRY, KISS, and YAGNI
DRY means one home for each rule. KISS means prefer a clear class over a clever framework. YAGNI means do not build what you do not need yet. The fee formula was copied into exit, the daily report, and a refund helper. GST changed; two copies were updated. The third undercharged a week of customers. Someone "fixed" it with a PricingFramework, plugins, and annotations — for one lot with one rate. These three habits would have stopped both messes.
Three questions, not a framework
Ask three questions: Is this a duplicated rule? Is this a clever framework where a clear class would do? Is this an abstraction I do not need yet? Those questions keep SOLID and GRASP from becoming premature abstraction. Protected Variations is right when something will vary. YAGNI is right when it will not. The skill is telling those apart in the next fifteen minutes of the interview.
DRY — do not copy a rule
DRY means: do not duplicate a business rule. Two copies of hours × rate is one future bug. You may copy a sentence of English in a comment if you must. Do not copy the formula. Shared rule, one home — often the Information Expert or a PricingStrategy. Duplicate shape is fine: two methods that look similar but mean different policies should not share a helper. Duplicate rules are not.
Java: the rule had three homes
// exit
Money a = rate.times(hours(ticket));
// report
Money b = rate.times(hours(ticket));
// refund
Money c = rate.times(hours(ticket));
// GST lands in a and b. c still undercharges.One home, still simple. Second policy? Then introduce PricingStrategy. Until then this class is enough. That sentence is DRY meeting YAGNI.
class HourlyPricing {
Money fee(Ticket ticket, Clock clock) {
long hours = Math.max(1, ticket.hoursParked(clock));
return ticket.hourlyRate().times(hours);
}
}
class ExitVehicle {
private final HourlyPricing pricing;
Money handle(Ticket ticket, Clock clock) { return pricing.fee(ticket, clock); }
}
class DailyReport {
private final HourlyPricing pricing;
Money total(List<Ticket> closed, Clock clock) {
return closed.stream()
.map(t -> pricing.fee(t, clock))
.reduce(Money.zero(), Money::plus);
}
}KISS — a clear class beats a clever framework
KISS means: if a HashMap plus a list tells the story, do not introduce a graph library. Interviews grade the object model, not the number of patterns named. A parking lot is floors, spots, tickets, and a couple of strategies. It is not an annotation processor, a plugin registry, and a rule engine — unless the problem was "design a rule engine."
Cleverness also hides bugs. A one-line fee in HourlyPricing is reviewable. A visitor that walks a pricing AST for one hourly rate is not. Keep the code as close to the sentence you would say to the interviewer as you can.
Java: KISS still allows a strategy when the strategy is earned
// Simple enough for one rate
class ParkingLot {
Money feeFor(Ticket ticket, Clock clock) {
return ticket.hourlyRate().times(ticket.hoursParked(clock));
}
}
// Still simple after the follow-up asks for event-day
class ParkingLot {
private final PricingStrategy pricing;
ParkingLot(PricingStrategy pricing) { this.pricing = pricing; }
Money feeFor(Ticket ticket, Clock clock) { return pricing.fee(ticket, clock); }
}
// Not simple: overbuilt framework for one lot
abstract class AbstractPricingEngine {
abstract Money compute(PricingContext ctx);
List<PricingPlugin> plugins;
Map<String, RateCard> cardsByAirportCode;
}YAGNI — you are not going to need it yet
YAGNI means: do not build the third pricing rule before the second exists. A strategy interface is earned when the follow-up asks for a second policy, or when you can already name two. Until then, one class is honest. AbstractPricingEngine with plugins is YAGNI dressed as Open-Closed. Wait until the interviewer asks. Then add a small interface, not a platform.
TicketIdFactory, SpotVisitor, and a generic Repository<T> for two types are the same smell. Machine coding rewards the design that can absorb the next sentence, not the design that absorbed an imaginary product roadmap.
When the three habits pull in different directions
DRY says extract the formula. YAGNI says do not extract a strategy until the second policy exists. Both can be true: extract a single HourlyPricing class (DRY, KISS) without an interface (YAGNI). When event-day arrives, introduce the interface (Open-Closed / Protected Variations). You did not copy the rule, and you did not build a plugin host.
DRY on the wrong thing is the other trap. Exit and refund might look like the same hours × rate, but refund might be "unused hours only" or "minus a cancellation fee." Forcing them through one helper will encode the wrong policy. Duplicate shape, different meaning: keep two methods. Shared meaning: one home.
KISS versus Protected Variations: if you already know allocation will be "first free or nearest," introduce the strategy interface now. That is not a YAGNI violation; that is naming a variation that is in the problem statement. If the statement is one lot, one rate, one first-free walk, leave the strategy interfaces out until the follow-up.
A framework for the second variant you imagined
Candidates who fear looking junior sometimes overbuild. Three interfaces, a registry, and Spring-shaped annotations for a parking lot read as fear, not seniority. Senior is HourlyPricing plus "if you want airport rates I will extract PricingStrategy." That sentence is YAGNI with an escape hatch. See Open-Closed and Protected Variations for when the hatch should already be open.
Design smells these habits catch
Copy-paste fee math is a DRY miss. A PricingFramework for one rate is a KISS and YAGNI miss. A God Manager is usually all three plus a GRASP miss. When you refactor, start with the duplicated rule. Put it on the expert. Only then decide whether the expert needs an interface. See design smells.
The GST week, done the simple way
Monday: one HourlyPricing class, tests with a frozen clock, GST as a field. Tuesday: interviewer asks for event-day. You extract PricingStrategy, wrap hourly, ship. You did not start Monday with plugins. You did not leave three copies of hours × rate. That week is DRY, KISS, and YAGNI in order.
class Gst {
private final BigDecimal rate;
Money apply(Money base) { return base.plus(base.times(rate)); }
}
class HourlyPricing implements PricingStrategy {
private final Gst gst;
public Money fee(Ticket ticket, Clock clock) {
Money base = ticket.hourlyRate().times(ticket.hoursParked(clock));
return gst.apply(base);
}
}
class EventDayPricing implements PricingStrategy {
private final Money flat;
private final PricingStrategy inner;
public Money fee(Ticket ticket, Clock clock) {
return flat.plus(inner.fee(ticket, clock));
}
}KISS in the object model, not in the domain rules
Simple does not mean "put everything in ParkingLot." That is simple to type and expensive to change. Simple means the fewest types that still protect the invariant and absorb the named follow-up. A Ticket with parked(clock) is simpler than a Manager with four getters, even though it is one more method on Ticket.
YAGNI does not mean "never an interface." It means "not this interface, not today, unless you can name two implementations." TicketRepository is usually earned on day one because tests need an in-memory map. TicketIdFactory is not. Judge each abstraction on whether a second implementation is already in the room.
Related
What you write on the whiteboard first
One Ticket, one ParkingLot, one HourlyPricing. Tests with a clock. Then you wait. If the next sentence is a second fee, you extract the interface in place. If the next sentence is persistence, you add TicketRepository. You do not start with an AbstractFactory of strategies. The board should look slightly incomplete until the follow-up earns the next type. That incompleteness is YAGNI made visible, and interviewers who have graded overbuilt lots will recognize it as confidence.
DRY still applies to that first board: hoursParked lives on Ticket from the first drawing, even if only exit calls it. You are not duplicating duration math "until the report exists." The rule has one home immediately. KISS is the missing plugin host. YAGNI is the missing third strategy. All three can sit on one diagram without crowding it.
Refund is not always the fee backwards
If refund is "the same money we charged," DRY says call pricing.fee again or store the charged amount on the ticket. If refund is "unused hours minus a fee," that is a different rule — a second home, maybe RefundPolicy. Copying the hourly multiply into refund looks like DRY failure and is actually two policies. Ask whether the meaning matches, not whether the tokens match.
class Ticket {
private Money charged;
void markCharged(Money amount) { this.charged = amount; }
Money charged() { return charged; }
}
class FullRefund {
Money amount(Ticket ticket) { return ticket.charged(); }
}
class UnusedHoursRefund implements RefundPolicy {
public Money amount(Ticket ticket, Clock clock) {
return ticket.hourlyRate().times(unusedHours(ticket, clock));
}
}Comments are not a third copy of the rule
A comment that restates hours × rate next to the method is fine. A comment that contains a second formula the code does not run is a DRY miss waiting for GST. Prefer a test named charges_two_hours_plus_gst over a comment that will rot. KISS likes tests more than essays. YAGNI likes not documenting the plugin host you did not build.
One HashMap is allowed to be boring
In-memory TicketRepository as a HashMap is KISS. A concurrent skip-list keyed by airport code is YAGNI for one lot. DRY still wants hoursParked on Ticket even in the HashMap sketch. Boring storage plus one fee home is the design that survives a two-hour window.
Wait for the second policy, not the third
YAGNI is not "wait forever." Two named fee rules in the problem statement already earn PricingStrategy. One rule does not. Count implementations in the room, not implementations you fear might appear next quarter.
Takeaway
Do not copy the fee rule. Keep the class as simple as the sentence you would say out loud. Do not build the third pricing engine before the second policy exists. Extract one HourlyPricing now; extract PricingStrategy when the follow-up asks. Duplicate meaning is the enemy. Duplicate shape with different meaning is allowed.