Beyond SOLID, there is a set of pragmatic principles that act as a compass in the day-to-day of design. DRY reminds us not to repeat knowledge; KISS advocates simplicity; YAGNI stops us from building what we do not yet need. To these are added the principle of least astonishment and the preference for composition over inheritance. These principles are seemingly simple, but their balanced application is what distinguishes an experienced designer: each one pulls in a direction, and knowing when to apply them —and when not to— is a key architectural skill. In this lesson we will study them with practical examples in Java.
Contents
- DRY: don't repeat yourself
- KISS: keep it simple
- YAGNI: you aren't gonna need it
- The tension among DRY, KISS, and YAGNI
- The principle of least astonishment
- Composition over inheritance
- Other useful principles
- Common mistakes and tips
- Exercises
- Conclusion
- DRY: don't repeat yourself
DRY (Don't Repeat Yourself) states that every piece of knowledge must have a single, unambiguous, authoritative representation within the system. The important nuance is that DRY is about knowledge, not about textual coincidences in code.
// DRY VIOLATION: the VAT rule (21%) is duplicated and scattered
public class Cart {
public double total(double base) { return base + base * 0.21; }
}
public class Invoice {
public double amount(double base) { return base + base * 0.21; }
}Explanation of the problem: the rule "VAT is 21%" lives in two places. If the tax rate changes, you have to find and modify all the copies, with the risk of forgetting one.
// CORRECTION: a single source of truth
public class TaxPolicy {
private static final double VAT = 0.21;
public double apply(double base) { return base + base * VAT; }
}
public class Cart {
private final TaxPolicy taxes;
public Cart(TaxPolicy taxes) { this.taxes = taxes; }
public double total(double base) { return taxes.apply(base); }
}Beware of false DRY: two code fragments may look identical today by coincidence but represent different pieces of knowledge that will evolve separately. Forcing their unification creates harmful coupling. The rule is: unify duplicated knowledge, not coincidental code.
- KISS: keep it simple
KISS (Keep It Simple, Stupid) recommends always choosing the simplest solution that solves the problem. Complexity must be justified by a real requirement.
// KISS VIOLATION: unnecessary complexity to check whether a number is even
public boolean isEven(int n) {
String binary = Integer.toBinaryString(n);
char last = binary.charAt(binary.length() - 1);
return last == '0';
}Explanation of the problem: it converts to binary, manipulates characters... all for something that has a trivial, direct expression.
KISS also applies to architecture: don't introduce microservices, message queues, or layers of abstraction if a well-organized modular monolith solves the problem. Accidental complexity is future debt.
- YAGNI: you aren't gonna need it
YAGNI (You Aren't Gonna Need It) warns against implementing functionality ahead of time "because someday it will be needed." Most of those predictions fail, and speculative code adds maintenance cost without providing value.
// YAGNI VIOLATION: speculative parameterization
public class CsvExporter {
// Nobody has asked for other separators, encodings, or compression
public String export(List<String> rows, char separator,
String encoding, boolean compress,
boolean includeHeader, String footer) {
// ... huge logic handling combinations that are never used
return "";
}
}Explanation of the problem: six parameters have been added for hypothetical scenarios. Each one has to be tested, documented, and maintained, even though only the comma is used.
// CORRECTION: solve only today's real case
public class CsvExporter {
public String export(List<String> rows) {
return String.join("\n", rows); // comma separator within each row
}
}If tomorrow another separator is needed, it is added then, with the real requirement in front of you. YAGNI is not an excuse for bad design; it is an invitation not to solve problems that do not yet exist.
- The tension among DRY, KISS, and YAGNI
These principles sometimes oppose one another, and the art lies in balancing them:
| Situation | Principle that pulls | Principle in tension | Decision criterion |
|---|---|---|---|
| I see repeated code | DRY (unify) | KISS/YAGNI (don't abstract yet) | Is it the same knowledge or a coincidence? |
| I want a generic abstraction | DRY | YAGNI | Are there 2-3 real cases or just one? |
| The design is becoming complex | KISS | DRY | Does simplicity justify some duplication? |
A widely cited practical heuristic is the rule of three: the first time you write the code; the second time something similar appears, you tolerate it; the third time, you abstract. This reconciles DRY with YAGNI: you wait for real evidence of a pattern before unifying it.
- The principle of least astonishment
The principle of least astonishment (Principle of Least Astonishment) says that a component should behave the way a reasonable user expects. The code should not hide surprising side effects or break conventions.
// VIOLATION: a getter with surprising side effects
public class Account {
private double balance;
public double getBalance() {
recordAccess(); // surprise: a getter that writes
balance -= 1; // surprise: querying charges a fee
return balance;
}
}Explanation of the problem: no one expects getBalance() to modify the state or charge fees. Whoever uses it will get a surprise that is hard to debug.
// CORRECTION: honest names and predictable behavior
public class Account {
private double balance;
public double getBalance() { return balance; } // query only
public double queryWithFee() { // explicit intent
balance -= 1;
return balance;
}
}Applied to APIs: respect naming conventions, return the expected types, throw the documented exceptions, and avoid hidden behaviors.
- Composition over inheritance
Inheritance creates strong, static coupling between classes. Composition —building objects by combining others— is usually more flexible.
// PROBLEMATIC: rigid inheritance and an explosion of subclasses
public class Bird {
public void fly() { /* ... */ }
}
public class Penguin extends Bird {
// penguins don't fly! Misapplied inheritance
public void fly() { throw new UnsupportedOperationException(); }
}Explanation of the problem: inheriting fly() forces the penguin to have a behavior that does not correspond to it. Moreover, combining capabilities (swims, flies, runs) through inheritance causes an explosion of subclasses.
// BETTER: compose behaviors
public interface Movement { void move(); }
public class Flight implements Movement {
public void move() { System.out.println("Flies"); }
}
public class Swim implements Movement {
public void move() { System.out.println("Swims"); }
}
public class Animal {
private final Movement movement; // composed, not inherited
public Animal(Movement movement) { this.movement = movement; }
public void move() { movement.move(); }
}
// Penguin = new Animal(new Swim()); Eagle = new Animal(new Flight());Explanation of the improvement: the behavior is injected as an object. It can be changed at runtime and combined freely. This is the basis of the Strategy pattern. The rule "favor composition over inheritance" comes from the design patterns book (GoF) and avoids fragile hierarchies.
| Aspect | Inheritance | Composition |
|---|---|---|
| Coupling | Strong, static | Weak, dynamic |
| Reuse | Limited to the hierarchy | Flexible and combinable |
| Change at runtime | No | Yes |
| Risk | Breaking LSP, deep hierarchies | More objects to orchestrate |
- Other useful principles
- Command-Query Separation (CQS): a method either changes state (command) or returns data (query), but not both. It reinforces least astonishment.
- Fail-fast: detect and report errors as early as possible (validate parameters on entry) instead of propagating invalid states.
- Encapsulation: hide the internal state and expose only behavior; the basis of low coupling.
- High cohesion / low coupling: already covered in the previous lesson, they are the backdrop of all these principles.
- Boy Scout Rule: leave the code a little cleaner than you found it; incremental maintenance that fights technical debt.
Common Mistakes and Tips
- Applying DRY dogmatically and creating premature abstractions that couple different pieces of knowledge. Remember: duplication is cheaper than the wrong abstraction.
- Confusing KISS with simplistic. Simple is not doing less than necessary; it is not doing more than necessary. A system that is too simplistic and does not cover the requirements is also a mistake.
- Using YAGNI as an excuse not to design. YAGNI rejects speculative functionality, not sound base design or reasonable extensibility.
- Inheriting to reuse lines of code. If the relationship is not a genuine "is-a," use composition.
- Surprises in names: a method called
validate()that also saves to the database violates least astonishment. Name things according to what they actually do. - Tip: use the rule of three as an arbiter between DRY and YAGNI. And measure: if an abstraction has only one real use, it is probably superfluous.
Exercises
Exercise 1 (DRY). Detect the duplication of knowledge and fix it:
class Employee { double bonus(double s) { return s * 0.1; } }
class Manager { double bonus(double s) { return s * 0.1; } }Exercise 2 (KISS/YAGNI). Simplify this code by applying KISS and YAGNI:
public String greet(String name, boolean formal, boolean uppercase,
String language, boolean withEmoji) {
String greeting = formal ? "Dear " : "Hi ";
greeting += name;
if (uppercase) greeting = greeting.toUpperCase();
if (withEmoji) greeting += " :)";
return greeting; // only greet(name) is used
}Exercise 3 (Composition). Convert this inheritance hierarchy into composition:
class Car { void start() {} }
class ElectricCar extends Car { void charge() {} }
class GasolineCar extends Car { void refuel() {} }Solutions
Solution 1. The bonus calculation is the same knowledge; we centralize it:
class BonusPolicy {
private static final double RATE = 0.1;
double calculate(double salary) { return salary * RATE; }
}
// Employee and Manager receive/use BonusPolicy instead of duplicating the rule.Solution 2. We remove the unused speculative parameters:
If the formal mode or languages are needed in the future, they are added with the real requirement.
Solution 3. We model the energy type as a component:
interface EnergySource { void recharge(); }
class Electric implements EnergySource { public void recharge() { /* charge */ } }
class Combustion implements EnergySource { public void recharge() { /* refuel */ } }
class Car {
private final EnergySource energy;
Car(EnergySource energy) { this.energy = energy; }
void start() {}
void recharge() { energy.recharge(); }
}
// new Car(new Electric()); new Car(new Combustion());Conclusion
DRY, KISS, and YAGNI form a trio that regulates the balance among rigor, simplicity, and pragmatism: don't repeat knowledge, keep things simple, and don't build more than needed. We have complemented them with the principle of least astonishment, which demands predictable behavior, and with the preference for composition over inheritance, which avoids fragile hierarchies. Applied with judgment —and arbitrated by heuristics such as the rule of three— these principles reduce accidental complexity. In the next lesson we will make the leap from class design to system design, studying the architectural tactics with which we achieve quality attributes such as availability, performance, or security.
Application Architecture Course
Module 1: Fundamentals of Application Architecture
- What Is Application Architecture?
- The Role of the Software Architect
- Quality Attributes and Non-Functional Requirements
- Architectural Decisions and Trade-offs
- Architecture Documentation: Views and the C4 Model
Module 2: Design Principles and Tactics
- Coupling, Cohesion and Separation of Concerns
- SOLID Principles Applied to Architecture
- DRY, KISS, YAGNI and Other Design Principles
- Architectural Tactics for Quality Attributes
- Managing Technical Debt
Module 3: Architectural Styles and Patterns
- Monolithic Architecture
- Layered Architecture (N-Tier)
- Client-Server Architecture
- Hexagonal Architecture (Ports and Adapters)
- Clean and Onion Architecture
Module 4: Distributed Architectures and Microservices
- Introduction to Distributed Systems
- Microservices Architecture
- Service Decomposition and Bounded Contexts
- API Gateway, Service Discovery and Inter-Service Communication
- Resilience Patterns: Circuit Breaker, Retry and Bulkhead
- The CAP Theorem and Data Consistency
Module 5: Event-Driven Architectures and Messaging
- Fundamentals of Event-Driven Architecture
- Asynchronous Messaging: Queues and Brokers
- Event Patterns: Event Sourcing and CQRS
- Managing Distributed Transactions: The Saga Pattern
- Real-Time Data Streaming
Module 6: Domain-Driven Design (DDD)
- Core DDD Concepts
- Strategic Design: Bounded Contexts and Ubiquitous Language
- Tactical Design: Entities, Aggregates and Repositories
- Context Mapping
Module 7: Data and Persistence
- Persistence Strategies: SQL vs NoSQL
- Data Access Patterns: Repository, Unit of Work and DAO
- Database per Service and Distributed Data Management
- Caching and Invalidation Strategies
Module 8: Cloud Architecture and Deployment
- Cloud Computing Fundamentals (IaaS, PaaS, SaaS)
- Containers and Orchestration with Docker and Kubernetes
- Serverless Architecture
- Cloud-Native Design Patterns
- Infrastructure as Code (IaC)
Module 9: Quality, Security and Observability
- Scalability: Horizontal vs Vertical and Load Balancing
- High Availability and Fault Tolerance
- Security by Design and Authentication/Authorization
- Observability: Logging, Metrics and Tracing
- Performance and Load Testing
