What is SOLID?
SOLID is a set of software design guidelines — not hard rules, but principles that guide how we organise our code.
The goal is simple: as your codebase grows and your team scales, things should get easier to change, not harder. SOLID is what makes that possible.
Five principles. One goal. Let’s walk through each one with a real, working example.
The Example
We’re building an order processing system for a ticket marketplace. When a customer buys a ticket, the system needs to:
- Process the payment
- Save the order to the database
- Send a confirmation to the buyer
- Generate an invoice
There are three order types — Standard, VIP (with SMS notification), and Resale (with seller notification).
We’ll start with the naive implementation and fix it one principle at a time.
The Naive Version
One class. Everything inside it. No abstractions, no separation.
import enum
import uuid
from dataclasses import dataclass
class OrderType(enum.Enum):
STANDARD = 1
VIP = 2
RESALE = 3
@dataclass
class Order:
event_name: str
start_seat: str
end_seat: str
order_type: int
customer_id: uuid.UUID
seller_id: uuid.UUID
seller_email: str
order_id: uuid.UUID = uuid.uuid4()
class OrderProcessingSystem:
def save_order(self, order: Order):
print(f"Saving order {order.order_id} to database")
def process_payment(self, order: Order):
print(f"Processing payment for order {order.order_id}")
def send_confirmation(self, order: Order):
print(f"Sending confirmation for order {order.order_id}")
def generate_invoice(self, order: Order):
print(f"Generating invoice for order {order.order_id}")
def process(self, order: Order):
self.process_payment(order)
self.save_order(order)
self.send_confirmation(order)
self.generate_invoice(order)
Enter fullscreen mode Exit fullscreen mode
This works. But as requirements grow, this class becomes a nightmare to maintain. Let’s fix it, one principle at a time.
S — Single Responsibility Principle
A class, function, or method should have one and only one reason to change.
OrderProcessingSystem has four responsibilities — payment, persistence, confirmation, and invoice generation. That’s four reasons to change. If the invoice format changes, you touch this class. If the payment gateway changes, you touch this class. If the email template changes, you touch this class.
The fix — split into focused classes:
import enum
import uuid
from dataclasses import dataclass
class OrderType(enum.Enum):
STANDARD = 1
VIP = 2
RESALE = 3
@dataclass
class Order:
event_name: str
start_seat: str
end_seat: str
order_type: int
customer_id: uuid.UUID
seller_id: uuid.UUID
seller_email: str
order_id: uuid.UUID = uuid.uuid4()
class InventoryManager:
def save_order(self, order: Order):
print(f"Save order {order.order_id} for customer {order.customer_id}")
class PaymentManager:
def process_payment(self, order: Order):
print(f"Process payment for order {order.order_id}")
class InvoiceGenerator:
def generate_invoice(self, order: Order):
print(f"Generate invoice for order {order.order_id}")
class TicketConfirmation:
def send_confirmation(self, order: Order):
print(f"Send confirmation for order {order.order_id} to customer {order.customer_id}")
class OrderProcessor:
def process(self, order: Order):
PaymentManager().process_payment(order)
InventoryManager().save_order(order)
TicketConfirmation().send_confirmation(order)
InvoiceGenerator().generate_invoice(order)
Enter fullscreen mode Exit fullscreen mode
Now each class has one responsibility and one reason to change. OrderProcessor is a clean orchestrator — it coordinates the flow but holds no logic itself.
O — Open/Closed Principle
A class should be open for extension but closed for modification.
Our three order types have different flows:
- Standard — payment, save, confirmation, invoice
- VIP — all of the above + SMS notification
- Resale — all of the above + notify the seller
The naive fix is if/else blocks inside OrderProcessor.process():
def process(self, order: Order):
if order.order_type == 1:
# standard flow
elif order.order_type == 2:
# VIP flow
elif order.order_type == 3:
# resale flow
Enter fullscreen mode Exit fullscreen mode
Every new order type means opening OrderProcessor and adding another elif. That’s an OCP violation — you’re modifying existing, working code to add new behaviour.
The fix — an abstract base class and a type map:
from abc import ABC, abstractmethod
class OrderCategorySchema(ABC):
@abstractmethod
def payment_processor(self, order: Order): pass
@abstractmethod
def save_order(self, order: Order): pass
@abstractmethod
def send_confirmation(self, order: Order): pass
@abstractmethod
def generate_invoice(self, order: Order): pass
def process(self, order: Order):
self.payment_processor(order)
self.save_order(order)
self.send_confirmation(order)
self.generate_invoice(order)
class StandardOrder(OrderCategorySchema):
def payment_processor(self, order): PaymentManager().process_payment(order)
def save_order(self, order): InventoryManager().save_order(order)
def send_confirmation(self, order): TicketConfirmation().send_confirmation(order)
def generate_invoice(self, order): InvoiceGenerator().generate_invoice(order)
class VipOrder(OrderCategorySchema):
def payment_processor(self, order): PaymentManager().process_payment(order)
def save_order(self, order): InventoryManager().save_order(order)
def send_confirmation(self, order): TicketConfirmation().send_confirmation(order)
def generate_invoice(self, order): InvoiceGenerator().generate_invoice(order)
def process(self, order: Order):
self.payment_processor(order)
self.save_order(order)
self.send_confirmation(order)
print(f"Sending SMS to customer {order.customer_id}")
self.generate_invoice(order)
class ResaleOrder(OrderCategorySchema):
def payment_processor(self, order): PaymentManager().process_payment(order)
def save_order(self, order): InventoryManager().save_order(order)
def send_confirmation(self, order): TicketConfirmation().send_confirmation(order)
def generate_invoice(self, order): InvoiceGenerator().generate_invoice(order)
def process(self, order: Order):
self.payment_processor(order)
self.save_order(order)
self.send_confirmation(order)
print(f"Notifying seller {order.seller_id} at {order.seller_email}")
self.generate_invoice(order)
type_map = {
1: StandardOrder,
2: VipOrder,
3: ResaleOrder
}
class OrderProcessor:
def process(self, order: Order):
order_class = type_map[order.order_type]
order_class().process(order)
Enter fullscreen mode Exit fullscreen mode
Adding a new order type tomorrow is just a new class and one entry in type_map. OrderProcessor never changes.
L — Liskov Substitution Principle
A child class should be fully substitutable for its parent. Wherever you use the parent, the child must work correctly — no surprises, no broken contracts.
With our current implementation, OrderProcessor calls process() on whatever OrderCategorySchema subclass it receives. Let’s verify all three are truly substitutable:
schemas = [StandardOrder(), VipOrder(), ResaleOrder()]
order = Order('Event', 'A1', 'A2', 1, uuid.uuid4(), uuid.uuid4(), '[email protected]')
for schema in schemas:
schema.process(order) # all three work correctly — LSP satisfied ✅
Enter fullscreen mode Exit fullscreen mode
LSP is violated when a subclass throws NotImplementedError, silently does nothing, or changes the behaviour a caller depends on. The classic example is a Penguin inheriting from a Bird class that has a fly() method. Penguins can’t fly — so Penguin.fly() either crashes or does nothing. The contract is broken.
In our case, every order type honours the process() contract. LSP satisfied.
I — Interface Segregation Principle
An interface should only contain methods that are genuinely related to each other.
Imagine a new order type arrives — DigitalTicketOrder. It’s an online-only ticket. No physical seat, no PDF invoice needed.
With our current OrderCategorySchema, DigitalTicketOrder is forced to implement generate_invoice() even though it has no business generating one. That’s an ISP violation — the interface has a method that not all implementors need.
The fix — split into focused interfaces:
from abc import ABC, abstractmethod
class Notificator(ABC):
@abstractmethod
def notify(self, order: Order): pass
class SMSNotification(Notificator):
def notify(self, order: Order):
print(f"Send SMS to customer {order.customer_id}")
class NotifySeller(Notificator):
def notify(self, order: Order):
print(f"Notify seller {order.seller_id} at {order.seller_email}")
class Invoiceable(ABC):
@abstractmethod
def generate_invoice(self, order: Order): pass
class PDFInvoiceGenerator(Invoiceable):
def generate_invoice(self, order: Order):
print(f"Generate PDF invoice for order {order.order_id}")
class TicketConfirmation(ABC):
@abstractmethod
def send_confirmation(self, order: Order): pass
class EmailTicketConfirmation(TicketConfirmation):
def send_confirmation(self, order: Order):
print(f"Email confirmation for order {order.order_id} to customer {order.customer_id}")
class WhatsappTicketConfirmation(TicketConfirmation):
def send_confirmation(self, order: Order):
print(f"WhatsApp confirmation for order {order.order_id} to customer {order.customer_id}")
class OrderCategorySchema(ABC):
@abstractmethod
def save_order(self, order: Order): pass
@abstractmethod
def process(self, order: Order): pass
class StandardOrder(OrderCategorySchema, PDFInvoiceGenerator):
def save_order(self, order): InventoryManager().save_order(order)
def process(self, order):
PaymentManager().process_payment(order)
self.save_order(order)
EmailTicketConfirmation().send_confirmation(order)
self.generate_invoice(order)
class VipOrder(OrderCategorySchema, PDFInvoiceGenerator, SMSNotification):
def save_order(self, order): InventoryManager().save_order(order)
def process(self, order):
PaymentManager().process_payment(order)
self.save_order(order)
EmailTicketConfirmation().send_confirmation(order)
self.notify(order)
self.generate_invoice(order)
class ResaleOrder(OrderCategorySchema, NotifySeller, PDFInvoiceGenerator):
def save_order(self, order): InventoryManager().save_order(order)
def process(self, order):
PaymentManager().process_payment(order)
self.save_order(order)
WhatsappTicketConfirmation().send_confirmation(order)
self.notify(order)
self.generate_invoice(order)
# DigitalTicketOrder — no invoice needed, doesn't implement Invoiceable
class DigitalTicketOrder(OrderCategorySchema):
def save_order(self, order): InventoryManager().save_order(order)
def process(self, order):
PaymentManager().process_payment(order)
self.save_order(order)
EmailTicketConfirmation().send_confirmation(order)
# no invoice — and nothing forces it to generate one ✅
Enter fullscreen mode Exit fullscreen mode
Each interface is focused and justified. Classes implement only what they need.
D — Dependency Inversion Principle
Classes should not depend directly on each other — they should both depend on an abstraction.
Look at what’s still hardcoded inside the order classes:
PaymentManager().process_payment(order) # concrete class
EmailTicketConfirmation().send_confirmation() # concrete class
Enter fullscreen mode Exit fullscreen mode
If the payment gateway changes from Stripe to Razorpay, you touch every order class. If confirmation switches from email to WhatsApp, same story. These are concrete dependencies baked into business logic.
The fix — define abstractions and inject from outside:
from abc import ABC, abstractmethod
import enum
import uuid
from dataclasses import dataclass
class OrderType(enum.Enum):
STANDARD = 1
VIP = 2
RESALE = 3
@dataclass
class Order:
event_name: str
start_seat: str
end_seat: str
order_type: int
customer_id: uuid.UUID
seller_id: uuid.UUID
seller_email: str
order_id: uuid.UUID = uuid.uuid4()
# ── Notification abstractions ─────────────────────────────────
class Notificator(ABC):
@abstractmethod
def notify(self, order: Order): pass
class SMSNotification(Notificator):
def notify(self, order: Order):
print(f"Send SMS to customer {order.customer_id}")
class NotifySeller(Notificator):
def notify(self, order: Order):
print(f"Notify seller {order.seller_id} at {order.seller_email}")
# ── Payment abstraction ───────────────────────────────────────
class PaymentService(ABC):
@abstractmethod
def process_payment(self, order: Order): pass
class StripeService(PaymentService):
def process_payment(self, order: Order):
print(f"Stripe: processing payment for order {order.order_id}")
class RazorPayService(PaymentService):
def process_payment(self, order: Order):
print(f"RazorPay: processing payment for order {order.order_id}")
# ── Inventory ─────────────────────────────────────────────────
class InventoryManager(ABC):
@abstractmethod
def save_order(self, order: Order): pass
@abstractmethod
def update_order(self, order: Order): pass
@abstractmethod
def delete_order(self, order: Order): pass
@abstractmethod
def create_order(self, order: Order): pass
class OrderManager(InventoryManager):
def save_order(self, order: Order):
print(f"Save order {order.order_id} for customer {order.customer_id}")
def update_order(self, order: Order):
print(f"Update order {order.order_id}")
def delete_order(self, order: Order):
print(f"Delete order {order.order_id}")
def create_order(self, order: Order):
print(f"Create order {order.order_id}")
# ── Invoice abstraction ───────────────────────────────────────
class Invoiceable(ABC):
@abstractmethod
def generate_invoice(self, order: Order): pass
class PDFInvoiceGenerator(Invoiceable):
def generate_invoice(self, order: Order):
print(f"Generate PDF invoice for order {order.order_id}")
# ── Confirmation abstraction ──────────────────────────────────
class TicketConfirmation(ABC):
@abstractmethod
def send_confirmation(self, order: Order): pass
class EmailTicketConfirmation(TicketConfirmation):
def send_confirmation(self, order: Order):
print(f"Email confirmation for order {order.order_id} to {order.customer_id}")
class WhatsappTicketConfirmation(TicketConfirmation):
def send_confirmation(self, order: Order):
print(f"WhatsApp confirmation for order {order.order_id} to {order.customer_id}")
# ── Order base ────────────────────────────────────────────────
class OrderCategorySchema(ABC):
def __init__(self, payment_gateway: PaymentService, confirmation_manager: TicketConfirmation):
self.payment_gateway = payment_gateway
self.confirmation_manager = confirmation_manager
@abstractmethod
def save_order(self, order: Order): pass
@abstractmethod
def process(self, order: Order): pass
# ── Concrete order types ──────────────────────────────────────
class StandardOrder(OrderCategorySchema, PDFInvoiceGenerator):
def save_order(self, order: Order):
OrderManager().save_order(order)
def process(self, order: Order):
self.payment_gateway.process_payment(order)
self.save_order(order)
self.confirmation_manager.send_confirmation(order)
self.generate_invoice(order)
class VipOrder(OrderCategorySchema, PDFInvoiceGenerator, SMSNotification):
def save_order(self, order: Order):
OrderManager().save_order(order)
def process(self, order: Order):
self.payment_gateway.process_payment(order)
self.save_order(order)
self.confirmation_manager.send_confirmation(order)
self.notify(order)
self.generate_invoice(order)
class ResaleOrder(OrderCategorySchema, NotifySeller, PDFInvoiceGenerator):
def save_order(self, order: Order):
OrderManager().save_order(order)
def process(self, order: Order):
self.payment_gateway.process_payment(order)
self.save_order(order)
self.confirmation_manager.send_confirmation(order)
self.notify(order)
self.generate_invoice(order)
# ── Wiring ────────────────────────────────────────────────────
type_map = {
1: StandardOrder,
2: VipOrder,
3: ResaleOrder
}
class OrderProcessor:
def process(self, order: Order):
order_class = type_map[order.order_type]
payment_service = RazorPayService()
confirmation_manager = WhatsappTicketConfirmation()
order_class(payment_service, confirmation_manager).process(order)
Enter fullscreen mode Exit fullscreen mode
Switching from RazorPay to Stripe, or from WhatsApp to email confirmation, is two lines in OrderProcessor. Zero changes to any order class.
# Switch payment and confirmation — zero order class changes
payment_service = StripeService()
confirmation_manager = EmailTicketConfirmation()
Enter fullscreen mode Exit fullscreen mode
OrderCategorySchema depends on PaymentService and TicketConfirmation abstractions — not on RazorPayService or WhatsappTicketConfirmation directly. DIP satisfied.
How the Principles Connect
Each principle solved a problem the previous one left behind:
SRP gave us small, focused classes. When a class does one thing, it becomes naturally easy to extend without modification — which is exactly what OCP requires.
OCP told us to extend via abstractions. But once you have inheritance hierarchies, LSP steps in and says: make sure your subclasses honour the parent’s contract.
LSP and ISP surface the same smell — a class implementing something it has no business implementing. LSP says fix your hierarchy. ISP says fix your interface. Two sides of the same coin.
DIP is the glue that ties everything together. SRP, OCP, LSP, and ISP all tell you to create abstractions. DIP says: now depend on those abstractions, not the concretions. This is what makes everything testable, swappable, and decoupled.
SRP creates focused classes → OCP makes them extendable → LSP keeps hierarchies honest → ISP keeps interfaces clean → DIP wires it all together.
The Tradeoffs
SOLID is a compass, not a religion. Over-applying it leads to premature abstraction — which is its own problem.
Principle Benefit Cost SRP Focused, easy to change in isolation More classes, more files, more navigation OCP Add features without breaking existing code More abstractions upfront — overkill if variants never appear LSP Subclasses are truly interchangeable Sometimes forces flat hierarchies or abandoning inheritance ISP Classes only know what they need Interface explosion — dozens of tiny interfaces DIP Testable, swappable, decoupled Wiring complexity — need a composition root
답글 남기기