Core principles that guide all development under SDP. These are non-negotiable.
| Principle | One-liner | Violation Example |
|---|---|---|
| SRP | One class = one reason to change | UserService does auth, email, and logging |
| OCP | Extend, don't modify | Adding if type == "new" everywhere |
| LSP | Subtypes must be substitutable | Square breaks Rectangle.setWidth() |
| ISP | Small, focused interfaces | IWorker with work() + eat() |
| DIP | Depend on abstractions | OrderService imports MySQLDatabase |
| DRY | Don't repeat yourself | Same validation logic in 5 places |
| KISS | Keep it simple | Regex for simple string check |
| YAGNI | Build only what's needed | Adding "future" config options |
| TDD | Tests first, then code | Writing tests after implementation |
A class should have only one reason to change.
# BAD: Multiple responsibilities
class UserService:
def authenticate(self, email: str, password: str) -> User: ...
def send_welcome_email(self, user: User) -> None: ...
def generate_report(self, user: User) -> str: ...
def log_activity(self, user: User, action: str) -> None: ...
# GOOD: Separated concerns
class AuthService:
def authenticate(self, email: str, password: str) -> User: ...
class EmailService:
def send_welcome(self, user: User) -> None: ...
class ReportGenerator:
def generate_user_report(self, user: User) -> str: ...
class ActivityLogger:
def log(self, user: User, action: str) -> None: ...AI Prompt: "Does this class have more than one reason to change? If yes, split it."
Open for extension, closed for modification.
# BAD: Modifying existing code for new types
class PaymentProcessor:
def process(self, payment_type: str, amount: float) -> None:
if payment_type == "credit_card":
self._process_credit_card(amount)
elif payment_type == "paypal":
self._process_paypal(amount)
elif payment_type == "crypto": # Adding new type = modifying class
self._process_crypto(amount)
# GOOD: Extending via new classes
class PaymentProcessor(Protocol):
def process(self, amount: float) -> None: ...
class CreditCardProcessor:
def process(self, amount: float) -> None: ...
class PayPalProcessor:
def process(self, amount: float) -> None: ...
class CryptoProcessor: # New type = new class, no modification
def process(self, amount: float) -> None: ...AI Prompt: "Can I add this feature without modifying existing code?"
Subtypes must be substitutable for their base types.
# BAD: Subtype breaks parent contract
class Rectangle:
def set_width(self, width: int) -> None:
self.width = width
def set_height(self, height: int) -> None:
self.height = height
class Square(Rectangle): # Violates LSP!
def set_width(self, width: int) -> None:
self.width = width
self.height = width # Unexpected side effect
def set_height(self, height: int) -> None:
self.width = height # Unexpected side effect
self.height = height
# GOOD: Proper abstraction
class Shape(Protocol):
def area(self) -> float: ...
class Rectangle:
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
class Square:
def __init__(self, side: float):
self.side = side
def area(self) -> float:
return self.side ** 2AI Prompt: "Can I replace the parent class with this subclass without breaking anything?"
Clients should not depend on interfaces they don't use.
# BAD: Fat interface
class IWorker(Protocol):
def work(self) -> None: ...
def eat(self) -> None: ...
def sleep(self) -> None: ...
class Robot: # Robots don't eat or sleep!
def work(self) -> None: ...
def eat(self) -> None:
raise NotImplementedError # Violation!
def sleep(self) -> None:
raise NotImplementedError # Violation!
# GOOD: Segregated interfaces
class IWorkable(Protocol):
def work(self) -> None: ...
class IFeedable(Protocol):
def eat(self) -> None: ...
class Human:
def work(self) -> None: ...
def eat(self) -> None: ...
class Robot:
def work(self) -> None: ... # Only implements what it needsAI Prompt: "Does this interface have methods that some implementations won't use?"
Depend on abstractions, not concretions.
# BAD: High-level depends on low-level
class OrderService:
def __init__(self):
self.db = MySQLDatabase() # Direct dependency on concrete class
self.emailer = SmtpEmailer() # Direct dependency
def create_order(self, order: Order) -> None:
self.db.save(order)
self.emailer.send_confirmation(order)
# GOOD: Depend on abstractions
class OrderRepository(Protocol):
def save(self, order: Order) -> None: ...
class EmailSender(Protocol):
def send_confirmation(self, order: Order) -> None: ...
class OrderService:
def __init__(
self,
repository: OrderRepository, # Abstract
emailer: EmailSender # Abstract
):
self.repository = repository
self.emailer = emailer
def create_order(self, order: Order) -> None:
self.repository.save(order)
self.emailer.send_confirmation(order)AI Prompt: "Am I importing concrete implementations or abstract interfaces?"
Every piece of knowledge must have a single, unambiguous representation.
# BAD: Repeated validation
def create_user(email: str) -> User:
if "@" not in email or "." not in email:
raise InvalidEmailError(email)
...
def update_email(user: User, email: str) -> None:
if "@" not in email or "." not in email: # Duplicated!
raise InvalidEmailError(email)
...
def send_invite(email: str) -> None:
if "@" not in email or "." not in email: # Duplicated!
raise InvalidEmailError(email)
...
# GOOD: Single source of truth
class Email:
def __init__(self, value: str):
if not self._is_valid(value):
raise InvalidEmailError(value)
self.value = value
@staticmethod
def _is_valid(value: str) -> bool:
return "@" in value and "." in value
def create_user(email: Email) -> User: ...
def update_email(user: User, email: Email) -> None: ...
def send_invite(email: Email) -> None: ...AI Prompt: "Is this logic duplicated elsewhere? Extract to a single place."
The simplest solution is usually the best.
# BAD: Over-engineered
def is_palindrome(s: str) -> bool:
import re
cleaned = re.sub(r'[^a-zA-Z0-9]', '', s).lower()
stack = []
for char in cleaned:
stack.append(char)
reversed_str = ''
while stack:
reversed_str += stack.pop()
return cleaned == reversed_str
# GOOD: Simple and clear
def is_palindrome(s: str) -> bool:
cleaned = ''.join(c.lower() for c in s if c.isalnum())
return cleaned == cleaned[::-1]AI Prompt: "Is there a simpler way to achieve this?"
Don't build features until they're actually needed.
# BAD: Building for hypothetical future
class Config:
def __init__(
self,
database_url: str,
cache_url: str | None = None, # "Might need cache later"
message_queue_url: str | None = None, # "Might need queue later"
feature_flags: dict[str, bool] | None = None, # "For A/B tests"
plugin_directory: str | None = None, # "For extensibility"
):
...
# GOOD: Build only what's needed NOW
class Config:
def __init__(self, database_url: str):
self.database_url = database_url
# Add cache_url when you actually need caching
# Add message_queue_url when you actually need a queueAI Prompt: "Do I need this right now, or am I building for a hypothetical future?"
Write tests before code. Red → Green → Refactor.
1. RED: Write a failing test
2. GREEN: Write minimal code to pass
3. REFACTOR: Improve code, tests still pass
# Step 1: RED - Write failing test
def test_user_can_be_created():
user = User(email="test@example.com", name="Test")
assert user.email == "test@example.com"
assert user.name == "Test"
# Result: NameError: name 'User' is not defined
# Step 2: GREEN - Minimal implementation
@dataclass
class User:
email: str
name: str
# Result: Test passes
# Step 3: REFACTOR - Add validation
@dataclass
class User:
email: str
name: str
def __post_init__(self) -> None:
if "@" not in self.email:
raise ValueError("Invalid email")
# Result: Tests still pass, code improved- Design: Tests force you to think about API first
- Documentation: Tests document expected behavior
- Confidence: Refactor without fear
- Coverage: 100% coverage by design
# BAD: Tests after implementation
class Calculator:
def add(self, a, b):
return a + b
# ... later ...
def test_add(): # Afterthought
assert Calculator().add(1, 2) == 3
# GOOD: Test first
def test_calculator_adds_two_numbers():
calc = Calculator()
assert calc.add(2, 3) == 5
# Now implement Calculator to make test passAI Prompt: "Write the test first, then implement the minimum code to pass it."
# BAD
def calc(d: list[dict]) -> float:
t = 0
for i in d:
t += i['a'] * i['p']
return t
# GOOD
def calculate_order_total(line_items: list[LineItem]) -> float:
total = 0.0
for item in line_items:
total += item.quantity * item.price
return total# BAD: 50-line function doing multiple things
def process_order(order: Order) -> None:
# validate (10 lines)
# calculate totals (10 lines)
# apply discounts (10 lines)
# update inventory (10 lines)
# send notification (10 lines)
# GOOD: Composed of focused functions
def process_order(order: Order) -> None:
validate_order(order)
totals = calculate_totals(order)
discounted = apply_discounts(totals)
update_inventory(order)
notify_customer(order)# BAD: Comment explaining confusing code
# Check if user is admin by looking at role field which is 1 for admin
if user.role == 1:
...
# GOOD: Self-documenting code
if user.is_admin():
...Dependencies point inward. Inner layers know nothing about outer layers.
┌─────────────────────────────────────────────────────┐
│ Presentation │
│ (Controllers, Views, API) │
├─────────────────────────────────────────────────────┤
│ Infrastructure │
│ (Database, External APIs, Frameworks) │
├─────────────────────────────────────────────────────┤
│ Application │
│ (Use Cases, Services) │
├─────────────────────────────────────────────────────┤
│ Domain │
│ (Entities, Business Rules) │
└─────────────────────────────────────────────────────┘
↑ Dependencies point INWARD (toward Domain)
Layer Rules:
| Layer | Can Import From | Cannot Import From |
|---|---|---|
| Domain | Nothing | Application, Infrastructure, Presentation |
| Application | Domain | Infrastructure, Presentation |
| Infrastructure | Domain, Application | Presentation |
| Presentation | Application | - |
Detailed guide: docs/concepts/clean-architecture/README.md
Before completing any workstream:
- SRP: Each class has one responsibility
- OCP: New features don't modify existing code
- LSP: Subtypes are substitutable
- ISP: Interfaces are minimal and focused
- DIP: Dependencies are abstract, not concrete
- DRY: No duplicated logic
- KISS: Simplest solution chosen
- YAGNI: No speculative features
- TDD: Tests written before implementation
- Clean Code: Readable, small functions, good names
- Clean Architecture: No layer violations
- Clean Architecture — Detailed layer guide
- Code Patterns — Implementation patterns
- Protocol — SDP guardrails and gates
Version: 1.0 Last Updated: 2026-01-12