Back to SOLID Principles
Q1Question 1 of 5

Each SOLID principle with violations and fixes, the SRP–DIP clash, and SOLID over-engineering

Key Concepts
  • SRP violation: a class that validates, transfers, emails, and logs changes for four reasons – split into focused services.
  • OCP violation: a growing if-else chain on account type – replace with an InterestStrategy interface and one class per type.
  • LSP violation: a SavingsAccount subclass that throws on withdraw breaks callers written for Account – redesign the hierarchy.
  • ISP violation: a fat BankingOperations interface forces a savings account to stub out stock trading – split into role interfaces.
  • DIP violation: a LoanService that does new CibilHttpClient() is welded to one vendor – depend on a CreditScoreProvider interface.
  • SRP and DIP can clash: aggressive splitting creates many tiny classes needing interfaces between every pair, so only abstract where you expect variation.
  • SOLID can over-engineer: an interface for a class with one implementation adds indirection for no benefit – apply principles when real pressure appears.

Definition

SOLID is five rules: one job per class, extend without modifying, subtypes must substitute, small focused interfaces, and depend on abstractions not concretions.
  • SOLID is a set of five object-oriented design principles that produce code easier to maintain, extend, and test.
  • S – Single Responsibility Principle (SRP) – a class should have only one reason to change.
  • O – Open/Closed Principle (OCP) – software should be open for extension but closed for modification.
  • L – Liskov Substitution Principle (LSP) – objects of a subtype must be usable anywhere the supertype is expected without breaking correctness.
  • I – Interface Segregation Principle (ISP) – no client should be forced to depend on methods it does not use.
  • D – Dependency Inversion Principle (DIP) – high-level modules should depend on abstractions, not low-level concrete classes.

Think of running National Bank of India cleanly. Single Responsibility means each employee has one clear job: the cashier only handles cash, the auditor only audits. Open/Closed means when you add a new loan product you open a new counter rather than rebuilding the whole branch. Liskov means any teller assigned to a counter must be able to do that counter's job properly. Interface Segregation means you do not force the cashier to also know the fire-safety manual. Dependency Inversion means the loan manager depends on the idea of a credit-check service, not on one specific vendor.

Understanding the Concepts

What is the Single Responsibility Principle (SRP)?

SRP says a class should have only one reason to change. When a class does many unrelated things, a change to one concern can break another concern. In a banking system, a TransferService that performs the transfer, formats an email, writes to a file, and generates a PDF statement changes for four unrelated reasons.

The fix is to split:

JAVA
package com.bank.service;

import java.math.BigDecimal;

// VIOLATION: one class, four reasons to change
public class TransferServiceBad {
    public void transfer(String from, String to, BigDecimal amount) {
        // 1. Validate and move money
        // 2. Format and send email
        // 3. Write audit log to file
        // 4. Generate PDF statement
    }
}

// FIX: each class has one reason to change
public class TransferService {
    private final NotificationService notifier;
    private final AuditService auditor;
    private final StatementService statements;

    public TransferService(NotificationService notifier,
                           AuditService auditor,
                           StatementService statements) {
        this.notifier = notifier;
        this.auditor = auditor;
        this.statements = statements;
    }

    public void transfer(String from, String to, BigDecimal amount) {
        // Only the transfer logic lives here
        System.out.println("Transferred " + amount + " from " + from + " to " + to);
        notifier.send(to, "You received " + amount);
        auditor.log(from, to, amount);
        statements.generate(from, to, amount);
    }
}

Now changing the email provider does not risk breaking the transfer logic.

What is the Open/Closed Principle (OCP)?

OCP says software should be open for extension but closed for modification. A violation is a method with a growing if-else chain on account type. Every new product forces an edit to that method, risking existing behaviour.

JAVA
package com.bank.service;

import java.math.BigDecimal;

// VIOLATION: adding Fixed Deposit means editing this method
public class InterestCalculatorBad {
    public BigDecimal calculate(String type, BigDecimal balance) {
        if ("SAVINGS".equals(type)) {
            return balance.multiply(new BigDecimal("0.04"));
        } else if ("CURRENT".equals(type)) {
            return balance.multiply(new BigDecimal("0.02"));
        }
        // every new type edits here
        return BigDecimal.ZERO;
    }
}

