All notes
System Design Low Level Design

Software Design Principles: A Comprehensive Guide

9 min read Engineering notes

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:

  1. Itself.
  2. Its parameters.
  3. Objects it creates or instantiates.
  4. 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

PrincipleCategoryCore DirectivePrimary Benefit
DRYPragmaticSingle source of truth for business logicEliminates copy-paste bugs & redundancy
KISSPragmaticPrefer simple solutions over clever codeEnhances readability & debugging ease
YAGNIPragmaticBuild only what is needed nowPrevents wasted effort on unused features
SRPSOLIDOne reason to change per classHigh cohesion & focused modules
OCPSOLIDOpen for extension, closed for modificationAdd new features without breaking existing code
LSPSOLIDSubtypes must be substitutable for base typesPredictable polymorphic behavior
ISPSOLIDFine-grained, specific interfacesAvoids forcing dummy implementations
DIPSOLIDDepend on abstractions, not concretionsLoose coupling & easy mock testing
Law of DemeterStructuralTalk only to immediate friendsPrevents deep object coupling