Understanding KISS, YAGNI, and DRY: Key Software Development Principles
Good code isn't just about syntax; it's about philosophy. We explain KISS (Keep It Simple), YAGNI...
Abstract AlgorithmsAI-assisted content. This post may have been written or enhanced with AI tools. Please verify critical information independently.
TLDR
TLDR: KISS (Keep It Simple), YAGNI (You Aren't Gonna Need It), and DRY (Don't Repeat Yourself) are the three most universally applicable software engineering mantras. They share a common enemy: unnecessary complexity.
π The Complexity Tax
Every line of code you write is a liability. Someone has to read it, test it, debug it, migrate it, document it. Code you don't write costs zero.
KISS, YAGNI, and DRY are three angles of attack on the same problem: code that does more than it needs to, more times than necessary, in harder ways than required.
π KISS β The Clever Trap
KISS: Keep It Simple, Stupid.
Clever code is not a virtue. Clever code means future-you (or a teammate at 2am during an incident) has to spend 15 minutes understanding what you meant.
Violation β the unnecessarily clever solution:
# "Clever" one-liner: hard to read, hard to debug
result = sum(x**2 for x in nums if x > 0) or default
KISS-compliant version:
# Clear and debuggable
positives = [x for x in nums if x > 0]
squared = [x**2 for x in positives]
result = sum(squared) if positives else default
The second version is slightly longer but O(1) to understand. The one-liner has a subtle or default operator that behaves differently from if not squared for edge cases.
KISS tests:
- Can a new team member understand this in 30 seconds?
- If this line throws an exception, can you find the bug without a debugger?
- Could you write this the same way in 6 months?
π Code Review Decision Tree: Which Principle Applies?
flowchart TD
A[Code Review] --> B{Logic Duplicated?}
B -- Yes --> C[Violates DRY]
C --> D[Extract to Function/Class]
B -- No --> E{Feature Not Needed Yet?}
E -- Yes --> F[Violates YAGNI]
F --> G[Remove Premature Code]
E -- No --> H{Is It Too Complex?}
H -- Yes --> I[Violates KISS]
I --> J[Simplify]
H -- No --> K[Code is Clean]
The flowchart guides a code reviewer through the three principles in priority order. The first gate checks for logic duplication (DRY violation), the second for unneeded features (YAGNI violation), and the third for unnecessary complexity (KISS violation). Reading the diagram top-to-bottom reveals that the principles are applied in sequence β clean code that passes all three gates reaches the "Code is Clean" terminal state, confirming that KISS, YAGNI, and DRY are complementary filters rather than competing rules.
βοΈ YAGNI β The Future-Proofing Fallacy
YAGNI: You Aren't Gonna Need It (from Extreme Programming).
Building for hypothetical future requirements is a tax on the present that usually never gets repaid.
Classic violation:
class PaymentProcessor:
def __init__(self, provider: str = "stripe"):
# "We might need PayPal someday" β not asked for
if provider == "stripe":
self.client = StripeClient()
elif provider == "paypal":
self.client = PayPalClient()
elif provider == "adyen":
self.client = AdyenClient() # Never implemented
else:
raise ValueError("Unknown provider")
For a startup on Stripe only, this code is three times more complex than necessary, supports two untested paths, and will need to change when real requirements arrive anyway.
YAGNI-compliant version:
class PaymentProcessor:
def __init__(self):
self.client = StripeClient() # The only provider we have
When PayPal is actually needed, refactor then β with real requirements, real tests, and real knowledge of what the interface needs to support.
Exceptions to YAGNI:
- Security: baking in auth hooks now costs little and saves a painful retrofit later.
- Public APIs: breaking changes to external client interfaces are very costly β design extension points deliberately.
π§ Deep Dive: DRY β The Copy-Paste Debt
DRY: Don't Repeat Yourself.
Every piece of knowledge should have a single, authoritative representation. When you copy-paste logic, you create a maintenance bomb: a future change must be made in every copy, and they will diverge.
Violation:
# File 1: user registration
if len(password) < 8 or not any(c.isdigit() for c in password):
raise ValidationError("Password too weak")
# File 2: password reset
if len(password) < 8 or not any(c.isdigit() for c in password):
raise ValidationError("Password too weak")
When security rules change (add special character requirement), both copies must be updated β but only one will be.
DRY fix:
def validate_password(password: str) -> None:
if len(password) < 8:
raise ValidationError("Password must be at least 8 characters")
if not any(c.isdigit() for c in password):
raise ValidationError("Password must contain a digit")
# Add future rules here β one place
# Both registration and reset call the same function
validate_password(new_password)
π DRY Refactoring: Centralising the Password Validation Rule
flowchart LR
subgraph Before_DRY[Before DRY]
A1[validate in registration]
A2[validate in password reset]
end
subgraph After_DRY[After DRY]
B1[validate_password]
C1[registration] --> B1
C2[password reset] --> B1
end
The diagram contrasts the before and after states of a DRY refactoring. In the "Before DRY" cluster, two separate call sites each maintain their own copy of the password validation logic, creating two independent points of failure whenever the rule changes. In the "After DRY" cluster, both registration and password reset call the single canonical validate_password function β any future rule change propagates to both consumers automatically. The key takeaway: DRY is not about removing repeated text but about eliminating the divergence risk that comes from duplicated logic.
βοΈ Trade-offs & Failure Modes: When DRY Becomes WET
DRY is about logic, not text. Two functions that look similar but have different reasons to change should stay separate.
# These look duplicated but should NOT be merged
def validate_user_registration_email(email: str) -> None:
# strict: must be a real domain, must not be disposable, must not exist in DB
...
def validate_password_reset_email(email: str) -> None:
# lenient: just check it's valid format; we don't care if it's in DB
...
Merging these with a mode parameter creates an SRP violation β one function with two reasons to change.
Rule: DRY applies to logic that changes for the same reasons. If two pieces of code look similar but diverge when requirements change, they aren't the same knowledge.
π Principle Decision Flow
When you are about to write code, run it through this decision flowchart to check which principle applies.
flowchart TD
A[About to write code] --> B{Is it solving a real current requirement?}
B -- NO --> C[YAGNI: Don't write it]
B -- YES --> D{Does similar logic already exist?}
D -- YES --> E{Does it change for same reason?}
E -- YES --> F[DRY: Extract and reuse]
E -- NO --> G[Keep separate not the same knowledge]
D -- NO --> H{Is there a simpler way to write this?}
H -- YES --> I[KISS: Use the simpler approach]
H -- NO --> J[Write it you're good]
Reading the flowchart: YAGNI is the first gate β don't build what you don't need. DRY is the second β if similar logic exists, check if it's truly the same knowledge before extracting. KISS is the third β always prefer the boring, obvious solution.
π Real-World Application: KISS, YAGNI, and DRY at Scale
These principles scale from one-liner decisions to architectural choices.
| Principle | Code level | Service level | Architecture level |
| KISS | Avoid ternary chains | Single-purpose microservice | Prefer synchronous HTTP over complex event mesh |
| YAGNI | Don't add PayPal until needed | Don't build auth service until auth is needed | Don't deploy multi-region until you have users there |
| DRY | Extract shared validation | Share auth library across services | Single config management service |
KISS in production incidents: Post-mortems frequently reveal that "clever" optimizations caused outages. A readable, boring approach to rate limiting (a Redis counter with TTL) beats a custom leaky-bucket implementation most engineers won't understand under 2am incident pressure.
YAGNI in startups: The most common startup engineering failure is over-engineering. Teams build multi-tenant SaaS infrastructure before their first paying customer, event-driven microservices before they need scale, and ML pipelines before they have data. YAGNI saves runway.
DRY in shared libraries: Centralizing business rules (pricing logic, validation rules, tax calculation) in shared libraries means a rule change propagates everywhere instantly. Without DRY, the same pricing bug must be patched in 8 services.
| Anti-Pattern | Principle Violated | Fix |
if feature == "x" elif feature == "y"... chain | KISS + OCP | Strategy pattern or plugin interface |
| "We might need this someday" abstraction | YAGNI | Delete it; add when required |
| Same regex copy-pasted in 6 files | DRY | Centralized validator module |
| One-liner that requires 10 min to understand | KISS | Expand to 4 readable lines |
Merging two functions with a mode parameter | DRY misapplied | Keep separate if they change for different reasons |
π§ͺ Worked Example: Order Price Calculation
This example walks through an OrderCalculator that simultaneously violates all three principles in a realistic e-commerce domain. It was chosen because it compresses KISS (unreadable nested ternaries), YAGNI (an unused cryptocurrency payment path), and DRY (duplicated currency-conversion logic) into a single relatable class β making all three problems visible side-by-side. As you read the "before" block, identify each violation by name before looking at the refactored version, then trace how each helper function addresses exactly one principle.
Starting point β everything wrong:
# π Violates KISS (overcomplicated), DRY (logic repeated), and YAGNI (crypto never needed)
class OrderCalculator:
def calculate_total(self, items, currency="USD", crypto_enabled=False,
apply_b2b_discount=False, loyalty_tier=None):
total = sum(
(i["price"] * i["qty"]) * (0.9 if apply_b2b_discount else 1) *
(0.95 if loyalty_tier == "gold" else 0.97 if loyalty_tier == "silver" else 1)
for i in items
) * (0.9 if currency == "EUR" else 1.1 if currency == "BTC" else 1)
return total
def calculate_shipping(self, items, currency="USD"):
# Copy-pasted currency logic from above
base = 5.0 if len(items) <= 3 else 10.0
return base * (0.9 if currency == "EUR" else 1.1 if currency == "BTC" else 1)
Problems:
- KISS: one-liner with chained ternaries β impossible to debug
- DRY: currency conversion logic duplicated in both methods
- YAGNI:
crypto_enabledparameter never used; BTC handling that was "just in case"
Fixed β applying all three principles:
# β
KISS: readable and debuggable
# β
DRY: currency conversion is centralized
# β
YAGNI: no crypto, no unused parameters
def _apply_currency_rate(amount: float, currency: str) -> float:
rates = {"USD": 1.0, "EUR": 0.9} # Only currencies we support NOW
return amount * rates.get(currency, 1.0) # Single place to update rates
def _apply_discount(amount: float, apply_b2b: bool, loyalty_tier: str) -> float:
if apply_b2b:
amount *= 0.90
if loyalty_tier == "gold":
amount *= 0.95
elif loyalty_tier == "silver":
amount *= 0.97
return amount
def calculate_total(items: list, currency: str = "USD",
apply_b2b: bool = False, loyalty_tier: str = None) -> float:
subtotal = sum(item["price"] * item["qty"] for item in items)
discounted = _apply_discount(subtotal, apply_b2b, loyalty_tier)
return _apply_currency_rate(discounted, currency)
def calculate_shipping(items: list, currency: str = "USD") -> float:
base_rate = 5.0 if len(items) <= 3 else 10.0
return _apply_currency_rate(base_rate, currency) # DRY: reuses same function
Toy dataset test:
items = [
{"price": 25.00, "qty": 2}, # $50
{"price": 10.00, "qty": 3}, # $30
]
# Subtotal: $80
print(calculate_total(items)) # $80.00 USD
print(calculate_total(items, currency="EUR")) # $72.00 EUR (Γ0.9)
print(calculate_total(items, apply_b2b=True)) # $72.00 USD (Γ0.9)
print(calculate_total(items, loyalty_tier="gold")) # $76.00 USD (Γ0.95)
print(calculate_shipping(items)) # $10.00 (4 items > 3)
print(calculate_shipping(items, currency="EUR")) # $9.00 EUR
Decision Guide
| Situation | Recommended Action |
| Code is hard to read or explain in 30 seconds | KISS: rewrite in the simplest, most readable form |
| Building a feature "we might need someday" | YAGNI: build only what is required now |
| Same logic copy-pasted in 3+ places | DRY: extract to a single canonical function |
| Two similar functions that change for different reasons | Keep separate β they are not the same knowledge |
| Clever optimization in a hot path | KISS unless profiling proves the optimization is needed |
π οΈ Lombok and MapStruct: Eliminating Boilerplate the DRY Way in Java
Lombok is a Java annotation processor that generates getters, setters, constructors, equals/hashCode, and builders at compile time β removing the copy-pasted accessor boilerplate that violates DRY in every POJO. MapStruct generates type-safe mapping code between DTOs and domain objects at compile time, eliminating hand-written field-by-field mapping that is both tedious (KISS violation) and error-prone (DRY violation β copied across multiple service classes).
How they solve the problem in this post: The before/after below shows a typical Java service class. The "before" is verbose and violates DRY (getters/setters/builder scattered across files). The "after" uses Lombok to declare intent once (@Value, @Builder) and MapStruct to declare the mapping once instead of repeating target.setField(source.getField()) in every service method.
// βββ BEFORE: Verbose POJO violates DRY (getters copy-pasted, builder manual) ββ
public class UserDto {
private String firstName;
private String lastName;
private String email;
public String getFirstName() { return firstName; }
public void setFirstName(String v) { this.firstName = v; }
// ... 4 more getters + setters β pure boilerplate repeated in every DTO class
}
// βββ AFTER: Lombok β declare intent once, compiler generates the rest ββββββββββ
// Dependencies: org.projectlombok:lombok:1.18.32 (annotationProcessor + provided)
import lombok.Value;
import lombok.Builder;
@Value // immutable: final fields + all-args constructor + getters + equals/hashCode/toString
@Builder // fluent builder: UserDto.builder().firstName("Ada").email("a@b.com").build()
public class UserDto {
String firstName;
String lastName;
String email;
}
// Domain entity (mutable, separate lifecycle)
@lombok.Data // mutable: getters + setters + equals + toString
@lombok.NoArgsConstructor
@lombok.AllArgsConstructor
public class User {
private String id;
private String firstName;
private String lastName;
private String email;
}
// βββ BEFORE: Hand-written mapping in every service violates DRY βββββββββββββββ
public UserDto toDto(User user) {
UserDto dto = new UserDto(user.getFirstName(), user.getLastName(), user.getEmail());
// Same pattern repeated in RegistrationService, AdminService, ProfileServiceβ¦
return dto;
}
// βββ AFTER: MapStruct generates type-safe mapping β one interface, zero loops ββ
// Dependency: org.mapstruct:mapstruct:1.5.5 + mapstruct-processor (annotationProcessor)
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper
public interface UserMapper {
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
UserDto toDto(User user); // MapStruct generates the body at compile time
User toEntity(UserDto dto); // bidirectional β DRY: one place to update field names
}
// βββ Usage in a Spring service ββββββββββββββββββββββββββββββββββββββββββββββββ
@Service
public class UserService {
public UserDto getUser(String id) {
User user = new User(id, "Ada", "Lovelace", "ada@example.com");
return UserMapper.INSTANCE.toDto(user); // one line instead of N field copies
}
}
// βββ Smoke test βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// UserDto dto = new UserService().getUser("42");
// System.out.println(dto);
// β UserDto(firstName=Ada, lastName=Lovelace, email=ada@example.com)
Lombok eliminates ~70% of the boilerplate in a typical Java service layer β no more copy-pasted getter blocks (DRY), no more verbose builder patterns (KISS). MapStruct's generated mapping code is faster than reflection-based mappers (ModelMapper, Dozer) and fails the build if a field is renamed without updating the mapper β catching DRY violations at compile time rather than at runtime.
For a full deep-dive on Lombok and MapStruct patterns in Spring Boot services, a dedicated follow-up post is planned.
π Lessons Learned From These Three Principles
- KISS saves you at 2am. The code you write during normal business hours must be debuggable during an incident at 2am by someone who didn't write it. Boring is better than clever.
- YAGNI is hardest to follow in high-excitement phases. The beginning of a project is when engineers are most tempted to build "enterprise-grade" infrastructure. This is when YAGNI has the most leverage β ship the simplest version that works.
- DRY is about knowledge, not text. Two functions that look identical but diverge under different business rules are not the same knowledge. Don't merge them. The test: if one changes independently of the other, they should stay separate.
- All three principles share a common enemy: future hypotheticals. YAGNI says don't build for them. KISS says don't optimize for them. DRY says don't abstract code that merely looks similar today but will diverge for different reasons.
- The sweet spot is boring code that does exactly what is needed. If a reviewer can understand a function in 30 seconds, it passes KISS. If it was written only because there was a real requirement, it passes YAGNI. If there's one canonical place to find each piece of logic, it passes DRY.
- SRP and DRY interact. Extracting a shared function (DRY) is correct when the extracted function has one responsibility. But if merging two similar-looking functions creates a multi-mode function that violates SRP, keep them separate.
π TLDR: Summary & Key Takeaways
- KISS: Prefer the boring, obvious solution. Clever code is a future liability.
- YAGNI: Don't build for hypothetical needs. Build when you have real requirements and real tests.
- DRY: Centralize logic that changes for the same reasons. Every piece of knowledge should have one home.
- Over-DRY warning: Don't merge code that merely looks similar β if it changes for different reasons, duplication is the right call.
π KISS, YAGNI, and DRY: Scope and Focus at a Glance
flowchart TD
A[Three Principles] --> B[DRY]
A --> C[YAGNI]
A --> D[KISS]
B --> E[Avoid Duplication]
B --> F[Single Source of Truth]
C --> G[Build What Is Needed]
C --> H[No Speculative Code]
D --> I[Simple Solutions]
D --> J[Easy to Understand]
The mind-map shows how all three principles fan out from a single root concern β avoiding unnecessary complexity. Each principle has two concrete sub-goals: DRY targets Avoid Duplication and Single Source of Truth; YAGNI targets Build What Is Needed and No Speculative Code; KISS targets Simple Solutions and Easy to Understand. The takeaway is that the three principles are not rivals but complementary lenses β YAGNI prevents you from building it, KISS keeps what you do build readable, and DRY ensures each piece of knowledge lives in exactly one place.
π Related Posts
- Single Responsibility Principle: One Class, One Job
- Open/Closed Principle: Extend Without Modifying
- Dependency Inversion Principle: Decoupling Your Code
- Exploring the Strategy Design Pattern
Test Your Knowledge
Ready to test what you just learned?
AI will generate 4 questions based on this article's content.

Written by
Abstract Algorithms
@abstractalgorithms
More Posts
RAG vs Fine-Tuning: When to Use Each (and When to Combine Them)
TLDR: RAG gives LLMs access to current knowledge at inference time; fine-tuning changes how they reason and write. Use RAG when your data changes. Use fine-tuning when you need consistent style, tone, or domain reasoning. Use both for production assi...
Fine-Tuning LLMs with LoRA and QLoRA: A Practical Deep-Dive
TLDR: LoRA freezes the base model and trains two tiny matrices per layer β 0.1 % of parameters, 70 % less GPU memory, near-identical quality. QLoRA adds 4-bit NF4 quantization of the frozen base, enabling 70B fine-tuning on 2Γ A100 80 GB instead of 8...
Build vs Buy: Deploying Your Own LLM vs Using ChatGPT, Gemini, and Claude APIs
TLDR: Use the API until you hit $10K/month or a hard data privacy requirement. Then add a semantic cache. Then evaluate hybrid routing. Self-hosting full model serving is only cost-effective at > 50M tokens/day with a dedicated MLOps team. The build ...
Watermarking and Late Data Handling in Spark Structured Streaming
TLDR: A watermark tells Spark Structured Streaming: "I will accept events up to N minutes late, and then I am done waiting." Spark tracks the maximum event time seen per partition, takes the global minimum across all partitions, subtracts the thresho...