// FIX: strategy interface – open for extension, closed for modification
public interface InterestStrategy {
    BigDecimal calculate(BigDecimal balance);
}

public class SavingsInterest implements InterestStrategy {
    public BigDecimal calculate(BigDecimal balance) {
        return balance.multiply(new BigDecimal("0.04"));
    }
}

public class CurrentInterest implements InterestStrategy {
    public BigDecimal calculate(BigDecimal balance) {
        return balance.multiply(new BigDecimal("0.02"));
    }
}

public class InterestCalculator {
    private final InterestStrategy strategy;

    public InterestCalculator(InterestStrategy strategy) {
        this.strategy = strategy;
    }

    public BigDecimal calculate(BigDecimal balance) {
        return strategy.calculate(balance);
    }
}

Adding a new NRE account type means adding one class. The calculator and existing strategies are never touched.

What is the Liskov Substitution Principle (LSP)?

LSP says a subtype must be usable anywhere its supertype is expected without breaking correctness. The classic violation is a SavingsAccount that throws on withdraw because savings has special rules, while callers expect withdraw to work.

JAVA
package com.bank.model;

import java.math.BigDecimal;

public class Account {
    protected BigDecimal balance = BigDecimal.ZERO;

    public void withdraw(BigDecimal amount) {
        balance = balance.subtract(amount);
        System.out.println("Withdrawn: " + amount);
    }
}

// VIOLATION: subclass breaks the contract
public class FixedDepositAccount extends Account {
    @Override
    public void withdraw(BigDecimal amount) {
        throw new UnsupportedOperationException(
            "Cannot withdraw from Fixed Deposit before maturity");
    }
}

// Any code written against Account breaks:
public class WithdrawProcessor {
    public void process(Account account, BigDecimal amount) {
        account.withdraw(amount); // throws if FixedDepositAccount!
    }
}

The fix is to redesign so the restricted behaviour is expressed explicitly. Use a common interface that both honour fully, or separate Withdrawable from Account.

What is the Interface Segregation Principle (ISP)?

ISP says no client should be forced to depend on methods it does not use. A fat BankingOperations interface forces a simple savings account class to implement stock trading it will never do.

JAVA
package com.bank.service;

import java.math.BigDecimal;

// VIOLATION: fat interface
public interface BankingOperations {
    void deposit(BigDecimal amount);
    void withdraw(BigDecimal amount);
    void tradeStock(String symbol, int qty);
    void issueCard(String type);
}

// SavingsAccount is forced to stub out trading
public class SavingsAccountService implements BankingOperations {
    public void deposit(BigDecimal amount) { /* ok */ }
    public void withdraw(BigDecimal amount) { /* ok */ }
    public void tradeStock(String symbol, int qty) {
        throw new UnsupportedOperationException("Not supported");
    }
    public void issueCard(String type) {
        throw new UnsupportedOperationException("Not supported");
    }
}

// FIX: split into focused role interfaces
public interface DepositOperations {
    void deposit(BigDecimal amount);
    void withdraw(BigDecimal amount);
}

public interface TradingOperations {
    void tradeStock(String symbol, int qty);
}

public interface CardOperations {
    void issueCard(String type);
}

// Each class implements only what it needs
public class SavingsAccountServiceFixed implements DepositOperations {
    public void deposit(BigDecimal amount) {
        System.out.println("Deposited: " + amount);
    }
    public void withdraw(BigDecimal amount) {
        System.out.println("Withdrawn: " + amount);
    }
}

What is the Dependency Inversion Principle (DIP)?

DIP says high-level modules should depend on abstractions, not low-level modules. A LoanService that directly constructs new CibilHttpClient() is welded to one concrete credit vendor.

JAVA
package com.bank.service;

import java.math.BigDecimal;

// VIOLATION: high-level policy depends on a concrete low-level class
public class LoanServiceBad {
    private final CibilHttpClient client = new CibilHttpClient();

    public boolean approve(String customerId, BigDecimal amount) {
        int score = client.fetchScore(customerId);
        return score > 700;
    }
}

// FIX: depend on abstraction, inject implementation
public interface CreditScoreProvider {
    int fetchScore(String customerId);
}

public class CibilProvider implements CreditScoreProvider {
    public int fetchScore(String customerId) {
        // HTTP call to CIBIL
        return 750;
    }
}

