Skip to main content

Command Palette

Search for a command to run...

When Can You Break Software Design Principles?

Updated
8 min read
When Can You Break Software Design Principles?

You must have heard of SOLID, DRY and KISS design principles somewhere at some point. If you don't know what they mean let's go through now. But firstly, they are design principles and not rules - meaning, you are not strictly required to follow them, but in general, it is considered that they make the program more readable and maintainable code that can be understood easily, without repetition and also be open to extensions without severe breakage.

Uncle Bob's SOLID Principles

Imagine you are building with LEGOs. The SOLID design principles are like good practices for your LEGO pieces and how you connect them, so your final creation is strong, easy to modify and doesn't collapse if you change one block.

S: Single Responsibility Principle (SRP)

A class should have only one reason to change.

Let’s say you have a User class. Its job should be to manage user-related data—like name, email, etc. It should not be responsible for sending emails.

Why? Because:

  • Sending emails is unrelated to the core idea of what a user is.

  • If the way you send emails changes, your User class would have to change too—introducing unnecessary fragility.

Instead, you’d have a User class and a separate EmailSender class. Each has one job. Now, if the email logic changes, only EmailSender needs updating—User stays untouched. That makes your code easier to understand, test, and maintain.

O: Open/Closed Principle (OCP)

Software entities should be open for extension, but closed for modification.

Imagine you’re building a house. You want to add a new room later without tearing down the whole structure, right?

OCP means: You should be able to add new behavior to code without changing existing working code.

For example, suppose you’re calculating the area of shapes. If you have an AreaCalculator with calculateCircleArea(), calculateSquareArea() like that, you will be needed to change this to add a calculateTriangleArea() method later. Instead you can have a Shape interface with the method area(). You create Circle, Square, and Triangle classes that each implement Shape.

An AreaCalculator can now use shape.area() without caring about which shape it is. If you add a Hexagon, you don’t modify the calculator—just implement a new Hexagon class.

The core logic stays stable. New functionality gets added on top, not inside old code. This reduces bugs and makes extension easy.

L: Liskov Substitution Principle (LSP)

Subclasses should be replaceable for their base classes without breaking the app.

Suppose you have a Vehicle class, and a Car extends it. Any code expecting a Vehicle should be able to work with a Car without issues.

This isn’t just about types—it's about behavioral compatibility. A subclass should honor the same “contract” as the parent class. A child class's implementation should not be stricter or restricting.

Bad LSP: If Bird has fly(), and Penguin extends Bird, but Penguin.fly() throws an error—your inheritance model is broken. Penguin isn't substitutable for Bird.

Use inheritance only when it makes logical sense. If the behaviors differ significantly, consider composition over inheritance. If a subclass violates expectations of the base class, the base class might be modeling the wrong abstraction.

“At its heart, LSP is about interfaces and contracts, and when to extend a class versus using another strategy such as composition to achieve your goal.” – A Stack Overflow answer

I: Interface Segregation Principle (ISP)

Don’t force classes to implement methods they don’t use.

Think of a TV remote with 30 buttons: HDMI, lights, fans, Blu-ray controls, volume, channels. But if you only ever use volume and channels, the rest is just clutter.

ISP says: Split large interfaces into smaller, specific ones.

If you have a TaskWorker interface with:

  • startTimer()

  • reportProgress()

  • sendEmailNotification()

  • printDocument()

…but a class only needs the first two, it shouldn’t be forced to implement email and printing methods too. Instead, split into:

  • WorkerInterface

  • EmailNotifierInterface

  • PrintableInterface

This improves flexibility, reduces unnecessary dependencies, and keeps things clean and understandable.

D: Dependency Inversion Principle (DIP)

High-level modules shouldn’t depend on low-level modules. Both should depend on abstractions.

Abstractions shouldn’t depend on details. Details should depend on abstractions.

Suppose you have an OrderProcessor that:

  • Uses a PayPalPaymentGateway to process payments.

  • Uses a PostgreSQLOrderSaver to save the order.

If you later switch to GooglePay and MongoDB, you’d have to modify OrderProcessor directly. That’s tight coupling.

Instead:

  • Create an abstract PaymentGateway interface with process_payment().

  • Create an abstract OrderSaver with save_order().

OrderProcessor should depend only on these abstractions.

Then you do:

  • PayPalPaymentGateway, GooglePayGateway → both implement PaymentGateway

  • PostgreSQLOrderSaver, MongoOrderSaver → both implement OrderSaver

