Definition
- KISS (Keep It Simple, Stupid) – the simplest solution that correctly solves the problem is usually the best; simple code is easier to read, test, debug, and change.
- YAGNI (You Aren't Gonna Need It) – do not build functionality or abstractions on speculation about future needs; build only what the current requirement demands.
- Balance with DRY – DRY can tempt premature abstraction; KISS and YAGNI push back.
- Rule of three – wait for three genuine repetitions that change together before unifying.
- Guiding phrase – "duplication is far cheaper than the wrong abstraction."
Rajesh needs a simple way to transfer money between two accounts at National Bank of India. KISS says: write a straightforward transfer method that debits one and credits the other – do not build a configurable, plugin-based, rule-engine-driven transfer framework. YAGNI says: do not add support for fifteen currencies, scheduled transfers, and a message broker today just because someone imagines the bank might need them next year. If you build the imaginary mansion before anyone needs the extra rooms, you waste effort and create corridors nobody walks down.
Understanding the Concepts
What is the KISS principle?
KISS is about favouring clarity over cleverness. A violation is a dense one-liner that nobody can read where a plain if-else would be obvious.
package com.bank.service;
import java.math.BigDecimal;
import java.util.List;
// VIOLATION: clever one-liner, hard to read
public class FeeCalculatorBad {
public BigDecimal totalFees(List<BigDecimal> amounts) {
return amounts.stream().reduce(BigDecimal.ZERO,
(a, b) -> a.add(b.compareTo(new BigDecimal("10000")) > 0
? b.multiply(new BigDecimal("0.002"))
: new BigDecimal("50")));
}
}
// KISS: simple loop, easy to understand
public class FeeCalculatorSimple {
private static final BigDecimal THRESHOLD = new BigDecimal("10000");
private static final BigDecimal FLAT_FEE = new BigDecimal("50");
private static final BigDecimal RATE = new BigDecimal("0.002");
public BigDecimal totalFees(List<BigDecimal> amounts) {
BigDecimal total = BigDecimal.ZERO;
for (BigDecimal amount : amounts) {
if (amount.compareTo(THRESHOLD) > 0) {
total = total.add(amount.multiply(RATE));
} else {
total = total.add(FLAT_FEE);
}
}
return total;
}
}
Simple code has fewer places for bugs to hide and is cheaper to change.
What is the YAGNI principle?
YAGNI says do not build features or abstractions on speculation. The temptation is to think "we might need multiple payment providers later" and build a pluggable framework now.
package com.bank.service;
import java.math.BigDecimal;
// YAGNI VIOLATION: plugin framework for ONE provider
public interface PaymentProvider {
void charge(String account, BigDecimal amount);
}
public class PaymentProviderRegistry {
// dynamic loading, service locator, etc. for ONE implementation
}
public class RazorpayProvider implements PaymentProvider {
public void charge(String account, BigDecimal amount) {
System.out.println("Charged " + amount + " via Razorpay");
}
}
// YAGNI-COMPLIANT: just use the provider directly
public class PaymentService {
public void charge(String account, BigDecimal amount) {
// Direct call – when a second provider actually arrives, refactor
System.out.println("Charged " + amount + " via Razorpay");
}
}
The future requirement, when it actually appears, is usually different from what you imagined.
How do KISS and YAGNI balance against DRY?
DRY tells you to remove duplication, but applied too eagerly it directly conflicts with KISS and YAGNI because it tempts premature abstraction.
package com.bank.service;
import java.math.BigDecimal;
// These look alike but represent DIFFERENT business rules
public class ProcessingFee {
public BigDecimal calculate(BigDecimal amount) {
return amount.multiply(new BigDecimal("0.01")); // loan processing
}
}
public class LatePenalty {
public BigDecimal calculate(BigDecimal amount) {
return amount.multiply(new BigDecimal("0.01")); // late penalty
}
}
// WRONG: merging them because they look the same
public class UnifiedFee {
private final boolean isLatePenalty;
public UnifiedFee(boolean isLatePenalty) { this.isLatePenalty = isLatePenalty; }
public BigDecimal calculate(BigDecimal amount) {
// Now flags and conditionals creep in as rules diverge
BigDecimal rate = isLatePenalty ? new BigDecimal("0.01") : new BigDecimal("0.01");
return amount.multiply(rate);
}
}
// RIGHT: keep them separate until they genuinely share knowledge
Let KISS and YAGNI be the default: build the simplest thing that meets the current requirement. Apply DRY only when you have real, repeated knowledge that genuinely changes together.
What are real YAGNI violations?
Real examples worth naming because interviewers ask for them:
- Building a plugin architecture with interfaces, a registry, and dynamic loading for a service with only one implementation.
- Adding a message broker like Kafka when a simple direct REST call between two services handles the load.
- Writing a generic configurable rules engine when there are only three fixed rules.
- Adding caching layers before measuring any performance problem.
- Creating abstract base classes with a single subclass "in case we need more."
package com.bank.service;
import java.math.BigDecimal;
// YAGNI VIOLATION: abstract base with one subclass
public abstract class AbstractInterestCalculator {
public abstract BigDecimal calculate(BigDecimal balance);
public void logResult(BigDecimal interest) {
System.out.println("Interest: " + interest);
}
}
public class SavingsInterestCalculator extends AbstractInterestCalculator {
public BigDecimal calculate(BigDecimal balance) {
return balance.multiply(new BigDecimal("0.04"));
}
}
// BETTER: plain class, no inheritance needed (yet)
public class SavingsInterest {
public BigDecimal calculate(BigDecimal balance) {
BigDecimal interest = balance.multiply(new BigDecimal("0.04"));
System.out.println("Interest: " + interest);
return interest;
}
}
How do you decide when to abstract?
The rule of three: wait until you have three genuine repetitions that truly change together before unifying. Until then, the duplication is cheap and honest.
// Day 1: one fee, plain method – KISS + YAGNI
public class TransferService {
public BigDecimal fee(BigDecimal amount) {
return new BigDecimal("50.00");
}
}
// Day 30: second fee arrives – now abstract
public interface FeeStrategy {
BigDecimal calculate(BigDecimal amount);
}
public class FlatFee implements FeeStrategy {
public BigDecimal calculate(BigDecimal amount) {
return new BigDecimal("50.00");
}
}
public class PercentageFee implements FeeStrategy {
public BigDecimal calculate(BigDecimal amount) {
return amount.multiply(new BigDecimal("0.001"));
}
}
What are common mistakes when balancing these principles?
- Applying DRY before KISS: extracting a shared module for two things that only look similar.
- Ignoring YAGNI in the name of "extensibility": building for imagined futures.
- Confusing simplicity with naivety: KISS does not mean incorrect or incomplete.
How do KISS and YAGNI look in banking code?
In banking, KISS means choosing the simplest correct solution. A fee calculation that could be a simple method does not need a Strategy pattern with an interface, a factory, and a registry. YAGNI means not building multi-currency support when the requirement is INR-only. You add complexity when the requirement arrives, not before.
package com.bank.service;
import java.math.BigDecimal;
public class KissYagniBanking {
// KISS: simple fee calculation
// WRONG: FeeStrategy interface + FeeStrategyFactory + FeeStrategyRegistry
// CORRECT: just a method
static BigDecimal processingFee(BigDecimal principal) {
return principal.multiply(new BigDecimal("0.01"));
}
// YAGNI: don't build multi-currency until required
// WRONG: CurrencyConverter + ExchangeRateService + Money<Currency>
// CORRECT: BigDecimal in INR (the only currency today)
static BigDecimal transferAmount(BigDecimal amount) {
return amount; // INR only -- add conversion when requirement arrives
}
public static void main(String[] args) {
BigDecimal fee = processingFee(new BigDecimal("100000"));
System.out.println("Processing fee: " + fee + " INR");
System.out.println("KISS: one method, no pattern needed");
System.out.println("YAGNI: INR only, no multi-currency until required");
}
}
Expected output:
Processing fee: 1000.00 INR
KISS: one method, no pattern needed
YAGNI: INR only, no multi-currency until required
What should a senior engineer know?
- "Duplication is far cheaper than the wrong abstraction" – Sandi Metz.
- KISS and YAGNI are the default stance. DRY is applied when knowledge genuinely repeats.
- The future requirement, when it actually arrives, is usually different from what you imagined.
- In banking, regulatory rules change unpredictably, so building for guesses is doubly wasteful.
package com.bank.demo;
import java.math.BigDecimal;
public class KissYagniDemo {
public static void main(String[] args) {
// KISS: plain, clear, correct
BigDecimal balance = new BigDecimal("100000");
BigDecimal interest = balance.multiply(new BigDecimal("0.04"));
System.out.println("Savings interest on 1L: " + interest);
// YAGNI: no framework, no factory, no registry – just the answer
BigDecimal fee = new BigDecimal("50.00");
System.out.println("Transfer fee: " + fee);
}
}
Savings interest on 1L: 4000.00
Transfer fee: 50.00
Visual Overview
Principle Balance
─────────────────
KISS ──── "Simplest thing that works"
│
├── YAGNI ── "Don't build until needed"
│ │
│ └── Resists: speculative patterns,
│ plugin frameworks,
│ message brokers
│
└── DRY ── "One home for each piece of knowledge"
│
└── Caution: can conflict with KISS/YAGNI
if applied to coincidental similarity
Decision Flow:
┌──────────────────────────┐
│ Is this the same │
│ knowledge repeated? │
└────────┬─────────────────┘
Yes │ No
▼ │ ▼
Extract │ Keep separate
(DRY) │ (KISS + YAGNI)
│
┌────────┴─────────────────┐
│ Has it repeated 3 times? │
└────────┬─────────────────┘
Yes │ No
▼ │ ▼
Unify │ Tolerate duplication
Key Takeaway
KISS and YAGNI are two principles that both push back against over-engineering. KISS says the simplest solution that correctly solves the problem is usually the best, because simple code is easier to read, test, debug, and change. YAGNI says do not build features or abstractions on speculation about future needs, because the imagined future rarely arrives as imagined, and meanwhile you pay for the guess in immediate complexity.
At National Bank of India, a colleague wanted to build a configurable rules engine with pluggable fee strategies and a Kafka topic to publish fee events, even though there was exactly one fee rule at the time. Rajesh wrote a plain fee method with a couple of constants. Later, when a genuine second rule actually arrived, extending the simple design was a tiny, obvious change.
The balancing act against DRY is critical. DRY is right that each piece of knowledge should have one home, but applied too eagerly it tempts premature abstraction. The classic trap is seeing two blocks that merely look alike and extracting them into one shared abstraction. If those blocks represent different rules that only coincide for now, the shared code becomes a mess of flags the moment they diverge. Rajesh lets KISS and YAGNI be the default and applies DRY only when the same knowledge genuinely repeats, often waiting for the rule of three.
Real YAGNI violations Rajesh has seen include building a plugin architecture for a service with only one implementation, adding a message broker when REST handles the load, and adding caching before measuring any performance problem. Each trades certain, immediate complexity for a speculative benefit that usually never materialises.
Remember one sentence: start with the simplest correct solution, do not build for imagined futures, and unify only when the same knowledge genuinely repeats and changes together.
---