Mastering SOLID Principles in Java: Scenario-Based Questions & Answers

Mastering SOLID Principles in Java: Scenario-Based Questions & Answers

Writing clean, maintainable, and extensible code is every developer’s goal. The SOLID principles — five core design principles for Object-Oriented Programming — are powerful tools to achieve this. In this article, we’ll explore scenario-based SOLID Principle interview questions to test and demonstrate real-world understanding, especially for Java developers preparing for interviews.

🎯 What are SOLID Principles?

Before diving into scenarios, let’s quickly revisit what SOLID stands for:

  • S — Single Responsibility Principle (SRP)
  • O — Open/Closed Principle (OCP)
  • L — Liskov Substitution Principle (LSP)
  • I — Interface Segregation Principle (ISP)
  • D — Dependency Inversion Principle (DIP)

For more 👇

Scenario-Based Interview Questions & Answers

1. Single Responsibility Principle (SRP)

📌 Scenario: You have a UserService class that handles user registration, sending welcome emails, and writing user logs to a file.

public class UserService {
    public void registerUser(User user) { ... }
    public void sendEmail(User user) { ... }
    public void writeLog(User user) { ... }
}        

💬 Interviewer Question: What’s wrong with this design, and how can you improve it?

Answer: The UserService class violates the Single Responsibility Principle. It handles three responsibilities: business logic, email communication, and logging. According to SRP, each class should have only one reason to change.

Refactor:

public class UserService {
    private EmailService emailService;
    private LoggingService loggingService;
public void registerUser(User user) {
        // registration logic
        emailService.sendEmail(user);
        loggingService.log(user);
    }
}        

Now each class focuses on a single concern.

2. Open/Closed Principle (OCP)

📌 Scenario: You have a payment processing class that supports multiple payment types.

public class PaymentProcessor {
    public void processPayment(String type) {
        if (type.equals("CreditCard")) { ... }
        else if (type.equals("PayPal")) { ... }
        else if (type.equals("UPI")) { ... }
    }
}        

💬 Interviewer Question: How does this violate OCP and how would you redesign it?

Answer: This class is open for modification whenever we introduce a new payment type. This violates the Open/Closed Principle, which states that classes should be open for extension, but closed for modification.

Refactor with Strategy Pattern:

public interface PaymentMethod {
    void pay();
}
public class CreditCardPayment implements PaymentMethod {
    public void pay() { /* logic */ }
}
public class PayPalPayment implements PaymentMethod {
    public void pay() { /* logic */ }
}
public class PaymentProcessor {
    public void process(PaymentMethod method) {
        method.pay();
    }
}        

Now, new payment methods can be added without modifying existing code.

3. Liskov Substitution Principle (LSP)

📌 Scenario: Consider the following inheritance:

public class Bird {
    public void fly() {
        System.out.println("Flying...");
    }
}

public class Ostrich extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("Ostriches can't fly!");
    }
}        

💬 Interviewer Question: What’s the issue here with respect to LSP?

Answer: The Liskov Substitution Principle requires that subclasses must be substitutable for their base classes without breaking the behavior. Here, substituting Bird with Ostrich violates this, because Ostrich cannot fly and throws an exception.

Fix using Composition:

public interface Bird {
    void eat();
}

public interface Flyable {
    void fly();
}
public class Sparrow implements Bird, Flyable {
    public void eat() { }
    public void fly() { }
}
public class Ostrich implements Bird {
    public void eat() { }
}        

Separate behaviors help avoid invalid assumptions.

4. Interface Segregation Principle (ISP)

📌 Scenario: You have an interface:

public interface Machine {
    void print();
    void scan();
    void fax();
}        

But not all classes use all methods:

public class PrinterOnly implements Machine {
    public void print() { }
    public void scan() {
        throw new UnsupportedOperationException();
    }
    public void fax() {
        throw new UnsupportedOperationException();
    }
}        

💬 Interviewer Question: How does this design violate ISP?

Answer: Interface Segregation Principle says: don’t force clients to depend on interfaces they don’t use. Here, PrinterOnly is forced to implement scan() and fax(), which it doesn’t support.

Refactor:

public interface Printer {
    void print();
}

public interface Scanner {
    void scan();
}
public interface Fax {
    void fax();
}
public class PrinterOnly implements Printer {
    public void print() { }
}        

Now, classes implement only what they need.

5. Dependency Inversion Principle (DIP)

📌 Scenario: You’re building a reporting module:

public class ReportService {
    private PdfReportGenerator pdfReportGenerator = new PdfReportGenerator();

public void generate() {
        pdfReportGenerator.generatePdf();
    }
}        

💬 Interviewer Question: What’s the violation here and how can you apply DIP?

Answer: This design tightly couples ReportService to a concrete class. According to the Dependency Inversion Principle, high-level modules should not depend on low-level modules. Both should depend on abstractions.

Refactor:

public interface ReportGenerator {
    void generate();
}

public class PdfReportGenerator implements ReportGenerator {
    public void generate() { /* generate PDF */ }
}
public class ReportService {
    private ReportGenerator generator;
    public ReportService(ReportGenerator generator) {
        this.generator = generator;
    }
    public void generateReport() {
        generator.generate();
    }
}        

Now, ReportService depends on an interface, not a concrete class.

Bonus: Combined Scenario

📌 Scenario: You’re designing a Notification System for a large application that can notify users via Email, SMS, or Push Notification.

💬 Interviewer Question: How would you design this system following all SOLID principles?

Answer:

  1. SRP: Separate classes for each notification channel.
  2. OCP: Add new notification types without modifying existing logic.
  3. LSP: Any notifier should be safely replaceable.
  4. ISP: If a client only needs Email, don’t force it to use SMS/Firebase logic.
  5. DIP: Use interfaces and inject dependencies.

Sample Design:

public interface Notifier {
    void notifyUser(String message);
}

public class EmailNotifier implements Notifier {
    public void notifyUser(String message) { /* send email */ }
}
public class SMSNotifier implements Notifier {
    public void notifyUser(String message) { /* send SMS */ }
}
public class NotificationService {
    private final Notifier notifier;
    public NotificationService(Notifier notifier) {
        this.notifier = notifier;
    }
    public void send(String msg) {
        notifier.notifyUser(msg);
    }
}        

This design fully respects the SOLID principles and is highly extensible, testable, and maintainable.

🔚 Final Thoughts

Understanding SOLID principles in theory is one thing, but being able to apply them in real-world scenarios is what sets a great developer apart. These scenario-based questions help you articulate your design thinking and show your problem-solving skills during interviews.

Best Resources to Learn SOLID Principles:

  • Clean Code by Robert C. Martin
  • Design Patterns in Java by O’Reilly
  • Spring & SOLID courses on Udemy / Pluralsight
  • Blogs from Baeldung, GeeksForGeeks, AKCoding.com

Let me know if you’d like a downloadable PDF version or visual UML diagrams for these examples in the comments!

To view or add a comment, sign in

More articles by Akshay Kumar

Insights from the community

Others also viewed

Explore topics