DRY, KISS and YAGNI Principles with Java Examples

Introduction

This tutorial explains three short design principles that are quoted in almost every code review - DRY (Don't Repeat Yourself), KISS (Keep It Simple, Stupid) and YAGNI (You Aren't Gonna Need It) - with Java examples. We will start with an overview of the three and where they come from. We will then look at each one in turn: what it really says, a Java example, and how it is commonly misapplied. Lastly, we will see how the three balance each other.

The three principles at a glance

Each principle answers one question you can ask about a piece of code -
  • DRY: is this piece of knowledge already written somewhere else? The principle comes from Andy Hunt and Dave Thomas's book The Pragmatic Programmer (1999).
  • KISS: is there a simpler way that does the same job? The phrase is credited to aircraft engineer Kelly Johnson of Lockheed's Skunk Works.
  • YAGNI: is this needed now, or only "maybe later"? It comes from Extreme Programming, as practised by Kent Beck and Ron Jeffries in the late 1990s.
DRY KISS and YAGNI principles overview - three questions to ask about code, and DRY shown with an employee id rule

DRY - Don't Repeat Yourself

The full statement of DRY in The Pragmatic Programmer is: "Every piece of knowledge must have a single, unambiguous, authoritative representation within a system." Note the word knowledge. DRY is not about lines of code that look alike; it is about rules and facts - a validation rule, a tax rate, a file format - being defined in one place, so that changing the rule means changing one place.

Let us see what happens when a rule is repeated. An employee id was EMP- followed by four digits. When the company grew, ids with five digits were introduced, and the payroll import was updated - but the onboarding form was not.
Java code showing a repeated validation rule and a single authoritative rule
//DryExample.java
import java.util.List;

public class DryExample {

  // ---------- The rule written in two places ----------
  static class OnboardingForm {
    boolean accepts(String id) { return id.matches("EMP-\\d{4}"); }
  }

  static class PayrollImport {
    boolean accepts(String id) { return id.matches("EMP-\\d{4,5}"); } // updated here only
  }

  // ---------- One authoritative representation ----------
  record EmployeeId(String value) {
    private static final String FORMAT = "EMP-\\d{4,5}";

    EmployeeId {
      if (!isValid(value)) throw new IllegalArgumentException("Invalid employee id: " + value);
    }

    static boolean isValid(String value) {
      return value != null && value.matches(FORMAT);
    }
  }

  public static void main(String[] args) {
    OnboardingForm form = new OnboardingForm();
    PayrollImport payroll = new PayrollImport();

    for (String id : List.of("EMP-1234", "EMP-12345")) {
      System.out.println(id + " -> onboarding: " + form.accepts(id)
          + ", payroll: " + payroll.accepts(id)
          + ", EmployeeId: " + EmployeeId.isValid(id));
    }

    try {
      new EmployeeId("E-12");
    } catch (IllegalArgumentException e) {
      System.out.println(e.getMessage());
    }
  }
}
 OUTPUT of the above code
EMP-1234 -> onboarding: true, payroll: true, EmployeeId: true
EMP-12345 -> onboarding: false, payroll: true, EmployeeId: true
Invalid employee id: E-12

Explanation of the code

  • The two copies of the rule have drifted apart: a new employee with id EMP-12345 is accepted by payroll but rejected by onboarding. Nothing failed to compile, and no test of either class alone would catch it.
  • EmployeeId is the single, authoritative home for the rule. Every part of the system that needs to know what a valid id is asks EmployeeId.
  • The compact constructor validates the value, so an invalid EmployeeId can never be created. Code that receives an EmployeeId does not need to check it again - which removes even more repetition.

Misusing DRY - the wrong abstraction

The common mistake is to treat DRY as "never write two similar-looking lines". Two pieces of code that look alike but represent different knowledge should stay separate -
Code that looks duplicated but is not the same knowledge
// Security policy: lock the account after 3 failed logins
static final int MAX_LOGIN_ATTEMPTS = 3;

