skip to content

Applying SOLID

Principles are easy to recite and hard to apply, so this node is about reading real code, naming the violation, and choosing a refactoring. It also covers the other direction: when applying SOLID too eagerly produces indirection nobody can follow.

part ofSoftware design & architectureoverview, primer and where to startread it →
on this pageshow

questions

6

A single class builds a report's text, writes it to a file, and emails it to subscribers. Which SOLID principle does this violate, how do you recognise the violation, and how would you refactor it?

level: juniorimportance: must knowfreq 82%

answer

  1. one reason to change = one actor
  2. the 'and' test in the class description
  3. LCOM / disjoint field clusters
  4. Extract Class + thin orchestrator
  5. don't split to one method per class

basics

~20 s

It violates the Single Responsibility Principle: the class changes for three unrelated reasons (report wording, file storage, email delivery). Split it into a formatter, a storage writer, and a sender, and let a small coordinator call all three.

solid answer

~40 s

This is a Single Responsibility Principle (SRP) violation. SRP says a module should have one reason to change - one stakeholder group it answers to. Here three change drivers are fused: business wants different wording, ops wants a different storage target, marketing wants a different delivery channel. Symptoms: the class description needs the word 'and'; it pulls in unrelated libraries (templating, filesystem, mail); a unit test of one behaviour needs three fakes; unrelated teams edit the same file and collide. Refactor with Extract Class: ReportFormatter (produces text), ReportStore (persistence), ReportNotifier (delivery), plus a thin ReportJob that receives the three collaborators via dependency injection and orchestrates them. The orchestrator's own reason to change is the workflow itself. Guard against over-splitting: SRP is about reasons to change, not one method per class.

code

pseudocode · 26 lines
pseudocode
// Before: three reasons to change in one class
class ReportService {
  fun run() {
    val text = buildText()      // business asks for changes
    File("/reports/r.txt").write(text)  // ops asks for changes
    smtp.send("team@x", text)   // comms asks for changes
  }
}

// After: one reason each, plus a workflow coordinator
interface ReportStore    { fun save(text: String) }
interface ReportNotifier { fun send(text: String) }

class ReportFormatter { fun format(data: Data): String { /* ... */ } }

class ReportJob(
  val formatter: ReportFormatter,
  val store: ReportStore,
  val notifier: ReportNotifier
) {
  fun run(data: Data) {
    val text = formatter.format(data)
    store.save(text)
    notifier.send(text)
  }
}

go deeper

for a junior

Name the principle, give the 'three unrelated reasons to change' argument, and describe splitting into three classes with a coordinator.

for a middle

Add concrete detection signals (mixed imports, test friction, cohesion clusters) and show the refactor with dependency injection so the collaborators are interfaces.

for a senior

Frame it as actors and change axes, use version-control change-coupling as evidence, discuss where the coordinator belongs, and name over-splitting as the opposing failure mode.

for a principal

Generalise to module and service boundaries: shared ownership across teams is the organisational form of the same violation. Tie it to cohesion/coupling as the underlying measure and to Conway's law when deciding where seams should fall.

