Ask
27
@architect_ayla ·

When are the SOLID principles the wrong thing to apply?

I come from a mathematics background, where a counterexample often teaches more than an example. I have read a great many demonstrations of how to apply the SOLID principles and essentially none of when not to.

That asymmetry is suspicious. Any principle applied without exception stops being a principle and becomes a ritual.

So: where does enforcing these actively make code worse, and how do you tell that you are in one of those situations rather than just finding the work tedious?

10 answers Share
Report

Answering anonymously, a moderator will review it first.

  • @junior_taught_solid · 3w ago

    Taught these before I understood them, which I now think is the actual failure mode. Applied as rules by someone without the experience that produced them, they generate exactly the code they were meant to prevent: many small pieces, no locality, nothing findable.

    The principles are compressed experience. Handing somebody the compression without the experience does not decompress.

    18
    Share
    Reply

    Answering anonymously, a moderator will review it first.

    Report
  • @architect_ayla · last mo. · 2 replies

    The framing that answers this: these principles all buy the same thing, and it is not free. They buy the ability to change one part without disturbing others. The price is indirection - more types, more interfaces, more files, more distance between what a thing is called and what it does.

    That trade is excellent when change is likely and expensive. It is a straight loss when change is unlikely or cheap.

    So the cases where applying them makes things worse:

    • Code with one implementation and no realistic second one. An interface with exactly one implementer, forever, is a file you now have to open twice to read one thing.
    • Small, self-contained scripts and tools. A hundred-line utility does not need a dependency inversion layer.
    • Exploratory work. While you are still learning what the problem is, structure encodes assumptions you have not tested yet, and every one you get wrong is now load-bearing.
    • Code you intend to delete. Prototypes, migrations, one-off imports.
    29
    Share
    Reply

    Answering anonymously, a moderator will review it first.

    Report
    • @removed_interfaces · 4w ago

      They all buy the same thing, which is the ability to change one part without the others. If nothing changes there, you paid for nothing.

      11
      Share
      Reply

      Answering anonymously, a moderator will review it first.

      Report
  • @maintainer_mika · last mo. · 4 replies

    The specific failure mode worth naming is speculative generality: building the flexibility for a change that never arrives.

    It is expensive in a way that is easy to miss, because the cost is not the day you write it. It is every subsequent reader tracing through three layers to find where the work happens, every debugging session that lands in an abstract base class, and: the sharpest one: the fact that when the change finally does come, it is usually not the change you prepared for. So you pay for the flexibility, and then you refactor anyway.

    My own test, since you asked how to distinguish it from tedium: can I name the second case? Not "we might need other payment providers one day" but "we are adding this specific one next quarter". If I cannot name it concretely, the abstraction is a guess and I write the direct version, which is easier to change later than an abstraction pointing the wrong way.

    26
    Share
    Reply

    Answering anonymously, a moderator will review it first.

    Report
    • @removed_interfaces · 4w ago · 3 replies

      Removed eleven interfaces from a codebase last year, each with exactly one implementation, and nothing broke. That is the counterexample the question is asking for, in its most common form.

      An interface with one implementation is not abstraction, it is a second file. It buys you the ability to swap something you have never swapped, and it costs a jump every time anyone reads the code.

      23
      Share
      Reply

      Answering anonymously, a moderator will review it first.

      Report
      • @maintainer_mika · 3w ago

        Speculative generality, and one implementation is the diagnostic. If there has never been a second, the flexibility was hypothetical.

        14
        Share
        Reply

        Answering anonymously, a moderator will review it first.

        Report
      • @api_dilan · 3w ago

        Worth noting the exception: a test double counts as a second implementation in some codebases and not in others, depending on the tooling.

        9
        Share
        Reply

        Answering anonymously, a moderator will review it first.

        Report
  • @api_dilan · last mo.

    Worth pushing back slightly on the framing though, because the counterexample hunt can go too far in the other direction.

    Most of these principles have a cheap version and an expensive version, and the cheap version is nearly always worth it. Keeping a function focused on one job costs nothing. Not making a subclass that violates its parent's contract costs nothing. Not forcing callers to depend on methods they never use costs nothing.

    What is expensive is the ceremonial version: an interface per class, a factory per interface, a container wiring it together. That is where the real cost lives, and it tends to be the version people mean when they say SOLID.

    So I would separate them rather than accepting or rejecting the set. Single responsibility and interface segregation are close to free. Dependency inversion is the one to apply deliberately, at boundaries you have identified, and not everywhere.

    21
    Share
    Reply

    Answering anonymously, a moderator will review it first.

    Report
  • @entropy_elif · last mo. · 2 replies

    One more context where they genuinely conflict with the goal: performance-critical code.

    Indirection has a runtime cost, virtual dispatch, allocation, cache behaviour. Usually irrelevant. In an inner loop, or in constrained embedded work, it is the difference between meeting a deadline and not.

    That is a narrow exemption and it gets claimed far more often than it applies. But it is real, and in those places the readable design and the fast design genuinely diverge, which no amount of principle will resolve for you.

    15
    Share
    Reply

    Answering anonymously, a moderator will review it first.

    Report
    • @junior_taught_solid · 4w ago

      Performance-critical code is the cleanest counterexample because the cost is measurable rather than aesthetic.

      8
      Share
      Reply

      Answering anonymously, a moderator will review it first.

      Report