public class LoanService {
    private final CreditScoreProvider provider;

    public LoanService(CreditScoreProvider provider) {
        this.provider = provider;
    }

    public boolean approve(String customerId, BigDecimal amount) {
        int score = provider.fetchScore(customerId);
        return score > 700;
    }
}

Now swapping CIBIL for Experian is injecting a different implementation. Unit tests inject a mock.

How can SRP and DIP clash during refactoring?

When you aggressively apply SRP, you split a class into many tiny classes, each with one job. Then to keep them decoupled you apply DIP by putting an interface between every pair. Taken to the extreme, you get an explosion of one-method classes and one-method interfaces that are harder to follow than the original.

JAVA
package com.bank.service;

// OVER-SPLIT: every tiny step has its own class and interface
public interface AccountValidator { void validate(String id); }
public interface BalanceChecker { boolean hasFunds(String id, java.math.BigDecimal amt); }
public interface DebitExecutor { void debit(String id, java.math.BigDecimal amt); }
public interface CreditExecutor { void credit(String id, java.math.BigDecimal amt); }
public interface TransferLogger { void log(String from, String to, java.math.BigDecimal amt); }

// The transfer service now depends on five interfaces for one operation
public class OverSplitTransferService {
    private final AccountValidator validator;
    private final BalanceChecker checker;
    private final DebitExecutor debiter;
    private final CreditExecutor crediter;
    private final TransferLogger logger;
    // ... constructor with five parameters, harder to read than the original
}

The tension is real: SRP pushes you to split more, while good DIP wants stable, meaningful abstractions. An interface with a single implementation that will never have another is a pointless abstraction. The practical resolution is to split by genuine, independent reasons to change (SRP), but only introduce an abstraction (DIP) where you actually expect variation or need a seam for testing.

Can SOLID lead to over-engineering?

Yes, if applied dogmatically. Creating an interface for a class that will only ever have one implementation, building a strategy hierarchy for a calculation that never varies, or splitting a cohesive class into five anaemic ones adds indirection and cognitive load for no benefit.

JAVA
package com.bank.service;

import java.math.BigDecimal;

// OVER-ENGINEERED: interface for a class that will never have a second implementation
public interface FlatFeeCalculator {
    BigDecimal calculate(BigDecimal amount);
}

public class FlatFeeCalculatorImpl implements FlatFeeCalculator {
    public BigDecimal calculate(BigDecimal amount) {
        return new BigDecimal("50.00");
    }
}

// SIMPLE AND SUFFICIENT: no interface needed for a single, stable calculation
public class FeeService {
    private static final BigDecimal FLAT_FEE = new BigDecimal("50.00");

    public BigDecimal calculateFee(BigDecimal amount) {
        return FLAT_FEE;
    }
}

The mature approach is to start simple, keep code cohesive, and apply a SOLID principle when a real pressure appears: a second implementation, a hard-to-test dependency, or a method that keeps growing.

What are common interview follow-up questions?

  • Which SOLID principle does the Strategy pattern enforce? Open/Closed.
  • Which principle does constructor injection use? Dependency Inversion.
  • How do you unit test a class that violates DIP? You cannot easily, because the concrete dependency cannot be mocked.
  • What is the trade-off of ISP? More interfaces to maintain, but each one is easier to implement and test.
  • How are SOLID and design patterns related? Patterns like Strategy (OCP), Factory (DIP), and Decorator (OCP + SRP) embody SOLID principles.
JAVA
package com.bank.demo;

import java.math.BigDecimal;

public class SolidDemo {
    public static void main(String[] args) {
        // DIP in action: inject the strategy
        InterestStrategy savings = balance -> balance.multiply(new BigDecimal("0.04"));
        InterestCalculator calc = new InterestCalculator(savings);
        BigDecimal interest = calc.calculate(new BigDecimal("100000"));
        System.out.println("Interest: " + interest);

        // DIP in action: inject the credit provider
        CreditScoreProvider mock = id -> 800;
        LoanService loan = new LoanService(mock);
        System.out.println("Approved: " + loan.approve("CUST-001", new BigDecimal("500000")));
    }
}
TEXT
Interest: 4000.00
Approved: true

How do SOLID principles work together in a real banking service?

