Skip to main content

Design Principles

Principles are heuristics, not laws. Know them deeply enough to know when to break them โ€” and be able to explain why.


SOLID Principlesโ€‹

SOLID is an acronym for five principles that make object-oriented designs more maintainable, flexible, and testable.


S โ€” Single Responsibility Principle (SRP)โ€‹

A class should have only one reason to change.

"Reason to change" = stakeholder/actor whose requirements drive that change.

// โŒ Violates SRP: three different reasons to change
public class Employee {
public double calculatePay() { /* Finance team owns this */ }
public String generateReport() { /* HR team owns this */ }
public void saveToDatabase() { /* DBA owns this */ }
}

// โœ… Three separate classes, each with one responsibility
public class PayrollCalculator {
public double calculatePay(Employee employee) {
return employee.getHoursWorked() * employee.getHourlyRate();
}
}

public class EmployeeReporter {
public String generateReport(Employee employee) {
return String.format("Employee: %s, Dept: %s", employee.getName(), employee.getDept());
}
}

public class EmployeeRepository {
private final DataSource dataSource;
public void save(Employee employee) { /* JDBC logic */ }
public Optional<Employee> findById(long id) { /* JDBC logic */ }
}
Interview Tip ๐ŸŽฏ

SRP is the first thing interviewers look for. If they see you put calculatePrice(), sendEmail(), and saveToDb() in one class, it's an immediate red flag. Split responsibilities early.


O โ€” Open/Closed Principle (OCP)โ€‹

Open for extension, closed for modification.

Add new behavior by adding new code โ€” not by changing existing, tested code.

// โŒ Every new discount type requires modifying this class
public class PriceCalculator {
public double calculate(Order order, String discountType) {
double price = order.getBasePrice();
if ("SEASONAL".equals(discountType)) {
price *= 0.9;
} else if ("LOYALTY".equals(discountType)) {
price *= 0.85;
} else if ("EMPLOYEE".equals(discountType)) { // new requirement โ†’ modify class
price *= 0.7;
}
return price;
}
}

// โœ… Add new discount types without touching PriceCalculator
@FunctionalInterface
public interface DiscountStrategy {
double apply(double basePrice);
}

public enum Discounts implements DiscountStrategy {
SEASONAL (p -> p * 0.90),
LOYALTY (p -> p * 0.85),
EMPLOYEE (p -> p * 0.70), // new โ€” no existing code modified
VIP (p -> p * 0.60); // new โ€” no existing code modified

private final DiscountStrategy strategy;
Discounts(DiscountStrategy s) { this.strategy = s; }

@Override
public double apply(double basePrice) { return strategy.apply(basePrice); }
}

public class PriceCalculator {
public double calculate(Order order, DiscountStrategy discount) {
return discount.apply(order.getBasePrice()); // closed for modification
}
}
Senior Deep Dive ๐Ÿ”ด

OCP doesn't mean never modify existing code. It means that for a given axis of variation (e.g., discount types), you should be able to add new variants without touching the core logic. Identify the axes of variation in your design and apply OCP there.


L โ€” Liskov Substitution Principle (LSP)โ€‹

Subtypes must be substitutable for their base types without breaking the program.

If S extends T, then anywhere a T is used, an S must work correctly โ€” with the same pre/postconditions.

// โŒ Classic LSP violation: ReadOnlyList "is-a" List? Behaviorally no.
public class ReadOnlyList<E> extends ArrayList<E> {
@Override
public boolean add(E e) {
throw new UnsupportedOperationException("Read-only!"); // breaks caller expectations
}
}

// Code that works with List breaks with ReadOnlyList:
void addItem(List<String> list) {
list.add("hello"); // throws if ReadOnlyList is passed โ€” LSP violation!
}
// โœ… Model the hierarchy correctly
public interface ReadableList<E> {
E get(int index);
int size();
boolean contains(Object o);
}

public interface MutableList<E> extends ReadableList<E> {
boolean add(E e);
E remove(int index);
}

// Now ReadOnlyList implements ReadableList โ€” callers can't call add() on it
public class ReadOnlyList<E> implements ReadableList<E> { ... }
public class StandardList<E> implements MutableList<E> { ... }

LSP Rules for subclasses:

  • Don't strengthen preconditions (don't demand more from callers)
  • Don't weaken postconditions (don't deliver less to callers)
  • Don't throw new checked exceptions not declared in the parent

I โ€” Interface Segregation Principle (ISP)โ€‹

Clients should not be forced to depend on interfaces they don't use.

Fat interfaces create tight coupling โ€” split them by client need.

// โŒ One fat interface forces implementors to implement irrelevant methods
public interface Animal {
void eat();
void sleep();
void fly(); // Dogs have to implement this?!
void swim(); // Eagles have to implement this?!
void run();
}

