SOLID Design Principles in Java: Interview Guide with Code Examples

High-Frequency Interview Questions

Question 1: Explain the five SOLID principles and their core ideas

Answer: SOLID represents the five fundamental principles of object-oriented design:

  • Single Responsibility Principle (SRP): A class should have only one reason to change. The goal is high cohesion—preventing a class from handling multiple unrelated responsibilities that could cause cascading changes when requirements shift.

  • Open-Closed Principle (OCP): Software entities should be open for extension but closed for modification. The key is depending on abstractions rather than concrete implementations. New features should be added through new subclasses or implementations, without touching stable existing code.

  • Liskov Substitution Principle (LSP): Subclasses must be substitutable for their base classes without altering program behavior. This ensures inheritance relationships are sound—subclasses cannot weaken method contracts or violate parent class invariants.

  • Interface Segregation Principle (ISP): Clients should not be forced to depend on interfaces they don't use. The solution is splitting bloated interfaces into focused, granular ones, preventing unrelated changes from affecting clients.

  • Dependency Inversion Principle (DIP): High-level modules should not depend on low-level modules; both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions. This enforces programming to interfaces and decouples modules.

Question 2: How do you implement the Open-Closed Principle in code?

Answer: The OCP centers on abstraction. Implementation involves three key aspects:

  1. Define a abstraction layer: Extract stable business logic into interfaces or abstract classes, encapsulating invariant behavior.

  2. Extend the abstraction: When adding features, create new classes that implement or extend the abstraction layer, leaving existing implementations untouched.

  3. Use dependency injection: High-level modules should interact through abstractions, never directly with concrete implementations.

Example: In a payment system, define a PaymentHandler interface. The existing AlipayHandler implements this interface. When adding WeChat Pay, simply create WechatPayHandler implementing PaymentHandler—no changes to existing payment logic required.

Question 3: What are the core requirements of the Liskov Substitution Principle? Provide an example violating LSP.

Answer: Core requirement: Any code using a parent type must work correctly when substituted with a child type. Specific constraints:

  • Subclasses cannot override non-abstract methods from the parent
  • Subclasses cannot strengthen method preconditions (if parent accepts Object, child cannot narrow to String)
  • Subclasses cannot weaken postconditions (if parent returns List, child cannot return a more restricted type)
  • Subclasses cannot throw checked exceptions not declared in the parent

Violating Example:

abstract class Animal {
    public abstract void move();
}

class Penguin extends Animal {
    @Override
    public void move() {
        throw new UnsupportedOperationException("Penguins cannot move through air");
    }
}

public class ZooFeeder {
    public static void transportAnimal(Animal animal) {
        animal.move();
    }
    
    public static void main(String[] args) {
        transportAnimal(new Penguin()); // Runtime exception—violates LSP
    }
}

Correct Approach: Introduce a FlightCapable interface. Only animals that can fly implement this interface, while penguins remain as standard Animal without flight capabilities.

Question 4: What's the difference between Composition/Aggregation Reuse Principle (CRP) and inheritance? Why prefer composition?

Aspect Inheritance (is-a) Composition/Aggregation (has-a)
Coupling Tight (parent changes force child changes) Loose (only dependency adjustments needed)
Reuse Flexibility Static (determined at compile time) Dynamic (can swap at runtime)
Code Bloat Prone to inheriting unnecessary methods No bloat—only needed dependencies

Reasons to prefer composition:

  1. No invasion: Subclasses inherit all public methods, even unwanted ones
  2. Lower coupling: Composition allows runtime dependency swapping; inheritance is fixed
  3. Supports OCP: Adding features requires only new collaborator classes, not modifying existing ones

Java Code Implementations

1. Single Responsibility Principle

Scenario: Separating user profile management from authentication logic.

class UserRepository {
    public void create(String username) {
        System.out.println("Creating user: " + username);
    }
    
    public void remove(String username) {
        System.out.println("Removing user: " + username);
    }
}

class UserAuthorization {
    public boolean validateAccess(String username) {
        System.out.println("Validating access for: " + username);
        return true;
    }
}

public class SRPDemo {
    public static void main(String[] args) {
        UserRepository repository = new UserRepository();
        UserAuthorization auth = new UserAuthorization();
        
        repository.create("alice");
        auth.validateAccess("alice");
    }
}

2. Open-Closed Principle

Scenario: Adding new notification channels without modifying existing code.

interface MessageSender {
    void send(double amount);
}

class EmailSender implements MessageSender {
    @Override
    public void send(double amount) {
        System.out.println("Email notification: $" + amount);
    }
}

class SmsSender implements MessageSender {
    @Override
    public void send(double amount) {
        System.out.println("SMS notification: $" + amount);
    }
}

public class OCPDemo {
    public static void main(String[] args) {
        MessageSender email = new EmailSender();
        MessageSender sms = new SmsSender();
        
        email.send(150.00);
        sms.send(200.00);
    }
}

3. Liskov Substitution Principle

Scenario: Shape hierarchy where all subclasses can substitute the parent.

abstract class GeometricFigure {
    public abstract double computeArea();
}

