Software Design Principles
Heuristics for making code decisions. Not laws — principles that, when violated, should have a reason. Knowing when NOT to apply them is as important as applying them.
Heuristics for making code decisions. Not laws. Principles that, when violated, should have a reason. Knowing when NOT to apply them is as important as applying them.
SOLID
S — Single Responsibility Principle
A class should have one reason to change.
Violation: UserService that handles auth, profiles, email, billing, and reports.
Fix: UserService, EmailService, BillingService — each with one concern.
O — Open/Closed Principle
Open for extension, closed for modification.
Adding new behaviour should not require changing existing code.
Violation: if/elif chains that grow with every new type.
Fix: polymorphism — new types implement an interface rather than adding branches.
L — Liskov Substitution Principle
Subtypes must be substitutable for their base types.
Violation: Square extends Rectangle but breaks width/height invariant.
Fix: don't use inheritance for "is-a" when behaviour differs.
I — Interface Segregation Principle
Clients shouldn't depend on interfaces they don't use.
Violation: IAnimal with fly(), swim(), run() — penguins can't fly.
Fix: ICanFly, ICanSwim, ICanRun — implement what's relevant.
D — Dependency Inversion Principle
High-level modules shouldn't depend on low-level modules. Both depend on abstractions.
Violation: OrderService directly imports PostgresOrderRepository.
Fix: OrderService depends on OrderRepository interface; inject concrete implementation.
DRY — Don't Repeat Yourself
# BAD — same logic in multiple places
def calculate_checkout_vat(subtotal: float) -> float:
return subtotal * 0.20
def calculate_invoice_vat(amount: float) -> float:
return amount * 0.20 # duplicated
def calculate_refund_vat(price: float) -> float:
return price * 0.20 # duplicated again
# GOOD — one authoritative source
VAT_RATE = Decimal("0.20")
def calculate_vat(amount: Decimal) -> Decimal:
return amount * VAT_RATE
# DRY violation to watch for: not just code duplication
# Also applies to: business logic, configuration, schema definitions
# Don't try to eliminate ALL duplication — three similar lines may be fine
# DRY is about knowledge (intent), not just textYAGNI — You Aren't Gonna Need It
# BAD — over-engineered for hypothetical future needs
class ProductRepository:
def __init__(self, db, cache=None, search_engine=None, event_bus=None,
analytics=None, audit_log=None):
self.db = db
self.cache = cache # "we might add caching later"
self.search_engine = search_engine # "we'll probably need search"
self.event_bus = event_bus # "we should emit events eventually"
...
# GOOD — build what you need now, add what you need when you need it
class ProductRepository:
def __init__(self, db):
self.db = db
def get(self, product_id: str) -> Product | None:
return self.db.query(Product).get(product_id)
# When caching is actually needed — add it
# When search is actually needed — add it
# Not beforeKISS — Keep It Simple, Stupid
# BAD — clever but hard to understand
def get_active_users(users):
return list(filter(lambda u: not u.deleted and u.last_seen >
datetime.now() - timedelta(days=30) and
(u.subscription_end is None or u.subscription_end > datetime.now()), users))
# GOOD — readable
def is_active_user(user: User) -> bool:
if user.deleted:
return False
seen_recently = user.last_seen > datetime.now() - timedelta(days=30)
has_valid_subscription = (user.subscription_end is None or
user.subscription_end > datetime.now())
return seen_recently and has_valid_subscription
def get_active_users(users: list[User]) -> list[User]:
return [u for u in users if is_active_user(u)]Coupling and Cohesion
Coupling: how much a module depends on other modules
High coupling: changing module A forces changes in B, C, D
Low coupling: modules communicate through stable interfaces
Target: low coupling
Cohesion: how related the responsibilities within a module are
High cohesion: all code in a class serves one clear purpose
Low cohesion: class does authentication, billing, email, and PDF generation
Target: high cohesion
The tension: increasing cohesion often reduces coupling
Extract the billing code → PaymentService (high cohesion)
Now OrderService doesn't know about payment internals (low coupling)
Metrics (informal):
Would a future engineer understand what this class does from its name?
How many places change when I change this class?
How many other classes does this class import?
Law of Demeter (Don't Talk to Strangers)
# BAD — reaching through the object graph
class OrderController:
def process(self, order_id: str):
order = self.order_repo.get(order_id)
address = order.user.profile.shipping_address.city # deep chain
tax_rate = self.tax_calculator.get_rate(order.user.profile.country.code)
# GOOD — each object knows its own data
class Order:
def shipping_city(self) -> str:
return self.user.shipping_city() # delegate to immediate neighbours
class User:
def shipping_city(self) -> str:
return self.profile.shipping_city()
class UserProfile:
def shipping_city(self) -> str:
return self.shipping_address.city
# Even better: pass what you need, not the whole object
def calculate_tax(amount: Decimal, country_code: str) -> Decimal:
rate = TAX_RATES[country_code]
return amount * rateComposition Over Inheritance
# BAD — deep inheritance hierarchy
class Animal: ...
class Mammal(Animal): ...
class Pet(Mammal): ...
class DomesticDog(Pet): ... # 4 levels deep — fragile
# GOOD — compose behaviours
from dataclasses import dataclass
@dataclass
class Dog:
name: str
can_bark: bool = True
can_swim: bool = False # some dogs can, some can't
is_domestic: bool = True
# Or with protocols
class Swimmable(Protocol):
def swim(self) -> None: ...
class Barkable(Protocol):
def bark(self) -> None: ...
# Dog composes the behaviours it needs
# No inheritance requiredConnections
se-hub · cs-fundamentals/clean-code · cs-fundamentals/oop-patterns · cs-fundamentals/architecture-patterns-se · cs-fundamentals/tdd-se · cs-fundamentals/code-review
Open Questions
- What are the most common misapplications of this concept in production codebases?
- When should you explicitly choose not to use this pattern or technique?
Related reading