Skip to content

Single Responsibility

TL;DR: A class should have only one reason to change. If a class handles content, formatting, and file I/O, any change to one of those concerns forces you to touch (and risk breaking) the others. Split them apart.

Single Responsibility Principle

A Note on SOLID in Modern Development

SOLID principles were formalized during Java's heyday of deep inheritance hierarchies and interface-heavy design. Outside of Java and C#, excessive application of SOLID is falling out of fashion. Modern languages favor simpler approaches — composition over class hierarchies, functions over interfaces, pragmatism over pattern worship. In Python and Go, you rarely see ISP or DIP discussed.

That said, SOLID remains highly relevant for LLD interviews because most problems expect you to design class hierarchies. Apply these principles when the problem calls for them, but recognize when you are adding complexity for its own sake. Don't break KISS by forcing SOLID patterns where simpler solutions work fine.

Interview tip: Interviewers care that you apply the principles, not that you can name them. Don't recite "Single Responsibility Principle" — just naturally keep your classes focused.

Why This Matters in Interviews

SRP is the most commonly tested SOLID principle because it directly affects how you decompose an LLD problem. When an interviewer says "design a report generator," they're watching whether you dump everything into one class or separate concerns cleanly. Getting SRP wrong cascades through your entire design.

The key insight is not "every class should do one thing." It's "every class should have one reason to change." A class that generates report content changes when business rules change. A class that formats PDFs changes when formatting requirements change. A class that writes files changes when storage requirements change. These are three different reasons, so they belong in three different classes.

The Problem: A Class That Does Everything

// BAD: Report does content generation, PDF formatting, AND file I/O
class Report {
    private String title;
    private List<String> data;

    public Report(String title, List<String> data) {
        this.title = title;
        this.data = data;
    }

    public String generateContent() {
        StringBuilder sb = new StringBuilder();
        sb.append("Report: ").append(title).append("\n");
        for (String line : data) {
            sb.append("- ").append(line).append("\n");
        }
        return sb.toString();
    }

    public byte[] formatAsPdf() {
        String content = generateContent();
        // 50 lines of PDF library calls...
        // Margins, fonts, headers, page breaks
        return pdfBytes;
    }

    public void saveToFile(String path) {
        byte[] pdf = formatAsPdf();
        try (FileOutputStream fos = new FileOutputStream(path)) {
            fos.write(pdf);
        } catch (IOException e) {
            throw new RuntimeException("Failed to save report", e);
        }
    }
}
// BAD: Same problem in C++
class Report {
private:
    std::string title;
    std::vector<std::string> data;

public:
    Report(std::string title, std::vector<std::string> data)
        : title(std::move(title)), data(std::move(data)) {}

    std::string generateContent() {
        std::string result = "Report: " + title + "\n";
        for (const auto& line : data) {
            result += "- " + line + "\n";
        }
        return result;
    }

    std::vector<uint8_t> formatAsPdf() {
        std::string content = generateContent();
        // PDF formatting logic...
        return pdfBytes;
    }

    void saveToFile(const std::string& path) {
        auto pdf = formatAsPdf();
        std::ofstream file(path, std::ios::binary);
        file.write(reinterpret_cast<const char*>(pdf.data()), pdf.size());
    }
};
# BAD: Same problem in Python
class Report:
    def __init__(self, title: str, data: list[str]):
        self._title = title
        self._data = data

    def generate_content(self) -> str:
        lines = [f"Report: {self._title}"]
        for line in self._data:
            lines.append(f"- {line}")
        return "\n".join(lines)

    def format_as_pdf(self) -> bytes:
        content = self.generate_content()
        # PDF formatting logic...
        return pdf_bytes

    def save_to_file(self, path: str) -> None:
        pdf = self.format_as_pdf()
        with open(path, "wb") as f:
            f.write(pdf)

What's Wrong Here?

This class has three reasons to change:

  1. Business rules change — the content format changes (new fields, different calculations)
  2. Formatting requirements change — switch from PDF to HTML, or change the PDF layout
  3. Storage requirements change — save to S3 instead of local disk, or add compression

Any of these changes forces you to modify the same class. That means re-testing everything, even the parts you didn't touch. It also means you can't reuse just the content generation without dragging along PDF and file I/O dependencies.