Pankaj, the architect at National Bank of India, designed the interest-calculation module using multiple SOLID principles together:

  • SRP: InterestCalculator only calculates interest. NotificationService only sends alerts.
  • OCP: New account types add a new InterestStrategy class. Existing code is untouched.
  • LSP: Every InterestStrategy implementation honours the same contract: it takes a balance and returns interest.
  • ISP: The strategy interface has one method. No class is forced to implement anything irrelevant.
  • DIP: InterestCalculator depends on the InterestStrategy interface, not on SavingsInterest or CurrentInterest.
JAVA
package com.bank.service;

import java.math.BigDecimal;
import java.util.Map;

public class InterestModule {
    private final Map<String, InterestStrategy> strategies;

    public InterestModule(Map<String, InterestStrategy> strategies) {
        this.strategies = strategies;
    }

    public BigDecimal calculate(String accountType, BigDecimal balance) {
        InterestStrategy strategy = strategies.get(accountType);
        if (strategy == null) {
            throw new IllegalArgumentException("Unknown type: " + accountType);
        }
        return strategy.calculate(balance);
    }
}

Adding an NRE account type is one new class plus one map entry. No existing code changes. That is SOLID working together.

Visual Overview

DIAGRAM
  SOLID Principles – What Each One Attacks
  ─────────────────────────────────────────

  SRP ─── Too many reasons to change in one class
           │
           ▼
  ┌──────────────┐   ┌──────────────┐   ┌──────────────┐
  │TransferService│   │NotificationSvc│   │StatementSvc  │
  │(transfer only)│   │(email only)   │   │(PDF only)    │
  └──────────────┘   └──────────────┘   └──────────────┘

  OCP ─── Adding behaviour requires editing existing code
           │
           ▼
  ┌──────────────────┐
  │ InterestStrategy  │ ◄── interface
  └──────┬───────────┘
         ├──── SavingsInterest
         ├──── CurrentInterest
         └──── NreInterest  (new class, no edits)

  LSP ─── Subtype breaks parent's contract
           │
           ▼
  Caller expects: account.withdraw(amount) works
  Violation: FixedDepositAccount.withdraw() throws!

  ISP ─── Fat interface forces unused implementations
           │
           ▼
  Split: DepositOps + TradingOps + CardOps

  DIP ─── High-level depends on low-level concrete class
           │
           ▼
  ┌────────────┐     ┌─────────────────────┐
  │ LoanService │────▶│ CreditScoreProvider │ ◄── interface
  └────────────┘     └──────────┬──────────┘
                                ├── CibilProvider
                                └── ExperianProvider

Key Takeaway

SOLID is five object-oriented design principles that together make code easier to maintain, extend, and test. Single Responsibility says a class should have only one reason to change, so Rajesh splits a class that did transfers, emails, and PDF generation into three focused services at National Bank of India. Open/Closed says code should be open for extension but closed for modification, which he achieves by replacing a growing if-else chain with a strategy interface per account type, so adding a product is a new class, not an edit.

Liskov Substitution says a subtype must be usable anywhere its supertype is expected without breaking the contract, so a FixedDepositAccount must not throw from withdraw if callers rely on Account.withdraw working. Interface Segregation says do not force a class to implement methods it does not need, so Pankaj keeps interfaces small and focused. Dependency Inversion says high-level policy should depend on abstractions, not concrete low-level classes, so services depend on interfaces and receive implementations by injection.

The interesting tension the interviewer usually probes is how SRP and DIP can clash during refactoring. When Rajesh aggressively splits classes to honour SRP, the pieces must collaborate, and if he puts an interface between every pair to honour DIP, the result is an explosion of tiny one-method classes and pointless single-implementation interfaces that are harder to read than the original. The resolution is to split only along genuine, independent reasons to change, and introduce an abstraction only where variation is expected or a testing seam is needed.

That leads to the honest answer on over-engineering: SOLID can absolutely cause over-engineering if applied dogmatically. An interface for a class that will only ever have one implementation adds indirection for no benefit. Pankaj treats SOLID as tools to manage change and coupling where they genuinely exist: start simple and cohesive, and apply a principle when a real pressure appears.

Remember one sentence: SOLID is five principles that manage change and coupling, but applied dogmatically they over-engineer, so split by real reasons to change and abstract only where you expect variation.

---

Share
Use ← → arrow keys to navigate