There’s a moment in every developer’s life where the code still works… but touching it feels illegal.
You open a file. It’s 900 lines. You scroll. You scroll more. Somewhere in the middle is the one line you need to change, and you already know whatever you touch is about to break three unrelated things.
That’s usually when people start talking about SOLID principles.
Not because they’re fancy. Mostly because they’re tired.
SOLID is just five ideas that try to stop your codebase from turning into that file.
Let me walk through them the way they finally clicked for me.
Single Responsibility Principle - or, “please pick a personality”
SRP says a class should have one reason to change.
Not one method. Not three methods. One job.
The easiest way to violate this is by building “manager” classes that slowly start doing… everything.
You start with something innocent like OrderService. Then it validates orders. Then it calculates totals. Then it saves to the database. Then it sends emails. Then it talks to a payment gateway. Now it’s basically the whole company.
It often looks like this:
class OrderService:
def process_order(self, order):
# validate
if not order.items:
raise Exception("Order is empty")
# calculate price
total = 0
for item in order.items:
total += item.price * item.quantity
# save to database
print("Saving order to database...")
# send email
print("Sending confirmation email...")
return totalThis works. Right up until literally anything changes.
New pricing rules? In here. New database? In here. New email service? In here.
Now one class has five different reasons to be edited. That’s where bugs start breeding.
Breaking it up feels boring, but boring is good:
class OrderValidator:
def validate(self, order):
if not order.items:
raise Exception("Order is empty")
class PriceCalculator:
def calculate_total(self, order):
total = 0
for item in order.items:
total += item.price * item.quantity
return total
class OrderRepository:
def save(self, order):
print("Saving order to database...")
class EmailService:
def send_confirmation(self, order):
print("Sending confirmation email...")
class OrderProcessor:
def __init__(self, validator, calculator, repository, email_service):
self.validator = validator
self.calculator = calculator
self.repository = repository
self.email_service = email_service
def process(self, order):
self.validator.validate(order)
total = self.calculator.calculate_total(order)
self.repository.save(order)
self.email_service.send_confirmation(order)
return totalNothing clever happened here. We just stopped mixing unrelated problems.
SRP isn’t about being “clean.” It’s about not creating a class that future-you is scared to open.
Open/Closed Principle - or, “stop reopening old wounds”
OCP says your code should be open for extension, closed for modification.
Which sounds dramatic, but really just means: stop editing stable code every time a new case shows up.
The classic way people start is this:
class Invoice:
def get_total(self, region, amount):
if region == "india":
return amount + amount * 0.18
elif region == "us":
return amount + amount * 0.08
elif region == "uk":
return amount + amount * 0.12
else:
return amountIt’s fine. Until Germany exists. Then France. Then UAE. Then someone says “tax holidays.” Now this function is a crime scene.
OCP pushes you toward a shape where new behavior is added, not jammed into old logic.
from abc import ABC, abstractmethod
class TaxCalculator(ABC):
@abstractmethod
def calculate_tax(self, amount):
pass
class IndiaTaxCalculator(TaxCalculator):
def calculate_tax(self, amount):
return amount * 0.18
class USTaxCalculator(TaxCalculator):
def calculate_tax(self, amount):
return amount * 0.08
class UKTaxCalculator(TaxCalculator):
def calculate_tax(self, amount):
return amount * 0.12
class Invoice:
def __init__(self, amount, tax_calculator):
self.amount = amount
self.tax_calculator = tax_calculator
def get_total(self):
return self.amount + self.tax_calculator.calculate_tax(self.amount)Now when Germany shows up, nobody panics:
class GermanyTaxCalculator(TaxCalculator):
def calculate_tax(self, amount):
return amount * 0.15No edits to working logic. Just a new piece added.
OCP isn’t about never touching code. It’s about not constantly poking the parts that already survived production.
Liskov Substitution Principle - or, “don’t lie with inheritance”
LSP is the one people nod at and then violate for the rest of their lives.
It says: if something is a subtype, you should be able to use it anywhere its parent is used without weirdness.
Not “without compile errors.” Without surprises.
The famous example:
class Rectangle:
def set_width(self, w):
self.width = w
def set_height(self, h):
self.height = h
def area(self):
return self.width * self.height
class Square(Rectangle):
def set_width(self, w):
self.width = w
self.height = w
def set_height(self, h):
self.height = h
self.width = hLooks harmless.
Then this happens:
def print_area(rectangle):
rectangle.set_width(5)
rectangle.set_height(10)
print(rectangle.area()) # expected 50
r = Square()
print_area(r)Output: 100.
Nothing crashed. Nothing warned you. Your program just quietly stopped being true.
That’s an LSP violation. The square changed the meaning of what “set width” and “set height” were supposed to do.
Good LSP code is usually boring:
class Notification:
def send(self):
print("Sending notification...")
class EmailNotification(Notification):
def send(self):
print("Sending email...")
class SMSNotification(Notification):
def send(self):
print("Sending SMS...")def notify(notification):
notification.send()
notify(EmailNotification())
notify(SMSNotification())No special rules. No type checks. No “except when it’s actually a…”.
If your subclasses force calling code to tiptoe, LSP is already broken.
Interface Segregation Principle - or, “fat interfaces are how suffering begins”
ISP says: don’t force classes to implement stuff they don’t use.
The smell is usually an interface that just keeps growing.
class AppUser(ABC):
def place_order(self): pass
def cancel_order(self): pass
def cook_food(self): pass
def deliver_order(self): passNow everyone is everything.
class Customer(AppUser):
def place_order(self): pass
def cancel_order(self): pass
def cook_food(self): pass # ??
def deliver_order(self): pass # ??At this point, the interface isn’t modeling reality. It’s just dumping methods.
Splitting by actual roles changes everything:
class CustomerInterface(ABC):
def place_order(self): pass
def cancel_order(self): pass
class KitchenInterface(ABC):
def cook_food(self): pass
class DeliveryInterface(ABC):
def deliver_order(self): passclass Customer(CustomerInterface):
def place_order(self): pass
def cancel_order(self): pass
class Chef(KitchenInterface):
def cook_food(self): pass
class DeliveryPartner(DeliveryInterface):
def deliver_order(self): passNow the code reads like the real world again.
ISP quietly keeps your system from turning into one giant, awkward “do-everything” shape.
Dependency Inversion Principle - or, “stop hard-coding your life choices”
DIP is the one that really changes how you design things.
Beginners wire logic directly to tools:
class MySQLDatabase:
def save(self, data):
print("Saving to MySQL...")
class OrderService:
def __init__(self):
self.db = MySQLDatabase()
def place_order(self, order):
self.db.save(order)Congrats, your business logic is now MySQL.
Tests get annoying. Changes get expensive. Everything gets sticky.
DIP says: depend on ideas, not on concrete stuff.
class Database(ABC):
def save(self, data):
pass
class MySQLDatabase(Database):
def save(self, data):
print("Saving to MySQL...")
class MongoDatabase(Database):
def save(self, data):
print("Saving to MongoDB...")
class OrderService:
def __init__(self, database):
self.database = database
def place_order(self, order):
self.database.save(order)service = OrderService(MongoDatabase())
service.place_order("order_1")Now the core logic doesn’t care what’s underneath.
Database, API, mock, file, future tech that doesn’t exist yet - all fine.
That’s what people really mean when they say “decoupled.”
Ending this like a normal human
SOLID isn’t about writing perfect code.
It’s about writing code that doesn’t fight you.
Code where adding a feature doesn’t feel like gambling. Where changing one thing doesn’t quietly corrupt three others. Where you don’t need a mental map just to fix a typo.
Most people don’t learn SOLID from books. They learn it from pain.
This is just what those lessons tend to look like once they settle.
