Software Engineering Principles
Applies foundational engineering principles to guide daily development decisions, ensuring code is modular, maintainable, and resilient. This skill turns abstract best practices into concrete implementation choices.
TL;DR Checklist
When to Use
Use this skill when:
- Starting a new module, feature, or service from scratch
- Refactoring a tangled codebase to improve modularity and reduce coupling
- Conducting a code review where maintainability concerns arise
- Onboarding a developer and establishing engineering standards for the team
- Making architectural trade-offs between speed of delivery and long-term maintainability
When NOT to Use
Avoid this skill for:
- One-off scripts or throwaway prototypes (overhead outweighs benefit)
- Performance-critical inner loops where micro-optimizations dominate (profile first, apply principles after)
- Situations with hard real-time constraints where simplicity in execution matters more than modularity
Core Workflow
Identify Natural Boundaries — Analyze the problem domain to find logical modules. Each module should own its data and expose a minimal interface.
Checkpoint: Can you describe this module's purpose in one sentence without mentioning internal implementation details?
Apply Separation of Concerns — Split code into layers: data persistence, business rules, and I/O (API/UI). Dependencies flow inward; never outward.
Checkpoint: Does the business logic layer import anything from the data access or presentation layer? If yes, refactor.
Design for Testability — Inject dependencies rather than constructing them inside modules. Every public function should be callable with deterministic inputs.
Checkpoint: Can you unit-test this module's core logic without spinning up a database, network, or file system?
Enforce Defensive Boundaries — Validate all external inputs at the system perimeter (API endpoints, CLI arguments, configuration files). Fail fast with clear error messages.
Checkpoint: Are there any try/except blocks swallowing errors silently? If yes, replace with explicit validation or re-raise with context.
Apply YAGNI and KISS — Resist adding abstractions, generic interfaces, or "just in case" features. Implement only what is required for the current specification.
Checkpoint: Would removing this abstraction change any existing behavior? If not, remove it.
Review Coupling and Cohesion — Measure how tightly modules are connected (coupling) versus how focused each module is (cohesion). High coupling + low cohesion = technical debt.
Checkpoint: Can you swap out one module's implementation without touching more than three other modules?
Implementation Patterns
Pattern 1: Single Responsibility Modules
A module should have exactly one reason to change. Group related functionality together and keep unrelated concerns separate.
# ❌ BAD: Violates single responsibility — handles parsing, validation, DB access, and formatting
class OrderProcessor:
def process(self, raw_data):
parsed = json.loads(raw_data) # parsing concern
if not self.validate(parsed): # validation concern
return None
order_id = self.db.insert(parsed) # persistence concern
return f"Order {order_id} created" # formatting concern
# ✅ GOOD: Separated concerns into focused modules
from dataclasses import dataclass
@dataclass
class Order:
item_id: str
quantity: int
customer_email: str
class OrderParser:
"""Handles deserialization of raw data into Order objects."""
def parse(self, raw_data: str) -> Order: ...
class OrderValidator:
"""Validates business rules for an Order instance."""
def validate(self, order: Order) -> bool: ...
class OrderRepository:
"""Manages persistence of Order instances to the database."""
def save(self, order: Order) -> str: ...
class OrderService:
"""Orchestrates parsing, validation, and persistence."""
def __init__(self, parser: OrderParser, validator: OrderValidator, repository: OrderRepository):
self.parser = parser
self.validator = validator
self.repository = repository
def process(self, raw_data: str) -> str:
order = self.parser.parse(raw_data) # Step 1
if not self.validator.validate(order): # Step 2
raise ValueError("Order validation failed")
order_id = self.repository.save(order) # Step 3
return f"Order {order_id} created" # Step 4 (formatting belongs here as the final output)
Pattern 2: Defensive Programming with Explicit Validation
Never trust external input. Validate at system boundaries and fail loudly with actionable errors.
# ❌ BAD: Silent failure, implicit assumptions, no validation
def calculate_discount(price, code):
discounts = {"SAVE10": 0.10, "SAVE20": 0.20}
return price * (1 - discounts[code]) # KeyError if code is invalid, wrong type crashes silently
# ✅ GOOD: Explicit validation, clear error contracts, fails fast
from typing import Final
VALID_DISCOUNT_CODES: Final = {"SAVE10", "SAVE20"}
class ValidationError(Exception):
"""Raised when input violates business rules."""
pass
def calculate_discount(price: float, code: str) -> float:
"""Apply a discount code to a price. Returns the final price after discount.
Args:
price: Original price (must be positive).
code: Discount code (must be one of the valid codes).
Returns:
Final price as a float, rounded to 2 decimal places.
Raises:
ValidationError: If inputs are invalid or the code is unrecognized.
"""
if not isinstance(price, (int, float)) or price <= 0:
raise ValidationError(f"Price must be a positive number, got: {price}")
if not isinstance(code, str):
raise ValidationError(f"Discount code must be a string, got: {type(code).__name__}")
discount_rate = VALID_DISCOUNT_CODES.get(code)
if discount_rate is None:
raise ValidationError(f"Invalid discount code: '{code}'. Valid codes: {', '.join(sorted(VALID_DISCOUNT_CODES))}")
return round(price * (1 - discount_rate), 2)
Pattern 3: Dependency Injection for Testability
Inject dependencies through constructors or function parameters rather than importing them directly. This enables testing in isolation and swapping implementations.
# ❌ BAD: Tight coupling to external service — impossible to unit test without network access
class NotificationService:
def send(self, user_email: str, message: str) -> bool:
# Directly creates a connection every time
import smtplib
server = smtplib.SMTP("smtp.example.com")
server.login("app@example.com", "password") # credentials hardcoded
server.sendmail("noreply@example.com", user_email, message)
return True
# ✅ GOOD: Dependency injection — email sender is swapped at runtime (production vs. test)
from abc import ABC, abstractmethod
import smtplib
class EmailSender(ABC):
@abstractmethod
def send(self, to: str, subject: str, body: str) -> None: ...
class SmtpEmailSender(EmailSender):
def __init__(self, host: str = "smtp.example.com", port: int = 587):
self.host = host
self.port = port
def send(self, to: str, subject: str, body: str) -> None:
with smtplib.SMTP(self.host, self.port) as server:
server.starttls()
server.login("app@example.com", "password")
server.sendmail("noreply@example.com", to, f"Subject: {subject}\n\n{body}")
class InMemoryEmailSender(EmailSender):
"""No-op sender for testing. Stores sent emails in memory."""
def __init__(self):
self.sent_emails: list[dict] = []
def send(self, to: str, subject: str, body: str) -> None:
self.sent_emails.append({"to": to, "subject": subject, "body": body})
class NotificationService:
def __init__(self, sender: EmailSender):
self.sender = sender # Decoupled from concrete implementation
def notify(self, user_email: str, message: str) -> None:
if not user_email or "@" not in user_email:
raise ValidationError(f"Invalid email address: {user_email}")
self.sender.send(user_email, "Notification", message)
Constraints
MUST DO
- Give each module a single, clearly documented responsibility (one-sentence test)
- Validate all external inputs at system boundaries before they enter business logic
- Inject dependencies via constructor or function parameters; never instantiate collaborators inside methods
- Write unit tests that verify contracts with mocks/stubs — no real I/O in unit tests
- Prefer composition over inheritance for reusing behavior
- Document the why behind non-obvious decisions; don't document what the code does (the code speaks for itself)
- Keep functions small: if a function exceeds 30 lines, ask whether it's doing too much
MUST NOT DO
- Build abstractions for problems you don't have yet (YAGNI) — generic interfaces that are only ever used once are dead weight
- Use exceptions for control flow in performance-critical paths — validate first, then execute
- Silently swallow errors with bare
except: or pass blocks — always log or re-raise with context
- Hardcode configuration values (ports, endpoints, credentials) inside application logic — externalize to config files or environment variables
- Mix persistence queries with business logic in the same function — keep data access and domain rules separate
- Rely on implicit type coercion — use explicit type hints and runtime validation for public interfaces
Output Template
When applying this skill to review or implement code, produce:
- Architecture Assessment — How well does the code follow separation of concerns? List any violations.
- Coupling & Cohesion Score — Rate each module's cohesion (high/medium/low) and identify cross-module dependencies that could be reduced.
- Defensive Programming Gaps — Point out unvalidated inputs, silent error handling, or implicit assumptions.
- YAGNI/KISS Review — Flag abstractions, generic interfaces, or features that are not required by the current spec.
- Refactoring Recommendations — Concrete, ordered steps to improve modularity with estimated effort per step.
Related Skills
| Skill |
Purpose |
coding-code-review |
Apply these principles during peer review to catch architectural drift early |
coding-testing |
Complementary testing strategies that verify engineering contracts and module boundaries |
coding-error-handling |
Deep dive into error handling patterns that complement defensive programming |
coding-refactoring |
Practical techniques for untangling high-coupling, low-cohesion codebases |
Live References
Authoritative documentation links for this skill's domain. The model follows markdown links at load time to resolve external references and inline content.
1---2name: software-engineering-principles3description: Applies core software engineering principles (modularity, separation of concerns, defensive programming, YAGNI) to produce maintainable, robust, and scalable code.4license: MIT5---678910# Software Engineering Principles1112Applies foundational engineering principles to guide daily development decisions, ensuring code is modular, maintainable, and resilient. This skill turns abstract best practices into concrete implementation choices.1314## TL;DR Checklist1516- [ ] Verify each module has a single, well-defined responsibility17- [ ] Confirm separation between data access, business logic, and presentation layers18- [ ] Apply defensive programming: validate all inputs at system boundaries19- [ ] Resist feature creep — ask "do we need this now?" (YAGNI) before adding complexity20- [ ] Prefer simple, readable solutions over clever, compact ones (KISS)21- [ ] Write tests that verify contracts, not implementation details22- [ ] Document the *why*, not the *what*2324---2526## When to Use2728Use this skill when:2930- Starting a new module, feature, or service from scratch31- Refactoring a tangled codebase to improve modularity and reduce coupling32- Conducting a code review where maintainability concerns arise33- Onboarding a developer and establishing engineering standards for the team34- Making architectural trade-offs between speed of delivery and long-term maintainability3536---3738## When NOT to Use3940Avoid this skill for:4142- One-off scripts or throwaway prototypes (overhead outweighs benefit)43- Performance-critical inner loops where micro-optimizations dominate (profile first, apply principles after)44- Situations with hard real-time constraints where simplicity in execution matters more than modularity4546---4748## Core Workflow49501. **Identify Natural Boundaries** — Analyze the problem domain to find logical modules. Each module should own its data and expose a minimal interface.51 **Checkpoint:** Can you describe this module's purpose in one sentence without mentioning internal implementation details?52532. **Apply Separation of Concerns** — Split code into layers: data persistence, business rules, and I/O (API/UI). Dependencies flow inward; never outward.54 **Checkpoint:** Does the business logic layer import anything from the data access or presentation layer? If yes, refactor.55563. **Design for Testability** — Inject dependencies rather than constructing them inside modules. Every public function should be callable with deterministic inputs.57 **Checkpoint:** Can you unit-test this module's core logic without spinning up a database, network, or file system?58594. **Enforce Defensive Boundaries** — Validate all external inputs at the system perimeter (API endpoints, CLI arguments, configuration files). Fail fast with clear error messages.60 **Checkpoint:** Are there any `try/except` blocks swallowing errors silently? If yes, replace with explicit validation or re-raise with context.61625. **Apply YAGNI and KISS** — Resist adding abstractions, generic interfaces, or "just in case" features. Implement only what is required for the current specification.63 **Checkpoint:** Would removing this abstraction change any existing behavior? If not, remove it.64656. **Review Coupling and Cohesion** — Measure how tightly modules are connected (coupling) versus how focused each module is (cohesion). High coupling + low cohesion = technical debt.66 **Checkpoint:** Can you swap out one module's implementation without touching more than three other modules?6768---6970## Implementation Patterns7172### Pattern 1: Single Responsibility Modules7374A module should have exactly one reason to change. Group related functionality together and keep unrelated concerns separate.7576```python77# ❌ BAD: Violates single responsibility — handles parsing, validation, DB access, and formatting78class OrderProcessor:79 def process(self, raw_data):80 parsed = json.loads(raw_data) # parsing concern81 if not self.validate(parsed): # validation concern82 return None83 order_id = self.db.insert(parsed) # persistence concern84 return f"Order {order_id} created" # formatting concern8586# ✅ GOOD: Separated concerns into focused modules87from dataclasses import dataclass8889@dataclass90class Order:91 item_id: str92 quantity: int93 customer_email: str9495class OrderParser:96 """Handles deserialization of raw data into Order objects."""97 def parse(self, raw_data: str) -> Order: ...9899class OrderValidator:100 """Validates business rules for an Order instance."""101 def validate(self, order: Order) -> bool: ...102103class OrderRepository:104 """Manages persistence of Order instances to the database."""105 def save(self, order: Order) -> str: ...106107class OrderService:108 """Orchestrates parsing, validation, and persistence."""109 def __init__(self, parser: OrderParser, validator: OrderValidator, repository: OrderRepository):110 self.parser = parser111 self.validator = validator112 self.repository = repository113114 def process(self, raw_data: str) -> str:115 order = self.parser.parse(raw_data) # Step 1116 if not self.validator.validate(order): # Step 2117 raise ValueError("Order validation failed")118 order_id = self.repository.save(order) # Step 3119 return f"Order {order_id} created" # Step 4 (formatting belongs here as the final output)120```121122### Pattern 2: Defensive Programming with Explicit Validation123124Never trust external input. Validate at system boundaries and fail loudly with actionable errors.125126```python127# ❌ BAD: Silent failure, implicit assumptions, no validation128def calculate_discount(price, code):129 discounts = {"SAVE10": 0.10, "SAVE20": 0.20}130 return price * (1 - discounts[code]) # KeyError if code is invalid, wrong type crashes silently131132# ✅ GOOD: Explicit validation, clear error contracts, fails fast133from typing import Final134135VALID_DISCOUNT_CODES: Final = {"SAVE10", "SAVE20"}136137class ValidationError(Exception):138 """Raised when input violates business rules."""139 pass140141def calculate_discount(price: float, code: str) -> float:142 """Apply a discount code to a price. Returns the final price after discount.143 144 Args:145 price: Original price (must be positive).146 code: Discount code (must be one of the valid codes).147 148 Returns:149 Final price as a float, rounded to 2 decimal places.150 151 Raises:152 ValidationError: If inputs are invalid or the code is unrecognized.153 """154 if not isinstance(price, (int, float)) or price <= 0:155 raise ValidationError(f"Price must be a positive number, got: {price}")156 if not isinstance(code, str):157 raise ValidationError(f"Discount code must be a string, got: {type(code).__name__}")158 159 discount_rate = VALID_DISCOUNT_CODES.get(code)160 if discount_rate is None:161 raise ValidationError(f"Invalid discount code: '{code}'. Valid codes: {', '.join(sorted(VALID_DISCOUNT_CODES))}")162 163 return round(price * (1 - discount_rate), 2)164```165166### Pattern 3: Dependency Injection for Testability167168Inject dependencies through constructors or function parameters rather than importing them directly. This enables testing in isolation and swapping implementations.169170```python171# ❌ BAD: Tight coupling to external service — impossible to unit test without network access172class NotificationService:173 def send(self, user_email: str, message: str) -> bool:174 # Directly creates a connection every time175 import smtplib176 server = smtplib.SMTP("smtp.example.com")177 server.login("app@example.com", "password") # credentials hardcoded178 server.sendmail("noreply@example.com", user_email, message)179 return True180181# ✅ GOOD: Dependency injection — email sender is swapped at runtime (production vs. test)182from abc import ABC, abstractmethod183import smtplib184185class EmailSender(ABC):186 @abstractmethod187 def send(self, to: str, subject: str, body: str) -> None: ...188189class SmtpEmailSender(EmailSender):190 def __init__(self, host: str = "smtp.example.com", port: int = 587):191 self.host = host192 self.port = port193 194 def send(self, to: str, subject: str, body: str) -> None:195 with smtplib.SMTP(self.host, self.port) as server:196 server.starttls()197 server.login("app@example.com", "password")198 server.sendmail("noreply@example.com", to, f"Subject: {subject}\n\n{body}")199200class InMemoryEmailSender(EmailSender):201 """No-op sender for testing. Stores sent emails in memory."""202 def __init__(self):203 self.sent_emails: list[dict] = []204 205 def send(self, to: str, subject: str, body: str) -> None:206 self.sent_emails.append({"to": to, "subject": subject, "body": body})207208class NotificationService:209 def __init__(self, sender: EmailSender):210 self.sender = sender # Decoupled from concrete implementation211 212 def notify(self, user_email: str, message: str) -> None:213 if not user_email or "@" not in user_email:214 raise ValidationError(f"Invalid email address: {user_email}")215 self.sender.send(user_email, "Notification", message)216```217218---219220## Constraints221222### MUST DO223- Give each module a single, clearly documented responsibility (one-sentence test)224- Validate all external inputs at system boundaries before they enter business logic225- Inject dependencies via constructor or function parameters; never instantiate collaborators inside methods226- Write unit tests that verify contracts with mocks/stubs — no real I/O in unit tests227- Prefer composition over inheritance for reusing behavior228- Document the *why* behind non-obvious decisions; don't document what the code does (the code speaks for itself)229- Keep functions small: if a function exceeds 30 lines, ask whether it's doing too much230231### MUST NOT DO232- Build abstractions for problems you don't have yet (YAGNI) — generic interfaces that are only ever used once are dead weight233- Use exceptions for control flow in performance-critical paths — validate first, then execute234- Silently swallow errors with bare `except:` or `pass` blocks — always log or re-raise with context235- Hardcode configuration values (ports, endpoints, credentials) inside application logic — externalize to config files or environment variables236- Mix persistence queries with business logic in the same function — keep data access and domain rules separate237- Rely on implicit type coercion — use explicit type hints and runtime validation for public interfaces238239---240241## Output Template242243When applying this skill to review or implement code, produce:2442451. **Architecture Assessment** — How well does the code follow separation of concerns? List any violations.2462. **Coupling & Cohesion Score** — Rate each module's cohesion (high/medium/low) and identify cross-module dependencies that could be reduced.2473. **Defensive Programming Gaps** — Point out unvalidated inputs, silent error handling, or implicit assumptions.2484. **YAGNI/KISS Review** — Flag abstractions, generic interfaces, or features that are not required by the current spec.2495. **Refactoring Recommendations** — Concrete, ordered steps to improve modularity with estimated effort per step.250251---252253## Related Skills254255| Skill | Purpose |256|---|---|257| `coding-code-review` | Apply these principles during peer review to catch architectural drift early |258| `coding-testing` | Complementary testing strategies that verify engineering contracts and module boundaries |259| `coding-error-handling` | Deep dive into error handling patterns that complement defensive programming |260| `coding-refactoring` | Practical techniques for untangling high-coupling, low-cohesion codebases |261262---263264## Live References265266> Authoritative documentation links for this skill's domain. The model follows markdown links at load time to resolve external references and inline content.267268- [Wikipedia — Software Engineering Principles](https://en.wikipedia.org/wiki/Software_engineering_principles)269- [IEEE 730 — IEEE Standard for Software Quality Assurance Processes](https://standards.ieee.org/standard/730-2014.html)270- [ISO/IEC/IEEE 15288 — Systems and Software Engineering — Life Cycle Processes](https://www.iso.org/standard/65694.html)271- [Robert Martin — Clean Architecture: Principles of Software Design](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)272- [SOLID Principles by Robert C. Martin](https://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod)