Skip to main content
SOLID Principles

What is SOLID?

SOLID is an acronym for five design principles that help you write maintainable, flexible, and testable code:
LetterPrincipleOne-Liner
SSingle ResponsibilityOne class, one reason to change
OOpen/ClosedOpen for extension, closed for modification
LLiskov SubstitutionSubtypes must be substitutable for base types
IInterface SegregationMany specific interfaces > one general interface
DDependency InversionDepend on abstractions, not concretions

🚨 Code Smell Recognition

Before diving into principles, learn to recognize when code violates SOLID:
# 🚨 GOD CLASS - does everything
class UserManager:
    def create_user(self): pass
    def authenticate(self): pass
    def send_email(self): pass      # ❌ Email is not user management
    def generate_report(self): pass  # ❌ Reporting is separate concern
    def backup_data(self): pass      # ❌ Backup is infrastructure

# Signs of SRP violation:
# - Class has many unrelated methods
# - Class name includes "Manager", "Handler", "Processor", "Utils"
# - You need to change the class for different reasons

Case Study: E-commerce Order System

We’ll refactor a poorly designed order system step by step.

❌ Initial Bad Design (Violates ALL SOLID Principles)

class Order:
    def __init__(self, items, customer_email):
        self.items = items
        self.customer_email = customer_email
        self.status = "pending"
    
    def calculate_total(self):
        total = sum(item.price * item.quantity for item in self.items)
        # Apply discount based on items
        if total > 100:
            total *= 0.9
        return total
    
    def process_payment(self, card_number, cvv):
        # Direct payment processing
        print(f"Processing payment of ${self.calculate_total()}")
        # Credit card API call here
        return True
    
    def update_inventory(self):
        for item in self.items:
            # Direct database call
            print(f"Reducing stock for {item.name}")
    
    def send_confirmation_email(self):
        # Direct email sending
        print(f"Sending email to {self.customer_email}")
    
    def generate_invoice_pdf(self):
        # PDF generation logic
        print("Generating PDF invoice")
    
    def save_to_database(self):
        # Database save logic
        print("Saving order to database")
Problems:
  • One class does everything (violates SRP)
  • Hard to test (depends on external services)
  • Can’t change payment/email without modifying Order
  • Tightly coupled to implementations

S - Single Responsibility Principle

Each class should have only one reason to change.

✅ Refactored: Separate Responsibilities

# Order only handles order data
class Order:
    def __init__(self, items: List[OrderItem], customer: Customer):
        self.id = uuid.uuid4()
        self.items = items
        self.customer = customer
        self.status = OrderStatus.PENDING
        self.created_at = datetime.now()
    
    def calculate_total(self) -> Decimal:
        return sum(item.subtotal for item in self.items)

# Separate class for pricing/discounts
class PricingService:
    def __init__(self, discount_rules: List[DiscountRule]):
        self.discount_rules = discount_rules
    
    def calculate_final_price(self, order: Order) -> Decimal:
        total = order.calculate_total()
        for rule in self.discount_rules:
            total = rule.apply(total, order)
        return total

# Separate class for payment
class PaymentProcessor:
    def process(self, order: Order, payment_method: PaymentMethod) -> PaymentResult:
        pass

# Separate class for notifications
class NotificationService:
    def send_order_confirmation(self, order: Order) -> None:
        pass

# Separate class for inventory
class InventoryService:
    def reserve_items(self, items: List[OrderItem]) -> bool:
        pass
    
    def release_items(self, items: List[OrderItem]) -> None:
        pass

# Separate class for persistence
class OrderRepository:
    def save(self, order: Order) -> None:
        pass
    
    def find_by_id(self, order_id: UUID) -> Optional[Order]:
        pass

O - Open/Closed Principle

Open for extension, closed for modification.

✅ Extensible Discount System

from abc import ABC, abstractmethod

class DiscountRule(ABC):
    @abstractmethod
    def apply(self, total: Decimal, order: Order) -> Decimal:
        pass
    
    @abstractmethod
    def is_applicable(self, order: Order) -> bool:
        pass

class BulkDiscountRule(DiscountRule):
    """10% off for orders over $100"""
    def is_applicable(self, order: Order) -> bool:
        return order.calculate_total() > 100
    
    def apply(self, total: Decimal, order: Order) -> Decimal:
        if self.is_applicable(order):
            return total * Decimal("0.9")
        return total

class FirstOrderDiscountRule(DiscountRule):
    """15% off for first-time customers"""
    def __init__(self, order_repository: OrderRepository):
        self.order_repository = order_repository
    
    def is_applicable(self, order: Order) -> bool:
        past_orders = self.order_repository.find_by_customer(order.customer.id)
        return len(past_orders) == 0
    
    def apply(self, total: Decimal, order: Order) -> Decimal:
        if self.is_applicable(order):
            return total * Decimal("0.85")
        return total

