> Introduction
Software design is not only about writing code that works — it’s about creating systems that are maintainable, scalable, and easy to understand. As applications grow, poorly structured code quickly becomes difficult to modify or extend, leading to bugs and unnecessary complexity.
To prevent this, developers rely on design principles: guidelines that help us write cleaner, more adaptable code. Among these, the most widely known are the SOLID principles.
> The SOLID Principles
Single Responsibility Principle (SRP)
A class should have only one reason to change. This avoids classes that are overloaded with multiple responsibilities.Open/Closed Principle (OCP)
Software entities should be open for extension but closed for modification. We should be able to add new features without rewriting existing code.Liskov Substitution Principle (LSP)
Subtypes must be substitutable for their base types without altering the correctness of the program.Interface Segregation Principle (ISP)
Clients should not be forced to depend on methods they don’t use. Interfaces should be small and focused.Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions.
> Real-World Example: A Notification System
Let’s imagine an application that sends notifications via different channels: email, SMS, and push.
❌ Poor Design (Violates SRP and OCP)
class NotificationService:
def send_notification(self, message: str, channel: str):
if channel == "email":
print(f"Sending EMAIL: {message}")
elif channel == "sms":
print(f"Sending SMS: {message}")
elif channel == "push":
print(f"Sending PUSH: {message}")
Problems:
- SRP Violation: One class handles multiple notification types.
- OCP Violation: Adding a new channel requires editing this class.
- Hard to Test: Logic for different responsibilities is mixed together.
✅ Improved Design (Applies SOLID in Python)
from abc import ABC, abstractmethod
# Abstraction (DIP, OCP)
class Notifier(ABC):
@abstractmethod
def send(self, message: str) -> None:
pass
# Concrete implementations (SRP: one responsibility each)
class EmailNotifier(Notifier):
def send(self, message: str) -> None:
print(f"Sending EMAIL: {message}")
class SMSNotifier(Notifier):
def send(self, message: str) -> None:
print(f"Sending SMS: {message}")
class PushNotifier(Notifier):
def send(self, message: str) -> None:
print(f"Sending PUSH: {message}")
# Service that depends on abstraction (DIP)
class NotificationService:
def __init__(self, notifier: Notifier):
self.notifier = notifier
def notify(self, message: str) -> None:
self.notifier.send(message)
# Example usage
if __name__ == "__main__":
email_service = NotificationService(EmailNotifier())
sms_service = NotificationService(SMSNotifier())
push_service = NotificationService(PushNotifier())
email_service.notify("Welcome to our platform!")
sms_service.notify("Your OTP is 123456")
push_service.notify("You have a new message")
Why This Design is Better
- SRP: Each class has a single job (email, SMS, or push).
- OCP: Adding a new channel (e.g., SlackNotifier) requires no modification to existing code.
- LSP: Any Notifier subclass can replace another without breaking the program.
- DIP: NotificationService depends on the abstract Notifier, not concrete implementations.
- Testability: Each notifier can be tested independently.
> Additional Design Concepts
While SOLID is a cornerstone, there are other important design principles:
- DRY (Don’t Repeat Yourself): Avoid code duplication by extracting reusable components.
- KISS (Keep It Simple, Stupid): Simpler solutions are easier to maintain and less error-prone.
- YAGNI (You Aren’t Gonna Need It): Don’t add functionality until it’s actually required.
- Composition Over Inheritance: Favor combining objects over deep inheritance hierarchies for flexibility.
Together, these principles help prevent code bloat, reduce bugs, and improve collaboration in large teams.
> GitHub Repository with Automation
A working implementation of this notification system (including tests and CI/CD) is available here:
Software_design_principles_python
The repository contains:
- Source code (src/)
- Unit tests (tests/)
- A GitHub Actions workflow (.github/workflows/python-app.yml) for continuous testing
> Conclusion
Design principles are more than abstract theory: they shape how maintainable and extendable our systems are. By applying principles like SOLID, DRY, and KISS to something as simple as a notification system, we get code that is flexible, testable, and future-proof.
When applied consistently, these principles enable developers to write software that can evolve gracefully — even as requirements change or systems grow.
Top comments (0)