The Fix: One Reason to Change Per Class

// GOOD: Each class has one reason to change

// Changes when: business rules for report content change
class Report {
    private String title;
    private List<String> data;

    public Report(String title, List<String> data) {
        this.title = title;
        this.data = data;
    }

    public String generateContent() {
        StringBuilder sb = new StringBuilder();
        sb.append("Report: ").append(title).append("\n");
        for (String line : data) {
            sb.append("- ").append(line).append("\n");
        }
        return sb.toString();
    }

    public String getTitle() { return title; }
    public List<String> getData() { return List.copyOf(data); }
}

// Changes when: PDF formatting requirements change
class PDFPrinter {
    public byte[] print(Report report) {
        String content = report.generateContent();
        // PDF library calls — margins, fonts, headers
        return pdfBytes;
    }
}

// Changes when: storage mechanism changes
class FileStorage {
    public void save(byte[] data, String path) {
        try (FileOutputStream fos = new FileOutputStream(path)) {
            fos.write(data);
        } catch (IOException e) {
            throw new RuntimeException("Failed to save file", e);
        }
    }
}
// GOOD: Separated concerns

class Report {
private:
    std::string title;
    std::vector<std::string> data;

public:
    Report(std::string title, std::vector<std::string> data)
        : title(std::move(title)), data(std::move(data)) {}

    std::string generateContent() const {
        std::string result = "Report: " + title + "\n";
        for (const auto& line : data) {
            result += "- " + line + "\n";
        }
        return result;
    }

    const std::string& getTitle() const { return title; }
    const std::vector<std::string>& getData() const { return data; }
};

class PDFPrinter {
public:
    std::vector<uint8_t> print(const Report& report) {
        std::string content = report.generateContent();
        // PDF formatting logic...
        return pdfBytes;
    }
};

class FileStorage {
public:
    void save(const std::vector<uint8_t>& data, const std::string& path) {
        std::ofstream file(path, std::ios::binary);
        file.write(reinterpret_cast<const char*>(data.data()), data.size());
    }
};
# GOOD: Separated concerns

class Report:
    def __init__(self, title: str, data: list[str]):
        self._title = title
        self._data = list(data)

    def generate_content(self) -> str:
        lines = [f"Report: {self._title}"]
        for line in self._data:
            lines.append(f"- {line}")
        return "\n".join(lines)

    @property
    def title(self) -> str:
        return self._title

    @property
    def data(self) -> list[str]:
        return list(self._data)


class PDFPrinter:
    def print(self, report: Report) -> bytes:
        content = report.generate_content()
        # PDF formatting logic...
        return pdf_bytes


class FileStorage:
    def save(self, data: bytes, path: str) -> None:
        with open(path, "wb") as f:
            f.write(data)

Now each class changes for exactly one reason. You can swap PDFPrinter for HTMLPrinter without touching Report. You can switch from local files to S3 without touching either Report or PDFPrinter.

Interview tip: When an interviewer asks you to design something, start by listing the different "reasons to change." Each reason maps to a separate class. This immediately shows you understand SRP without you having to name-drop it.

Don't Over-Apply SRP

A common mistake is taking SRP too far and creating a class for every single method. SRP says one reason to change, not one method.

// TOO FAR: This is pointless fragmentation
class ReportTitleGenerator {
    public String generate(String title) { return "Report: " + title; }
}

class ReportLineFormatter {
    public String format(String line) { return "- " + line; }
}

class ReportContentAssembler {
    public String assemble(String title, List<String> lines) { ... }
}

These three classes all change for the same reason: the report content format changes. They belong together in one Report class. SRP groups things that change together, not just things that are different.

Interview tip: If splitting a class doesn't reduce the number of places you'd need to change when a requirement shifts, the split isn't helping. SRP is about managing change, not minimizing class size.

Quick Recap

Concept What it means Why it matters
One reason to change Each class is affected by only one kind of requirement change Changes are isolated — modifying formatting doesn't risk breaking content logic
Identify the actors Different stakeholders drive different changes The business team changes report content; the design team changes formatting; ops changes storage
Don't over-split Things that change together should stay together Fragmenting a single concern across multiple classes adds complexity without benefit