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 - 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());
}
}
}
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-12345is accepted by payroll but rejected by onboarding. Nothing failed to compile, and no test of either class alone would catch it. EmployeeIdis the single, authoritative home for the rule. Every part of the system that needs to know what a valid id is asksEmployeeId.- The compact constructor validates the value, so an invalid
EmployeeIdcan never be created. Code that receives anEmployeeIddoes 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;
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;
}
Some practical forms of KISS in Java -
- Use the JDK before writing your own:
Year.isLeap(),String.join(),Collectors.groupingBy()andObjects.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.
- An interface with one implementation "in case we add another" - see the common mistakes in the SOLID principles overviewClick to read tutorial: SOLID Principles in Java: Overview with Examples.
- Configuration options, flags and plug-in points that no deployment uses.
- Generic type parameters on classes that are only ever used with one type.
- Supporting several databases, file formats or currencies when the system uses one.
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 singleEmployeeId 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.Programming Principles Tutorials
Coupling and Cohesion Explained with Java ExamplesClick to read tutorial: Coupling and Cohesion Explained with Java Examples Dependency Injection vs Dependency Inversion vs IoCClick to read tutorial: Dependency Injection vs Dependency Inversion vs IoC SOLID Principles in Java: Overview with ExamplesClick to read tutorial: SOLID Principles in Java: Overview with Examples Composition over Inheritance in Java with ExamplesClick to read tutorial: Composition over Inheritance in Java with Examples Law of Demeter in Java Explained with ExamplesClick to read tutorial: Law of Demeter in Java Explained with Examples Tell, Don't Ask Principle Explained with Java ExamplesClick to read tutorial: Tell, Don't Ask Principle Explained with Java Examples Separation of Concerns Explained with Java ExamplesClick to read tutorial: Separation of Concerns Explained with Java Examples
Coupling and Cohesion Explained with Java ExamplesClick to read tutorial: Coupling and Cohesion Explained with Java Examples Dependency Injection vs Dependency Inversion vs IoCClick to read tutorial: Dependency Injection vs Dependency Inversion vs IoC SOLID Principles in Java: Overview with ExamplesClick to read tutorial: SOLID Principles in Java: Overview with Examples Composition over Inheritance in Java with ExamplesClick to read tutorial: Composition over Inheritance in Java with Examples Law of Demeter in Java Explained with ExamplesClick to read tutorial: Law of Demeter in Java Explained with Examples Tell, Don't Ask Principle Explained with Java ExamplesClick to read tutorial: Tell, Don't Ask Principle Explained with Java Examples Separation of Concerns Explained with Java ExamplesClick to read tutorial: Separation of Concerns Explained with Java Examples