DRY, KISS, YAGNI and the Law of Demeter
Part of the free Low-Level Design course on LogicWiz, in the chapter: SOLID, without the slogans. The class-diagram exercises run in the app.
Eleven classes for two coupons
"You built a plugin system for coupons. We have two coupons. And the GST rate is still typed in four places."
Priya, reviewing Arjun's refactor of the cart
Too much design, and too little
After learning SOLID, Arjun refactored SwiftKart's cart. He added a CouponPlugin interface, a CouponPluginLoader, a CouponFactory and a configuration file. All this was for two coupons that have not changed in a year. At the same time, he copied the 5% GST rate (GST is India's goods and services tax) into four classes.
The design was too big in one place and too loose in another. SOLID did not warn him about either. This lesson adds four everyday principles that sit beside SOLID:
- DRY: each rule lives in one place.
- KISS: pick the simplest design that meets the requirements.
- YAGNI: do not build for requirements nobody asked for.
- The Law of Demeter: talk only to your direct friends.
Interviewers use all four, often without naming them. "Why is this class here?" and "Where else would I change this?" are how they ask.
DRY: one rule, one place
Here is the GST rate as Arjun left it.
public class Cart {
private long subtotalPaise;
public long totalPaise() {
return subtotalPaise + subtotalPaise * 5 / 100;
}
}
public class Invoice {
private long itemsPaise;
public long gstPaise() {
return itemsPaise * 5 / 100;
}
}
// the same "* 5 / 100" is also in OrderSummary and RefundCalculator
When the rate changes, someone must find all four copies. They will find three. The fourth gives customers wrong refunds for months.
DRY stands for "Do not Repeat Yourself". It means every piece of knowledge, such as a business rule, has one home in the code. Here the knowledge is "how GST is worked out".
public class GstPolicy {
private static final int RATE_PERCENT = 5;
public static long gstOn(long amountPaise) {
return amountPaise * RATE_PERCENT / 100;
}
}
// Cart, Invoice, OrderSummary and RefundCalculator all call:
// long gst = GstPolicy.gstOn(amountPaise);
Think of a contact saved in your phone. You save a friend's number once, and every app reads it from there. When the number changes, you edit one entry.
Now the trap. Arjun had believed that any two blocks of code that look the same should be merged. He found these two methods.
// what the customer pays for delivery
public long deliveryFeePaise(int km) {
return km * 1000;
}
// what SwiftKart pays the rider
public long riderPayoutPaise(int km) {
return km * 1000;
}
He merged them into one perKmPaise(km). A month later, finance raised rider pay, and every customer's delivery fee went up too. The code looked alike, but it held two different rules, owned by two different teams. They matched by chance.
This is where the phone-contact analogy stops working. Two family members may share a home number today. They are still two people, and when one moves out you need two entries. DRY is about one rule in one place, not about text that looks alike. Before you merge, ask: if one changes, must the other change too? If not, keep them apart.
KISS: the simplest design that meets the requirements
Back to coupons. Arjun's loader read class names from a file and created coupon objects at runtime. Only two coupons existed: ₹50 off, and 10% off.
KISS stands for "Keep It Simple". Pick the simplest design that meets today's requirements. Simple means few moving parts, and code a new teammate can follow in one read. It does not mean the fewest lines, and it does not mean clever.
public interface Coupon {
long discountPaise(long subtotalPaise);
}
public class FlatCoupon implements Coupon {
private long offPaise;
public FlatCoupon(long offPaise) {
this.offPaise = offPaise;
}
@Override
public long discountPaise(long subtotalPaise) {
return Math.min(offPaise, subtotalPaise);
}
}
// PercentCoupon has the same shape, with a percent field.
// A Map<String, Coupon> built in main finds a coupon by its code.
That is one interface, two small classes and a map. There is no loader, no configuration file and no factory. A new coupon is still one new class, so nothing was lost.
Arjun asked, "But the loader is more flexible. Isn't flexible always better?" Priya said flexibility you do not use is a cost. Every teammate must learn the loader before adding a coupon, and bugs can hide in it. The flexibility was also in the wrong place. The next real request was a coupon with a cap, and that needed a new class anyway.
Think of carrying home one bag of groceries. You carry it; you do not hire a truck. The analogy stops working at scale. When you move house, you do need the truck. Simple is measured against the real requirements, not against the smallest possible code.
YAGNI: build what was asked
YAGNI stands for "You Aren't Gonna Need It". Do not build for a requirement nobody has asked for. The guess is often wrong, and the extra code costs time to write, test and read today.
In a 90-minute machine-coding round, YAGNI decides what you skip. Interviewers mark down over-engineering: a design so large that the core flow does not work by the end.
- Skip: a real database, login, a web API layer, a plugin loader, a general event system, configuration files.
- Keep: in-memory maps, one
mainthat shows the flow, and an interface where the problem names more than one kind, such as "UPI, card and cash". - Ask: when unsure, ask the interviewer whether X is in scope. Their answer decides.
YAGNI does not mean "never prepare". An interface that the requirement already hints at costs one small file. A plugin system costs an hour. Prepare where the cost is small and the change is likely.
💡 Tip: Say what you skip out loud: "I keep orders in a HashMap; a real database would sit behind the same repository interface." It shows you know, and it saves the time.
The Law of Demeter: talk to your direct friends
Here is Arjun's checkout.
public class Checkout {
public void complete(Order order) {
order.getCustomer().getWallet().debit(order.totalPaise());
order.markPaid();
}
}
Checkout reaches through the order, into the customer, into the wallet. It now depends on three classes and on how they are wired together. Suppose customers start paying from a family wallet. Checkout breaks, though it has nothing to do with wallets.
The Law of Demeter says a method should call methods only on its direct friends:
- its own object (
this) and its fields, - the parameters passed to it,
- objects it creates itself.
It should not call methods on an object that a friend handed back. A quick check: count the dots in a chain that changes something. More than one is a warning sign.
public class Order {
private Customer customer;
private long totalPaise;
private boolean paid;
public Receipt chargeCustomer() {
Receipt receipt = customer.pay(totalPaise);
paid = true;
return receipt;
}
}
// Customer.pay asks its own wallet to debit.
// Checkout now calls only: order.chargeCustomer();
sequenceDiagram
participant Checkout
participant Order
participant Customer
participant Wallet
Checkout->>Order: chargeCustomer()
Order->>Customer: pay(totalPaise)
Customer->>Wallet: debit(totalPaise)
Wallet-->>Customer: done
Customer-->>Order: Receipt
Order-->>Checkout: Receipt
Each object talks only to the next one. If the wallet rules change, only Customer notices. This is "tell, don't ask" from the encapsulation lesson, applied along a chain.
Think of a rider at the door. The rider asks the customer to pay. The rider does not take the wallet out of the customer's pocket. The analogy stops working for plain values. code.trim().toUpperCase() has two dots, but each call returns a new string and reaches into no one. Demeter is about reaching into other objects to change them.
When these pull against SOLID and patterns
The principles do not always agree.
- OCP and YAGNI. The open/closed principle says add an interface so new cases do not edit old code. YAGNI says wait until a second case exists. Let the requirement decide: several named kinds mean an interface, one kind means a plain class.
- DRY and SRP. Merging the delivery fee and the rider payout made one method serve two teams. That breaks single responsibility, so keep the two rules apart.
- Demeter and KISS. Every hop you hide adds a small method. A few are fine. Dozens of methods that only pass calls on mean the classes are split in the wrong place.
- Patterns and KISS. A pattern is a known answer to a known problem. Without the problem, it is extra classes. The coming chapters say when not to use each one.
An interviewer grades a design on fit, not on size. Eleven classes for two coupons scores lower than four classes that work and change easily.
Try it on Arjun's design. Checkout reaches through the customer into the wallet, and two classes keep their own GST rate. Make checkout talk only to the order, and give the GST rule one home that both classes use.
📌 Summary: DRY: one rule, one place, but do not merge code that only looks alike. KISS and YAGNI: build the simplest design for today's requirements. Demeter: talk to direct friends, not their parts.
What Arjun learns next
Arjun's design is small again, and every rule has one home. But somewhere, code still decides which notifier to build for each customer: SMS, email or push. In the chapter on creational patterns, Arjun puts that decision in one place with a factory.
Check your understanding
- deliveryFeePaise(km) and riderPayoutPaise(km) both return km * 1000 today. Should Arjun merge them into one method?
- Twenty minutes into a 90-minute round, the problem says customers pay by UPI or card. What should you build?
- Which line breaks the Law of Demeter?
- A friend says, "The more design patterns I show in an LLD round, the higher I score." What is the best reply?