SOLID Principles Are Not Rules. They Are Warning Signs.

by Arif Ikhsanudin, Backend Developer

When Principles Become a Religion

You're in a code review and someone flags a class as violating the Single Responsibility Principle. The class handles order creation — it validates the input, persists to the database, and publishes an event. The reviewer wants it split into three classes. An animated debate follows about what a "responsibility" is and whether persistence and validation are truly separable concerns.

Forty-five minutes later, you have an OrderValidator, an OrderRepository, and an OrderEventPublisher — plus an orchestration layer that now knows about all three. The code is more SOLID-compliant and harder to follow. The original question — whether this code works correctly and can be safely modified — was never answered.

This happens when SOLID is treated as a rulebook rather than a diagnostic framework.

What the Principles Are Actually For

Robert Martin formulated SOLID (Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion) as a way to describe properties of code that is easy to change. The goal was always maintainability and changeability — not structural purity.

The useful question is never "does this code comply with SOLID?" The useful question is "does this code exhibit the problems SOLID is pointing at?" Those problems have names:

  • SRP violation: A class or function changes for more than one reason — meaning changes to unrelated requirements keep breaking the same component. This is the warning sign. Not "it does multiple things."
  • OCP violation: Every time a new case is added, you're modifying existing code and risking regressions rather than extending it. The warning sign is the risky modification pattern, not the absence of interfaces.
  • LSP violation: You have a subtype that requires callers to know which concrete implementation they're using to behave correctly. The warning sign is instanceof checks and casts to subclasses.
  • ISP violation: Implementors are forced to provide methods they have no business providing — stub implementations, throwing UnsupportedOperationException. The warning sign is the stub.
  • DIP violation: High-level business logic directly depends on a database driver or HTTP client. Changing the infrastructure layer forces changes to business code. The warning sign is the coupling, not the absence of an interface.

The Overengineering Failure Mode

Here is how mechanical SOLID compliance goes wrong on real projects.

A service needs to send a notification when an order ships. The naive implementation: call the email library directly from the order service. A SOLID-aware developer correctly notes that the Dependency Inversion Principle suggests depending on an abstraction. So they create:

interface NotificationSender {
    void send(Notification notification);
}

class EmailNotificationSender implements NotificationSender { ... }
class SmsNotificationSender implements NotificationSender { ... }
class PushNotificationSender implements NotificationSender { ... }

This is fine — if you actually have multiple notification channels being used. But if the application only ever sends email, you've added an interface and an indirection layer for a flexibility you don't have and haven't committed to building. You've made the codebase harder to navigate for the sake of a principle applied in the absence of the problem it solves.

The correct question was: is there evidence we'll need to swap notification implementations? No? Then the interface is premature.

When SOLID Advice Is Right

The principles are genuinely useful as retrospective diagnostics:

  • You're modifying a service to change how orders are taxed, and you keep accidentally breaking the refund calculation in the same file. That's an SRP problem.
  • Every time a new payment method is added, you're adding a case to a switch statement that's already forty cases long and regularly causes merge conflicts. That's an OCP problem.
  • Your PremiumUser extends User, but PremiumUser.getDiscount() throws an exception for users who haven't set up a billing account, so every caller wraps it in a check. That's an LSP problem.

In all of these cases, SOLID is pointing at genuine pain. The principle names a pattern you've been experiencing as friction.

The Practical Heuristic

Before applying a SOLID transformation, ask: what is the specific change or maintenance problem this is solving? If the answer is "it's just better design" or "it follows the principle," that's not enough. If the answer is "every time we change tax logic we break order fulfillment" or "we've had three bugs in as many weeks because this class does too many things," then the principle is pointing at something real.

Apply the fix proportional to the pain. A class that does two things isn't automatically a problem. A class that regularly causes merge conflicts and bugs because it does two things — that one earns a refactor.

The Practical Takeaway

In your next code review, when a SOLID principle gets invoked, ask the proposer: what specific change, maintenance problem, or failure mode are we protecting against with this restructuring? If they can name it concretely, the review is productive. If they can't, the discussion is about aesthetics, not engineering. Redirect it.

Scale Your Backend - Need an Experienced Backend Developer?

We provide backend engineers who join your team as contractors to help build, improve, and scale your backend systems.

We focus on clean backend design, clear documentation, and systems that remain reliable as products grow. Our goal is to strengthen your team and deliver backend systems that are easy to operate and maintain.

We work from our own development environments and support teams across US, EU, and APAC timezones. Our workflow emphasizes documentation and asynchronous collaboration to keep development efficient and focused.

  • Production Backend Experience. Experience building and maintaining backend systems, APIs, and databases used in production.
  • Scalable Architecture. Design backend systems that stay reliable as your product and traffic grow.
  • Contractor Friendly. Flexible engagement for short projects, long-term support, or extra help during releases.
  • Focus on Backend Reliability. Improve API performance, database stability, and overall backend reliability.
  • Documentation-Driven Development. Development guided by clear documentation so teams stay aligned and work efficiently.
  • Domain-Driven Design. Design backend systems around real business processes and product needs.

Tell us about your project

Our offices

  • Copenhagen
    1 Carlsberg Gate
    1260, København, Denmark
  • Magelang
    12 Jalan Bligo
    56485, Magelang, Indonesia

More articles

Spring Boot Logging in Production — Structured Logs, Correlation IDs, and What to Alert On

Unstructured logs are difficult to query and impossible to alert on reliably. Structured logging with consistent correlation IDs and the right log levels transforms logs from a last resort into a first-line diagnostic tool.

Read more

The Hidden Danger of Teams Without a Tech Lead

Having skilled developers isn’t enough. Without a tech lead, small issues can spiral into major problems. Understanding the risks helps teams stay efficient and maintain code quality.

Read more

The Difference Between Continuous Integration and Continuous Delivery Most Teams Blur

CI and CD are not a single acronym — they are two distinct disciplines with different goals, failure modes, and organizational requirements. Conflating them explains why most pipelines deliver neither well.

Read more

When You Spend More Time Debugging Than Coding

You sit down to write a few lines of code and suddenly realize you’ve spent the last three hours chasing a bug. Why does debugging sometimes feel like the real work?

Read more