public class Dog implements Animal {
@Override public void eat() { System.out.println("Nom nom"); }
@Override public void sleep() { System.out.println("Zzz"); }
@Override public void fly() { throw new UnsupportedOperationException(); } // ๐Ÿคข
@Override public void swim() { System.out.println("Splashing"); }
@Override public void run() { System.out.println("Running"); }
}
// โœ… Segregated interfaces โ€” implement only what makes sense
public interface Eatable { void eat(); }
public interface Sleepable { void sleep(); }
public interface Flyable { void fly(); }
public interface Swimmable { void swim(); }
public interface Runnable { void run(); }

public class Dog implements Eatable, Sleepable, Swimmable, Runnable {
@Override public void eat() { System.out.println("Nom nom"); }
@Override public void sleep() { System.out.println("Zzz"); }
@Override public void swim() { System.out.println("Splashing"); }
@Override public void run() { System.out.println("Running"); }
// No fly() โ€” doesn't need it!
}

public class Eagle implements Eatable, Sleepable, Flyable {
@Override public void eat() { System.out.println("Hunting"); }
@Override public void sleep() { System.out.println("Zzz"); }
@Override public void fly() { System.out.println("Soaring"); }
}
Interview Tip ๐ŸŽฏ

When designing interfaces in an LLD interview, ask yourself: "Is there a client that would use every method in this interface?" If not, it's a candidate for splitting.


D โ€” Dependency Inversion Principle (DIP)โ€‹

Depend on abstractions, not concretions. High-level modules should not depend on low-level modules.

// โŒ High-level OrderService depends on low-level MySQLOrderRepository
public class OrderService {
private MySQLOrderRepository repository = new MySQLOrderRepository(); // concrete!
private SmtpEmailSender emailSender = new SmtpEmailSender(); // concrete!

public void placeOrder(Order order) {
repository.save(order);
emailSender.send(order.getCustomer().getEmail(), "Order confirmed!");
}
}
// Now switching from MySQL to PostgreSQL requires changing OrderService!
// โœ… OrderService depends on abstractions โ€” injected via constructor
public interface OrderRepository {
void save(Order order);
Optional<Order> findById(String id);
}

public interface EmailSender {
void send(String to, String subject, String body);
}

public class OrderService {
private final OrderRepository repository; // abstraction
private final EmailSender emailSender; // abstraction

// Dependencies are INJECTED โ€” OrderService doesn't create them
public OrderService(OrderRepository repository, EmailSender emailSender) {
this.repository = repository;
this.emailSender = emailSender;
}

public void placeOrder(Order order) {
repository.save(order);
emailSender.send(order.getCustomerEmail(), "Order Confirmed", buildBody(order));
}
}

// Wiring (in your main / DI container):
OrderRepository repo = new PostgreSQLOrderRepository(dataSource);
EmailSender mailer = new SmtpEmailSender(smtpConfig);
OrderService service = new OrderService(repo, mailer);
// Swap PostgreSQL for Mongo? Change one line here, OrderService unchanged.
Senior Deep Dive ๐Ÿ”ด

DIP is why dependency injection frameworks (Spring, Guice) exist. In an interview without a framework, demonstrate DIP manually through constructor injection. Avoid new inside business logic classes โ€” say: "I'd inject this dependency through the constructor to keep this class testable and decoupled."


Additional Principlesโ€‹

DRY โ€” Don't Repeat Yourselfโ€‹

Every piece of knowledge should have a single, authoritative representation.

// โŒ DRY violation: magic number 0.08 repeated everywhere
double tax1 = price1 * 0.08;
double tax2 = price2 * 0.08;
double tax3 = price3 * 0.08;

// โœ… Single source of truth
public class TaxCalculator {
private static final double TAX_RATE = 0.08; // one place to change

public double calculate(double price) {
return price * TAX_RATE;
}
}

KISS โ€” Keep It Simple, Stupidโ€‹

The simplest solution that works is usually the best. Don't over-engineer.

// โŒ Over-engineered for a simple task
public class StringReverser {
private final StringReversingAlgorithmFactory factory;
private final StringReversingStrategy strategy;
// ... 50 lines of code

public String reverse(String s) {
return strategy.reverse(s);
}
}

// โœ… KISS
public String reverse(String s) {
return new StringBuilder(s).reverse().toString();
}

YAGNI โ€” You Aren't Gonna Need Itโ€‹

Don't implement features until they're actually needed.

Interview Tip ๐ŸŽฏ

When an interviewer asks "what if we need to support X in the future?", a great answer is: "I'd design the interface so it's easy to add X without breaking existing code, but I wouldn't implement X now since it's not in the current requirements. This keeps our codebase lean and the design focused."


SOLID Quick Referenceโ€‹

PrincipleViolation SignalFix
SRPClass has multiple import groups from different domainsExtract classes by actor/responsibility
OCPif/switch on type tag that grows over timeReplace with polymorphism / Strategy pattern
LSPinstanceof checks, UnsupportedOperationException in overridesRedesign the hierarchy; use composition
ISPImplementing an interface with throw new UnsupportedOperationException()Split the fat interface
DIPnew ConcreteClass() inside business logicConstructor inject an interface

Next โ†’ Design Patterns Overview