Software Design Principles

Table of Contents

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.
← Solid Principles