// Integration policy: retry the payroll API 3 times
static final int MAX_API_RETRIES = 3;

// Wrong: merging them because the numbers match
static final int THREE = 3;
The two limits are equal today by coincidence. When security asks for five attempts, a shared constant would change the API retries too. Merging code that only looks the same couples unrelated parts of the system, which is often worse than the duplication. A good guide is the rule of three: write it once, copy it the second time, and extract a shared abstraction the third time - by then you can see what really varies. The exception is duplicated business knowledge, like the employee id rule, which should get a single home straight away.

KISS - Keep It Simple, Stupid

KISS says that most systems work best when they are kept simple, so unnecessary complexity should be avoided. In code, "simple" means easy to read and easy to change - not the fewest lines, and not the most clever. Here is a requirement: an employee's notice period is 30 days, or 60 days for managers. The first version below was designed for every possible future rule; the second one meets the requirement.
Over-engineered code for a simple rule
interface NoticePeriodRule { boolean appliesTo(Employee e); int days(); }

class NoticePeriodRuleEngine {
  private final List<NoticePeriodRule> rules = new ArrayList<>();
  void register(NoticePeriodRule rule) { rules.add(rule); }
  int noticePeriodFor(Employee e) {
    return rules.stream().filter(r -> r.appliesTo(e)).mapToInt(NoticePeriodRule::days)
        .max().orElseThrow(() -> new IllegalStateException("No rule for " + e));
  }
}
// ...plus two rule classes, and code somewhere that registers them in the right order
The simple version
int noticePeriodDays(Employee e) {
  return e.isManager() ? 60 : 30;
}
The rule engine is not wrong, but it costs a reader several classes and a registration step to learn that managers get 60 days. If the rules really do multiply later, the one-line method is trivial to replace. Simple code is easier to change than complicated code - including when it needs to become less simple.

Some practical forms of KISS in Java -
  • Use the JDK before writing your own: Year.isLeap(), String.join(), Collectors.groupingBy() and Objects.requireNonNull() replace many hand-written helpers.
  • Prefer a loop over a clever stream when the loop is clearer, and the reverse when the stream is.
  • Name things for what they mean: a well-named method is simpler than a comment explaining a badly named one.
  • Avoid premature optimisation: write the clear version first, and optimise only what measurements show is slow.

YAGNI - You Aren't Gonna Need It

YAGNI says: do not build a capability until you actually need it. It does not say "do not think ahead"; it says do not write the code for a guess. The rule engine above is a YAGNI violation as well as a KISS one: it was built for rules that no one had asked for.

Martin Fowler's article on YAGNI describes what a feature built "just in case" really costs. Even if the guess turns out right, you have paid to carry the code until then, and delayed the things that were needed.
YAGNI costs of a presumptive feature - build, delay, carry, repair - and the rule of three for removing duplicated code
Typical YAGNI violations in Java code - YAGNI applies to features, not to quality. Tests, clear names, and refactoring that keeps the code easy to change are not speculative; they are what make it cheap to add the feature on the day it is really needed.

How DRY, KISS and YAGNI balance each other

The three principles pull in slightly different directions, and they are most useful together -
  • DRY taken too far breaks KISS: removing every similarity creates abstractions that are harder to understand than the duplication they replaced.
  • YAGNI limits DRY: do not build a general mechanism for duplication that might happen later; extract it when the third case arrives.
  • KISS needs DRY: a rule repeated in five places is not simple, because every change has to find all five.
  • All three keep code small, and small code is easier to keep highly cohesive and loosely coupledClick to read tutorial: Coupling and Cohesion Explained with Java Examples.

Summary

In this tutorial we first looked at what DRY, KISS and YAGNI ask and where they come from. Next we saw that DRY is about knowledge: a repeated employee id rule drifted apart, and a single EmployeeId record fixed it - while code that only looks alike should not be merged, and the rule of three tells you when to extract. We then compared an over-engineered rule engine with a one-line method to see KISS in practice. We then saw YAGNI and the real cost of building features in advance. Lastly, we saw how the three principles balance each other.