I've been stung more than once by code that looked fine until a new feature arrived. Suddenly I'm editing a dozen unrelated classes and wondering why the whole system fights me.
The first rule that saved me was the Single Responsibility Principle. I once had a PaymentProcessor that handled payment logic, database queries, and email notifications. Splitting those concerns into three focused classes cut the code size by half and made each piece trivial to unit‑test.
Open/Closed was the next revelation. My ReportGenerator started out hard‑coding PDF, CSV, and HTML paths. By extracting an IReportFormatter interface and letting each format implement it, I added a new JSON exporter without touching the generator itself. No regression, no extra risk.
Liskov sounds like a textbook phrase until you see a Penguin subclass breaking a Bird contract. I wrote a Bird base class with a fly() method, then forced a Penguin to throw an exception. The bug surfaced only when a generic bird collection tried to iterate. The fix was to separate flying behavior from the core bird definition.
Interface Segregation kept my Robot from pretending it could eat. I originally forced every worker to implement both work() and eat(). Splitting those into IWorker and IEater let the robot class implement only what it needed, and the codebase stopped complaining about absurd method bodies.
Dependency Inversion saved my AuthService from being glued to a specific repository. By depending on an IUserRepository abstraction instead of a concrete DatabaseUserRepository, I could swap in an in‑memory store for tests and later a cloud‑based store without touching the service logic.
Applying SOLID turned my code from a brittle web into a set of interchangeable parts. Adding a feature now means touching one or two small classes instead of a shotgun surgery across the repo, and my test suite runs faster because each class has a clear, narrow responsibility.
I don't treat SOLID as a law for every script. A quick one‑off utility that reads a CSV and prints a summary can live with a few mixed concerns. The cost of extra interfaces outweighs the benefit when the code won't be maintained.
My habit now is to ask myself whether a class has more than one reason to change, whether I can add behavior without editing existing code, and whether my abstractions are leaking details. If the answer is yes, I refactor. It keeps the code honest and the team sane.