class HolidayDiscountRule(DiscountRule):
    """20% off during holidays"""
    def __init__(self, holiday_calendar: HolidayCalendar):
        self.holiday_calendar = holiday_calendar
    
    def is_applicable(self, order: Order) -> bool:
        return self.holiday_calendar.is_holiday(order.created_at)
    
    def apply(self, total: Decimal, order: Order) -> Decimal:
        if self.is_applicable(order):
            return total * Decimal("0.8")
        return total

# Adding new discount = new class, no modification to existing code!
class LoyaltyDiscountRule(DiscountRule):
    """5% off for loyalty members"""
    def is_applicable(self, order: Order) -> bool:
        return order.customer.is_loyalty_member
    
    def apply(self, total: Decimal, order: Order) -> Decimal:
        if self.is_applicable(order):
            return total * Decimal("0.95")
        return total

L - Liskov Substitution Principle

Subtypes must be substitutable for their base types.

❌ Violation Example

class Bird:
    def fly(self):
        return "Flying"

class Penguin(Bird):
    def fly(self):
        raise Exception("Penguins can't fly!")  # Violates LSP!

✅ Correct Hierarchy

class Bird(ABC):
    @abstractmethod
    def move(self):
        pass

class FlyingBird(Bird):
    def move(self):
        return self.fly()
    
    def fly(self):
        return "Flying through the air"

class SwimmingBird(Bird):
    def move(self):
        return self.swim()
    
    def swim(self):
        return "Swimming in water"

class Sparrow(FlyingBird):
    pass

class Penguin(SwimmingBird):
    pass

# Now any Bird can be used interchangeably
def make_bird_move(bird: Bird):
    print(bird.move())  # Works for all birds!

✅ Payment Example

class PaymentMethod(ABC):
    @abstractmethod
    def pay(self, amount: Decimal) -> PaymentResult:
        pass
    
    @abstractmethod
    def supports_refund(self) -> bool:
        pass

class CreditCardPayment(PaymentMethod):
    def pay(self, amount: Decimal) -> PaymentResult:
        # Process credit card
        return PaymentResult(success=True)
    
    def supports_refund(self) -> bool:
        return True
    
    def refund(self, amount: Decimal) -> RefundResult:
        return RefundResult(success=True)

class CashOnDelivery(PaymentMethod):
    def pay(self, amount: Decimal) -> PaymentResult:
        # Mark as COD
        return PaymentResult(success=True, is_cod=True)
    
    def supports_refund(self) -> bool:
        return False  # No refund for COD until delivered

# Client code works with any PaymentMethod
def process_order(order: Order, payment: PaymentMethod):
    result = payment.pay(order.total)
    if result.success:
        order.mark_as_paid()

I - Interface Segregation Principle

Clients should not depend on interfaces they don’t use.

❌ Fat Interface

class Worker(ABC):
    @abstractmethod
    def work(self):
        pass
    
    @abstractmethod
    def eat(self):
        pass
    
    @abstractmethod
    def sleep(self):
        pass
    
    @abstractmethod
    def attend_meeting(self):
        pass
    
    @abstractmethod
    def write_report(self):
        pass

✅ Segregated Interfaces

class Workable(ABC):
    @abstractmethod
    def work(self):
        pass

class Feedable(ABC):
    @abstractmethod
    def eat(self):
        pass

class Sleepable(ABC):
    @abstractmethod
    def sleep(self):
        pass

class Manageable(ABC):
    @abstractmethod
    def attend_meeting(self):
        pass
    
    @abstractmethod
    def write_report(self):
        pass

# Human employee implements all
class Employee(Workable, Feedable, Sleepable, Manageable):
    def work(self):
        print("Working on tasks")
    
    def eat(self):
        print("Having lunch")
    
    def sleep(self):
        print("Sleeping at night")
    
    def attend_meeting(self):
        print("In meeting")
    
    def write_report(self):
        print("Writing report")

# Robot only implements what it can do
class Robot(Workable):
    def work(self):
        print("Processing tasks 24/7")

✅ E-commerce Example

# Instead of one huge OrderService interface
class OrderReader(ABC):
    @abstractmethod
    def find_by_id(self, order_id: UUID) -> Order:
        pass
    
    @abstractmethod
    def find_by_customer(self, customer_id: UUID) -> List[Order]:
        pass

class OrderWriter(ABC):
    @abstractmethod
    def save(self, order: Order) -> None:
        pass
    
    @abstractmethod
    def update(self, order: Order) -> None:
        pass

