Mastering the SOLID Principles: A Guide with Practical Java Examples

JavaOpsPro
JavaOpsPro
Published on Nov, 06 2025 3 min read 0 comments
image

In software engineering, writing code that is easy to maintain, extend, and understand is crucial. The SOLID principles are a set of five design guidelines that help developers achieve these goals by promoting clean, modular, and robust object-oriented design.

Let's explore each principle with clear Java examples.

1. Single Responsibility Principle (SRP)

Principle: A class should have only one reason to change, meaning it should have only one responsibility.

❌ Bad Example:

public class BankAccount {
    private double balance;

    public void deposit(double amount) { balance += amount; }
    public void withdraw(double amount) { balance -= amount; }

    // Violation: This class is also handling report generation
    public void generateStatement() {
        System.out.println("Statement generated for balance: " + balance);
    }
}

✅ Good Example:

public class BankAccount {
    private double balance;
    public void deposit(double amount) { balance += amount; }
    public void withdraw(double amount) { balance -= amount; }
    public double getBalance() { return balance; }
}

public class StatementService {
    public void generateStatement(BankAccount account) {
        System.out.println("Statement generated for balance: " + account.getBalance());
    }
}

Why it works: BankAccount manages account operations, while StatementService handles statements. Each class has a single, clear responsibility.

2. Open/Closed Principle (OCP)

Principle: Software entities should be open for extension but closed for modification.

❌ Bad Example:

public class InterestCalculator {
    public double calculateInterest(String accountType, double balance) {
        if (accountType.equals("SAVINGS")) {
            return balance * 0.04;
        } else if (accountType.equals("FIXED")) {
            return balance * 0.06;
        }
        return 0;
    }
}

Adding a new account type requires modifying this class.

✅ Good Example:

public interface InterestPolicy {
    double calculateInterest(double balance);
}

public class SavingsAccountInterest implements InterestPolicy {
    public double calculateInterest(double balance) { return balance * 0.04; }
}

public class FixedDepositInterest implements InterestPolicy {
    public double calculateInterest(double balance) { return balance * 0.06; }
}

Why it works: New account types can be added by creating new classes without altering existing code.

3. Liskov Substitution Principle (LSP)

Principle: Objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program.

❌ Bad Example:

public class Account {
    public void withdraw(double amount) {
        System.out.println("Withdrawn: " + amount);
    }
}

public class FixedDepositAccount extends Account {
    @Override
    public void withdraw(double amount) {
        throw new UnsupportedOperationException("Cannot withdraw from Fixed Deposit!");
    }
}

Using FixedDepositAccount in place of Account breaks the program.

✅ Good Example:

public abstract class Account {
    protected double balance;
    public abstract double getBalance();
}

public class SavingsAccount extends Account {
    public void withdraw(double amount) { balance -= amount; }
    public double getBalance() { return balance; }
}

public class FixedDepositAccount extends Account {
    public double getBalance() { return balance; }
}

Why it works: Both subclasses can be used interchangeably via the Account abstraction without unexpected errors.

4. Interface Segregation Principle (ISP)

Principle: Clients should not be forced to depend on interfaces they do not use.

❌ Bad Example:

public interface BankingOperations {
    void deposit(double amount);
    void withdraw(double amount);
    void applyForLoan(double amount);
}

public class LoanAccount implements BankingOperations {
    // Forced to implement irrelevant methods
    public void deposit(double amount) { /* Not applicable */ }
    public void withdraw(double amount) { /* Not applicable */ }
    public void applyForLoan(double amount) { /* logic */ }
}

✅ Good Example:

public interface DepositOperation {
    void deposit(double amount);
}

public interface WithdrawOperation {
    void withdraw(double amount);
}

public interface LoanOperation {
    void applyForLoan(double amount);
}

public class SavingsAccount implements DepositOperation, WithdrawOperation {
    public void deposit(double amount) { /* logic */ }
    public void withdraw(double amount) { /* logic */ }
}

public class LoanAccount implements LoanOperation {
    public void applyForLoan(double amount) { /* logic */ }
}

Why it works: Interfaces are small and specific, so classes only implement what they need.

5. Dependency Inversion Principle (DIP)

Principle: Depend on abstractions, not on concrete implementations.

❌ Bad Example:

@RestController
public class AccountController {
    // Tightly coupled to a concrete class
    private RegularAccountService accountService = new RegularAccountService();

    @GetMapping("/{id}")
    public Account getAccount(@PathVariable Long id) {
        return accountService.getAccountById(id);
    }
}

✅ Good Example:

public interface AccountService {
    Account getAccountById(Long id);
}

@Service
public class RegularAccountService implements AccountService {
    public Account getAccountById(Long id) { /* logic */ }
}

@RestController
public class AccountController {
    private final AccountService accountService;

    // Dependency is injected via interface
    public AccountController(AccountService accountService) {
        this.accountService = accountService;
    }
}

Why it works: The controller depends on the AccountService interface, making it easy to switch implementations without code changes.

Conclusion

The SOLID principles are foundational to writing clean, scalable, and maintainable object-oriented code. By applying:

  • SRP to keep classes focused,
  • OCP to enable easy extension,
  • LSP to ensure reliable inheritance,
  • ISP to create lean interfaces, and
  • DIP to decouple dependencies,

you can significantly improve the quality and longevity of your software systems.

 

0 Comments