## The principle **SRP (Single Responsibility Principle)** is the 'S' of SOLID, five object-oriented design guidelines popularised by Robert C. Martin. It is usually quoted as *'a class should have only one reason to change'*, and later restated as *'a module should be responsible to one, and only one, actor'*. An **actor** is a person or group that can ask for a change: a business owner, an operations team, a compliance officer. The point is not counting methods. The point is that when two unrelated groups can each demand edits to the same file, their changes collide, and a change requested by one group can silently break the other group's behaviour. ## Why the example violates it The class mixes three axes of change: | Concern | Who asks for changes | Typical change | |---|---|---| | Building the text | business / product | new column, new wording, localisation | | Writing to a file | operations / platform | move to object storage, change path layout | | Emailing subscribers | marketing / comms | switch provider, add a digest schedule | Each is a separate reason to change, so the class has three. ## How to *recognise* it in real code - **The 'and' test** - you cannot describe the class without 'and' or 'also'. - **Import/dependency spread** - unrelated technologies imported side by side (templating + filesystem + SMTP client). - **Test friction** - to test formatting you must stub a filesystem and a mail server; the test is slow and fragile. - **Low cohesion** - method groups touch disjoint sets of fields. The classic metric is **LCOM (Lack of Cohesion of Methods)**: high LCOM means the methods form separate clusters. - **Git evidence** - the file appears in commits from several teams for unrelated tickets; frequent merge conflicts. - **Shotgun sensitivity** - a formatting tweak forces re-testing the delivery path. ## Refactoring strategy 1. **Identify the seams** - group methods and fields by which actor cares about them. 2. **Extract Class** per group. Give each a name that is a noun for its single job. 3. **Introduce an orchestrator** (a service, use-case, or job class) that owns the *sequence* only. 4. **Invert the dependencies** - the orchestrator depends on interfaces (`ReportStore`, `ReportNotifier`) rather than concrete file/mail classes, so tests use in-memory fakes. That is DIP helping SRP land. 5. **Move the tests** with the code: formatting tests become pure and fast. After refactoring, a wording change touches one file; a storage migration touches another; the orchestrator changes only when the workflow changes. ## Trade-offs and edge cases - **Over-splitting is a real failure mode.** Splitting until every class has one method produces a scatter of anaemic types and pushes the complexity into wiring. Split along *observed* change axes, not imagined ones. - **Coordination has to live somewhere.** After extraction, someone must call all three. That coordinator is legitimate; it is not an SRP violation just because it references three collaborators - it has one reason to change (the workflow). - **SRP is not 'one class per database table' or 'one method per class'.** Those are misreadings. - **Cohesion is the deeper idea.** SRP is a heuristic for high cohesion and low coupling; when SRP advice and cohesion disagree, follow cohesion. - **Small scripts.** A 30-line throwaway job does not need three classes. SRP pays off where change is frequent and multiple actors exist.

  • After the split, the coordinator class references three collaborators. Isn't that itself a Single Responsibility Principle violation?
    No. Referencing several collaborators is not the test; having several *reasons to change* is. The coordinator changes only when the workflow changes (e.g. save before sending, or send only on success). Its dependencies are abstractions, so a change of mail provider or storage backend does not touch it.
  • How would you decide where to draw the seams if the class had ten methods and no obvious grouping?
    Look at which fields each method touches (cohesion clusters), then at version-control history: which methods change together in the same commits, and which tickets/teams drive those commits. Change-coupling in history is stronger evidence than intuition. If no clusters emerge, leave it alone until a second change axis actually appears.
  • Does the Single Responsibility Principle apply above the class level?
    Yes - the same 'one actor, one reason to change' test applies to modules, packages, deployable services and bounded contexts. At service level it is the main argument against a shared service edited by three teams for unrelated reasons.

A restaurant where one person cooks, waits tables and does the books: any change in the menu, the seating plan, or the tax rules interrupts the same person. Splitting the roles means each request lands on exactly one desk.

saying these in an interview costs you the question

  • Saying SRP means 'a class should only have one method' or 'one public method'.
  • Claiming any class with more than one dependency violates SRP.
  • Splitting by technical layer only (DTO/mapper/util) and calling that SRP while all three still change for the same reason.
  • Assuming SRP is about code size (line count) rather than reasons to change.
  • Extracting classes but leaving the orchestrator constructing concrete file/mail objects, so tests still need real infrastructure.

context

open as a page

What concrete signals in a codebase tell you the Liskov Substitution Principle is being violated, and what refactorings restore substitutability?

level: middleimportance: must knowfreq 68%

basics

~20 s

Signals: an override that throws 'not supported', callers doing type checks before calling, or a subclass that rejects inputs the parent accepts. Fix by narrowing the interface, using composition instead of inheritance, or splitting the hierarchy so each type only promises what it can deliver.

open as a page

Every time a new payment method is added, developers edit the same switch statement in three different files. Which SOLID principle is being violated, and what refactoring restores compliance?

level: middleimportance: must knowfreq 74%

basics

~20 s

It violates the Open/Closed Principle: adding a variant forces editing existing code. Replace the repeated conditionals with polymorphism - one interface per payment method with fee, validate and charge behaviour - so a new method means adding a class, not editing three.

open as a page

When does applying SOLID make a codebase worse? How do you decide whether a given piece of code needs an abstraction or should stay concrete?

level: seniorimportance: must knowfreq 58%

basics

~20 s

When the abstraction guesses wrong or is never needed: interfaces with one implementation forever, factories wrapping factories, layers you must step through to read simple logic. Add the abstraction when a second real variant appears, not before.

open as a page

How do the five SOLID principles reinforce one another? Give a concrete chain showing how violating one causes violations of the others.

level: seniorimportance: should knowfreq 52%

basics

~20 s

They are not independent. A fat interface (ISP violation) forces implementations to stub methods they cannot support (LSP violation), so callers add type checks, which means adding a type forces editing callers (OCP violation) - and callers now depend on concrete types (DIP violation).

open as a page

How do the SOLID principles translate from classes to module, service, and API boundaries - and where do they stop being useful guidance?

level: principalimportance: nice to knowfreq 34%

basics

~20 s

The same ideas scale up: one owner per service (SRP), plugins and versioned APIs instead of edits (OCP), backward-compatible drop-in implementations (LSP), consumer-specific APIs (ISP), and core logic depending on interfaces the adapters implement (DIP). They say nothing about concurrency, data layout, or failure handling.

open as a page