What problem are we solving?

Illustrative composite. A benefits platform receives a new eligibility rule: dependents who turn twenty-six remain covered through the end of that month. The rule sounds local. The pull request changes the evaluator, a regulatory export, a nightly enrollment job, rejection messages in two interfaces, and tests owned by three teams.

The file count is not the problem by itself. A cohesive feature can legitimately span files. The problem is that one policy decision crossed code serving different people for different reasons. Compliance owns the decision. Operations owns the export. Product and legal own the explanation. The batch team owns the nightly feed. Those concerns move together because the code put them together.

That creates a human cost as well as a technical one. A developer must reconstruct unrelated behavior, find several owners, wait for several reviews, and run a wider regression suite before changing one rule. Time that could have gone into a new capability is spent proving that unrelated behavior survived.

What is this practice?

SOLID is a set of five questions for noticing when code has become expensive to change. The questions ask whether unrelated decisions are tangled together, whether one implementation can safely stand in for another, whether callers depend on capabilities they do not use, and whether business policy owns the dependencies it needs. They are diagnostic lenses, not five commandments and not a demand for an interface in front of every class.

The acronym names five principles. Their useful forms are easier to remember as questions:

  • Single Responsibility Principle: which person or policy can cause this module to change?
  • Open-Closed Principle: which variation has the system actually observed and needs a stable home?
  • Liskov Substitution Principle: can a replacement honor the behavior its callers rely on?
  • Interface Segregation Principle: does each caller depend only on capabilities it needs?
  • Dependency Inversion Principle: does policy describe what it needs, or must it speak in a technical system's vocabulary?

Robert C. Martin's module-level explanation of the Single Responsibility Principle focuses on people or groups who can cause change. That is more useful than the common slogan that a class should “do one thing,” because “one thing” does not tell a team where to draw a boundary.

How does it change the work?

Start by separating decisions that have different owners. Eligibility, explanation, and audit evidence are related, but they do not change for the same reason:

type EligibilityDecision =
  | Eligible { through: Date, reasons: Reason[] }
  | Ineligible { reasons: Reason[] }

module EligibilityPolicy {
  evaluate(application: Application, on: Date): EligibilityDecision
}

module DecisionExplainer {
  explain(decision: EligibilityDecision, locale: Locale): Message
}

module AuditRecorder {
  record(applicationId: Id, decision: EligibilityDecision): void
}

The important change is not that three names replaced one. The policy decision can now change without forcing wording and evidence formats to change with it. If the same team always changes all three together, this separation may buy nothing.

Protect only variation the system has evidence for. If two regulated products already calculate coverage end dates differently, a named rule can contain that difference:

contract DependentCoverageRule {
  coverageEnd(dependent: Dependent, contract: Contract): Date
}

rule EndOfBirthMonth satisfies DependentCoverageRule
rule EndOfCalendarYear satisfies DependentCoverageRule

That contract does not promise that every future regulation will fit. A new rule based on employment status may need different facts and therefore a different model. Jon Skeet's review of the Open-Closed Principle makes the practical point: software can accommodate anticipated extensions and still require modification when an unanticipated feature changes the shape of the problem.

A shared method signature is also not enough to make two rules interchangeable. A replacement that throws during renewals is unsafe if callers are entitled to evaluate every validated contract. Behavioral substitution means preserving the preconditions, results, and failure behavior on which callers rely. Liskov and Wing's original behavioral account of subtyping is about those expectations, not merely matching a type signature.

The last two questions keep callers and policy from depending on too much. A support console should not receive policy-publication and audit-deletion capabilities merely because they live on a broad administration interface. Eligibility policy should request household facts in business terms rather than importing a database driver or provider SDK.

This is the point where SOLID overlaps with Clean Architecture's larger policy boundary: dependency ownership can begin inside one module and later protect business policy from entire frameworks or storage systems. The ideas support each other, but they answer questions at different scales.

What does it buy?

Used selectively, these questions can make the edit surface resemble the decision surface. A compliance rule changes policy code and focused policy tests. A wording change stays in the explainer. A new evidence format stays in the recorder. Reviewers can reason about a smaller set of consequences without loading the entire application into working memory.

The quality-of-life benefit is conditional. Fewer unrelated modules may mean fewer review handoffs, a shorter focused test loop, and less fear that a routine policy change will disturb an export or customer message. Those hours return to new work only when the boundary actually contains change.

What does it cost?

Every boundary needs a name. Every behavioral promise needs tests. Indirection makes a call path harder to follow, especially when an abstraction has one implementation and no independent reason to exist. Premature interfaces freeze guesses about future variation. Tiny classes can scatter a use case across a directory. Mock-heavy tests can prove that collaborators were called while missing whether the business outcome is correct.

Common ways to turn SOLID into additional maintenance include:

  • interpreting responsibility as one operation per class instead of one source of change;
  • creating extension points before a second real variation exists;
  • assuming inheritance is safe because signatures match;
  • splitting interfaces by method count instead of caller need;
  • adding a one-to-one interface for every concrete class even though no dependency has been inverted;
  • measuring design quality by object count rather than the cost of comparable changes.

The practice moves complexity. It does not eliminate it. The question is whether explicit responsibilities and contracts cost less than repeated collateral edits in this system.

When is it worth using?

For an MVP, keep the policy cohesive and concrete. Name a boundary only where an external dependency or a second observed variation already obstructs learning. One framework-free policy module may be enough.

For a growing product, look at change history. Extract a responsibility when different owners repeatedly edit the same module, or when a familiar policy change repeatedly disturbs unrelated behavior.

For a mature platform, shared rules may need explicit behavioral contracts, ownership, focused test suites, and compatibility guarantees because many teams depend on them. Maturity changes the risk calculation; it does not make abstraction free.

A short-lived experiment, a stable data transformation, or a small CRUD screen may remain clearest as direct code. Do not schedule a project-wide “SOLID refactor.” Apply one question to one recurring source of friction.

How do you know it is helping?

Compare similar changes before and after the boundary. Useful signals include:

  • unrelated modules touched by the change;
  • number of owner or actor reviews required;
  • time to run the focused test loop;
  • regressions outside the changed policy;
  • time developers spend locating the rule and following its call path;
  • whether developers report that navigation became easier or harder.

These signals support a local claim such as “coverage-rule changes now stay in policy and explanation modules.” They do not prove that SOLID universally increases productivity. The SPACE framework is a useful reminder that developer productivity includes satisfaction, performance, activity, communication, and flow; no file-count metric represents all of it.

When should you simplify it?

Simplify or remove an abstraction when its implementations never vary, its callers always change together, developers routinely bypass it, or navigation and mocking cost more than the change it contains. Merge tiny objects when their separation no longer corresponds to independent decisions.

An interface is maintained software. If it has no meaningful client boundary, behavioral variation, or dependency to invert, keeping the concrete code may be the more sustainable design.

Sources