Home Development Principles
Post
Cancel
Development Principles | SEG

Development Principles

This comprehensive guide covers three essential software development principles that every developer should master. Understanding KISS, DRY, and YAGNI will transform how you approach coding challenges!

Introduction: The Three Pillars of Pragmatic Development

As a software engineer, you’ll constantly face decisions about how to structure your code. Should you add flexibility for future needs? Should you extract that duplicated logic? Is your solution too complex?

Three principles guide these decisions:

  • KISS (Keep It Simple, Stupid) - Simplicity is key
  • DRY (Don’t Repeat Yourself) - Eliminate duplication of knowledge
  • YAGNI (You Aren’t Gonna Need It) - Don’t build what you don’t need yet

These principles aren’t just theoretical - they’re practical tools that help you write code that’s easier to read, maintain, and evolve. Let’s explore each one in depth and learn how they work together.

flowchart TB
    subgraph Principles["Development Principles"]
        direction LR
        KISS[KISS<br/>Keep It Simple]
        DRY[DRY<br/>Don't Repeat Yourself]
        YAGNI[YAGNI<br/>You Aren't Gonna Need It]
    end

    KISS --> Quality[Quality Code]
    DRY --> Quality
    YAGNI --> Quality

    Quality --> Benefits["Easy to read<br/>Easy to maintain<br/>Fewer bugs<br/>Happy developers:)"]

    style KISS fill:#90EE90
    style DRY fill:#87CEEB
    style YAGNI fill:#DDA0DD
    style Benefits fill:#FFE4B5

KISS - Keep It Simple, Stupid

What Is the KISS Principle?

KISS stands for “Keep It Simple, Stupid” - a design principle that emphasizes simplicity as a key goal in software development. Despite its blunt name, KISS carries a profound message: systems work best when they’re kept simple rather than made complex.

The principle reminds us that unnecessary complexity is the enemy of good software. Every line of code you write becomes code someone (including future you) needs to read, understand, maintain, and debug. The simpler your solution, the easier these tasks become.

Origin and Philosophy

The KISS principle originated in the U.S. Navy in the 1960s, coined by engineer Kelly Johnson. The idea was that military equipment should be repairable by an average mechanic in combat conditions with minimal tools. This philosophy translates perfectly to software engineering: your code should be understandable by an average developer without needing a PhD.

flowchart TB
    subgraph Problem["The Same Problem"]
        P[Business Requirement]
    end

    subgraph Approaches["Two Approaches"]
        direction LR
        Simple[Simple Solution<br/>Easy to read<br/>Easy to maintain<br/>Fewer bugs]
        Complex[Complex Solution<br/>Hard to understand<br/>Difficult to modify<br/>More bugs]
    end

    Problem --> Simple
    Problem --> Complex

    Simple --> Success[Successful Project<br/>Happy Team]
    Complex --> Failure[Technical Debt<br/>Frustrated Team]

    style Simple fill:#90EE90
    style Complex fill:#FFB6C6
    style Success fill:#90EE90
    style Failure fill:#FFB6C6

Remember: Simple doesn’t mean simplistic or incomplete. It means elegant, straightforward, and easy to understand.

Why Complexity Hurts Software Projects

Complexity in software development isn’t just an aesthetic issue - it has real, measurable costs that impact every aspect of your project.

The Cost of Complexity

Impact AreaSimple CodeComplex Code
Reading TimeMinutes to understandHours to decipher
Bug RateLower - fewer places to hideHigher - bugs lurk in complexity
Modification TimeQuick changesRisky, time-consuming changes
OnboardingNew devs productive quicklySteep learning curve
TestingFewer test cases neededExponentially more scenarios
DebuggingStraightforwardLike finding needles in haystacks
Real-World Case Study - How complexity nearly killed a startup.

A startup built an e-commerce platform with a “flexible architecture” that could theoretically handle any business model. They created abstract factories, strategy patterns, and configuration systems for features they might need someday.

Result:

  • 6 months to add a simple discount feature
  • New developers took 3+ weeks to make their first commit
  • Bug fixes regularly broke unrelated features
  • The company nearly went bankrupt before rewriting the system simply

Lesson: Build what you need now, not what you might need someday. The rewrite took 3 months and delivered everything the complex version had, plus better performance and fewer bugs.

How Complexity Grows

flowchart LR
    A[Initial Simple Code] --> B{Developer Thinks}
    B -->|"What if we need..."| C[Add Abstraction]
    B -->|"This might change..."| D[Add Configuration]
    B -->|"Future flexibility..."| E[Add Framework]

    C --> F[More Complex]
    D --> F
    E --> F

    F --> G{Next Developer}
    G -->|Confused| H[Add More Layers]
    H --> I[Unmaintainable Mess]

    style A fill:#90EE90
    style F fill:#FFE4B5
    style I fill:#FFB6C6

Simple vs. Complex Code: Examples

Let’s look at real Java examples that demonstrate the difference between simple and complex approaches.

Example: Calculating Discount

Complex Version (Over-engineered)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Complex: Using Strategy Pattern for simple discount
interface DiscountStrategy {
    double applyDiscount(double price);
}

class PercentageDiscountStrategy implements DiscountStrategy {
    private final double percentage;

    public PercentageDiscountStrategy(double percentage) {
        this.percentage = percentage;
    }

    @Override
    public double applyDiscount(double price) {
        return price * (1 - percentage / 100);
    }
}

class DiscountCalculator {
    private DiscountStrategy strategy;

    public void setStrategy(DiscountStrategy strategy) {
        this.strategy = strategy;
    }

    public double calculate(double price) {
        if (strategy == null) {
            throw new IllegalStateException("Strategy not set");
        }
        return strategy.applyDiscount(price);
    }
}

// Usage
DiscountCalculator calculator = new DiscountCalculator();
calculator.setStrategy(new PercentageDiscountStrategy(10));
double finalPrice = calculator.calculate(100.0);

KISS approach

1
2
3
4
5
6
7
8
9
// Simple: Direct calculation
public class DiscountCalculator {
    public static double applyDiscount(double price, double discountPercent) {
        return price * (1 - discountPercent / 100);
    }
}

// Usage
double finalPrice = DiscountCalculator.applyDiscount(100.0, 10);

Why is the simple version better? It does exactly what we need, nothing more. If you need multiple discount types later, you can refactor then. Don’t build flexibility you don’t need yet.

Example: User Validation

Complex Version

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// Complex: Over-abstracted validation
interface ValidationRule<T> {
    ValidationResult validate(T value);
}

class ValidationResult {
    private boolean valid;
    private List<String> errors;
    // ... constructor, getters
}

class EmailValidationRule implements ValidationRule<String> {
    @Override
    public ValidationResult validate(String email) {
        List<String> errors = new ArrayList<>();
        if (email == null || email.isEmpty()) {
            errors.add("Email is required");
        }
        if (email != null && !email.contains("@")) {
            errors.add("Email must contain @");
        }
        return new ValidationResult(errors.isEmpty(), errors);
    }
}

class ValidationEngine<T> {
    private List<ValidationRule<T>> rules = new ArrayList<>();

    public void addRule(ValidationRule<T> rule) {
        rules.add(rule);
    }

    public ValidationResult validateAll(T value) {
        // Combine all validation results...
    }
}

KISS approach

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Simple: Direct validation with clear error messages
public class UserValidator {
    public static void validateEmail(String email) {
        if (email == null || email.trim().isEmpty()) {
            throw new IllegalArgumentException("Email is required");
        }
        if (!email.contains("@")) {
            throw new IllegalArgumentException("Invalid email format");
        }
    }

    public static void validatePassword(String password) {
        if (password == null || password.length() < 8) {
            throw new IllegalArgumentException("Password must be at least 8 characters");
        }
    }
}

// Usage
try {
    UserValidator.validateEmail(userEmail);
    UserValidator.validatePassword(userPassword);
} catch (IllegalArgumentException e) {
    System.out.println("Validation error: " + e.getMessage());
}

Common KISS Violations

Premature Optimization

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Violates KISS: Optimizing before measuring
public class UserCache {
    private ConcurrentHashMap<String, SoftReference<User>> cache;
    private ScheduledExecutorService cleanupService;
    private AtomicInteger hitCount;
    private AtomicInteger missCount;

    public UserCache() {
        // Complex caching logic for 10 users...
    }
}

// Follows KISS: Simple solution first
public class UserCache {
    private Map<String, User> cache = new HashMap<>();

    public User get(String id) {
        return cache.get(id);
    }

    public void put(String id, User user) {
        cache.put(id, user);
    }
}

Remember: Premature optimization is the root of all evil. - Donald Knuth

Design Pattern Overuse

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Violates KISS: Using patterns unnecessarily
public class LoggerFactory {
    private static LoggerFactory instance;
    private LoggerBuilder builder;

    private LoggerFactory() {
        builder = new LoggerBuilder();
    }

    public static synchronized LoggerFactory getInstance() {
        if (instance == null) {
            instance = new LoggerFactory();
        }
        return instance;
    }

    public Logger createLogger() {
        return builder.build();
    }
}

// Follows KISS: Direct approach
public class Logger {
    public static void log(String message) {
        System.out.println("[" + LocalDateTime.now() + "] " + message);
    }
}

Over-Generic Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Violates KISS: Generic for no reason
public class Calculator<T extends Number> {
    public <U extends Number, V extends Number> T calculate(
        U a, V b, BiFunction<U, V, T> operation) {
        return operation.apply(a, b);
    }
}

// Follows KISS: Specific and clear
public class Calculator {
    public static int add(int a, int b) {
        return a + b;
    }

    public static int subtract(int a, int b) {
        return a - b;
    }
}

Simple vs. Simplistic: Finding the Balance

Simple doesn’t mean simplistic. There’s a critical difference:

SimpleSimplistic
Elegant and clearNaive and incomplete
Handles real requirementsIgnores important cases
Easy to extend when neededRequires full rewrite to grow
Well-tested core functionalitySkips error handling
Appropriate for the problemInsufficient for the problem

Simplistic (too simple)

1
2
3
4
5
6
// Simplistic: Ignores error handling and edge cases
public class FileReader {
    public String read(String filename) {
        return new String(Files.readAllBytes(Paths.get(filename)));
    }
}

Simple (just right)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Simple: Handles essentials without over-engineering
public class FileReader {
    public String read(String filename) throws IOException {
        if (filename == null || filename.trim().isEmpty()) {
            throw new IllegalArgumentException("Filename cannot be empty");
        }

        Path path = Paths.get(filename);
        if (!Files.exists(path)) {
            throw new FileNotFoundException("File not found: " + filename);
        }

        return Files.readString(path, StandardCharsets.UTF_8);
    }
}

Pro Tip: Start simple. Add complexity only when you have a concrete need. It’s much easier to make simple code complex than to simplify complex code.


DRY - Don’t Repeat Yourself

What Is the DRY Principle?

DRY stands for “Don’t Repeat Yourself” - a fundamental principle of software development that states: every piece of knowledge must have a single, unambiguous, authoritative representation within a system. In simpler terms: don’t write the same code (or logic) more than once.

The principle was formulated by Andy Hunt and Dave Thomas in their influential book “The Pragmatic Programmer” (1999). While it sounds simple, DRY is much more than just “don’t copy-paste code” - it’s about reducing duplication of knowledge and intent throughout your system.

Why DRY Matters

When you violate DRY, you create what developers call WET code - variously interpreted as “Write Everything Twice,” “We Enjoy Typing,” or “Waste Everyone’s Time.” WET code leads to:

  • More bugs - Fix a bug in one place, miss it in another
  • Slower development - Change requirements? Update code in multiple places
  • Higher cognitive load - Developers must remember to update all duplicates
  • Harder maintenance - The more repetition, the more that can go wrong
  • Inconsistency - Duplicated code inevitably diverges over time
flowchart TB
    subgraph WET["WET Code (Write Everything Twice)"]
        direction TB
        WC1[Code Block A<br/>Login Validation]
        WC2[Code Block B<br/>Login Validation]
        WC3[Code Block C<br/>Login Validation]
    end

    subgraph DRY["DRY Code (Don't Repeat Yourself)"]
        direction TB
        DS[Single Source<br/>Login Validation]
        DC1[Usage Point A] --> DS
        DC2[Usage Point B] --> DS
        DC3[Usage Point C] --> DS
    end

    WET -.->|Refactor| DRY

    style WET fill:#ffe6e6
    style DRY fill:#e6ffe6

Levels of Duplication

Not all duplication is equal. Understanding the different levels helps you prioritize refactoring efforts.

Example: Code Duplication

The most obvious form - identical or very similar code in multiple places.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// WET: Code duplication
public class UserService {
    public void createUser(String name, String email) {
        if (name == null || name.trim().isEmpty()) {
            throw new IllegalArgumentException("Name cannot be empty");
        }
        if (email == null || !email.contains("@")) {
            throw new IllegalArgumentException("Invalid email");
        }
        // ... create user
    }

    public void updateUser(String name, String email) {
        if (name == null || name.trim().isEmpty()) {
            throw new IllegalArgumentException("Name cannot be empty");
        }
        if (email == null || !email.contains("@")) {
            throw new IllegalArgumentException("Invalid email");
        }
        // ... update user
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// DRY: Extracted validation
public class UserService {
    public void createUser(String name, String email) {
        validateUserInput(name, email);
        // ... create user
    }

    public void updateUser(String name, String email) {
        validateUserInput(name, email);
        // ... update user
    }

    private void validateUserInput(String name, String email) {
        if (name == null || name.trim().isEmpty()) {
            throw new IllegalArgumentException("Name cannot be empty");
        }
        if (email == null || !email.contains("@")) {
            throw new IllegalArgumentException("Invalid email");
        }
    }
}

Example: Logic Duplication

Same logic implemented differently - harder to spot but equally problematic.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// WET: Logic duplication (same business rule, different implementation)
public class OrderService {
    public boolean canOrderBook(User user) {
        return user.getAge() >= 18 || user.hasParentalConsent();
    }
}

public class ReviewService {
    public boolean canWriteReview(User user) {
        // Same logic, different code!
        if (user.getAge() < 18) {
            return user.hasParentalConsent();
        }
        return true;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// DRY: Shared business rule
public class UserEligibilityService {
    public boolean isEligibleForAdultContent(User user) {
        return user.getAge() >= 18 || user.hasParentalConsent();
    }
}

public class OrderService {
    private UserEligibilityService eligibilityService;

    public boolean canOrderBook(User user) {
        return eligibilityService.isEligibleForAdultContent(user);
    }
}

public class ReviewService {
    private UserEligibilityService eligibilityService;

    public boolean canWriteReview(User user) {
        return eligibilityService.isEligibleForAdultContent(user);
    }
}

Example: Data Duplication

The same data stored or represented in multiple places.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// WET: Data duplication
public class Order {
    private String productName;
    private double productPrice;
    private String productSku;
    // Product data duplicated in Order
}

// DRY: Reference instead of duplication
public class Order {
    private Product product;  // Reference to single source of truth
    private int quantity;
    private LocalDateTime orderDate;
}

Note: Sometimes data duplication is intentional for performance (caching) or historical records (snapshots). The key is making it a conscious decision, not an accident.

Example: Magic Numbers and Strings

Hardcoded values repeated throughout code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// WET: Magic numbers everywhere
public class SubscriptionService {
    public boolean isEligibleForPremium(User user) {
        return user.getAccountAge() >= 30;  // 30 days
    }

    public void sendWelcomeEmail(User user) {
        if (user.getAccountAge() < 30) {  // Same 30, different context
            // Send trial info
        }
    }

    public double getDiscount(User user) {
        if (user.getAccountAge() >= 30) {  // Again 30!
            return 0.10;  // 10% discount
        }
        return 0.0;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// DRY: Named constants with clear meaning
public class SubscriptionService {
    private static final int TRIAL_PERIOD_DAYS = 30;
    private static final double PREMIUM_MEMBER_DISCOUNT = 0.10;

    public boolean isEligibleForPremium(User user) {
        return user.getAccountAge() >= TRIAL_PERIOD_DAYS;
    }

    public void sendWelcomeEmail(User user) {
        if (user.getAccountAge() < TRIAL_PERIOD_DAYS) {
            // Send trial info
        }
    }

    public double getDiscount(User user) {
        if (user.getAccountAge() >= TRIAL_PERIOD_DAYS) {
            return PREMIUM_MEMBER_DISCOUNT;
        }
        return 0.0;
    }
}

Pro Tip: If you need to change a number in more than one place, it should be a constant!

When DRY Makes Sense (And When It Doesn’t)

Apply DRY When:

Same Business Logic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// DRY is appropriate - same business rule
public class PaymentValidator {
    private static final double MIN_PAYMENT_AMOUNT = 0.01;

    private void validateAmount(double amount, String operation) {
        if (amount < MIN_PAYMENT_AMOUNT) {
            throw new InvalidPaymentException(
                String.format("Minimum %s is $%.2f", operation, MIN_PAYMENT_AMOUNT)
            );
        }
    }

    public void validateCheckoutPayment(double amount) {
        validateAmount(amount, "payment");
    }

    public void validateRefundAmount(double amount) {
        validateAmount(amount, "refund");
    }
}

Avoid DRY When:

Coincidental Similarity

Sometimes code looks similar by coincidence, not because it represents the same knowledge.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// DON'T apply DRY - these are different concepts
public class UserService {
    public void validateUser(User user) {
        if (user.getAge() < 18) {  // Business rule: age restriction
            throw new ValidationException("Must be 18+");
        }
    }
}

public class ReportFilter {
    public List<User> filterAdults(List<User> users) {
        return users.stream()
            .filter(user -> user.getAge() >= 18)  // Data filtering, not validation
            .collect(Collectors.toList());
    }
}

// These both check age >= 18, but for different reasons!
// Extracting to a shared method would create false coupling.

Warning: Premature abstraction is worse than duplication. If you’re not sure whether code represents the same knowledge, keep it separate until the pattern becomes clear.

Different Rates of Change

Code that changes for different reasons shouldn’t be DRYed together.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// DON'T apply DRY - different change drivers
public class PricingEngine {
    // Changes when business rules change
    private double calculateBusinessDiscount(Order order) {
        return order.getTotal() * 0.10;
    }

    // Changes when promotions change
    private double calculatePromotionalDiscount(Order order) {
        return order.getTotal() * 0.10;
    }
}

// Even though both calculate 10%, they exist for different reasons
// and will likely diverge. Keep them separate!

Common DRY Pitfalls

Premature Abstraction

“Duplication is far cheaper than the wrong abstraction.” — Sandi Metz, Ruby developer and author

Pro Tip: Follow the “Rule of Three” - wait until you see duplication three times before abstracting. This gives you enough examples to identify the real pattern.

Over-Engineering

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Over-engineered DRY - more complex than the problem
public class FieldValidator {
    private Map<String, List<ValidationRule>> rules = new HashMap<>();
    private Map<String, ValidationErrorHandler> errorHandlers = new HashMap<>();

    public void registerRule(String field, ValidationRule rule) { }
    public void registerErrorHandler(String field, ValidationErrorHandler handler) { }
    public ValidationResult validate(String field, Object value) { }
    // ... 100 more lines
}

// Simple approach - good enough for most cases
public class UserValidator {
    public void validateEmail(String email) {
        if (email == null || !email.contains("@")) {
            throw new ValidationException("Invalid email");
        }
    }

    public void validateAge(int age) {
        if (age < 0 || age > 150) {
            throw new ValidationException("Invalid age");
        }
    }
}

YAGNI - You Aren’t Gonna Need It

What Is the YAGNI Principle?

YAGNI stands for “You Aren’t Gonna Need It” - a principle that encourages developers to implement only the functionality that’s needed right now, not what you think you might need in the future. It’s a core practice in Extreme Programming (XP) and Agile methodologies.

The principle challenges a common developer instinct: building systems with extensive flexibility and features “just in case” we need them later. While this sounds prudent, it often leads to wasted effort, increased complexity, and technical debt.

The Core Philosophy

YAGNI is built on a simple observation: most features you think you’ll need in the future, you won’t actually need. And even when you do need them, requirements often change so much that your premature implementation becomes obsolete anyway.

flowchart TB
    Start[Feature Idea] --> Decision{Is it needed NOW?}
    Decision -->|Yes| Implement[Implement it]
    Decision -->|No| Wait[Don't implement yet]

    Implement --> Ship[Ship working software]
    Wait --> Monitor[Monitor actual needs]

    Monitor --> Future{Actually needed later?}
    Future -->|Yes| ImplementLater[Implement with real requirements]
    Future -->|No| Saved[Saved time & effort!]

    ImplementLater --> Better[Better implementation<br/>based on real needs]

    style Wait fill:#90EE90
    style Saved fill:#90EE90
    style Better fill:#90EE90

The cost of implementing a feature before you need it is almost always higher than the cost of implementing it when you actually need it.

Origin and History

The YAGNI principle was formalized by Ron Jeffries, one of the founders of Extreme Programming (XP), in the late 1990s. It emerged as a response to the common practice of “future-proofing” code.

The Problem YAGNI Solves - Understanding the traditional development trap.

In traditional waterfall development, teams would spend months planning and building systems with extensive functionality to handle every conceivable future scenario. This led to several problems:

  • Wasted effort: 64% of features in software are rarely or never used (Standish Group research)
  • Increased complexity: More code means more bugs, more maintenance, and higher cognitive load
  • Wrong implementations: Future requirements are often guessed incorrectly, making the premature work useless
  • Delayed delivery: Time spent on unnecessary features delays actually needed functionality
  • Higher maintenance cost: Every line of code needs to be tested, documented, and maintained

YAGNI challenged this approach by advocating for incremental development: build what you need today, and trust that you can add more later when requirements are clearer.

When to Apply YAGNI

Apply YAGNI When:

Adding “just in case” features

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// YAGNI Violation: Adding unused flexibility
public class UserService {
    // Nobody asked for XML, JSON is enough for now
    public String exportUsers(ExportFormat format) {
        switch (format) {
            case JSON: return toJson();
            case XML: return toXml();      // YAGNI!
            case CSV: return toCsv();       // YAGNI!
            case YAML: return toYaml();     // YAGNI!
            default: throw new IllegalArgumentException();
        }
    }
}

// YAGNI Applied: Build what you need
public class UserService {
    public String exportUsersAsJson() {
        return toJson();
    }
    // Add other formats when actually requested
}

Building extensibility you don’t need

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// YAGNI Violation: Complex plugin system nobody requested
public class PaymentPluginRegistry {
    private Map<String, PaymentProcessor> plugins = new HashMap<>();

    public void registerPlugin(String name, PaymentProcessor processor) {
        plugins.put(name, processor);
    }
    // Lots of plugin management code...
}

// YAGNI Applied: Simple, direct implementation
public class PaymentService {
    private final StripePaymentProcessor stripeProcessor;

    public void processPayment(Payment payment) {
        stripeProcessor.process(payment);
    }
    // Add more processors when you actually integrate them
}

Premature abstraction

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// YAGNI Violation: Abstracting with only one implementation
public interface EmailSender {
    void send(Email email);
}

public abstract class AbstractEmailSender implements EmailSender {
    protected abstract void connect();
    protected abstract void disconnect();
    // Lots of abstract framework code...
}

public class SmtpEmailSender extends AbstractEmailSender {
    // Only concrete implementation
}

// YAGNI Applied: Concrete implementation first
public class EmailSender {
    public void send(Email email) {
        // Direct SMTP implementation
        // Abstract when you add a second implementation
    }
}

Start concrete, abstract later. Wait until you have at least 2-3 real use cases before creating abstractions.

When NOT to Apply YAGNI:

YAGNI is powerful, but it’s not absolute. There are times when planning ahead is the right choice.

Security and Privacy

1
2
3
4
5
6
7
8
9
10
11
12
13
// DO implement proper security from the start
public class UserService {
    public void createUser(UserData data) {
        // Hash passwords - don't wait for a breach!
        String hashedPassword = passwordHasher.hash(data.getPassword());

        // Validate input - don't wait for SQL injection!
        validator.validate(data);

        // Log sensitive operations - don't wait for an audit!
        auditLog.log("User created: " + data.getUsername());
    }
}

Core architectural decisions

1
2
3
4
5
6
7
8
// DO consider scalability for core architecture
public class OrderService {
    // Use proper database from the start, not CSV files
    private final OrderRepository repository;

    // Use proper IDs, not sequential integers
    private final IdGenerator idGenerator; // UUIDs
}

Regulatory and compliance requirements

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// DO build GDPR compliance from the start
public class UserService {
    // Consent tracking - legally required
    private final ConsentRepository consentRepository;

    // Audit logging - required for compliance
    private final AuditLogger auditLogger;

    public void deleteUser(UserId userId) {
        // Right to be forgotten - GDPR requirement
        auditLogger.log("User deletion requested", userId);
        userRepository.anonymize(userId);
        consentRepository.revokeAll(userId);
    }
}

Known immediate needs

Use this decision tree to determine whether a feature should be built now or deferred:

flowchart TB
    Feature[Proposed Feature] --> Q1{Is it a security/<br/>privacy requirement?}
    Q1 -->|Yes| Build[Build it now]
    Q1 -->|No| Q2{Is it a core<br/>architectural decision?}

    Q2 -->|Yes| Build
    Q2 -->|No| Q3{Is it legally/<br/>contractually required?}

    Q3 -->|Yes| Build
    Q3 -->|No| Q4{Is it needed in<br/>current sprint?}

    Q4 -->|Yes| Build
    Q4 -->|No| Q5{Is it confirmed for<br/>next sprint?}

    Q5 -->|Yes| Consider[Consider building now]
    Q5 -->|No| Wait[Apply YAGNI - Wait]

    style Build fill:#90EE90
    style Wait fill:#FFB6C6
    style Consider fill:#FFE4B5

YAGNI is about avoiding speculative features, not avoiding good engineering practices like security, testing, and maintainable architecture.

Common YAGNI Violations

Configuration Overkill

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Building a complex configuration system "in case" we need flexibility
public class DatabaseConfig {
    private String host;
    private int port;
    private String database;
    private int minConnections;
    private int maxConnections;
    private int connectionTimeout;
    private int idleTimeout;
    private boolean enableSSL;
    private String sslMode;
    private boolean enableCompression;
    // ... 20 more configuration options nobody asked for
}

// Start with what you actually need
public class DatabaseConfig {
    private final String host;
    private final int port;
    private final String database;

    public DatabaseConfig(String host, int port, String database) {
        this.host = host;
        this.port = port;
        this.database = database;
    }

    // Add more options when they're actually requested
}

Feature Creep in APIs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// API with methods nobody uses
public interface UserRepository {
    User findById(String id);
    List<User> findAll();
    List<User> findByName(String name);
    List<User> findByEmail(String email);
    List<User> findByAge(int age);
    List<User> findByAgeRange(int min, int max);
    List<User> findByStatus(UserStatus status);
    // ... 15 more "maybe useful" finder methods
}

// Implement only what you need now
public interface UserRepository {
    User findById(String id);
    void save(User user);
    void delete(String id);

    // Add methods as features require them
}

Real-World Case Study: E-Commerce Platform

Without YAGNI (Over-Engineered)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Trying to predict every future need
public class ProductService {
    // Multiple database support "just in case"
    private final DatabaseAbstractionLayer dal;

    // Caching layer "for scalability"
    private final MultiLevelCacheManager cacheManager;

    // Event sourcing "for audit trail"
    private final EventStore eventStore;

    // Plugin system "for extensibility"
    private final PluginManager pluginManager;

    public Product getProduct(String id) {
        // Check L1 cache, L2 cache, query database,
        // emit events, trigger plugins...
        // Complex code for simple retrieval
    }
}

// Result: 6 months of development, zero customers

With YAGNI (Pragmatic)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Build what you need now
public class ProductService {
    private final ProductRepository productRepository;

    public Product getProduct(String id) {
        return productRepository.findById(id)
            .orElseThrow(() -> new ProductNotFoundException(id));
    }
}

@Repository
public interface ProductRepository extends JpaRepository<Product, String> {
    // Spring Data JPA provides the implementation
}

// Result: 2 weeks of development, MVP launched, customers onboarded

Build your MVP fast, learn from real users, then evolve based on actual needs. This is faster and produces better results than trying to predict every future requirement.


How KISS, DRY, and YAGNI Work Together

The Relationship Between Principles

These three principles aren’t isolated rules - they form a cohesive philosophy of pragmatic software development.

PrincipleFocusKey Question
KISSSimplicity“Is this the simplest solution?”
DRYAvoid duplication“Am I repeating myself?”
YAGNIAvoid speculation“Do I need this now?”
flowchart TB
    subgraph Principles["The Three Principles"]
        KISS[KISS<br/>Keep solutions simple]
        DRY[DRY<br/>Single source of truth]
        YAGNI[YAGNI<br/>Build only what's needed]
    end

    KISS --> Balance[Balanced Approach]
    DRY --> Balance
    YAGNI --> Balance

    Balance --> Result[Simple, clean code<br/>that solves today's problems<br/>and can evolve tomorrow]

    style Result fill:#90EE90

When Principles Conflict

Sometimes these principles can conflict. Here’s how to navigate those situations:

KISS vs. DRY

Sometimes DRY can make code more complex. When they conflict, favor KISS for small amounts of duplication.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Strictly DRY but complex
class Validator {
    private <T> boolean validate(T value, Predicate<T> rule, String errorMessage) {
        if (!rule.test(value)) {
            throw new ValidationException(errorMessage);
        }
        return true;
    }
}

// Slightly repeats but simpler to understand
class Validator {
    public static void validateEmail(String email) {
        if (email == null || !email.contains("@")) {
            throw new ValidationException("Invalid email");
        }
    }

    public static void validateAge(int age) {
        if (age < 0 || age > 150) {
            throw new ValidationException("Invalid age");
        }
    }
}

When to prefer DRY over KISS

  • The duplication is significant (10+ lines)
  • The logic is complex and error-prone
  • Changes need to be synchronized across duplicates
  • The abstraction is obvious and natural

DRY vs. YAGNI

Don’t DRY prematurely! Apply DRY to existing code, not hypothetical future code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Violates YAGNI: DRYing for imagined future needs
public abstract class AbstractDataExporter<T> {
    public final void export(List<T> items) {
        validate(items);
        transform(items);
        write(items);
    }

    protected abstract void validate(List<T> items);
    protected abstract void transform(List<T> items);
    protected abstract void write(List<T> items);
}
// Created when you only have one exporter!

// Better: Concrete implementation, DRY when pattern emerges
public class UserExporter {
    public void export(List<User> users) {
        // Direct implementation
        // Abstract when you add ProductExporter, OrderExporter, etc.
    }
}

Decision Framework

Use this flowchart when making design decisions:

flowchart TB
    Start[Design Decision] --> Q1{Is it needed NOW?}
    Q1 -->|No| YAGNI[Don't build it<br/>YAGNI]
    Q1 -->|Yes| Q2{Is there existing<br/>duplication?}

    Q2 -->|No| Q3{Is the solution<br/>simple?}
    Q2 -->|Yes| Q4{Is it the same<br/>knowledge?}

    Q3 -->|Yes| Build[Build it!]
    Q3 -->|No| Simplify[Simplify first<br/>KISS]

    Q4 -->|Yes| Q5{Seen 3 times?}
    Q4 -->|No| Build

    Q5 -->|Yes| Extract[Extract & DRY]
    Q5 -->|No| Build

    Simplify --> Build
    Extract --> Build

    style YAGNI fill:#DDA0DD
    style Simplify fill:#90EE90
    style Extract fill:#87CEEB
    style Build fill:#90EE90

Practical Checklist

Before committing code, ask yourself:

KISS Checklist

  • Can a junior developer understand this in 5 minutes?
  • Is this the simplest solution that works?
  • Am I using patterns/frameworks only when justified?
  • Have I avoided premature optimization?

DRY Checklist

  • Have I extracted truly duplicated logic?
  • Are magic numbers/strings replaced with constants?
  • Does the duplication represent the same knowledge?
  • Did I follow the Rule of Three before abstracting?

YAGNI Checklist

  • Is this feature needed for current requirements?
  • Am I building for confirmed needs, not speculation?
  • Have I avoided “just in case” flexibility?
  • Can I add this later when actually needed?

Conclusion

The KISS, DRY, and YAGNI principles are essential tools in every software engineer’s toolkit. Together, they guide you toward code that is:

  • Simple - Easy to understand and modify
  • Maintainable - Single sources of truth, no scattered duplicates
  • Focused - Built for real needs, not imagined futures

Summary

KISS:

  • Simple code is not simplistic code
  • Complexity has real costs: time, bugs, maintenance
  • Start simple, add complexity only when needed
  • Prefer clarity over cleverness

DRY:

  • DRY is about knowledge, not just code syntax
  • Balance DRY with KISS - don’t over-abstract
  • Wait for patterns to emerge (Rule of Three)
  • Recognize when duplication is acceptable

YAGNI:

  • Build for today’s requirements, not tomorrow’s guesses
  • Trust that you can add features later
  • Exceptions: security, architecture, compliance
  • Measure feature usage to validate decisions

Next Steps

Start applying these principles in your daily work:

  1. Question each decision: Is it simple? Am I repeating? Do I need this now?
  2. Simplify your first draft: Can you remove anything?
  3. Defer abstraction: Wait for real patterns
  4. Review regularly: Delete unused code and features
  5. Embrace incremental development: Build, learn, adapt

The best code is no code. The second best code is simple code that solves today’s problems and can adapt to tomorrow’s needs.

Remember: you’re not predicting the future, you’re building software that can evolve. These principles help you stay focused on what matters right now while keeping your codebase lean, maintainable, and ready to adapt.

Now go forth and build only what you need, keep it simple, and don’t repeat yourself!


Stay Tuned!

This post covered the fundamentals of development principles - KISS, DRY, and YAGNI. These three principles form the foundation of pragmatic software development and will serve you throughout your career.

As you progress through our roadmap, you’ll encounter more advanced principles and patterns that build upon this foundation.

Keep practicing these principles in your daily coding, and you’ll find yourself naturally writing cleaner, more maintainable code!

This post is licensed under CC BY 4.0 by the author.