class OrderCanceller(ABC):
    @abstractmethod
    def cancel(self, order_id: UUID, reason: str) -> None:
        pass

# Services can depend on only what they need
class OrderDisplayService:
    def __init__(self, reader: OrderReader):  # Only needs read
        self.reader = reader

class CheckoutService:
    def __init__(self, writer: OrderWriter):  # Only needs write
        self.writer = writer

D - Dependency Inversion Principle

Depend on abstractions, not concretions.

❌ Tight Coupling

class OrderService:
    def __init__(self):
        self.db = PostgreSQLDatabase()  # Directly depends on implementation
        self.payment = StripePayment()
        self.email = SendGridEmail()

✅ Dependency Injection

# Abstractions
class Database(ABC):
    @abstractmethod
    def save(self, entity: Any) -> None:
        pass

class PaymentGateway(ABC):
    @abstractmethod
    def charge(self, amount: Decimal) -> PaymentResult:
        pass

class EmailService(ABC):
    @abstractmethod
    def send(self, to: str, subject: str, body: str) -> None:
        pass

# Implementations
class PostgreSQLDatabase(Database):
    def save(self, entity: Any) -> None:
        print("Saving to PostgreSQL")

class StripePaymentGateway(PaymentGateway):
    def charge(self, amount: Decimal) -> PaymentResult:
        print("Charging via Stripe")
        return PaymentResult(success=True)

# Service depends on abstractions
class OrderService:
    def __init__(
        self,
        database: Database,
        payment: PaymentGateway,
        email: EmailService
    ):
        self.database = database
        self.payment = payment
        self.email = email
    
    def place_order(self, order: Order) -> None:
        self.payment.charge(order.total)
        self.database.save(order)
        self.email.send(
            order.customer.email,
            "Order Confirmed",
            f"Your order {order.id} is confirmed"
        )

# Easy to swap implementations
service = OrderService(
    database=PostgreSQLDatabase(),
    payment=StripePaymentGateway(),
    email=SendGridEmailService()
)

# Easy to test with mocks
test_service = OrderService(
    database=MockDatabase(),
    payment=MockPayment(),
    email=MockEmail()
)

SOLID Summary

PrincipleOne-linerBenefit
Single ResponsibilityOne reason to changeEasier maintenance
Open/ClosedExtend without modifyingSafer changes
Liskov SubstitutionSubtypes are interchangeableReliable polymorphism
Interface SegregationSmall, focused interfacesLess coupling
Dependency InversionDepend on abstractionsFlexible, testable

🎯 Quick Decision Guide

When designing classes, ask yourself:
1

Single Responsibility Check

Question: Does this class have only one reason to change?Red Flag: Class name contains “Manager”, “Handler”, “Utils”, “Helper”Fix: Extract each responsibility into its own class
2

Open/Closed Check

Question: Can I add new behavior without modifying existing code?Red Flag: Long if-elif chains based on typeFix: Use polymorphism, Strategy pattern, or plugin architecture
3

Liskov Substitution Check

Question: Can I use any subtype wherever the parent is expected?Red Flag: Type checking before method calls, “not implemented” exceptionsFix: Redesign hierarchy or use composition instead
4

Interface Segregation Check

Question: Does every implementer need ALL methods in the interface?Red Flag: Empty stub implementations, methods throwing “not supported”Fix: Split into smaller, focused interfaces
5

Dependency Inversion Check

Question: Does this class depend on abstractions, not concretions?Red Flag: Using new or direct instantiation of dependenciesFix: Inject dependencies through constructor/parameters

💡 Interview Tips

During LLD interviews, explicitly mention when you’re applying SOLID:
  • “I’m separating payment processing into its own class for Single Responsibility”
  • “Using an interface here so we can add new payment methods without modifying existing code - that’s Open/Closed”
  • “I’ll inject the database as a dependency so it’s easy to test”
SOLID adds complexity. Don’t apply it to:
  • Simple scripts or one-off utilities
  • Classes with only 1-2 methods
  • Early prototypes (refactor later)
Apply SOLID when you expect:
  • Multiple developers
  • Long-term maintenance
  • Frequent changes/extensions
SOLID code is easy to test:
# With DIP, testing is trivial:
def test_order_service():
    mock_db = MockDatabase()
    mock_email = MockEmailService()
    service = OrderService(mock_db, mock_email)
    
    service.place_order(test_order)
    
    assert mock_db.save_called
    assert mock_email.send_called
Remember: SOLID principles are guidelines, not rules. Apply them where they add value. Over-engineering is also a problem! The goal is clean, maintainable code - not perfect adherence to principles.