Home YAGNI Principle
Post
Cancel
YAGNI Principle | SEG

YAGNI Principle

This post introduces the YAGNI principle, a key concept in Agile development. To understand how YAGNI relates to simplicity, check out our post on the KISS Principle!

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 - adding extensibility, abstraction, and features based on anticipated future requirements.

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.

YAGNI in Context

YAGNI doesn’t exist in isolation - it’s part of a broader philosophy of pragmatic software development:

PrincipleFocusRelationship to YAGNI
YAGNIDon’t build features you don’t need yetCore principle - prevent over-engineering
KISSKeep solutions simpleYAGNI helps maintain simplicity by limiting scope
DRYDon’t Repeat YourselfApply DRY to existing code, not hypothetical future code
AgileRespond to change over following a planYAGNI enables agility by reducing upfront commitments

When to Apply YAGNI

Understanding when to apply YAGNI is crucial. It’s not about being short-sighted - it’s about being pragmatic.

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
20
21
22
23
// ❌ YAGNI Violation: Complex plugin system nobody requested
public interface PaymentProcessor {
    void processPayment(Payment payment);
}

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
9
10
11
12
13
// ✅ 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
    
    public Order createOrder(OrderData data) {
        String orderId = idGenerator.generate(); // UUID
        return repository.save(new Order(orderId, data));
    }
}

❌ Regulatory and compliance requirements

1
2
3
4
5
6
7
8
9
// ✅ DO implement required data retention policies
public class DataService {
    // GDPR requires data deletion capability - build it now
    public void deleteUserData(String userId) {
        userRepository.delete(userId);
        analyticsRepository.deleteByUser(userId);
        backupService.scheduleRemoval(userId);
    }
}

❌ Known immediate needs

1
2
3
4
5
6
// ✅ DO implement confirmed upcoming requirements
public class ReportService {
    // If you KNOW you need both reports next sprint, build both
    public Report generateDailyReport() { }
    public Report generateWeeklyReport() { }
}
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

Let’s examine real-world scenarios where developers commonly violate YAGNI and how to fix them.

1. Configuration Overkill

The Violation:

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
// ❌ 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;
    private String compressionAlgorithm;
    private int compressionLevel;
    private boolean enableRetry;
    private int maxRetries;
    private int retryDelay;
    private String loadBalancingStrategy;
    private boolean enableMetrics;
    private int metricsPort;
    // ... 20 more configuration options nobody asked for
    
    // Giant builder pattern with fluent API
    public static class Builder { /* ... */ }
}

The Fix:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ✅ 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
}

2. Premature Generalization

The Violation:

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
38
39
40
41
// ❌ Creating a "universal" data processor for one use case
public class DataProcessor<T, R, E extends Exception> {
    private final Function<T, R> transformer;
    private final Predicate<T> validator;
    private final Consumer<E> errorHandler;
    private final List<ProcessorPlugin<T, R>> plugins;
    
    public DataProcessor(Builder<T, R, E> builder) {
        this.transformer = builder.transformer;
        this.validator = builder.validator;
        this.errorHandler = builder.errorHandler;
        this.plugins = builder.plugins;
    }
    
    public R process(T input) {
        // Complex generic processing pipeline
        if (!validator.test(input)) {
            throw new ValidationException();
        }
        
        for (ProcessorPlugin<T, R> plugin : plugins) {
            plugin.beforeProcess(input);
        }
        
        R result = transformer.apply(input);
        
        for (ProcessorPlugin<T, R> plugin : plugins) {
            plugin.afterProcess(result);
        }
        
        return result;
    }
    
    public static class Builder<T, R, E extends Exception> { /* ... */ }
}

// Used in exactly one place:
DataProcessor<String, Integer, RuntimeException> processor = 
    new DataProcessor.Builder<String, Integer, RuntimeException>()
        .transformer(Integer::parseInt)
        .build();

The Fix:

1
2
3
4
5
6
7
8
// ✅ Simple, direct solution
public class StringToIntConverter {
    public int convert(String input) {
        return Integer.parseInt(input);
    }
}

