Software Design Principles
Software Design Principles
Software design principles solve fundamental issues in building reliable, scalable software systems. They tackle problems like code rigidity, duplication, and complexity that plague real-world development.
We will be covering the following topics:
- DRY (Don't Repeat Yourself)
- KISS (Keep It Simple, Stupid)
- YAGNI (You Aren't Gonna Need It)
1. DRY (Don't Repeat Yourself)
DRY states that we should avoid duplicating code or logic. Every piece of knowledge must have a single, unambiguous, authoritative representation. That means, every code, data, config - should exist in one, and only one, authoritative place.
- It reduces maintenance overhead (fix bugs once).
- It improves code reusability and consistency.
- It lowers risk of introducing inconsistencies.
Use Case: User Validation
Problem statement: Create a user validation logic for user creation and authentication.
Let's look at a case where we don't use DRY principle.
// UserService.java
public class UserService {
public void createUser(String username, String email) {
if (username == null || username.length() < 5) {
throw new IllegalArgumentException("Invalid username");
}
if (email == null || !email.contains("@")) {
throw new IllegalArgumentException("Invalid email");
}
// Create user...
}
}
// AuthService.java
public class AuthService {
public void login(String username, String email) {
if (username == null || username.length() < 5) {
throw new IllegalArgumentException("Invalid username");
}
if (email == null || !email.contains("@")) {
throw new IllegalArgumentException("Invalid email");
}
// Authenticate...
}
}
Key issues:
- Validation logic duplicated. For both user creation and authentication, we are validating the username and email.
- Changing rules (e.g., username length) requires updates in multiple places.
Solution:
// Validator.java (Single source of truth)
public class Validator {
public static void validateUser(String username, String email) {
if (username == null || username.length() < 5) {
throw new IllegalArgumentException("Invalid username");
}
if (email == null || !email.contains("@")) {
throw new IllegalArgumentException("Invalid email");
}
}
}
// UserService.java
public class UserService {
public void createUser(String username, String email) {
Validator.validateUser(username, email);
// Create user...
}
}
// AuthService.java
public class AuthService {
public void login(String username, String email) {
Validator.validateUser(username, email);
// Authenticate...
}
}
Outcome:
- Validation logic centralized. Now, we don't need to add the validation logic in multiple places.
- Changes only needed in
Validator.java.
2. KISS (Keep It Simple, Stupid)
We should always solves problems using the simplest solution possible. Avoid overcomplicating design or implementation.
- It makes simple code easier to read, debug, and maintain.
- It reduces cognitive load for developers.
- It minimizes the chance of errors.
Use Case: Discount Calculation
Problem: Calculate a discount for an order.
Before (Overcomplicated):
// DiscountService.java
public class DiscountService {
public double calculateDiscount(Order order, User user) {
// Complex nested logic with multiple conditions
double discount = 0;
if (user.isPremium()) {
if (order.getTotal() > 1000) {
discount = order.getTotal() * 0.2;
if (user.hasLoyaltyCard()) {
discount += 50;
}
} else {
discount = order.getTotal() * 0.1;
}
} else {
if (order.getTotal() > 500) {
discount = order.getTotal() * 0.05;
}
}
return discount;
}
}
Issues:
- Hard to read and modify.
- Logic intertwined with user/order details.
After (KISS Compliant):
// DiscountService.java
public class DiscountService {
public double calculateDiscount(Order order, User user) {
double discount = 0;
// Apply premium discount
if (user.isPremium()) {
discount += order.getTotal() * 0.2;
} else if (order.getTotal() > 500) {
discount += order.getTotal() * 0.05;
}
// Apply loyalty bonus
if (user.hasLoyaltyCard()) {
discount += 50;
}
return discount;
}
}
Outcome:
- Linear flow, easy to understand.
- Each rule is isolated and explicit.
3. YAGNI (You Aren't Gonna Need It)
Only implement functionality that is immediately required. Avoid speculative features.
- Prevents wasted effort on unused code.
- Reduces codebase complexity.
- Accelerates delivery of core features.
Use Case: Payment Processing
Problem: Design a payment system with credit card and paypal as services.
Before: An overengineered solution where we implemented 5 payment methods when we only need 2.
// PaymentProcessor.java
public class PaymentProcessor {
public void processPayment(Payment payment) {
// Supports 5 payment methods (Credit Card, PayPal, Crypto, etc.)
if (payment.getType() == PaymentType.CREDIT_CARD) {
processCreditCard(payment);
} else if (payment.getType() == PaymentType.PAYPAL) {
processPayPal(payment);
}
// ... 3 more unused payment methods
}
private void processCreditCard(Payment payment) { /* ... */ }
private void processPayPal(Payment payment) { /* ... */ }
// Unused methods for Crypto, Bank Transfer, etc.
}
Issues:
- Implemented 5 payment methods when only 2 are needed now.
- Dead code increases maintenance burden.
After: A simple solution where we only implement what's needed now.
// PaymentProcessor.java
public class PaymentProcessor {
public void processPayment(Payment payment) {
// Only implement what's needed NOW
if (payment.getType() == PaymentType.CREDIT_CARD) {
processCreditCard(payment);
} else if (payment.getType() == PaymentType.PAYPAL) {
processPayPal(payment);
} else {
throw new UnsupportedOperationException("Unsupported payment type");
}
}
private void processCreditCard(Payment payment) { /* ... */ }
private void processPayPal(Payment payment) { /* ... */ }
}
Outcome:
- No unused code.
- Easy to extend later when new payment methods are required.
Some conclusive points:
Q. When to Apply these principles?
- DRY: When you see identical code/logic in ≥2 places.
- KISS: When a solution requires more than 10 seconds to understand.
- YAGNI: When adding "just-in-case" features.
Q. What are anti-patterns to avoid?
- DRY: Copy-pasting code.
- KISS: Using 10 design patterns for a simple task.
- YAGNI: Building a "framework" for a one-time script.