SOLID Principles Explained with Examples
Learn the five SOLID principles with practical code examples: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion.
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
SOLID Principles Explained with Examples
Introduction
SOLID is an acronym for five design principles that make software designs more understandable, flexible, and maintainable. They were introduced by Robert C. Martin and are foundational to object-oriented design.
| Letter | Principle | Core Idea |
|---|---|---|
| S | Single Responsibility | A class should have one reason to change |
| O | Open/Closed | Open for extension, closed for modification |
| L | Liskov Substitution | Subtypes must be substitutable for their base types |
| I | Interface Segregation | Clients should not depend on interfaces they don’t use |
| D | Dependency Inversion | Depend on abstractions, not concretions |
S — Single Responsibility Principle (SRP)
A class should have only one reason to change.
# Bad: one class handles order logic AND reporting
class OrderManager:
def create_order(self, items):
...
def cancel_order(self, order_id):
...
def generate_monthly_report(self):
... # completely different concern
# Good: separate responsibilities
class OrderService:
def create_order(self, items):
...
def cancel_order(self, order_id):
...
class ReportGenerator:
def generate_monthly_report(self):
...
Why it matters: When a class has multiple responsibilities, changes to one responsibility can break another. Small, focused classes are easier to understand, test, and reuse. See Clean Code Principles for related practices.
O — Open/Closed Principle (OCP)
Software entities should be open for extension but closed for modification.
# Bad: modify existing code for every new payment method
class PaymentProcessor:
def process(self, payment):
if payment.type == "credit_card":
...
elif payment.type == "paypal":
...
elif payment.type == "crypto": # added later
...
# Good: extend via new classes
class PaymentMethod(ABC):
@abstractmethod
def process(self, amount):
pass
class CreditCardPayment(PaymentMethod):
def process(self, amount):
...
class PayPalPayment(PaymentMethod):
def process(self, amount):
...
class PaymentProcessor:
def __init__(self, method: PaymentMethod):
self.method = method
def process(self, amount):
self.method.process(amount)
# Adding a new method requires zero changes to existing code
class CryptoPayment(PaymentMethod):
def process(self, amount):
...
Why it matters: Modifying existing, working code introduces risk. By extending through new code, you preserve the stability of what already works. See Strategy Pattern for interchangeable behavior.
L — Liskov Substitution Principle (LSP)
Objects of a superclass should be replaceable with objects of its subclasses without breaking the program.
# Bad: Square violates LSP when used as Rectangle
class Rectangle:
def __init__(self, width, height):
self._width = width
self._height = height
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): # violates LSP
def set_width(self, w):
self._width = w
self._height = w # surprising side effect!
def set_height(self, h):
self._width = h # surprising side effect!
self._height = h
# A function expecting Rectangle behavior breaks with Square
def resize_rectangle(rect: Rectangle):
rect.set_width(5)
rect.set_height(4)
assert rect.area() == 20 # fails for Square!
# Good: model Square independently or as a value object
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
@dataclass(frozen=True)
class Square:
side: int
def area(self):
return self.side * self.side
Why it matters: Violating LSP leads to subtle bugs when polymorphism is used. The subclass must honor the contract of the parent class.
I — Interface Segregation Principle (ISP)
Clients should not be forced to depend on interfaces they do not use.
# Bad: one fat interface
class Worker(ABC):
@abstractmethod
def work(self):
pass
@abstractmethod
def eat(self): # robots don't eat
pass
@abstractmethod
def sleep(self): # robots don't sleep
pass
# Good: split into focused interfaces
class Workable(ABC):
@abstractmethod
def work(self):
pass
class Feedable(ABC):
@abstractmethod
def eat(self):
pass
class HumanWorker(Workable, Feedable):
def work(self): ...
def eat(self): ...
class RobotWorker(Workable):
def work(self): ...
# no need to implement eat() or sleep()
Why it matters: Fat interfaces create unnecessary coupling. When a client depends on methods it doesn’t use, changes to those methods can force unnecessary recompilation or retesting.
D — Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions.
# Bad: high-level module depends on concrete low-level module
class EmailService:
def send(self, to, subject, body):
... # SMTP logic
class NotificationManager: # high-level
def __init__(self):
self.email = EmailService() # hardcoded dependency
def notify_user(self, user):
self.email.send(user.email, "Hello", "...")
# Good: depend on abstraction
class NotificationChannel(ABC):
@abstractmethod
def send(self, to, subject, body):
pass
class EmailService(NotificationChannel):
def send(self, to, subject, body):
...
class SMSService(NotificationChannel):
def send(self, to, subject, body):
...
class NotificationManager:
def __init__(self, channel: NotificationChannel):
self.channel = channel
def notify_user(self, user):
self.channel.send(user.email, "Hello", "...")
# Easy to swap implementations without changing NotificationManager
email_notifier = NotificationManager(EmailService())
sms_notifier = NotificationManager(SMSService())
Why it matters: Depending on abstractions makes the system flexible. You can swap implementations (for testing, different environments, or new requirements) without touching the high-level business logic. See Factory Pattern for creating abstractions.
Applying SOLID Together
SOLID principles reinforce each other:
| Principle | Supports |
|---|---|
| SRP → | Makes OCP easier (smaller classes = easier to extend) |
| OCP → | Enables LSP (extension via inheritance/substitution) |
| LSP → | Enables polymorphism used by DIP |
| ISP → | Reduces the surface area of dependencies for DIP |
| DIP → | Enables OCP by allowing behavior injection |
Common Mistakes
- Creating a class per method to force SRP — not every function needs its own class
- Using OCP as an excuse for premature abstraction — YAGNI still applies
- Misapplying LSP to value objects that aren’t meant to be substitutable
- Splitting interfaces so finely that the system becomes fragmented
- Injecting dependencies everywhere including trivial, stable utilities
Frequently Asked Questions
Should I apply all SOLID principles to every class?
No. These are guidelines, not laws. Apply them where they reduce complexity and coupling. Small scripts and CRUD operations often don’t need full SOLID treatment.
Do SOLID principles apply only to OOP?
The concepts translate well to other models. Functional programming achieves DIP via higher-order functions, and SRP applies to modules and functions in any model. See design patterns for practical examples.
How do I convince my team to refactor toward SOLID?
Don’t refactor for the sake of the principles. Wait until a change is needed, then use the principles to guide a cleaner design. Show before/after comparisons in PRs.
Advanced Topics
Scenario: Refactoring a Payment Service with SOLID
// Before: violates SRP, OCP, and DIP
class PaymentProcessor {
processPayment(payment: Payment) {
// Validates (SRP violation)
if (!payment.amount || payment.amount <= 0) throw new Error("Invalid");
// Processes with Stripe (OCP violation - hardcoded)
const stripe = new StripeClient("sk_live_xxx");
const result = stripe.charge(payment.amount, payment.currency);
// Logs to console (SRP violation)
console.log("Payment processed:", result);
// Sends email (SRP violation)
const email = new EmailService();
email.send(payment.customerEmail, "Payment received");
// Saves to MySQL (DIP violation - hardcoded)
const db = new MySQLConnection("localhost", "payments");
db.query("INSERT INTO payments ...", result);
return result;
}
}
// After: SOLID-compliant refactor
// SRP: Each class has one responsibility
interface PaymentValidator {
validate(payment: Payment): void;
}
class PaymentValidatorImpl implements PaymentValidator {
validate(payment: Payment) {
if (!payment.amount || payment.amount <= 0)
throw new Error("Invalid amount");
if (!payment.currency || payment.currency.length !== 3)
throw new Error("Invalid currency");
}
}
// OCP: New gateways without modifying existing code
interface PaymentGateway {
charge(amount: number, currency: string): Promise<PaymentResult>;
}
class StripeGateway implements PaymentGateway {
constructor(private client: StripeClient) {}
async charge(amount: number, currency: string) {
return this.client.charge(amount, currency);
}
}
class PayPalGateway implements PaymentGateway {
constructor(private client: PayPalClient) {}
async charge(amount: number, currency: string) {
return this.client.createOrder(amount, currency);
}
}
// ISP: Segregated interfaces
interface PaymentRepository {
save(result: PaymentResult): Promise<void>;
}
interface NotificationService {
notify(email: string, subject: string, body: string): Promise<void>;
}
interface Logger {
log(level: string, message: string, meta?: object): void;
}
// DIP: Depends on abstractions, not concretions
class PaymentProcessor {
constructor(
private validator: PaymentValidator,
private gateway: PaymentGateway,
private repo: PaymentRepository,
private notifier: NotificationService,
private logger: Logger
) {}
async processPayment(payment: Payment): Promise<PaymentResult> {
this.validator.validate(payment);
this.logger.log("info", "Processing payment", { amount: payment.amount });
const result = await this.gateway.charge(payment.amount, payment.currency);
await this.repo.save(result);
await this.notifier.notify(payment.customerEmail, "Payment received",
`Your payment of ${payment.amount} ${payment.currency} was processed.`);
this.logger.log("info", "Payment completed", { id: result.id });
return result;
}
}
// LSP: Any PaymentGateway implementation works
const processor = new PaymentProcessor(
new PaymentValidatorImpl(),
new StripeGateway(stripeClient), // or new PayPalGateway(paypalClient)
new PostgresPaymentRepository(db),
new EmailNotificationService(smtp),
new WinstonLogger()
);
// Testing: mock everything via interfaces
const testProcessor = new PaymentProcessor(
mockValidator, mockGateway, mockRepo, mockNotifier, mockLogger
);
// Each dependency is swappable and testable independently
How do SOLID principles apply to microservices?
SRP maps to service boundaries: each service owns one business capability. OCP means new features are new services or new endpoints, not modifications to existing ones. LSP ensures API contracts are respected across versions. ISP means clients get only the endpoints they need (BFF pattern). DIP means services depend on contracts (API specs), not implementations.
Related Resources
Clean Code Principles: Writing Maintainable Software
A practical guide to clean code: meaningful names, short functions, DRY, SOLID foundations, and habits that make codebases easier to read and maintain.
GuidePractical Design Patterns Guide
A guide to selecting and applying the right design pattern for common software engineering problems.
GuideDomain-Driven Design (DDD) — A Practical Guide
Learn DDD fundamentals: bounded contexts, entities, value objects, aggregates, and how to model complex business domains in code.
GuideClean Architecture
A practical guide to Uncle Bob's Clean Architecture: organize code into layers so that frameworks, UI, and databases are details, not dependencies.
GuideTest-Driven Development (TDD) — A Practical Workflow
Learn TDD step by step: write a failing test, make it pass, refactor. Red-Green-Refactor with real examples in Python, JavaScript, and Java.