// Generalize when you have multiple similar use cases

3. Feature Creep in APIs

The Violation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ❌ 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);
    List<User> findByCreatedDate(LocalDate date);
    List<User> findByCreatedDateRange(LocalDate start, LocalDate end);
    List<User> findByNameAndEmail(String name, String email);
    List<User> findByNameOrEmail(String name, String email);
    // ... 15 more "maybe useful" finder methods
    
    void save(User user);
    void update(User user);
    void delete(String id);
    void deleteAll();
    void deleteByStatus(UserStatus status);
    // etc...
}

The Fix:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ✅ 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
}

// Later, when search is needed:
public interface UserRepository {
    User findById(String id);
    List<User> search(UserSearchCriteria criteria); // One flexible method
    void save(User user);
    void delete(String id);
}

4. Over-Engineered Error Handling

The Violation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ❌ Complex error handling system for simple application
public class ErrorManager {
    private final Map<ErrorCode, ErrorHandler> handlers = new HashMap<>();
    private final ErrorLogger logger;
    private final ErrorNotifier notifier;
    private final ErrorRecoveryStrategy recoveryStrategy;
    
    public void registerHandler(ErrorCode code, ErrorHandler handler) {
        handlers.put(code, handler);
    }
    
    public void handleError(ApplicationError error) {
        ErrorHandler handler = handlers.get(error.getCode());
        if (handler != null) {
            handler.handle(error);
        }
        
        logger.log(error);
        notifier.notify(error);
        recoveryStrategy.recover(error);
    }
}

// In practice, only used for: throw new RuntimeException(message);

The Fix:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ✅ Simple, sufficient error handling
public class OrderService {
    public Order createOrder(OrderRequest request) {
        try {
            validateOrder(request);
            return orderRepository.save(request.toOrder());
        } catch (ValidationException e) {
            throw new BadRequestException("Invalid order: " + e.getMessage());
        } catch (DataAccessException e) {
            throw new ServiceException("Failed to create order", e);
        }
    }
}

// Add sophisticated error handling when you have real requirements

Real-World Case Study: E-Commerce Platform

Let’s walk through a realistic scenario showing YAGNI in action.

The Scenario

You’re building an e-commerce platform MVP. The product owner wants to launch quickly with basic functionality: users can browse products, add to cart, and checkout.

❌ 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
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 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
        Product product = cacheManager.getFromL1Cache(id);
        if (product != null) return product;
        
        // Check L2 cache
        product = cacheManager.getFromL2Cache(id);
        if (product != null) {
            cacheManager.putInL1Cache(id, product);
            return product;
        }
        
        // Query database through abstraction layer
        product = dal.query(
            new QueryBuilder()
                .from("products")
                .where("id", id)
                .build()
        );
        
        // Store in caches
        cacheManager.putInL2Cache(id, product);
        cacheManager.putInL1Cache(id, product);
        
        // Emit event
        eventStore.append(new ProductAccessedEvent(id));
        
        // Trigger plugins
        pluginManager.trigger("product.accessed", product);
        
        return product;
    }
}

// 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

Evolution with Real Requirements

After launch, you discover actual needs through real usage:

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
// Iteration 1: Add caching after performance monitoring shows need
@Service
public class ProductService {
    private final ProductRepository productRepository;
    
    @Cacheable("products")  // Simple caching when needed
    public Product getProduct(String id) {
        return productRepository.findById(id)
            .orElseThrow(() -> new ProductNotFoundException(id));
    }
}

// Iteration 2: Add search when customers request it
@Service
public class ProductService {
    private final ProductRepository productRepository;
    
    @Cacheable("products")
    public Product getProduct(String id) {
        return productRepository.findById(id)
            .orElseThrow(() -> new ProductNotFoundException(id));
    }
    
    public List<Product> searchProducts(String query) {
        return productRepository.findByNameContaining(query);
    }
}

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.


Balancing YAGNI with Good Design