Now, you can swap implementations easily—without touching business logic. This promotes loose coupling, testability, and flexibility.

DRY: Don't Repeat Yourself

This principle is probably the easiest to grasp and incredibly important.

It's pretty self-explanatory: every piece of knowledge must have a single, unambiguous and authoritative representation within a system.

Think about writing an essay. You don't write the exact same paragraph over and over again in different sections. Instead, you'd write that paragraph once and then refer to it later.

In programming, this means if you have the same piece of code (like a calculation, a validation rule, or a way to format data) appearing in multiple places, you should extract it into a function, a method, or a class, and then call that single piece of code wherever you need it.

But sometimes excessive usage of DRY can reduce readability. Also sometimes you may find yourself extracting similar looking code into same utility function, though they should be independent. Do keep that in mind.

KISS: Keep It Simple, Stupid

This is a classic and very practical advice.

The idea is that simplicity should be the key goal in design and unnecessary complexity should always be avoided.

When you're designing or writing code, avoid the urge to over-engineer. It can also be applied to the naming conventions, logics of functions, comments, almost everywhere. It can also be stated as - when even someone stupid looks at your program, they should still be able to understand what it's doing.

This also means that, don't spend a lot of time making something super-efficient if it's not a bottleneck right now. Don't use a complex design pattern if a simple function call will suffice. And if you find a part of your code becoming overly complex, break it down into smaller, simpler pieces.

KISS says that simple code is easier to understand, easier to debug and less prone to error. And when you yourself or someone else has to work with your code later, it makes both your lives easier.

YAGNI: You Aren't Gonna Need It

The name says it all. Do not add functionality until it's actually required.

Think of it like packing for a trip. You might be tempted to pack all sorts of "just in case" items. Before you know it, you have a massive, heavy suitcase, and you probably won't use half the stuff in it.

It's the same in software development. It's about resisting the temptation to add code or features that you think you might need in the future, but aren't explicitly required right now for the current problem you're solving.

Adding extra code will waste time and effort, it is harder to understand later, bloats the code base and increases the risk of bugs just because you made assumptions about how the software will evolve without concrete requirements. So always follow YAGNI unless you know with high certainty that extra work will be helpful in the near future.

How to know when to violate a principle

"Pragmatism over Purity"

Before you ever deviate, ask yourself:

  1. What problem does violating this principle solve right now? (e.g., "It saves us 2 days of development time for a critical deadline.")

  2. What are the long-term consequences of this violation? (e.g., "The code will be harder to change in this one spot, but we'll likely rewrite this module anyway in 2 or 3 months.")

  3. Are these consequences acceptable given the current situation? (e.g., "Yes, because missing this deadline means losing the client.")

If you can't articulate clear, justifiable answers to these questions, then you probably shouldn't violate the principle.

When can you violate SOLID?

  • When responsibilities are tightly coupled and always change together in smaller applications, you may break SRP.

  • For extending very small and stable modules, the overhead of introducing complex abstractions for trivial changes often outweighs the benefit of changing the module. If the code is already simple, and the change is small and unlikely to break anything else, you may break OCP.

  • Also when there is a fundamental change in a core component, it is not an extension but re-architecture. You will need to break OCP.

  • When you implement interfaces from third party libraries, you may need to implement methods you don't use, potentially breaking ISP.

  • You may also need to calculate the risk of breaking either YAGNI or OCP or find a balance in the initial stages of development as YAGNI says don't build for future while OCP says keep the future in mind when building.

  • You may also break all of SOLID principles during rapid prototyping with throwaway code.

When can you violate DRY?

  • Sometimes, two pieces of code look identical now but are logically independent and will likely evolve differently. So you write it twice, and if the third time the logic is identical, then you abstract.

  • If extracting a very small, simple piece of logic makes the code harder to follow and it is clearly understood inline, then repeating it might be more readable.

When can you violate KISS?

  • Only when it is performance critical. Otherwise, always try to follow it.

When can you violate YAGNI?

  • When you know a future requirement with high certainty.

  • When changing things in future takes considerably more effort than doing it now.

Final Thoughts

Like already said before, these are principles - not strict laws. They’re tools to help you write better software.

SOLID helps you avoid fragility and rigidity

DRY helps you avoid redundancy

KISS helps you avoid complexity

Try to understand them. Apply them when they make sense. Be cautious when breaking them.