class Triangle extends GeometricFigure {
    private double base;
    private double height;
    
    public Triangle(double base, double height) {
        this.base = base;
        this.height = height;
    }
    
    @Override
    public double computeArea() {
        return 0.5 * base * height;
    }
}

class Square extends GeometricFigure {
    private double side;
    
    public Square(double side) {
        this.side = side;
    }
    
    @Override
    public double computeArea() {
        return side * side;
    }
}

public class LSPDemo {
    public static void displayArea(GeometricFigure figure) {
        System.out.println("Area: " + figure.computeArea());
    }
    
    public static void main(String[] args) {
        displayArea(new Triangle(6, 4));
        displayArea(new Square(5));
    }
}

4. Interface Segregation Principle

Scenario: Breaking down vehicle capabilities into focused interfaces.

interface Drivable {
    void drive();
}

interface Flyable {
    void fly();
}

interface Floating {
    void floatOnWater();
}

class AmphibiousVehicle implements Drivable, Floating {
    @Override
    public void drive() { System.out.println("Driving on road"); }
    @Override
    public void floatOnWater() { System.out.println("Floating on water"); }
}

class Seaplane implements Drivable, Flyable, Floating {
    @Override
    public void drive() { System.out.println("Taxiing on water"); }
    @Override
    public void fly() { System.out.println("Flying through air"); }
    @Override
    public void floatOnWater() { System.out.println("Floating on water"); }
}

public class ISPDemo {
    public static void main(String[] args) {
        AmphibiousVehicle tank = new AmphibiousVehicle();
        Seaplane craft = new Seaplane();
        
        tank.drive();
        tank.floatOnWater();
        
        craft.drive();
        craft.fly();
    }
}

5. Dependency Inversion Principle

Scenario: Business logic depending on abstractions rather than concrete implementations.

interface PersistenceLayer {
    void persist(String record);
}

class PostgreSqlLayer implements PersistenceLayer {
    @Override
    public void persist(String record) {
        System.out.println("Writing to PostgreSQL: " + record);
    }
}

class MongoDbLayer implements PersistenceLayer {
    @Override
    public void persist(String record) {
        System.out.println("Writing to MongoDB: " + record);
    }
}

class ShoppingCart {
    private PersistenceLayer storage;
    
    public ShoppingCart(PersistenceLayer storage) {
        this.storage = storage;
    }
    
    public void checkout(String itemId) {
        storage.persist("Checkout: " + itemId);
    }
}

public class DIPDemo {
    public static void main(String[] args) {
        ShoppingCart cart1 = new ShoppingCart(new PostgreSqlLayer());
        cart1.checkout("PROD-100");
        
        ShoppingCart cart2 = new ShoppingCart(new MongoDbLayer());
        cart2.checkout("PROD-200");
    }
}

6. Law of Demeter

Scenario: Reducing indirect dependencies by encapsulating nested calls.

Violating Example:

class Department {
    private Employee manager;
    public Employee getManager() { return manager; }
}

class Employee {
    private ContactInfo contact;
    public ContactInfo getContactInfo() { return contact; }
}

class ContactInfo {
    private String phoneNumber;
    public String getPhone() { return phoneNumber; }
}

class OrganizationReport {
    private Department dept;
    
    public String fetchManagerPhone() {
        return dept.getManager().getContactInfo().getPhone();
    }
}

Compliant Example:

class Department {
    private Employee manager;
    
    public String retrieveManagerPhone() {
        return manager.getContactInfo().getPhone();
    }
}

class OrganizationReport {
    private Department dept;
    
    public String fetchManagerPhone() {
        return dept.retrieveManagerPhone();
    }
}

7. Composition/Aggregation Reuse Principle

Scenario: Building vehicles by composing smaller components instead of inheriting.

class CombustionEngine {
    public void ignite() {
        System.out.println("Engine started");
    }
}

class RubberTire {
    public void spin() {
        System.out.println("Tires rotating");
    }
}

class Truck {
    private CombustionEngine engine = new CombustionEngine();
    private RubberTire tires = new RubberTire();
    
    public void transport() {
        engine.ignite();
        tires.spin();
        System.out.println("Truck delivering cargo");
    }
}

class LogisticsCompany {
    private Truck truck = new Truck();
    
    public void moveFreight() {
        truck.transport();
        System.out.println("Package delivered");
    }
}

public class CRPDemo {
    public static void main(String[] args) {
        LogisticsCompany logistics = new LogisticsCompany();
        logistics.moveFreight();
    }
}

Key Takeaways

  1. Prioritize SOLID principles: SRP ensures focused classes, OCP enables extensibility, LSP validates inheritance, ISP reduces interface bloat, and DIP promotes abstraction.

  2. Favor composition over inheritance: Composition provides flexibility, runtime substitutability, and looser coupling.

  3. Apply principles pragmatically: Balance design elegance with implementation complexity. Small utilities may not require elaborate interfaces, while frequently changed modules benefit from strict adherence.

Tags: SOLID Design Patterns java Object-Oriented Design Interview Preparation

Posted on Tue, 01 Sep 2026 16:14:10 +0000 by jamjam1