YAGNI doesn’t mean writing sloppy code or ignoring design principles. It means being strategic about what you build.

The Balance

AspectYAGNI SaysGood Design SaysThe Balance
AbstractionsDon’t abstract prematurelyAvoid duplicationAbstract after 3rd similar case
TestingTest what existsEnsure qualityTest current code thoroughly
DocumentationDocument what’s thereExplain design decisionsDocument actual usage, not hypotheticals
ExtensibilityAdd when neededConsider future changesUse open/closed principle for known extension points
Error HandlingHandle actual errorsBe robustHandle errors you’ve seen or are likely

YAGNI-Compliant Design Practices

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
// ✅ Good: Clean, testable code without speculation
public class OrderService {
    private final OrderRepository orderRepository;
    private final PaymentService paymentService;
    private final InventoryService inventoryService;
    
    public Order createOrder(OrderRequest request) {
        // Validate current requirements
        validateOrder(request);
        
        // Check inventory
        inventoryService.reserve(request.getItems());
        
        // Process payment
        Payment payment = paymentService.process(request.getPayment());
        
        // Create order
        Order order = new Order(request, payment);
        return orderRepository.save(order);
    }
    
    private void validateOrder(OrderRequest request) {
        if (request.getItems().isEmpty()) {
            throw new ValidationException("Order must contain items");
        }
        // Only validations you actually need
    }
}
flowchart LR
    subgraph Good["Good Design Practices"]
        direction TB
        A[Clean Code]
        B[SOLID Principles]
        C[Testing]
        D[Documentation]
    end
    
    subgraph YAGNI["YAGNI Mindset"]
        direction TB
        E[Current Needs]
        F[No Speculation]
        G[Incremental]
    end
    
    Good --> Balance[Balanced Approach]
    YAGNI --> Balance
    
    Balance --> Result[Simple, clean code<br/>that solves today's problems<br/>and can evolve tomorrow]
    
    style Result fill:#90EE90

Identifying YAGNI Violations

How do you recognize when you’re violating YAGNI? Here are warning signs:

Red Flags 🚩

Warning SignExampleFix
“Just in case” language“Let’s add XML support just in case”Only add JSON if that’s what’s needed now
No current requirementBuilding multi-tenancy for single tenant appWait for second tenant
Abstracting with one use caseCreating framework for one screenDirect implementation until pattern emerges
“We might need this”Adding 10 configuration optionsAdd one when first one is requested
Speculative performance optimizationComplex caching before measuringProfile first, optimize later
Feature nobody asked forAdmin dashboard for no adminsBuild when admins exist

Self-Assessment Questions

Before adding a feature, ask yourself:

flowchart TB
    Start[Proposed Feature] --> Q1{Is it in current sprint<br/>or user story?}
    Q1 -->|No| Stop[Don't build it yet]
    Q1 -->|Yes| Q2{Do you have specific<br/>requirements?}
    
    Q2 -->|No| Stop
    Q2 -->|Yes| Q3{Can you test it<br/>with real users?}
    
    Q3 -->|No| Stop
    Q3 -->|Yes| Q4{Is it the simplest<br/>solution?}
    
    Q4 -->|No| Simplify[Simplify the design]
    Q4 -->|Yes| Build[Build it!]
    
    Simplify --> Build
    
    style Stop fill:#FFB6C6
    style Build fill:#90EE90
  1. Is it required now? If not in the current sprint/story, don’t build it.
  2. Can you test it? If there’s no way to validate it works, you don’t understand the requirement well enough.
  3. Is it the simplest solution? If you can solve it more simply, you probably should.
  4. Will removing it break existing functionality? If no, you might not need it.

When in doubt, leave it out. It’s easier to add code than to maintain code that shouldn’t exist.


Common Pitfalls and Best Practices

Pitfall 1: Confusing YAGNI with Laziness

Wrong interpretation:

1
2
3
4
5
6
7
8
9
// ❌ This is NOT YAGNI - this is just bad code
public class UserService {
    public void createUser(String username, String password) {
        // No validation "because YAGNI"
        // No error handling "because YAGNI"
        // No logging "because YAGNI"
        users.put(username, password); // Storing plain text passwords!
    }
}

Correct interpretation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ✅ YAGNI with good engineering practices
public class UserService {
    public void createUser(String username, String password) {
        // Validate inputs (requirement)
        validateUsername(username);
        validatePassword(password);
        
        // Hash password (security requirement)
        String hashedPassword = passwordHasher.hash(password);
        
        // Save user (requirement)
        User user = new User(username, hashedPassword);
        userRepository.save(user);
        
        // Log for debugging (operational requirement)
        log.info("User created: {}", username);
    }
}

YAGNI is about avoiding speculative features, not avoiding good practices like validation, security, error handling, and logging.

Pitfall 2: Using YAGNI to Avoid Refactoring

Wrong:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ❌ Refusing to refactor duplicate code "because YAGNI"
public class ReportService {
    public Report generateDailyReport() {
        List<Order> orders = orderRepository.findToday();
        double total = 0;
        for (Order order : orders) {
            total += order.getTotal();
        }
        return new Report("Daily", total, orders.size());
    }
    
    public Report generateWeeklyReport() {
        List<Order> orders = orderRepository.findThisWeek();
        double total = 0;
        for (Order order : orders) {
            total += order.getTotal();
        }
        return new Report("Weekly", total, orders.size());
    }
    // Duplication because "we might not need more reports"
}

Correct:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ✅ Refactor existing code to remove duplication
public class ReportService {
    public Report generateDailyReport() {
        return generateReport("Daily", orderRepository.findToday());
    }
    
    public Report generateWeeklyReport() {
        return generateReport("Weekly", orderRepository.findThisWeek());
    }
    
    private Report generateReport(String type, List<Order> orders) {
        double total = orders.stream()
            .mapToDouble(Order::getTotal)
            .sum();
        return new Report(type, total, orders.size());
    }
}

Pitfall 3: Avoiding Design Discussions

YAGNI doesn’t mean you shouldn’t think about design or discuss architecture.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ✅ Good: Discuss and plan architecture
/*
 * OrderService handles order creation and validation.
 * 
 * Design decisions:
 * - Using repository pattern for data access
 * - Payment processing delegated to PaymentService
 * - Inventory checked synchronously (may need async later)
 * 
 * Future considerations (when needed):
 * - Order cancellation workflow
 * - Partial refunds
 * - Multi-currency support
 */
public class OrderService {
    // Current implementation
}

Best Practices for Applying YAGNI

✅ Start with the simplest solution that could possibly work

✅ Refactor mercilessly when duplication appears

✅ Write tests for existing functionality

✅ Document actual decisions, not hypothetical scenarios

✅ Use feature flags for uncertain requirements

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ✅ Feature flag for uncertain feature
@Service
public class SearchService {
    private final FeatureFlags featureFlags;
    
    public List<Product> search(String query) {
        if (featureFlags.isEnabled("advanced-search")) {
            return advancedSearch(query);
        }
        return basicSearch(query);
    }
    
    private List<Product> basicSearch(String query) {
        // Simple implementation that works
        return productRepository.findByNameContaining(query);
    }
    
    private List<Product> advancedSearch(String query) {
        // Advanced features being tested
        return elasticSearchService.search(query);
    }
}

✅ Embrace change - make code easy to modify

1
2
3
4
5
6
7
8
9
// ✅ Code that's easy to change
public class NotificationService {
    public void notifyUser(User user, String message) {
        emailSender.send(user.getEmail(), message);
    }
    
    // When SMS is needed later, easy to add:
    // smsSender.send(user.getPhone(), message);
}

YAGNI and Technical Debt

There’s a common misconception that YAGNI creates technical debt. The opposite is usually true.

Technical Debt Comparison

flowchart LR
    subgraph Without["Without YAGNI"]
        direction TB
        A1[Build 20 features]
        A2[Use 3 features]
        A3[Maintain all 20]
        A4[High debt]
        A1 --> A2 --> A3 --> A4
    end
    
    subgraph With["With YAGNI"]
        direction TB
        B1[Build 3 features]
        B2[Use 3 features]
        B3[Maintain 3]
        B4[Low debt]
        B1 --> B2 --> B3 --> B4
    end
    
    style A4 fill:#FFB6C6
    style B4 fill:#90EE90
ApproachCode WrittenCode UsedCode MaintainedDebt
Without YAGNI10,000 lines3,000 lines10,000 linesHigh
With YAGNI3,000 lines3,000 lines3,000 linesLow

Real Technical Debt

Technical debt comes from:

  • ❌ Poor code quality
  • ❌ Skipping tests
  • ❌ Ignoring design principles
  • ❌ Taking shortcuts on security

NOT from:

  • ✅ Building only requested features
  • ✅ Waiting for clear requirements
  • ✅ Keeping code simple
  • ✅ Deferring speculation

Every line of unused code is technical debt. YAGNI reduces debt by reducing unnecessary code.


Practical Tips for Junior Developers

As a junior developer, YAGNI can feel counterintuitive. Here’s how to build the right instincts:

1. Listen to Requirements Carefully

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Requirement: "Users need to export their data"

// ❌ Over-interpretation
public class DataExporter {
    public void export(User user, ExportFormat format, CompressionType compression, 
                      EncryptionType encryption, DeliveryMethod delivery) {
        // Assumed we need all these options
    }
}

// ✅ Clarify and build what's actually needed
public class DataExporter {
    public String exportAsJson(User user) {
        // That's literally all they wanted
        return jsonMapper.writeValueAsString(user);
    }
}

2. Resist “Cool Technology” Syndrome

1
2
3
4
5
6
7
8
9
10
11
12
13
// ❌ "I just learned about reactive programming!"
public Flux<Product> getProducts() {
    return productRepository.findAll()
        .subscribeOn(Schedulers.parallel())
        .publishOn(Schedulers.elastic())
        .cache();
}
// For a list of 10 products...

// ✅ Use appropriate technology for the problem
public List<Product> getProducts() {
    return productRepository.findAll();
}

3. Ask “Why?” Three Times

Before adding a feature:

  1. Why do we need this? - Understand the real requirement
  2. Why not add it later? - Challenge the timing
  3. Why this approach? - Ensure it’s the simplest solution

4. Use Code Reviews to Learn

When reviewing code, ask:

  • “Is this feature being used?”
  • “Could we solve this more simply?”
  • “What requirement drove this design?”

5. Track Features Post-Launch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Add simple usage metrics
@Service
public class ReportService {
    private final MetricsService metrics;
    
    public Report generateReport(ReportType type) {
        metrics.increment("report.generated." + type);
        // ... generate report
    }
}

// After a month, check metrics:
// report.generated.daily: 1,247
// report.generated.weekly: 43
// report.generated.monthly: 0  <- Maybe we don't need this yet!

Track feature usage to validate whether you’re building the right things. Let data guide your decisions.


Conclusion

The YAGNI principle is deceptively simple but profoundly impactful: don’t build features until you actually need them. It’s not about being short-sighted or lazy - it’s about being pragmatic and focusing your efforts on delivering value.

Key Takeaways

✅ YAGNI prevents over-engineering by deferring speculative features

✅ Focus on current requirements, not imagined future needs

✅ YAGNI complements KISS and DRY - they work together for better code

✅ Balance YAGNI with good practices - security, testing, and maintainability are not negotiable

✅ Exceptions exist - security, architecture, and legal requirements shouldn’t wait

✅ Trust in refactoring - you can add complexity when needed

✅ Measure feature usage - let data guide your development

Applying YAGNI in Your Work

Start small:

  1. Question each new feature: Is it needed now?
  2. Simplify your first draft: Can you remove anything?
  3. Defer abstraction: Wait for the third use case
  4. Review unused code: Delete features that aren’t being used
  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. YAGNI helps 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 - your future self will thank you!


Further Reading

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