Software Design Principles: A Comprehensive Guide
Software design principles are foundational guidelines that help software engineers build systems that are easy to understand, maintain, test, and extend. These principles apply across both high-level system architecture and low-level code design (LLD).
1. Pragmatic Development Principles
1.1 DRY: Don't Repeat Yourself
"Every piece of knowledge must have a single, unambiguous, authoritative representation within a system." — Andy Hunt & Dave Thomas (The Pragmatic Programmer)
Avoid duplicating business logic or code across multiple locations. Duplication leads to maintenance nightmares, where updating a business rule requires remembering to change code in multiple places.
❌ Bad Example (Violating DRY)
public class Main {
public static void main(String[] args) {
int length1 = 10, width1 = 5;
int area1 = length1 * width1;
System.out.println("Area1: " + area1);
int length2 = 8, width2 = 4;
int area2 = length2 * width2;
System.out.println("Area2: " + area2);
}
}
✅ Good Example (Applying DRY)
class AreaCalculator {
public static int calculateArea(int length, int width) {
return length * width;
}
}
public class Main {
public static void main(String[] args) {
int area1 = AreaCalculator.calculateArea(10, 5);
int area2 = AreaCalculator.calculateArea(8, 4);
System.out.println("Area1: " + area1);
System.out.println("Area2: " + area2);
}
}
When NOT to Apply DRY
- Coincidental Duplication: Two pieces of code look identical now, but represent separate domain concepts that will evolve independently.
- Performance-Critical Inner Loops: Where function calls or indirection overhead prevent compiler optimizations.
- Sacrificing Readability: When forcing shared code requires passing multiple confusing boolean flag arguments.
1.2 KISS: Keep It Simple, Stupid
"Simplicity is a prerequisite for reliability." — Edsger W. Dijkstra
Prefer the simplest implementation that correctly solves the problem. Avoid premature abstractions, clever tricks, or unnecessary complexity.
❌ Bad Example (Over-Engineered)
public class NumberUtils {
public static boolean isEven(int number) {
boolean isEven = false;
if (number % 2 == 0) {
isEven = true;
} else {
isEven = false;
}
return isEven;
}
}
✅ Good Example (KISS Principle)
public class NumberUtils {
public static boolean isEven(int number) {
return number % 2 == 0;
}
}
1.3 YAGNI: You Aren't Gonna Need It
"Always implement things when you actually need them, never when you just foresee that you need them." — Extreme Programming (XP)
Do not add functionality until it is strictly necessary. Speculative feature building wastes engineering hours, increases code surface area, and introduces unneeded complexity.
Practical Example
- YAGNI Approach: Build standard CRUD endpoints for a note-taking application.
- Violating YAGNI: Preemptively building multi-tenant organization hierarchies, cloud sync engines, and markdown plugins before validating basic core user adoption.
2. SOLID Principles (The 5 Pillars of OOD)
The SOLID principles, introduced by Robert C. Martin ("Uncle Bob"), represent five fundamental design principles for object-oriented software development.
┌─────────────────────────────────────────────────────────┐
│ SOLID Principles │
├───────┬─────────────────────────────────────────────────┤
│ S │ Single Responsibility Principle │
│ O │ Open/Closed Principle │
│ L │ Liskov Substitution Principle │
│ I │ Interface Segregation Principle │
│ D │ Dependency Inversion Principle │
└───────┴─────────────────────────────────────────────────┘
2.1 Single Responsibility Principle (SRP)
"A class should have one, and only one, reason to change."
A class should be responsible for a single part of the functionality provided by the software.
❌ Bad Example (Violating SRP)
// Violates SRP: Handles order logic, database persistence, AND notification email sending
public class OrderManager {
public void processOrder() { /* ... */ }
public void saveToDatabase() { /* ... */ }
public void sendReceiptEmail() { /* ... */ }
}
✅ Good Example (Applying SRP)
public class OrderProcessor {
public void processOrder() { /* Business logic */ }
}
public class OrderRepository {
public void saveToDatabase() { /* Database logic */ }
}
public class EmailNotificationService {
public void sendReceiptEmail() { /* Email logic */ }
}
2.2 Open/Closed Principle (OCP)
"Software entities (classes, modules, functions) should be open for extension, but closed for modification."
You should be able to extend a class's behavior without modifying its existing source code.
❌ Bad Example (Violating OCP)
// Violates OCP: Adding a new payment type requires modifying this class
public class PaymentProcessor {
public void processPayment(String type) {
if ("CREDIT".equalsIgnoreCase(type)) {
// Process credit card
} else if ("UPI".equalsIgnoreCase(type)) {
// Process UPI
}
// Adding PAYPAL requires modifying this method!
}
}
✅ Good Example (Applying OCP with Strategy/Polymorphism)
public interface PaymentMethod {
void pay();
}
public class CreditCardPayment implements PaymentMethod {
@Override
public void pay() { /* Credit card logic */ }
}
public class UPIPayment implements PaymentMethod {
@Override
public void pay() { /* UPI logic */ }
}
// Adding PaypalPayment requires ZERO changes to existing payment classes!
public class PaypalPayment implements PaymentMethod {
@Override
public void pay() { /* PayPal logic */ }
}
2.3 Liskov Substitution Principle (LSP)
"Subtypes must be substitutable for their base types without altering the correctness of the program."
Subclasses must satisfy all contracts guaranteed by the superclass.
❌ Bad Example (Violating LSP - Classic Square/Rectangle Problem)
public class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) { this.width = width; }
public void setHeight(int height) { this.height = height; }
public int getArea() { return width * height; }
}
public class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width; // Unexpected side effect! Alters height as well
}
@Override
public void setHeight(int height) {
this.width = height;
this.height = height;
}
}
// Passing Square to a function expecting Rectangle breaks logic when setting width/height independently!
✅ Good Example (Applying LSP)
public interface Shape {
int getArea();
}
public class Rectangle implements Shape {
private final int width;
private final int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
@Override
public int getArea() { return width * height; }
}
public class Square implements Shape {
private final int side;
public Square(int side) {
this.side = side;
}
@Override
public int getArea() { return side * side; }
}
2.4 Interface Segregation Principle (ISP)
"Clients should not be forced to depend upon interfaces that they do not use."
Prefer smaller, specialized interfaces over large, bloated "god" interfaces.
❌ Bad Example (Violating ISP)
public interface MultiFunctionDevice {
void print();
void scan();
void fax();
}
// SimplePrinter is forced to implement unused scan() and fax() methods!
public class SimplePrinter implements MultiFunctionDevice {
@Override
public void print() { /* Printing logic */ }
@Override
public void scan() { throw new UnsupportedOperationException("Scan not supported"); }
@Override
public void fax() { throw new UnsupportedOperationException("Fax not supported"); }
}
✅ Good Example (Applying ISP)
public interface Printer {
void print();
}
public interface Scanner {
void scan();
}
public class SimplePrinter implements Printer {
@Override
public void print() { /* Printing logic */ }
}
public class AdvancedMachine implements Printer, Scanner {
@Override
public void print() { /* Printing logic */ }
@Override
public void scan() { /* Scanning logic */ }
}
2.5 Dependency Inversion Principle (DIP)
"High-level modules should not depend on low-level modules. Both should depend on abstractions."
Rely on interfaces or abstract classes rather than concrete implementations to achieve loose coupling.
❌ Bad Example (Violating DIP)
public class MySQLDatabase {
public void connect() { /* Connect to MySQL */ }
}
// High-level AppController is tightly coupled to concrete MySQLDatabase
public class AppController {
private MySQLDatabase db = new MySQLDatabase();
public void run() {
db.connect();
}
}
✅ Good Example (Applying DIP)
public interface DatabaseConnection {
void connect();
}
public class MySQLDatabase implements DatabaseConnection {
@Override
public void connect() { /* MySQL connection logic */ }
}
public class PostgreSQLDatabase implements DatabaseConnection {
@Override
public void connect() { /* Postgres connection logic */ }
}
public class AppController {
private final DatabaseConnection db;
// Dependency Injection via constructor
public AppController(DatabaseConnection db) {
this.db = db;
}
public void run() {
db.connect();
}
}
3. Structural & Coupling Principles
3.1 High Cohesion & Low Coupling
- High Cohesion: A module's elements should belong together. All methods inside a class should closely support a single purpose.
- Low Coupling: Modules should have minimal knowledge of and reliance on other modules.
High Coupling (Bad) Low Coupling (Good)
┌───┐ ◄──────────► ┌───┐ ┌───┐ ┌───┐
│ A │ ───────────► │ B │ │ A │ ───► [Interface] ◄─── │ B │
└───┘ ◄─────────── └───┘ └───┘ └───┘
3.2 Composition Over Inheritance
Prefer has-a relationships (composition/delegation) over is-a relationships (inheritance). Inheritance introduces rigid compile-time dependencies, whereas composition enables dynamic runtime configuration.
// Using Composition (Flexible)
public interface Engine {
void start();
}
public class ElectricEngine implements Engine {
@Override
public void start() {
System.out.println("Silent electric engine start");
}
}
public class Car {
private final Engine engine; // Car HAS-A Engine
public Car(Engine engine) {
this.engine = engine;
}
public void startCar() {
engine.start();
}
}
3.3 Law of Demeter (Principle of Least Knowledge)
A method of an object should only invoke methods of:
- Itself.
- Its parameters.
- Objects it creates or instantiates.
- Its direct component fields.
Avoid "train wrecks" (long method chains across multiple objects):
// ❌ Bad (Violates Law of Demeter)
String zipCode = user.getAccount().getProfile().getAddress().getZipCode();
// ✅ Good (Encapsulated Navigation)
String zipCode = user.getZipCode();
4. Master Principles Comparison Table
| Principle | Category | Core Directive | Primary Benefit |
|---|---|---|---|
| DRY | Pragmatic | Single source of truth for business logic | Eliminates copy-paste bugs & redundancy |
| KISS | Pragmatic | Prefer simple solutions over clever code | Enhances readability & debugging ease |
| YAGNI | Pragmatic | Build only what is needed now | Prevents wasted effort on unused features |
| SRP | SOLID | One reason to change per class | High cohesion & focused modules |
| OCP | SOLID | Open for extension, closed for modification | Add new features without breaking existing code |
| LSP | SOLID | Subtypes must be substitutable for base types | Predictable polymorphic behavior |
| ISP | SOLID | Fine-grained, specific interfaces | Avoids forcing dummy implementations |
| DIP | SOLID | Depend on abstractions, not concretions | Loose coupling & easy mock testing |
| Law of Demeter | Structural | Talk only to immediate friends | Prevents deep object coupling |