
Clean Code Principles
- 960 installs
- 58 repo stars
- Updated May 16, 2026
- asyrafhussin/agent-skills
clean-code-principles is a code quality skill that enforces SOLID, DRY, KISS, YAGNI, and design-pattern rules during AI code generation, refactoring, and architecture reviews for developers shipping maintainable software
About
clean-code-principles is a language-agnostic agent skill (version 1.0.2, MIT) from asyrafhussin/agent-skills documenting 23 rules across 7 priority categories—10 SOLID, 12 core principles, and design patterns—with bad and good examples per rule. Categories span CRITICAL SOLID and core principles down to LOW-priority comment guidance, giving agents concrete refactoring guardrails instead of vague style advice. Activate it when generating new modules, reviewing pull requests, or refactoring legacy services where drift toward god objects and duplication is likely. The skill fits any stack because rules reference patterns, not framework APIs. A fourth pattern category is noted as planned in the documentation. Reach for clean-code-principles when you want agents to self-check architecture during implementation—not when you only need a language-specific linter config. Each rule pairs explanations with practical before-and-after snippets agents can apply immediately.
- 23 enforceable rules across 10 SOLID, 12 Core Principles and 1 Design Pattern category
- Language-agnostic guidance with concrete bad/good code examples for every rule
- Organized into 7 priority tiers from CRITICAL (SOLID + Core) to LOW (Comments)
- Activates on trigger phrases such as "review architecture", "SOLID principles", "refactoring advice" and "code smells"
- Hard-gated review workflow that surfaces violations before code is committed
Clean Code Principles by the numbers
- 960 all-time installs (skills.sh)
- +64 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #129 of 1,382 Code Review & Quality skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/asyrafhussin/agent-skills --skill clean-code-principlesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 960 |
|---|---|
| repo stars | ★ 58 |
| Security audit | 3 / 3 scanners passed |
| Last updated | May 16, 2026 |
| Repository | asyrafhussin/agent-skills ↗ |
How do you enforce SOLID principles in AI-generated code?
Enforce SOLID, DRY, KISS and design-pattern rules during code generation, refactoring and architecture reviews.
Who is it for?
Developers using AI coding agents who want SOLID, DRY, and design-pattern enforcement during generation and architecture reviews.
Skip if: Teams that only need language-specific linter configs without architectural principle guidance for agent workflows.
When should I use this skill?
The user asks for clean code review, SOLID enforcement, refactoring guidance, or design-pattern checks during AI code generation.
What you get
Refactored code aligned to 23 documented rules across SOLID, DRY, KISS, YAGNI, and design-pattern categories.
- Rule-guided refactored code
- Architecture review annotations
- Bad/good example-aligned implementations
By the numbers
- Version 1.0.2 with 23 documented rules across 7 categories
- 10 SOLID rules plus 12 core principle rules plus design-pattern coverage
Files
Clean Code Principles
Fundamental software design principles, SOLID, design patterns, and clean code practices. Language-agnostic guidelines for writing maintainable, scalable software.
When to Apply
Reference these guidelines when:
- Designing new features or systems
- Reviewing code architecture
- Refactoring existing code
- Discussing design decisions
- Improving code quality
Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|---|---|---|---|
| 1 | SOLID Principles | CRITICAL | solid- |
| 2 | Core Principles | CRITICAL | core- |
| 3 | Design Patterns | HIGH | pattern- |
| 4 | Code Organization | HIGH | org- |
| 5 | Naming & Readability | MEDIUM | name- |
| 6 | Functions & Methods | MEDIUM | func- |
| 7 | Comments & Documentation | LOW | doc- |
Quick Reference
1. SOLID Principles (CRITICAL)
solid-srp- Single Responsibility Principlesolid-ocp- Open/Closed Principlesolid-lsp- Liskov Substitution Principlesolid-isp- Interface Segregation Principlesolid-dip- Dependency Inversion Principle
2. Core Principles (CRITICAL)
core-dry- Don't Repeat Yourselfcore-kiss- Keep It Simple, Stupidcore-yagni- You Aren't Gonna Need Itcore-separation-of-concerns- Separate different responsibilitiescore-composition-over-inheritance- Favor compositioncore-law-of-demeter- Principle of least knowledgecore-fail-fast- Detect and report errors earlycore-encapsulation- Hide implementation details
3. Design Patterns (HIGH)
pattern-factory- Factory pattern for object creationpattern-strategy- Strategy pattern for algorithmspattern-repository- Repository pattern for data accesspattern-decorator- Decorator pattern for behavior extensionpattern-observer- Observer pattern for event handlingpattern-adapter- Adapter pattern for interface conversionpattern-facade- Facade pattern for simplified interfacespattern-dependency-injection- DI for loose coupling
4. Code Organization (HIGH) — planned
org-feature-folders- Organize by feature, not layerorg-module-boundaries- Clear module boundariesorg-layered-architecture- Proper layer separationorg-package-cohesion- Related code togetherorg-circular-dependencies- Avoid circular imports
5. Naming & Readability (MEDIUM) — planned
name-meaningful- Use intention-revealing namesname-consistent- Consistent naming conventionsname-searchable- Avoid magic numbers/stringsname-avoid-encodings- No Hungarian notationname-domain-language- Use domain terminology
6. Functions & Methods (MEDIUM) — planned
func-small- Keep functions smallfunc-single-purpose- Do one thingfunc-few-arguments- Limit parametersfunc-no-side-effects- Minimize side effectsfunc-command-query- Separate commands and queries
7. Comments & Documentation (LOW) — planned
doc-self-documenting- Code should explain itselfdoc-why-not-what- Explain why, not whatdoc-avoid-noise- No redundant commentsdoc-api-docs- Document public APIs
Essential Guidelines
For detailed examples and explanations, see the rule files:
- core-dry.md - Don't Repeat Yourself principle
- pattern-repository.md - Repository pattern for data access
SOLID Principles (Summary)
| Principle | Definition |
|---|---|
| Single Responsibility | A class should have only one reason to change |
| Open/Closed | Open for extension, closed for modification |
| Liskov Substitution | Subtypes must be substitutable for base types |
| Interface Segregation | Don't force clients to depend on unused interfaces |
| Dependency Inversion | Depend on abstractions, not concretions |
Core Principles (Summary)
| Principle | Definition |
|---|---|
| DRY | Don't Repeat Yourself - single source of truth |
| KISS | Keep It Simple - avoid over-engineering |
| YAGNI | You Aren't Gonna Need It - build only what's needed |
Quick Examples
// Single Responsibility - one class, one job
class UserService {
constructor(
private validator: UserValidator,
private repository: UserRepository,
) {}
createUser(data) {
this.validator.validate(data);
return this.repository.create(data);
}
}
// Dependency Inversion - depend on abstractions
interface Repository<T> {
find(id: string): Promise<T | null>;
save(entity: T): Promise<T>;
}
class OrderService {
constructor(private repository: Repository<Order>) {}
}
// DRY - single source of truth
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const isValidEmail = (email: string) => EMAIL_REGEX.test(email);
// Meaningful names over magic numbers
const MINIMUM_AGE = 18;
if (user.age >= MINIMUM_AGE) { }Output Format
When auditing code, output findings in this format:
file:line - [principle] Description of issueExample:
src/services/UserService.ts:15 - [solid-srp] Class handles validation, persistence, and notifications
src/utils/helpers.ts:42 - [core-dry] Email validation duplicated from validators/email.ts
src/models/Order.ts:28 - [name-meaningful] Variable 'x' should describe its purposeHow to Use
Read individual rule files for detailed explanations:
rules/solid-srp-class.md
rules/core-dry.md
rules/pattern-repository.mdReferences
This skill is built on established software engineering principles:
Core Books
- Clean Code by Robert C. Martin - Foundation for clean code practices
- Design Patterns by Gang of Four - Classic design pattern catalog
- Refactoring by Martin Fowler - Improving code structure
- The Pragmatic Programmer by Hunt & Thomas - Practical wisdom
Online Resources
- Refactoring Guru - Design patterns and code smells
- Martin Fowler's Refactoring Catalog - Comprehensive refactoring techniques
- Uncle Bob's Clean Coder Blog - Software craftsmanship articles
Pattern Catalogs
Metadata
Version: 1.0.2 Status: Active Coverage: 23 rules across 3 implemented categories (SOLID, Core Principles, Design Patterns); 4 planned Last Updated: 2026-03-07
Rule Statistics
- SOLID Principles: 10 rules
- Core Principles: 12 rules
- Design Patterns: 1 rule
Clean Code Principles - Agent Documentation
Version: 1.0.2 Focus: SOLID Principles, Core Principles (DRY, KISS, YAGNI), Design Patterns Rules: 23 (10 SOLID + 12 Core + 1 Pattern); 4 categories planned License: MIT
---
This skill provides comprehensive clean code principles, SOLID guidelines, and design patterns for building maintainable, scalable software.
Overview
The clean-code-principles skill offers language-agnostic software design principles organized into 7 categories, from CRITICAL (SOLID, Core Principles) to LOW priority (Comments). Each rule provides bad/good examples, explanations, and practical guidance.
When to Use This Skill
Activate this skill when:
- Reviewing code architecture or design
- Refactoring existing code
- Making design decisions
- Establishing coding standards
- Teaching software design principles
- Addressing technical debt
- Improving code quality and maintainability
Trigger Phrases
The skill activates on:
- "review architecture"
- "check code quality"
- "SOLID principles"
- "design patterns"
- "clean code"
- "refactoring advice"
- "code smells"
- "best practices"
- "DRY principle"
- "separation of concerns"
Skill Structure
clean-code-principles/
├── SKILL.md # Main skill definition
├── AGENTS.md # This file - agent documentation
├── README.md # User-facing documentation
├── metadata.json # Structured metadata and references
└── rules/
├── _sections.md # Category definitions and organization
├── _template.md # Template for new rules
├── solid-*.md # SOLID principles (10 rules)
├── core-*.md # Core principles (12 rules)
└── pattern-*.md # Design patterns (1 rule)Rule Categories
1. SOLID Principles (CRITICAL - 10 rules)
Prefix: solid-
Five fundamental object-oriented design principles:
- Single Responsibility:
solid-srp-class,solid-srp-function - Open/Closed:
solid-ocp-extension,solid-ocp-abstraction - Liskov Substitution:
solid-lsp-contracts,solid-lsp-preconditions - Interface Segregation:
solid-isp-clients,solid-isp-interfaces - Dependency Inversion:
solid-dip-abstractions,solid-dip-injection
Use when: Designing architecture, planning refactoring, discussing system design
2. Core Principles (CRITICAL - 12 rules)
Prefix: core-
Fundamental coding practices:
- DRY (Don't Repeat Yourself): 3 rules
- KISS (Keep It Simple): 2 rules
- YAGNI (You Aren't Gonna Need It): 2 rules
- Other: Separation of Concerns, Composition Over Inheritance, Law of Demeter, Fail Fast, Encapsulation
Use when: Daily coding, code reviews, addressing duplication or complexity
3. Design Patterns (HIGH - 1 rule)
Prefix: pattern-
Common solutions to recurring problems:
- Repository Pattern (data access abstraction)
Use when: Solving architectural problems, abstracting infrastructure concerns
4-7. Future Categories
- Code Organization (
org-): Module structure, boundaries - Naming & Readability (
name-): Identifier naming conventions - Functions & Methods (
func-): Function-level best practices - Comments & Documentation (
doc-): Documentation guidelines
How to Use Rules
Accessing Rules
1. By ID: Reference specific rules using their ID
Check against solid-srp-class and core-dry2. By Category: Apply all rules in a category
Review this class against SOLID principles3. By Scenario: Choose relevant rules for the context
This has duplicated validation logic - check DRY rulesRule Format
Each rule follows a consistent structure:
---
id: {rule-id}
title: {Full Title}
category: {category}
priority: {critical|high|medium|low}
tags: [{tags}]
related: [{related-rule-ids}]
---
# {Rule Title}
{One-sentence summary}
## Bad Example
{Anti-pattern code with problems listed}
## Good Example
{Correct implementation with benefits}
## Why
{5-7 benefits explaining the value}
## When to Apply
{Practical scenarios}Output Format
When identifying violations, use:
file:line - [rule-id] Description of issueExample:
src/services/UserService.ts:15 - [solid-srp-class] Class handles validation, persistence, and notifications
src/utils/helpers.ts:42 - [core-dry] Email validation duplicated from validators/email.ts
src/models/Order.ts:28 - [core-kiss-simplicity] Overly complex abstraction for simple use caseAgent Strategies
Strategy 1: Architecture Review
Goal: Assess overall system design
Approach: 1. Start with SOLID principles (highest impact) 2. Identify violations of SRP, DIP, OCP 3. Check for proper separation of concerns 4. Evaluate composition vs inheritance 5. Assess interface design (ISP)
Output: Prioritized list of architectural issues with rule references
Strategy 2: Code Quality Audit
Goal: Find code quality issues in specific files
Approach: 1. Scan for duplication (DRY rules) 2. Check complexity (KISS rules) 3. Look for overengineering (YAGNI rules) 4. Verify single responsibility 5. Assess encapsulation
Output: File-by-file findings with specific line references
Strategy 3: Refactoring Guidance
Goal: Provide actionable refactoring steps
Approach: 1. Identify the primary issue (which rule violated) 2. Reference the good example from that rule 3. Suggest specific refactoring steps 4. Mention related rules that may also help 5. Prioritize changes by impact
Output: Step-by-step refactoring plan with rule references
Strategy 4: Design Decision Support
Goal: Help choose between design alternatives
Approach: 1. Analyze each option against relevant principles 2. Consider YAGNI (simplest solution first) 3. Evaluate against SOLID principles 4. Check alignment with KISS 5. Recommend based on principle adherence
Output: Comparative analysis with principle-based recommendation
Strategy 5: Pattern Application
Goal: Suggest appropriate design patterns
Approach: 1. Identify the problem being solved 2. Check if any patterns apply 3. Verify pattern aligns with SOLID principles 4. Ensure pattern isn't overengineering (YAGNI) 5. Provide implementation guidance
Output: Pattern recommendation with integration guidance
Best Practices for Agents
Do:
- Start with CRITICAL rules (SOLID, Core)
- Provide specific line numbers when possible
- Reference rule IDs in all findings
- Explain WHY a rule matters, not just that it's violated
- Give actionable refactoring suggestions
- Consider the codebase context
- Prioritize by impact (CRITICAL > HIGH > MEDIUM > LOW)
- Show code examples from the rules
- Mention related rules that apply together
Don't:
- Don't cite every minor violation
- Don't recommend patterns without clear need (YAGNI)
- Don't ignore context (some violations may be acceptable)
- Don't focus on LOW priority rules when CRITICAL issues exist
- Don't provide fixes without explaining the principle
- Don't recommend complex solutions for simple problems (KISS)
- Don't suggest abstractions without concrete use cases (YAGNI)
Prioritization
When multiple issues exist: 1. CRITICAL - SOLID violations: Architecture-level problems 2. CRITICAL - Core violations: Duplication, complexity, overengineering 3. HIGH - Pattern opportunities: Where patterns genuinely help 4. MEDIUM - Naming/Functions: Readability improvements 5. LOW - Documentation: Comment and doc issues
Example Interactions
Example 1: Architecture Review
User: "Review the architecture of this OrderService class"
Agent Approach: 1. Check class responsibilities against solid-srp-class 2. Evaluate dependencies against solid-dip-injection 3. Look for data access against pattern-repository 4. Check for proper separation against core-separation-concerns
Agent Output:
Architecture Review - OrderService
CRITICAL Issues:
1. [solid-srp-class] Class has multiple responsibilities:
- Order validation (should be separate validator)
- Direct database access (should use repository)
- Email sending (should be separate service)
- Payment processing (should be separate service)
2. [solid-dip-abstractions] Direct dependency on concrete implementations:
- Instantiates MySQLDatabase directly (line 15)
- Creates StripePaymentGateway in constructor (line 18)
3. [pattern-repository] Direct SQL queries in service layer:
- Lines 45-50: Raw SQL should be in repository
Recommendations:
1. Split into focused classes following SRP
2. Inject dependencies through constructor (DIP)
3. Create repository abstraction for data access
4. Extract email and payment to separate services
See: solid-srp-class.md, solid-dip-injection.md, pattern-repository.mdExample 2: Code Quality Check
User: "Check this validation function for code quality issues"
Agent Approach: 1. Check for duplication (core-dry) 2. Check complexity (core-kiss-simplicity) 3. Check function responsibility (solid-srp-function)
Agent Output:
Code Quality Issues:
utils/validation.ts:
Line 23 - [core-dry] Email regex duplicated from line 45 and validators/email.ts
Line 30 - [core-dry-extraction] Password validation logic should be extracted to reusable function
Line 50 - [solid-srp-function] Function validates both email AND password - split into two functions
Suggested refactoring:
1. Extract common regex patterns to constants file (core-dry-single-source)
2. Create separate validateEmail() and validatePassword() functions (solid-srp-function)
3. Import from shared validators module (core-dry-extraction)
See: core-dry.md, core-dry-extraction.md, solid-srp-function.mdExample 3: Design Decision
User: "Should I use inheritance or composition for this User/Admin relationship?"
Agent Approach: 1. Apply core-composition principle 2. Check LSP implications (solid-lsp-contracts) 3. Consider YAGNI (core-yagni-abstractions)
Agent Output:
Recommendation: Use Composition
Analysis:
1. [core-composition] Composition is preferred over inheritance for flexibility
- Admins can have additional capabilities without inheritance constraints
- Easier to test each capability independently
- Can mix different permission sets dynamically
2. [solid-lsp-contracts] Inheritance risks LSP violations
- If Admin overrides User methods differently
- If Admin has different behavior expectations
3. [core-yagni-abstractions] Start simple
- Don't create inheritance hierarchy unless you need polymorphism
- Compose from Permission and Role objects instead
Suggested approach:class User { constructor( private permissions: PermissionSet, private roleManager: RoleManager ) {} }
See: core-composition.md, solid-lsp-contracts.mdIntegration with Other Skills
This skill complements:
- refactoring: Provides principles for refactoring decisions
- architecture-review: Supplies SOLID and design principles
- code-review: Offers quality criteria for reviews
- testing: Promotes testable design (DIP, SRP)
Rule Reference Quick Guide
Most Common Rules
For duplication:
core-dry- Main DRY principlecore-dry-extraction- How to extract duplicated codecore-dry-single-source- Configuration and constants
For complex code:
core-kiss-simplicity- Avoid overengineeringcore-kiss-readability- Optimize for readabilitycore-yagni-features- Don't build unused featurescore-yagni-abstractions- Don't abstract prematurely
For class design:
solid-srp-class- Single responsibility for classessolid-dip-injection- Dependency injectioncore-separation-concerns- Separate different concernscore-composition- Favor composition over inheritance
For function design:
solid-srp-function- Single responsibility for functionscore-kiss-readability- Clear, readable functions
For interfaces:
solid-isp-interfaces- Small, focused interfacessolid-isp-clients- Client-specific interfaces
For extensibility:
solid-ocp-extension- Open for extension, closed for modificationsolid-ocp-abstraction- Use abstractions for extension points
For inheritance:
solid-lsp-contracts- Subtypes must honor contractssolid-lsp-preconditions- Pre/postcondition rulescore-composition- Prefer composition
For data access:
pattern-repository- Abstract data persistence
Metadata
Version: 1.0.2 Rules: 23 (10 SOLID, 12 Core, 1 Pattern) Categories: 7 (3 implemented, 4 planned) Languages: Language-agnostic (examples in TypeScript) Last Updated: 2026-03-07
Resources
Books
- Clean Code (Robert C. Martin)
- Design Patterns (Gang of Four)
- Refactoring (Martin Fowler)
- The Pragmatic Programmer (Hunt & Thomas)
Online
- Refactoring Guru - Design patterns and code smells
- Martin Fowler's Catalog - Refactoring techniques
- Uncle Bob's Blog - Software craftsmanship
Contributing New Rules
When adding new rules: 1. Use rules/_template.md as starting point 2. Follow naming convention: {prefix}-{concept}-{specificity}.md 3. Include YAML frontmatter with all required fields 4. Provide clear bad/good examples 5. Explain 5-7 benefits in "Why" section 6. Add to metadata.json rules array 7. Update category counts in _sections.md 8. Reference related rules in frontmatter 9. Keep examples language-agnostic (TypeScript preferred) 10. Aim for 300-400 lines of content
License
MIT License. This skill is provided as-is for educational and development purposes.
{
"name": "clean-code-principles",
"version": "1.0.2",
"author": "AsyrafHussin",
"license": "MIT",
"description": "SOLID principles, design patterns, DRY, KISS, and clean code fundamentals for writing maintainable, scalable software",
"author": "Agent Skills",
"license": "MIT",
"tags": [
"SOLID",
"clean-code",
"design-patterns",
"DRY",
"KISS",
"YAGNI",
"best-practices",
"software-architecture",
"code-quality",
"refactoring"
],
"categories": [
{
"id": "solid-principles",
"name": "SOLID Principles",
"priority": "critical",
"prefix": "solid-",
"count": 10,
"description": "Five fundamental principles of object-oriented design"
},
{
"id": "core-principles",
"name": "Core Principles",
"priority": "critical",
"prefix": "core-",
"count": 12,
"description": "Fundamental coding principles like DRY, KISS, and YAGNI"
},
{
"id": "design-patterns",
"name": "Design Patterns",
"priority": "high",
"prefix": "pattern-",
"count": 1,
"description": "Common design patterns for recurring problems"
},
{
"id": "code-organization",
"name": "Code Organization",
"priority": "high",
"prefix": "org-",
"count": 0,
"description": "Project structure and module boundaries"
},
{
"id": "naming-readability",
"name": "Naming & Readability",
"priority": "medium",
"prefix": "name-",
"count": 0,
"description": "Naming conventions and code readability"
},
{
"id": "functions-methods",
"name": "Functions & Methods",
"priority": "medium",
"prefix": "func-",
"count": 0,
"description": "Function-level best practices"
},
{
"id": "comments-documentation",
"name": "Comments & Documentation",
"priority": "low",
"prefix": "doc-",
"count": 0,
"description": "Documentation and commenting guidelines"
}
],
"rules": [
{
"id": "solid-srp-class",
"title": "SOLID - Single Responsibility Principle (Class Level)",
"category": "solid-principles",
"priority": "critical"
},
{
"id": "solid-srp-function",
"title": "SOLID - Single Responsibility Principle (Function Level)",
"category": "solid-principles",
"priority": "critical"
},
{
"id": "solid-ocp-extension",
"title": "SOLID - Open/Closed Principle (Extension)",
"category": "solid-principles",
"priority": "critical"
},
{
"id": "solid-ocp-abstraction",
"title": "SOLID - Open/Closed (Abstraction)",
"category": "solid-principles",
"priority": "critical"
},
{
"id": "solid-lsp-contracts",
"title": "SOLID - Liskov Substitution (Contracts)",
"category": "solid-principles",
"priority": "critical"
},
{
"id": "solid-lsp-preconditions",
"title": "SOLID - Liskov Substitution (Preconditions)",
"category": "solid-principles",
"priority": "critical"
},
{
"id": "solid-isp-clients",
"title": "SOLID - Interface Segregation (Client-Specific)",
"category": "solid-principles",
"priority": "critical"
},
{
"id": "solid-isp-interfaces",
"title": "SOLID - Interface Segregation (Small Interfaces)",
"category": "solid-principles",
"priority": "critical"
},
{
"id": "solid-dip-abstractions",
"title": "SOLID - Dependency Inversion (Abstractions)",
"category": "solid-principles",
"priority": "critical"
},
{
"id": "solid-dip-injection",
"title": "SOLID - Dependency Inversion (Injection)",
"category": "solid-principles",
"priority": "critical"
},
{
"id": "core-dry",
"title": "Don't Repeat Yourself (DRY)",
"category": "core-principles",
"priority": "critical"
},
{
"id": "core-dry-extraction",
"title": "DRY - Code Extraction",
"category": "core-principles",
"priority": "critical"
},
{
"id": "core-dry-single-source",
"title": "DRY - Single Source of Truth",
"category": "core-principles",
"priority": "critical"
},
{
"id": "core-kiss-simplicity",
"title": "KISS - Simplicity",
"category": "core-principles",
"priority": "critical"
},
{
"id": "core-kiss-readability",
"title": "KISS - Readability",
"category": "core-principles",
"priority": "critical"
},
{
"id": "core-yagni-features",
"title": "YAGNI - Features",
"category": "core-principles",
"priority": "critical"
},
{
"id": "core-yagni-abstractions",
"title": "YAGNI - Abstractions",
"category": "core-principles",
"priority": "critical"
},
{
"id": "core-separation-concerns",
"title": "Separation of Concerns",
"category": "core-principles",
"priority": "critical"
},
{
"id": "core-composition",
"title": "Composition Over Inheritance",
"category": "core-principles",
"priority": "critical"
},
{
"id": "core-law-demeter",
"title": "Law of Demeter",
"category": "core-principles",
"priority": "critical"
},
{
"id": "core-fail-fast",
"title": "Fail Fast Principle",
"category": "core-principles",
"priority": "critical"
},
{
"id": "core-encapsulation",
"title": "Encapsulation",
"category": "core-principles",
"priority": "critical"
},
{
"id": "pattern-repository",
"title": "Design Pattern - Repository",
"category": "design-patterns",
"priority": "high"
}
],
"references": {
"books": [
{
"title": "Clean Code: A Handbook of Agile Software Craftsmanship",
"author": "Robert C. Martin",
"year": 2008,
"isbn": "978-0132350884",
"url": "https://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882"
},
{
"title": "Design Patterns: Elements of Reusable Object-Oriented Software",
"author": "Gang of Four (Gamma, Helm, Johnson, Vlissides)",
"year": 1994,
"isbn": "978-0201633610",
"url": "https://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612"
},
{
"title": "Refactoring: Improving the Design of Existing Code",
"author": "Martin Fowler",
"year": 2018,
"isbn": "978-0134757599",
"url": "https://martinfowler.com/books/refactoring.html"
},
{
"title": "The Pragmatic Programmer",
"author": "Andrew Hunt and David Thomas",
"year": 2019,
"isbn": "978-0135957059",
"url": "https://pragprog.com/titles/tpp20/the-pragmatic-programmer-20th-anniversary-edition/"
},
{
"title": "Patterns of Enterprise Application Architecture",
"author": "Martin Fowler",
"year": 2002,
"isbn": "978-0321127426",
"url": "https://martinfowler.com/books/eaa.html"
}
],
"websites": [
{
"title": "Refactoring Guru - Design Patterns",
"url": "https://refactoring.guru/design-patterns",
"description": "Comprehensive catalog of design patterns with examples in multiple languages"
},
{
"title": "Refactoring Guru - Code Smells",
"url": "https://refactoring.guru/refactoring/smells",
"description": "Catalog of code smells and refactoring techniques"
},
{
"title": "Martin Fowler - Refactoring Catalog",
"url": "https://refactoring.com/catalog/",
"description": "Comprehensive refactoring catalog by Martin Fowler"
},
{
"title": "Martin Fowler - Bliki",
"url": "https://martinfowler.com/bliki/",
"description": "Martin Fowler's blog with articles on software design"
},
{
"title": "SOLID Principles",
"url": "https://en.wikipedia.org/wiki/SOLID",
"description": "Wikipedia article on SOLID principles"
},
{
"title": "Robert C. Martin (Uncle Bob) - Blog",
"url": "https://blog.cleancoder.com/",
"description": "Articles on clean code and software craftsmanship"
}
],
"articles": [
{
"title": "The DRY Principle",
"author": "Martin Fowler",
"url": "https://martinfowler.com/ieeeSoftware/repetition.pdf",
"description": "In-depth article on avoiding duplication"
},
{
"title": "SOLID Principles Explained",
"author": "Various",
"url": "https://www.digitalocean.com/community/conceptual-articles/s-o-l-i-d-the-first-five-principles-of-object-oriented-design",
"description": "Practical explanation of SOLID principles"
},
{
"title": "Composition vs Inheritance",
"author": "Gang of Four",
"url": "https://en.wikipedia.org/wiki/Composition_over_inheritance",
"description": "Discussion of composition over inheritance principle"
}
],
"videos": [
{
"title": "Clean Code - Uncle Bob / Lesson 1",
"url": "https://www.youtube.com/watch?v=7EmboKQH8lM",
"description": "Introduction to clean code principles"
},
{
"title": "SOLID Principles of Object Oriented Design",
"url": "https://www.youtube.com/watch?v=TMuno5RZNeE",
"description": "Conference talk on SOLID principles"
}
]
},
"keywords": [
"software design",
"architecture",
"maintainability",
"scalability",
"testability",
"code quality",
"best practices",
"object-oriented design",
"functional programming",
"design principles",
"code organization",
"refactoring",
"technical debt",
"clean architecture"
],
"languages": [
"typescript",
"javascript",
"python",
"java",
"go",
"rust",
"c-sharp",
"php",
"ruby"
],
"compatibility": {
"paradigms": [
"object-oriented",
"functional",
"procedural",
"declarative"
],
"scales": [
"single-file",
"module",
"application",
"microservices",
"distributed-systems"
]
},
"metadata": {
"created": "2026-01-17",
"updated": "2026-03-07",
"status": "active",
"maturity": "stable",
"coverage": {
"total_categories": 7,
"implemented_categories": 3,
"total_rules": 23,
"coverage_percentage": 42.86
}
}
}
Clean Code Principles
Fundamental software design principles for writing maintainable, scalable code.
Version: 1.0.2 Rules: 23 (10 SOLID + 12 Core + 1 Pattern); 4 categories planned License: MIT
---
Overview
Language-agnostic guidelines covering SOLID principles, core coding principles (DRY, KISS, YAGNI), and design patterns. Examples are written in TypeScript but apply to any object-oriented or functional language.
Categories (23 rules implemented)
1. SOLID Principles (Critical) — 10 rules
Five fundamental object-oriented design principles: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion.
2. Core Principles (Critical) — 12 rules
DRY (3 rules), KISS (2 rules), YAGNI (2 rules), Separation of Concerns, Composition over Inheritance, Law of Demeter, Fail Fast, Encapsulation.
3. Design Patterns (High) — 1 rule
Repository pattern for data access abstraction.
4. Code Organization (High) — planned
Feature folders, module boundaries, layered architecture, package cohesion, circular dependency prevention.
5. Naming & Readability (Medium) — planned
Meaningful names, consistent conventions, no magic numbers, domain language.
6. Functions & Methods (Medium) — planned
Small functions, single purpose, limited parameters, pure functions, command-query separation.
7. Comments & Documentation (Low) — planned
Self-documenting code, explain why not what, avoid noise, document public APIs.
Usage
Ask Claude to:
- "Review architecture" — triggers SOLID + Separation of Concerns analysis
- "Check SOLID principles" — targeted SOLID review
- "Check code quality" — DRY, KISS, YAGNI audit
- "Suggest design patterns" — pattern recommendations
- "Refactoring advice" — actionable improvements with rule references
Key Principles
SOLID
| Principle | Rule | Summary |
|---|---|---|
| Single Responsibility | solid-srp-class, solid-srp-function | One reason to change |
| Open/Closed | solid-ocp-extension, solid-ocp-abstraction | Open for extension, closed for modification |
| Liskov Substitution | solid-lsp-contracts, solid-lsp-preconditions | Subtypes must be substitutable |
| Interface Segregation | solid-isp-clients, solid-isp-interfaces | Small, focused interfaces |
| Dependency Inversion | solid-dip-abstractions, solid-dip-injection | Depend on abstractions |
Core
| Principle | Rules | Summary |
|---|---|---|
| DRY | core-dry, core-dry-extraction, core-dry-single-source | Single source of truth |
| KISS | core-kiss-simplicity, core-kiss-readability | Simplest solution that works |
| YAGNI | core-yagni-features, core-yagni-abstractions | Build only what's needed |
Output Format
When auditing code:
file:line - [rule-id] Description of issueExample:
src/services/UserService.ts:15 - [solid-srp-class] Class handles validation, persistence, and email
src/utils/helpers.ts:42 - [core-dry] Email validation duplicated from validators/email.ts
src/models/Order.ts:28 - [core-yagni-abstractions] Generic abstraction used in only one placeReferences
- Clean Code by Robert C. Martin — Foundation for clean code practices
- Design Patterns by Gang of Four — Classic design pattern catalog
- Refactoring by Martin Fowler — Improving code structure
- The Pragmatic Programmer by Hunt & Thomas — Practical software wisdom
- Refactoring Guru — Design patterns and code smells
- Martin Fowler's Refactoring Catalog — Comprehensive techniques
Clean Code Principles - Rule Categories
This document defines the organizational structure for clean code principles, ordered by priority and impact.
Category Overview
| Priority | Category | Impact | Rule Count | Prefix |
|---|---|---|---|---|
| 1 | SOLID Principles | CRITICAL | 10 | solid- |
| 2 | Core Principles | CRITICAL | 12 | core- |
| 3 | Design Patterns | HIGH | 1 | pattern- |
| 4 | Code Organization | HIGH | 0 | org- |
| 5 | Naming & Readability | MEDIUM | 0 | name- |
| 6 | Functions & Methods | MEDIUM | 0 | func- |
| 7 | Comments & Documentation | LOW | 0 | doc- |
1. SOLID Principles (CRITICAL)
Priority: CRITICAL Impact: Architectural foundation, affects entire codebase structure Prefix: solid-
The five fundamental principles of object-oriented design that guide maintainable, scalable software architecture.
Rules
Single Responsibility Principle (SRP)
solid-srp-class- A class should have only one reason to changesolid-srp-function- A function should do one thing and do it well
Open/Closed Principle (OCP)
solid-ocp-extension- Open for extension, closed for modificationsolid-ocp-abstraction- Use abstractions to enable extension
Liskov Substitution Principle (LSP)
solid-lsp-contracts- Subtypes must honor base type contractssolid-lsp-preconditions- Cannot strengthen preconditions or weaken postconditions
Interface Segregation Principle (ISP)
solid-isp-clients- Client-specific interfaces, not general-purposesolid-isp-interfaces- Small, cohesive interfaces
Dependency Inversion Principle (DIP)
solid-dip-abstractions- Depend on abstractions, not concretionssolid-dip-injection- Inject dependencies from outside
Key Concepts:
- Architectural soundness
- Maintainability at scale
- Testability through design
- Flexibility for change
- Reduced coupling
When to Apply:
- Designing new features or systems
- Refactoring existing architecture
- Addressing technical debt
- Improving testability
- Planning for future extensibility
---
2. Core Principles (CRITICAL)
Priority: CRITICAL Impact: Daily coding practices, code quality foundation Prefix: core-
Fundamental principles that apply to every line of code you write, regardless of paradigm or language.
Rules
DRY (Don't Repeat Yourself)
core-dry- Every piece of knowledge should have a single representationcore-dry-extraction- Extract duplicated code into reusable functionscore-dry-single-source- Single source of truth for configuration and data
KISS (Keep It Simple, Stupid)
core-kiss-simplicity- Choose the simplest solution that workscore-kiss-readability- Optimize for readability over cleverness
YAGNI (You Aren't Gonna Need It)
core-yagni-features- Don't implement features before they're neededcore-yagni-abstractions- Don't create abstractions prematurely
Other Core Principles
core-separation-concerns- Different concerns in different modulescore-composition- Favor composition over inheritancecore-law-demeter- Only talk to immediate friendscore-fail-fast- Detect and report errors earlycore-encapsulation- Hide implementation details
Key Concepts:
- Code duplication elimination
- Simplicity over complexity
- Lean development
- Modularity
- Information hiding
When to Apply:
- Writing any new code
- Code reviews
- Refactoring sessions
- Bug fixes
- Performance optimization
---
3. Design Patterns (HIGH)
Priority: HIGH Impact: Solves recurring problems with proven solutions Prefix: pattern-
Common design patterns that provide tested solutions to recurring software design problems.
Rules
pattern-repository- Abstraction for data access layerpattern-factory- Object creation without specifying exact class (planned)pattern-strategy- Encapsulate algorithms for runtime selection (planned)pattern-decorator- Add behavior without modifying objects (planned)pattern-observer- Define one-to-many dependencies (planned)pattern-adapter- Make incompatible interfaces work together (planned)pattern-facade- Simplified interface to complex subsystems (planned)
Key Concepts:
- Proven solutions
- Common vocabulary
- Design reusability
- Best practices codified
- Language-agnostic approaches
When to Apply:
- Solving common architectural problems
- Improving code structure
- Reducing coupling between components
- Making systems more testable
- Communicating design intent
---
4. Code Organization (HIGH)
Priority: HIGH Impact: Project structure, module boundaries, discoverability Prefix: org-
Principles for organizing code into modules, packages, and directories for maintainability and scalability.
Rules (Planned)
org-feature-folders- Organize by feature, not by layerorg-module-boundaries- Clear boundaries between modulesorg-layered-architecture- Proper separation of layersorg-package-cohesion- Keep related code togetherorg-circular-dependencies- Avoid circular imports
Key Concepts:
- Feature-based organization
- Module boundaries
- Layer separation
- Dependency direction
- Discoverability
When to Apply:
- Starting new projects
- Restructuring existing codebases
- Scaling applications
- Onboarding new team members
- Managing microservices
---
5. Naming & Readability (MEDIUM)
Priority: MEDIUM Impact: Code comprehension, maintenance speed Prefix: name-
Conventions and principles for naming variables, functions, classes, and other identifiers.
Rules (Planned)
name-meaningful- Use intention-revealing namesname-consistent- Follow consistent naming conventionsname-searchable- Avoid magic numbers and stringsname-avoid-encodings- No Hungarian notationname-domain-language- Use ubiquitous domain language
Key Concepts:
- Intention revelation
- Consistency
- Searchability
- Domain terminology
- Avoid abbreviations
When to Apply:
- Creating new identifiers
- Refactoring unclear names
- Code reviews
- Domain modeling
- API design
---
6. Functions & Methods (MEDIUM)
Priority: MEDIUM Impact: Code readability, testability at function level Prefix: func-
Principles for writing clean, focused functions and methods.
Rules (Planned)
func-small- Keep functions small and focusedfunc-single-purpose- Do one thing onlyfunc-few-arguments- Limit function parametersfunc-no-side-effects- Minimize or document side effectsfunc-command-query- Separate commands from queries
Key Concepts:
- Small functions
- Single purpose
- Few parameters
- Pure functions when possible
- Predictable behavior
When to Apply:
- Writing new functions
- Refactoring long methods
- Improving testability
- Code reviews
- Performance optimization
---
7. Comments & Documentation (LOW)
Priority: LOW Impact: Code maintainability, knowledge transfer Prefix: doc-
Guidelines for when and how to use comments and documentation effectively.
Rules (Planned)
doc-self-documenting- Write code that explains itselfdoc-why-not-what- Comments should explain why, not whatdoc-avoid-noise- No redundant or obvious commentsdoc-api-docs- Document public APIs and interfaces
Key Concepts:
- Self-documenting code
- Intent over implementation
- Avoid redundancy
- Public API documentation
- Living documentation
When to Apply:
- Complex business logic
- Non-obvious algorithms
- Public APIs
- Architectural decisions
- Workarounds and hacks
---
Rule Naming Convention
All rules follow a consistent naming pattern:
{prefix}-{concept}-{specificity}Examples:
solid-srp-class- SOLID principle, SRP concept, class levelcore-dry-extraction- Core principle, DRY concept, extraction techniquepattern-repository- Design pattern category, repository pattern
Priority Levels Explained
- CRITICAL: Core architectural and coding principles. Violations significantly impact maintainability, testability, and scalability.
- HIGH: Important patterns and organizational principles. Violations complicate future development.
- MEDIUM: Best practices that improve code quality. Violations make code harder to read and maintain.
- LOW: Nice-to-have practices. Violations have minimal impact but reduce clarity.
Impact Assessment
- CRITICAL Impact: Affects entire system architecture, multiple teams, long-term maintainability
- HIGH Impact: Affects module design, team productivity, medium-term maintainability
- MEDIUM Impact: Affects code readability, individual developer productivity
- LOW Impact: Affects code clarity, documentation quality
Usage Guidelines
1. Start with SOLID and Core Principles - these are non-negotiable 2. Apply Design Patterns when solving specific architectural problems 3. Use Code Organization principles when structuring projects 4. Follow Naming & Readability guidelines for all new code 5. Apply Function principles during refactoring and new development 6. Add Comments only when necessary to explain complex logic
Cross-References
Rules often relate to each other. The related field in each rule's frontmatter indicates:
- Rules that commonly apply together
- Rules that solve similar problems
- Rules that complement each other
- Rules that provide context or prerequisites
Evolution
This categorization will evolve as:
- New rules are added
- Patterns emerge from practice
- Team feedback is incorporated
- Language-specific adaptations are needed
{Rule Title}
{One or two sentence summary explaining the principle and why it matters. Should be clear and actionable.}
Bad Example
// Anti-pattern: {Brief description of what's wrong}
{Code example demonstrating the violation}
// Problems:
// 1. {Specific issue 1}
// 2. {Specific issue 2}
// 3. {Specific issue 3}Why This Is Wrong:
- {Consequence 1}
- {Consequence 2}
- {Consequence 3}
Good Example
// Correct approach: {Brief description of the solution}
{Code example demonstrating proper implementation}
// Benefits:
// 1. {Benefit 1}
// 2. {Benefit 2}
// 3. {Benefit 3}Alternative Approach (Optional):
// Another valid solution: {When this might be preferred}
{Alternative code example if applicable}Why
Explanation of the principle and its benefits:
1. {Benefit Category 1}: {Detailed explanation}
2. {Benefit Category 2}: {Detailed explanation}
3. {Benefit Category 3}: {Detailed explanation}
4. {Benefit Category 4}: {Detailed explanation}
5. {Benefit Category 5}: {Detailed explanation}
6. {Benefit Category 6}: {Detailed explanation}
7. {Benefit Category 7}: {Detailed explanation}
When to Apply
- {Situation 1}
- {Situation 2}
- {Situation 3}
- {Situation 4}
When NOT to Apply (Optional)
// Acceptable exception: {Scenario where the rule can be relaxed}
{Code example of acceptable violation with clear reasoning}
// This is acceptable because:
// - {Reason 1}
// - {Reason 2}Common Mistakes (Optional)
Mistake 1: {Common misunderstanding}
// ❌ Wrong
{Code showing mistake}
// ✅ Correct
{Code showing correction}Mistake 2: {Another common issue}
// ❌ Wrong
{Code showing mistake}
// ✅ Correct
{Code showing correction}Testing Implications (Optional)
How this principle affects testing:
// Test example showing improved testability
{Test code demonstrating benefits}Real-World Example (Optional)
{Brief description of how this applies in production scenarios}
// Production scenario: {Description}
{Realistic code example}Related Principles
- {Related Rule 1}: {Brief explanation of relationship}
- {Related Rule 2}: {Brief explanation of relationship}
- {Related Rule 3}: {Brief explanation of relationship}
Further Reading (Optional)
- {Resource title} - {URL or reference}
- {Resource title} - {URL or reference}
Language-Specific Notes (Optional)
TypeScript/JavaScript
{Language-specific considerations}
Python
{Language-specific considerations}
Java
{Language-specific considerations}
Go
{Language-specific considerations}
---
Template Guidelines
Frontmatter
- id: Use format
{prefix}-{concept}-{specificity}. Must be unique and match filename. - title: Full descriptive title, human-readable
- category: One of the 7 defined categories
- priority: critical (SOLID, Core) | high (Patterns, Org) | medium (Naming, Functions) | low (Comments)
- tags: 3-5 relevant tags for searchability
- related: 2-4 related rule IDs that commonly apply together
Content Structure
1. Title & Summary: Clear, one-sentence explanation 2. Bad Example: Show the anti-pattern with clear problems listed 3. Good Example: Show proper implementation with benefits 4. Why: 5-7 benefits explaining the value 5. When to Apply: Practical scenarios 6. Optional Sections: Add as needed for complex rules
Code Examples
- Use TypeScript for primary examples (language-agnostic)
- Keep examples focused and minimal
- Show realistic scenarios, not toy examples
- Include comments explaining key points
- Use ❌ for bad examples, ✅ for good examples
Writing Style
- Be direct and actionable
- Focus on "why" not just "what"
- Use active voice
- Keep explanations concise
- Provide context for decisions
- Assume intermediate developer knowledge
Length Guidelines
- Minimum: 200 lines (simple rules)
- Target: 300-400 lines (most rules)
- Maximum: 600 lines (complex patterns)
Quality Checklist
- [ ] Frontmatter complete and accurate
- [ ] Clear bad example with explained problems
- [ ] Clear good example with explained benefits
- [ ] At least 5 benefits in "Why" section
- [ ] Practical "When to Apply" scenarios
- [ ] Related rules referenced
- [ ] Code examples are realistic
- [ ] Comments explain key concepts
- [ ] Language-agnostic where possible
- [ ] Proofread for clarity and typos
Composition Over Inheritance
Favor composing objects from smaller, focused pieces over building deep inheritance hierarchies. Composition provides more flexibility, better encapsulation, and avoids the fragile base class problem.
Bad Example
// Anti-pattern: Deep inheritance hierarchy
class Animal {
protected name: string;
protected energy: number = 100;
constructor(name: string) {
this.name = name;
}
eat(amount: number): void {
this.energy += amount;
console.log(`${this.name} is eating. Energy: ${this.energy}`);
}
sleep(hours: number): void {
this.energy += hours * 10;
console.log(`${this.name} slept for ${hours} hours. Energy: ${this.energy}`);
}
}
class Bird extends Animal {
fly(): void {
this.energy -= 20;
console.log(`${this.name} is flying. Energy: ${this.energy}`);
}
}
class Duck extends Bird {
swim(): void {
this.energy -= 5;
console.log(`${this.name} is swimming. Energy: ${this.energy}`);
}
quack(): void {
console.log(`${this.name} says quack!`);
}
}
class FlyingFish extends Animal {
// Problem: Can't inherit from both Bird and Fish
// Must duplicate flying code or create awkward hierarchy
swim(): void {
this.energy -= 5;
console.log(`${this.name} is swimming. Energy: ${this.energy}`);
}
// Duplicated from Bird class!
fly(): void {
this.energy -= 20;
console.log(`${this.name} is flying. Energy: ${this.energy}`);
}
}
class Penguin extends Bird {
// Problem: Penguins can't fly but inherit fly()
// Must override to throw error - LSP violation
fly(): void {
throw new Error('Penguins cannot fly!');
}
swim(): void {
this.energy -= 5;
console.log(`${this.name} is swimming. Energy: ${this.energy}`);
}
}
// More problems:
// - What about a robot bird? It doesn't eat or sleep.
// - What about a bat? It flies but isn't a bird.
// - Every change to Animal affects all subclasses.
// - Testing requires understanding entire hierarchy.Good Example
// Correct approach: Composition with focused behaviors
// Define behaviors as interfaces
interface Eater {
eat(amount: number): void;
}
interface Sleeper {
sleep(hours: number): void;
}
interface Flyer {
fly(): void;
}
interface Swimmer {
swim(): void;
}
interface Speaker {
speak(): void;
}
// Implement behaviors as standalone classes
class StandardEater implements Eater {
constructor(private entity: { name: string; energy: number }) {}
eat(amount: number): void {
this.entity.energy += amount;
console.log(`${this.entity.name} is eating. Energy: ${this.entity.energy}`);
}
}
class StandardSleeper implements Sleeper {
constructor(private entity: { name: string; energy: number }) {}
sleep(hours: number): void {
this.entity.energy += hours * 10;
console.log(`${this.entity.name} slept for ${hours} hours. Energy: ${this.entity.energy}`);
}
}
class WingedFlyer implements Flyer {
constructor(
private entity: { name: string; energy: number },
private energyCost: number = 20
) {}
fly(): void {
this.entity.energy -= this.energyCost;
console.log(`${this.entity.name} is flying. Energy: ${this.entity.energy}`);
}
}
class AquaticSwimmer implements Swimmer {
constructor(
private entity: { name: string; energy: number },
private energyCost: number = 5
) {}
swim(): void {
this.entity.energy -= this.energyCost;
console.log(`${this.entity.name} is swimming. Energy: ${this.entity.energy}`);
}
}
// Compose animals from behaviors
class Duck implements Eater, Sleeper, Flyer, Swimmer, Speaker {
public name: string;
public energy: number = 100;
private eater: Eater;
private sleeper: Sleeper;
private flyer: Flyer;
private swimmer: Swimmer;
constructor(name: string) {
this.name = name;
this.eater = new StandardEater(this);
this.sleeper = new StandardSleeper(this);
this.flyer = new WingedFlyer(this);
this.swimmer = new AquaticSwimmer(this);
}
eat(amount: number): void {
this.eater.eat(amount);
}
sleep(hours: number): void {
this.sleeper.sleep(hours);
}
fly(): void {
this.flyer.fly();
}
swim(): void {
this.swimmer.swim();
}
speak(): void {
console.log(`${this.name} says quack!`);
}
}
// Penguin: swims but doesn't fly - no problem!
class Penguin implements Eater, Sleeper, Swimmer, Speaker {
public name: string;
public energy: number = 100;
private eater: Eater;
private sleeper: Sleeper;
private swimmer: Swimmer;
constructor(name: string) {
this.name = name;
this.eater = new StandardEater(this);
this.sleeper = new StandardSleeper(this);
this.swimmer = new AquaticSwimmer(this);
}
eat(amount: number): void {
this.eater.eat(amount);
}
sleep(hours: number): void {
this.sleeper.sleep(hours);
}
swim(): void {
this.swimmer.swim();
}
speak(): void {
console.log(`${this.name} says squawk!`);
}
}
// Flying fish: swims and flies - easy!
class FlyingFish implements Swimmer, Flyer {
public name: string;
public energy: number = 100;
private swimmer: Swimmer;
private flyer: Flyer;
constructor(name: string) {
this.name = name;
this.swimmer = new AquaticSwimmer(this);
this.flyer = new WingedFlyer(this, 30); // Different energy cost
}
swim(): void {
this.swimmer.swim();
}
fly(): void {
this.flyer.fly();
}
}
// Robot bird: flies but doesn't eat or sleep
class RobotBird implements Flyer, Speaker {
public name: string;
public energy: number = 100;
private flyer: Flyer;
constructor(name: string) {
this.name = name;
this.flyer = new WingedFlyer(this, 10); // Efficient robot
}
fly(): void {
this.flyer.fly();
}
speak(): void {
console.log(`${this.name} says BEEP BOOP!`);
}
recharge(): void {
this.energy = 100;
console.log(`${this.name} recharged to full energy.`);
}
}
// Functions work with any entity that has the required behavior
function makeEntityFly(flyer: Flyer): void {
flyer.fly();
}
function feedEntity(eater: Eater, amount: number): void {
eater.eat(amount);
}
// Works with duck, flying fish, or robot bird
makeEntityFly(new Duck('Donald'));
makeEntityFly(new FlyingFish('Nemo'));
makeEntityFly(new RobotBird('R2D2'));
// Works with duck or penguin, but not robot bird (correctly!)
feedEntity(new Duck('Donald'), 50);
feedEntity(new Penguin('Pingu'), 50);
// feedEntity(new RobotBird('R2D2'), 50); // Type error - RobotBird isn't an EaterWhy
1. Flexibility: Compose any combination of behaviors. No artificial hierarchy constraints.
2. Avoids Diamond Problem: No multiple inheritance issues. Just implement multiple interfaces.
3. LSP Compliance: No need to override methods to throw errors. Types only have methods they actually support.
4. Reusability: Behaviors can be reused across unrelated types.
5. Testability: Test behaviors in isolation. Mock specific behaviors easily.
6. Runtime Flexibility: Can change behaviors at runtime by swapping implementations.
7. Stable Dependencies: Behavior implementations are stable. Adding new composed types doesn't affect existing code.
DRY - Code Extraction
Don't Repeat Yourself. When you find duplicated code, extract it into a reusable function, method, or module. Every piece of knowledge should have a single, unambiguous representation.
Bad Example
// Anti-pattern: Same validation logic repeated in multiple places
class UserController {
async createUser(req: Request, res: Response): Promise<void> {
const { email, password, name } = req.body;
// Email validation - duplicated
if (!email) {
res.status(400).json({ error: 'Email is required' });
return;
}
if (!email.includes('@') || !email.includes('.')) {
res.status(400).json({ error: 'Invalid email format' });
return;
}
if (email.length > 255) {
res.status(400).json({ error: 'Email too long' });
return;
}
// Password validation - duplicated
if (!password) {
res.status(400).json({ error: 'Password is required' });
return;
}
if (password.length < 8) {
res.status(400).json({ error: 'Password must be at least 8 characters' });
return;
}
if (!/[A-Z]/.test(password)) {
res.status(400).json({ error: 'Password must contain uppercase letter' });
return;
}
if (!/[0-9]/.test(password)) {
res.status(400).json({ error: 'Password must contain a number' });
return;
}
// Create user...
}
async updateUser(req: Request, res: Response): Promise<void> {
const { email, password } = req.body;
// Same email validation repeated
if (email) {
if (!email.includes('@') || !email.includes('.')) {
res.status(400).json({ error: 'Invalid email format' });
return;
}
if (email.length > 255) {
res.status(400).json({ error: 'Email too long' });
return;
}
}
// Same password validation repeated
if (password) {
if (password.length < 8) {
res.status(400).json({ error: 'Password must be at least 8 characters' });
return;
}
if (!/[A-Z]/.test(password)) {
res.status(400).json({ error: 'Password must contain uppercase letter' });
return;
}
if (!/[0-9]/.test(password)) {
res.status(400).json({ error: 'Password must contain a number' });
return;
}
}
// Update user...
}
async resetPassword(req: Request, res: Response): Promise<void> {
const { email, newPassword } = req.body;
// Email validation repeated again
if (!email) {
res.status(400).json({ error: 'Email is required' });
return;
}
if (!email.includes('@') || !email.includes('.')) {
res.status(400).json({ error: 'Invalid email format' });
return;
}
// Password validation repeated again
if (!newPassword) {
res.status(400).json({ error: 'New password is required' });
return;
}
if (newPassword.length < 8) {
res.status(400).json({ error: 'Password must be at least 8 characters' });
return;
}
if (!/[A-Z]/.test(newPassword)) {
res.status(400).json({ error: 'Password must contain uppercase letter' });
return;
}
if (!/[0-9]/.test(newPassword)) {
res.status(400).json({ error: 'Password must contain a number' });
return;
}
// Reset password...
}
}Good Example
// Correct approach: Extract reusable validation functions
// Validation result type
interface ValidationResult {
isValid: boolean;
errors: string[];
}
// Reusable validation functions
class Validators {
static email(email: string | undefined, options: { required?: boolean } = {}): ValidationResult {
const errors: string[] = [];
if (!email) {
if (options.required) {
errors.push('Email is required');
}
return { isValid: !options.required, errors };
}
if (!email.includes('@') || !email.includes('.')) {
errors.push('Invalid email format');
}
if (email.length > 255) {
errors.push('Email must be 255 characters or less');
}
return { isValid: errors.length === 0, errors };
}
static password(password: string | undefined, options: { required?: boolean } = {}): ValidationResult {
const errors: string[] = [];
if (!password) {
if (options.required) {
errors.push('Password is required');
}
return { isValid: !options.required, errors };
}
if (password.length < 8) {
errors.push('Password must be at least 8 characters');
}
if (!/[A-Z]/.test(password)) {
errors.push('Password must contain at least one uppercase letter');
}
if (!/[a-z]/.test(password)) {
errors.push('Password must contain at least one lowercase letter');
}
if (!/[0-9]/.test(password)) {
errors.push('Password must contain at least one number');
}
return { isValid: errors.length === 0, errors };
}
static combine(...results: ValidationResult[]): ValidationResult {
const errors = results.flatMap(r => r.errors);
return { isValid: errors.length === 0, errors };
}
}
// Reusable error response helper
function validationError(res: Response, errors: string[]): void {
res.status(400).json({ errors });
}
// Clean controller using extracted validations
class UserController {
async createUser(req: Request, res: Response): Promise<void> {
const { email, password, name } = req.body;
const validation = Validators.combine(
Validators.email(email, { required: true }),
Validators.password(password, { required: true })
);
if (!validation.isValid) {
return validationError(res, validation.errors);
}
// Create user...
}
async updateUser(req: Request, res: Response): Promise<void> {
const { email, password } = req.body;
const validation = Validators.combine(
Validators.email(email),
Validators.password(password)
);
if (!validation.isValid) {
return validationError(res, validation.errors);
}
// Update user...
}
async resetPassword(req: Request, res: Response): Promise<void> {
const { email, newPassword } = req.body;
const validation = Validators.combine(
Validators.email(email, { required: true }),
Validators.password(newPassword, { required: true })
);
if (!validation.isValid) {
return validationError(res, validation.errors);
}
// Reset password...
}
}
// Validators can be reused across the application
class AdminController {
async inviteUser(req: Request, res: Response): Promise<void> {
const { email } = req.body;
const validation = Validators.email(email, { required: true });
if (!validation.isValid) {
return validationError(res, validation.errors);
}
// Send invite...
}
}
// Easy to test in isolation
describe('Validators', () => {
describe('email', () => {
it('should reject invalid email format', () => {
const result = Validators.email('invalid');
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Invalid email format');
});
it('should accept valid email', () => {
const result = Validators.email('user@example.com');
expect(result.isValid).toBe(true);
});
it('should require email when required option is set', () => {
const result = Validators.email(undefined, { required: true });
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Email is required');
});
});
describe('password', () => {
it('should reject short passwords', () => {
const result = Validators.password('short');
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Password must be at least 8 characters');
});
it('should require uppercase letter', () => {
const result = Validators.password('lowercase123');
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Password must contain at least one uppercase letter');
});
});
});Why
1. Single Source of Truth: Password rules are defined once. Change them in one place, and all usages are updated.
2. Consistency: All email validations behave the same way. No risk of inconsistent error messages or rules.
3. Easier Testing: Test the validation logic once, thoroughly. No need to test the same logic in every controller.
4. Bug Fixes Propagate: Fix a bug in Validators.email(), and it's fixed everywhere.
5. Reduced Code Size: Less code means less to read, less to maintain, and fewer places for bugs.
6. Better Abstraction: Controllers focus on HTTP concerns, validators focus on validation.
7. Reusability: Same validators work in controllers, services, CLI tools, or anywhere else.
DRY - Single Source of Truth
Every piece of knowledge or configuration should exist in exactly one place. When data or logic needs to be referenced from multiple locations, use a single authoritative source.
Bad Example
// Anti-pattern: Same values defined in multiple places
// In constants file
const API_BASE_URL = 'https://api.example.com/v1';
// In another file - duplicate!
const baseUrl = 'https://api.example.com/v1';
// In config.ts - another duplicate!
export const config = {
apiUrl: 'https://api.example.com/v1'
};
// In API client - yet another!
class ApiClient {
private baseUrl = 'https://api.example.com/v1'; // Duplicated!
}
// Status codes defined in multiple places
class OrderService {
async getOrder(id: string): Promise<Order> {
const order = await this.repository.findById(id);
if (order.status === 'pending') { // Magic string
// ...
}
if (order.status === 'completed') { // Magic string
// ...
}
}
}
class OrderController {
async listPendingOrders(): Promise<Order[]> {
return this.repository.findByStatus('pending'); // Same magic string
}
}
// In frontend code
const isPending = order.status === 'pending'; // And again
// Database seeds with duplicated data
const seedRoles = [
{ id: 1, name: 'admin', permissions: ['read', 'write', 'delete', 'admin'] },
{ id: 2, name: 'editor', permissions: ['read', 'write'] },
{ id: 3, name: 'viewer', permissions: ['read'] }
];
// In authorization middleware - duplicated permission logic
function checkPermission(user: User, action: string): boolean {
if (user.role === 'admin') {
return true; // Admin can do anything - duplicated knowledge
}
if (user.role === 'editor' && ['read', 'write'].includes(action)) {
return true; // Editor permissions - duplicated
}
if (user.role === 'viewer' && action === 'read') {
return true; // Viewer permissions - duplicated
}
return false;
}Good Example
// Correct approach: Single source of truth for all shared knowledge
// Configuration - one authoritative source
// config/index.ts
export const Config = {
api: {
baseUrl: process.env.API_BASE_URL || 'https://api.example.com/v1',
timeout: Number(process.env.API_TIMEOUT) || 30000,
retries: Number(process.env.API_RETRIES) || 3
},
database: {
url: process.env.DATABASE_URL!,
poolSize: Number(process.env.DB_POOL_SIZE) || 10
}
} as const;
// All code references the single config
class ApiClient {
constructor(private baseUrl: string = Config.api.baseUrl) {}
}
// Enums for finite sets of values - single source of truth
// domain/order/status.ts
export const OrderStatus = {
PENDING: 'pending',
PROCESSING: 'processing',
SHIPPED: 'shipped',
DELIVERED: 'delivered',
CANCELLED: 'cancelled'
} as const;
export type OrderStatus = typeof OrderStatus[keyof typeof OrderStatus];
// All code uses the enum
class OrderService {
async getOrder(id: string): Promise<Order> {
const order = await this.repository.findById(id);
if (order.status === OrderStatus.PENDING) {
// Single source of truth
}
}
}
class OrderController {
async listPendingOrders(): Promise<Order[]> {
return this.repository.findByStatus(OrderStatus.PENDING); // Same source
}
}
// Roles and permissions - single authoritative definition
// domain/auth/roles.ts
export const Permission = {
READ: 'read',
WRITE: 'write',
DELETE: 'delete',
ADMIN: 'admin'
} as const;
export type Permission = typeof Permission[keyof typeof Permission];
export const RoleDefinitions = {
admin: {
name: 'Administrator',
permissions: [Permission.READ, Permission.WRITE, Permission.DELETE, Permission.ADMIN]
},
editor: {
name: 'Editor',
permissions: [Permission.READ, Permission.WRITE]
},
viewer: {
name: 'Viewer',
permissions: [Permission.READ]
}
} as const;
export type RoleName = keyof typeof RoleDefinitions;
// Permission checking uses the definitions
export function hasPermission(role: RoleName, permission: Permission): boolean {
const roleDef = RoleDefinitions[role];
return roleDef.permissions.includes(permission);
}
// Database seeds generated from the single source
export function generateRoleSeeds(): RoleSeed[] {
return Object.entries(RoleDefinitions).map(([key, def], index) => ({
id: index + 1,
name: key,
displayName: def.name,
permissions: [...def.permissions]
}));
}
// Validation rules - single source
// domain/user/validation.ts
export const UserValidationRules = {
email: {
maxLength: 255,
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
},
password: {
minLength: 8,
maxLength: 128,
requireUppercase: true,
requireLowercase: true,
requireNumber: true,
requireSpecial: false
},
username: {
minLength: 3,
maxLength: 30,
pattern: /^[a-zA-Z0-9_]+$/
}
} as const;
// Validators use the rules
export function validateEmail(email: string): ValidationResult {
const rules = UserValidationRules.email;
if (email.length > rules.maxLength) {
return { valid: false, error: `Email must be ${rules.maxLength} characters or less` };
}
if (!rules.pattern.test(email)) {
return { valid: false, error: 'Invalid email format' };
}
return { valid: true };
}
// Database schema uses the same rules
// migrations/001_create_users.ts
export const createUsersTable = `
CREATE TABLE users (
id UUID PRIMARY KEY,
email VARCHAR(${UserValidationRules.email.maxLength}) NOT NULL UNIQUE,
username VARCHAR(${UserValidationRules.username.maxLength}) NOT NULL UNIQUE,
-- ...
)
`;
// Frontend validation uses the same rules (shared module)
// shared/validation.ts (used by both frontend and backend)
export function getPasswordRequirements(): string[] {
const rules = UserValidationRules.password;
const requirements: string[] = [];
requirements.push(`At least ${rules.minLength} characters`);
if (rules.requireUppercase) requirements.push('At least one uppercase letter');
if (rules.requireLowercase) requirements.push('At least one lowercase letter');
if (rules.requireNumber) requirements.push('At least one number');
if (rules.requireSpecial) requirements.push('At least one special character');
return requirements;
}
// Error messages - single source
// errors/messages.ts
export const ErrorMessages = {
user: {
notFound: 'User not found',
alreadyExists: 'A user with this email already exists',
invalidCredentials: 'Invalid email or password',
accountLocked: 'Account is locked. Please contact support.'
},
order: {
notFound: 'Order not found',
alreadyCancelled: 'Order has already been cancelled',
cannotCancel: 'Order cannot be cancelled in its current state'
},
auth: {
tokenExpired: 'Your session has expired. Please log in again.',
unauthorized: 'You do not have permission to perform this action'
}
} as const;
// All code uses the same error messages
class UserService {
async findById(id: string): Promise<User> {
const user = await this.repository.findById(id);
if (!user) {
throw new NotFoundError(ErrorMessages.user.notFound);
}
return user;
}
}Why
1. Consistency: The same value is always the same everywhere. No "this worked yesterday" bugs.
2. Easy Updates: Change a validation rule, config value, or status code in one place.
3. Prevents Drift: Without a single source, values diverge over time as different developers make changes.
4. Documentation: The source file serves as documentation for what values are valid.
5. Type Safety: TypeScript can enforce that only valid values are used.
6. Searchability: Easy to find all usages by searching for the constant name.
7. Refactoring: Rename a status? Change it in one place and let the compiler find all usages.
Don't Repeat Yourself (DRY)
Why It Matters
"Don't Repeat Yourself" means every piece of knowledge should have a single, authoritative representation. Duplication leads to inconsistencies, increases maintenance burden, and makes bugs harder to fix. When you change one copy but forget others, bugs creep in.
Incorrect
// ❌ Duplicated validation logic
class UserController {
createUser(data) {
if (!data.email || !data.email.includes('@')) {
throw new Error('Invalid email');
}
if (!data.password || data.password.length < 8) {
throw new Error('Password too short');
}
// create user...
}
updateUser(id, data) {
if (!data.email || !data.email.includes('@')) { // Duplicated
throw new Error('Invalid email');
}
if (!data.password || data.password.length < 8) { // Duplicated
throw new Error('Password too short');
}
// update user...
}
}
// ❌ Duplicated business rules
function calculateOrderTotal(items) {
let total = 0;
for (const item of items) {
total += item.price * item.quantity;
}
if (total > 100) {
total = total * 0.9; // 10% discount over $100
}
return total;
}
function calculateCartTotal(cartItems) {
let total = 0;
for (const item of cartItems) {
total += item.price * item.quantity; // Duplicated
}
if (total > 100) {
total = total * 0.9; // Duplicated discount logic
}
return total;
}
// ❌ Duplicated constants
// file: checkout.ts
const TAX_RATE = 0.08;
const FREE_SHIPPING_THRESHOLD = 50;
// file: cart.ts
const TAX_RATE = 0.08; // Duplicated
const FREE_SHIPPING_THRESHOLD = 50; // DuplicatedCorrect
Extract Shared Validation
// ✅ Single validation module
// validators/user.ts
export class UserValidator {
static validateEmail(email: string): void {
if (!email || !email.includes('@')) {
throw new ValidationError('Invalid email address');
}
}
static validatePassword(password: string): void {
if (!password || password.length < 8) {
throw new ValidationError('Password must be at least 8 characters');
}
}
static validate(data: UserData): void {
this.validateEmail(data.email);
this.validatePassword(data.password);
}
}
// controller.ts
class UserController {
createUser(data) {
UserValidator.validate(data);
// create user...
}
updateUser(id, data) {
UserValidator.validate(data);
// update user...
}
}Extract Shared Business Logic
// ✅ Single source of truth for pricing
// services/pricing.ts
export class PricingService {
private static readonly BULK_DISCOUNT_THRESHOLD = 100;
private static readonly BULK_DISCOUNT_RATE = 0.1;
static calculateSubtotal(items: LineItem[]): number {
return items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
}
static applyDiscount(subtotal: number): number {
if (subtotal > this.BULK_DISCOUNT_THRESHOLD) {
return subtotal * (1 - this.BULK_DISCOUNT_RATE);
}
return subtotal;
}
static calculateTotal(items: LineItem[]): number {
const subtotal = this.calculateSubtotal(items);
return this.applyDiscount(subtotal);
}
}
// Both order and cart use the same logic
const orderTotal = PricingService.calculateTotal(order.items);
const cartTotal = PricingService.calculateTotal(cart.items);Centralize Constants
// ✅ Single constants file
// constants/pricing.ts
export const PRICING = {
TAX_RATE: 0.08,
FREE_SHIPPING_THRESHOLD: 50,
BULK_DISCOUNT_THRESHOLD: 100,
BULK_DISCOUNT_RATE: 0.1,
} as const;
// Used everywhere
import { PRICING } from '@/constants/pricing';
const tax = subtotal * PRICING.TAX_RATE;
const freeShipping = total >= PRICING.FREE_SHIPPING_THRESHOLD;Extract Shared Components
// ❌ Duplicated UI patterns
function UserCard({ user }) {
return (
<div className="p-4 rounded-lg shadow bg-white">
<img src={user.avatar} className="w-12 h-12 rounded-full" />
<h3 className="font-bold">{user.name}</h3>
<p className="text-gray-600">{user.email}</p>
</div>
);
}
function TeamMemberCard({ member }) {
return (
<div className="p-4 rounded-lg shadow bg-white"> {/* Same styles */}
<img src={member.avatar} className="w-12 h-12 rounded-full" />
<h3 className="font-bold">{member.name}</h3>
<p className="text-gray-600">{member.role}</p>
</div>
);
}
// ✅ Reusable component
function Card({ children, className }) {
return (
<div className={cn("p-4 rounded-lg shadow bg-white", className)}>
{children}
</div>
);
}
function Avatar({ src, alt }) {
return <img src={src} alt={alt} className="w-12 h-12 rounded-full" />;
}
function UserCard({ user }) {
return (
<Card>
<Avatar src={user.avatar} alt={user.name} />
<h3 className="font-bold">{user.name}</h3>
<p className="text-gray-600">{user.email}</p>
</Card>
);
}
function TeamMemberCard({ member }) {
return (
<Card>
<Avatar src={member.avatar} alt={member.name} />
<h3 className="font-bold">{member.name}</h3>
<p className="text-gray-600">{member.role}</p>
</Card>
);
}Extract Shared Hooks
// ❌ Duplicated fetch logic
function UserProfile() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch('/api/user')
.then(res => res.json())
.then(setUser)
.catch(setError)
.finally(() => setLoading(false));
}, []);
// ...
}
function ProductList() {
const [products, setProducts] = useState(null);
const [loading, setLoading] = useState(true); // Duplicated
const [error, setError] = useState(null); // Duplicated
useEffect(() => {
fetch('/api/products') // Same pattern
.then(res => res.json())
.then(setProducts)
.catch(setError)
.finally(() => setLoading(false));
}, []);
// ...
}
// ✅ Custom hook (or use React Query)
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
return { data, loading, error };
}
function UserProfile() {
const { data: user, loading, error } = useFetch<User>('/api/user');
// ...
}
function ProductList() {
const { data: products, loading, error } = useFetch<Product[]>('/api/products');
// ...
}When NOT to DRY
// ⚠️ Don't extract coincidentally similar code
// These might look similar but serve different purposes
function validateUserAge(age: number) {
return age >= 18; // Legal adult age
}
function validateMinimumOrderQuantity(quantity: number) {
return quantity >= 18; // Business rule: minimum order
}
// These should remain separate even though both check >= 18
// Their reasons for change are differentRule of Three
Wait until you see duplication three times before extracting.
Two occurrences might be coincidental. Three indicates a pattern.Benefits
- Single source of truth
- Fix bugs in one place
- Consistent behavior across codebase
- Easier refactoring
- Reduced code size
- Lower maintenance cost
Encapsulation
Hide internal implementation details and expose only what's necessary through a well-defined interface. Protect data integrity by controlling access to internal state.
Bad Example
// Anti-pattern: Exposed internals, no encapsulation
class BankAccount {
// Public fields - anyone can modify directly
public accountNumber: string;
public balance: number;
public transactions: Transaction[];
public overdraftLimit: number;
public isLocked: boolean;
constructor(accountNumber: string, initialBalance: number) {
this.accountNumber = accountNumber;
this.balance = initialBalance;
this.transactions = [];
this.overdraftLimit = 0;
this.isLocked = false;
}
}
// External code can violate business rules
const account = new BankAccount('12345', 1000);
// Direct modification bypasses validation
account.balance = -999999; // Negative balance without check!
account.balance = account.balance + 1000; // No transaction record!
// Can manipulate transaction history
account.transactions.push({
id: 'fake',
amount: 1000000,
type: 'deposit'
}); // Fraudulent transaction!
// Can unlock locked accounts
account.isLocked = false; // Bypasses security!
// Can change overdraft without authorization
account.overdraftLimit = 100000; // Unauthorized overdraft!
// Other classes depend on internal structure
class AccountReport {
generate(account: BankAccount): Report {
// Directly accesses internal array
const deposits = account.transactions.filter(t => t.type === 'deposit');
const withdrawals = account.transactions.filter(t => t.type === 'withdrawal');
// Depends on internal structure of Transaction
const totalDeposits = deposits.reduce((sum, t) => sum + t.amount, 0);
return {
balance: account.balance,
totalDeposits,
transactionCount: account.transactions.length
};
}
}
// Problems:
// 1. Anyone can modify balance without recording transaction
// 2. Business rules can be bypassed
// 3. No audit trail for changes
// 4. Internal structure changes break external code
// 5. No way to add validation later without breaking changesGood Example
// Correct approach: Proper encapsulation
class BankAccount {
private readonly _accountNumber: string;
private _balance: number;
private readonly _transactions: Transaction[] = [];
private _overdraftLimit: number = 0;
private _isLocked: boolean = false;
private _lockReason: string | null = null;
constructor(accountNumber: string, initialBalance: number) {
if (!accountNumber || accountNumber.length < 5) {
throw new Error('Invalid account number');
}
if (initialBalance < 0) {
throw new Error('Initial balance cannot be negative');
}
this._accountNumber = accountNumber;
this._balance = initialBalance;
this._transactions.push(
Transaction.createInitial(initialBalance)
);
}
// Read-only access to account number
get accountNumber(): string {
return this._accountNumber;
}
// Read-only access to balance
get balance(): number {
return this._balance;
}
// Read-only access to lock status
get isLocked(): boolean {
return this._isLocked;
}
// Controlled deposit with validation and audit trail
deposit(amount: number, description: string = 'Deposit'): Transaction {
this.ensureNotLocked();
if (amount <= 0) {
throw new InvalidAmountError('Deposit amount must be positive');
}
this._balance += amount;
const transaction = Transaction.createDeposit(amount, description, this._balance);
this._transactions.push(transaction);
return transaction;
}
// Controlled withdrawal with business rules
withdraw(amount: number, description: string = 'Withdrawal'): Transaction {
this.ensureNotLocked();
if (amount <= 0) {
throw new InvalidAmountError('Withdrawal amount must be positive');
}
const availableBalance = this._balance + this._overdraftLimit;
if (amount > availableBalance) {
throw new InsufficientFundsError(amount, availableBalance);
}
this._balance -= amount;
const transaction = Transaction.createWithdrawal(amount, description, this._balance);
this._transactions.push(transaction);
return transaction;
}
// Transfer with proper validation
transferTo(recipient: BankAccount, amount: number): TransferResult {
this.ensureNotLocked();
recipient.ensureNotLocked();
if (amount <= 0) {
throw new InvalidAmountError('Transfer amount must be positive');
}
const withdrawalTx = this.withdraw(amount, `Transfer to ${recipient.accountNumber}`);
const depositTx = recipient.deposit(amount, `Transfer from ${this._accountNumber}`);
return { withdrawalTx, depositTx };
}
// Controlled overdraft limit modification
setOverdraftLimit(limit: number, authorizedBy: string): void {
if (limit < 0) {
throw new Error('Overdraft limit cannot be negative');
}
if (limit > 10000) {
throw new Error('Overdraft limit exceeds maximum allowed');
}
this._overdraftLimit = limit;
// Audit trail for limit changes
this._transactions.push(
Transaction.createAdministrative(
`Overdraft limit set to ${limit} by ${authorizedBy}`
)
);
}
// Security control with audit
lock(reason: string): void {
this._isLocked = true;
this._lockReason = reason;
this._transactions.push(
Transaction.createAdministrative(`Account locked: ${reason}`)
);
}
unlock(authorizedBy: string): void {
this._isLocked = false;
this._lockReason = null;
this._transactions.push(
Transaction.createAdministrative(`Account unlocked by ${authorizedBy}`)
);
}
// Return copy of transactions, not the internal array
getTransactionHistory(): ReadonlyArray<Transaction> {
return [...this._transactions];
}
// Provide summary without exposing internals
getSummary(): AccountSummary {
const deposits = this._transactions.filter(t => t.type === 'deposit');
const withdrawals = this._transactions.filter(t => t.type === 'withdrawal');
return {
accountNumber: this._accountNumber,
balance: this._balance,
isLocked: this._isLocked,
totalDeposits: deposits.reduce((sum, t) => sum + t.amount, 0),
totalWithdrawals: withdrawals.reduce((sum, t) => sum + t.amount, 0),
transactionCount: this._transactions.length
};
}
// Private helper method
private ensureNotLocked(): void {
if (this._isLocked) {
throw new AccountLockedError(this._lockReason || 'Account is locked');
}
}
}
// Transaction is also encapsulated
class Transaction {
private constructor(
public readonly id: string,
public readonly type: TransactionType,
public readonly amount: number,
public readonly description: string,
public readonly balanceAfter: number,
public readonly timestamp: Date
) {}
static createDeposit(amount: number, description: string, balanceAfter: number): Transaction {
return new Transaction(
generateId(),
'deposit',
amount,
description,
balanceAfter,
new Date()
);
}
static createWithdrawal(amount: number, description: string, balanceAfter: number): Transaction {
return new Transaction(
generateId(),
'withdrawal',
amount,
description,
balanceAfter,
new Date()
);
}
static createInitial(balance: number): Transaction {
return new Transaction(
generateId(),
'initial',
balance,
'Account opened',
balance,
new Date()
);
}
static createAdministrative(description: string): Transaction {
return new Transaction(
generateId(),
'administrative',
0,
description,
0,
new Date()
);
}
}
// Usage - business rules are enforced
const account = new BankAccount('12345', 1000);
// Proper deposit with audit trail
account.deposit(500, 'Paycheck');
// Withdrawal validates funds
try {
account.withdraw(2000); // Will throw InsufficientFundsError
} catch (error) {
console.log('Cannot withdraw more than available');
}
// Cannot manipulate balance directly
// account.balance = 999999; // Error: Property 'balance' is read-only
// Cannot manipulate transactions
// account.getTransactionHistory().push(fakeTx); // Original array unaffected
// Account summary provides what reports need
const summary = account.getSummary();
console.log(`Balance: ${summary.balance}, Transactions: ${summary.transactionCount}`);Why
1. Data Integrity: Balance can only change through proper deposit/withdraw methods that maintain consistency.
2. Business Rules: All rules (positive amounts, sufficient funds, locked accounts) are enforced in one place.
3. Audit Trail: Every change is recorded. Cannot modify history without proper methods.
4. Flexibility: Internal representation can change without affecting clients. Switch from array to database later.
5. Security: Cannot bypass validation or manipulate internal state directly.
6. Testing: Can verify all edge cases through the public interface. Internal state is controlled.
7. Documentation: The public interface documents what operations are allowed and how to use them.
Fail Fast Principle
Detect and report errors as early as possible. Validate inputs at system boundaries, check preconditions at the start of functions, and throw exceptions immediately when something is wrong.
Bad Example
// Anti-pattern: Delayed error detection
class OrderProcessor {
async processOrder(order: any): Promise<ProcessResult> {
// No validation - problems will surface later
// Proceeds even with potentially invalid data
const customer = await this.customerRepo.findById(order.customerId);
// Customer might be null, but we keep going
const items = order.items;
// Calculates total even if items might be undefined or empty
let total = 0;
if (items) {
for (const item of items) {
// item.productId might not exist
const product = await this.productRepo.findById(item.productId);
// product might be null
if (product) {
total += product.price * (item.quantity || 1);
}
}
}
// Attempts payment even if customer is null
let paymentResult;
if (customer && customer.paymentMethod) {
paymentResult = await this.paymentService.charge(
customer.paymentMethod,
total
);
}
// Creates order record even if payment failed
const orderRecord = await this.orderRepo.create({
customerId: order.customerId,
total,
status: paymentResult?.success ? 'paid' : 'pending'
});
// Sends email even if we don't have a valid email
if (customer?.email) {
await this.emailService.send(customer.email, 'Order confirmation');
}
// Returns "success" even though many things might have gone wrong
return { success: true, orderId: orderRecord.id };
}
}
// Problems:
// 1. Null customer leads to silent failures
// 2. Empty order goes through system doing nothing useful
// 3. Payment failure creates orphaned order records
// 4. No indication of what went wrong
// 5. Database in inconsistent stateGood Example
// Correct approach: Fail fast with immediate validation
// Custom error types for clear failure reasons
class ValidationError extends Error {
constructor(
message: string,
public readonly field: string,
public readonly value: unknown
) {
super(message);
this.name = 'ValidationError';
}
}
class NotFoundError extends Error {
constructor(
public readonly entity: string,
public readonly id: string
) {
super(`${entity} not found: ${id}`);
this.name = 'NotFoundError';
}
}
class PaymentError extends Error {
constructor(
message: string,
public readonly code: string
) {
super(message);
this.name = 'PaymentError';
}
}
// Input validation schema
interface CreateOrderInput {
customerId: string;
items: OrderItemInput[];
}
interface OrderItemInput {
productId: string;
quantity: number;
}
class OrderProcessor {
async processOrder(input: unknown): Promise<ProcessResult> {
// STEP 1: Validate input immediately
const validatedInput = this.validateInput(input);
// STEP 2: Verify customer exists before proceeding
const customer = await this.loadCustomer(validatedInput.customerId);
// STEP 3: Verify all products exist before any processing
const products = await this.loadProducts(validatedInput.items);
// STEP 4: Verify payment method before creating order
this.verifyPaymentMethod(customer);
// STEP 5: Calculate total (now safe - all data validated)
const total = this.calculateTotal(validatedInput.items, products);
// STEP 6: Verify sufficient inventory before payment
await this.verifyInventory(validatedInput.items, products);
// STEP 7: Process payment before creating order
const payment = await this.processPayment(customer, total);
// STEP 8: Create order only after successful payment
const order = await this.createOrder(validatedInput, customer, total, payment);
// STEP 9: Send confirmation (non-critical, can fail gracefully)
await this.sendConfirmation(customer, order);
return { success: true, orderId: order.id };
}
private validateInput(input: unknown): CreateOrderInput {
if (!input || typeof input !== 'object') {
throw new ValidationError('Input must be an object', 'input', input);
}
const obj = input as Record<string, unknown>;
if (!obj.customerId || typeof obj.customerId !== 'string') {
throw new ValidationError('customerId is required and must be a string', 'customerId', obj.customerId);
}
if (!Array.isArray(obj.items) || obj.items.length === 0) {
throw new ValidationError('items must be a non-empty array', 'items', obj.items);
}
const validatedItems: OrderItemInput[] = [];
for (let i = 0; i < obj.items.length; i++) {
const item = obj.items[i];
if (!item || typeof item !== 'object') {
throw new ValidationError(`Item at index ${i} must be an object`, `items[${i}]`, item);
}
const itemObj = item as Record<string, unknown>;
if (!itemObj.productId || typeof itemObj.productId !== 'string') {
throw new ValidationError(
`productId is required at index ${i}`,
`items[${i}].productId`,
itemObj.productId
);
}
if (typeof itemObj.quantity !== 'number' || itemObj.quantity < 1) {
throw new ValidationError(
`quantity must be a positive number at index ${i}`,
`items[${i}].quantity`,
itemObj.quantity
);
}
validatedItems.push({
productId: itemObj.productId,
quantity: itemObj.quantity
});
}
return {
customerId: obj.customerId,
items: validatedItems
};
}
private async loadCustomer(customerId: string): Promise<Customer> {
const customer = await this.customerRepo.findById(customerId);
if (!customer) {
throw new NotFoundError('Customer', customerId);
}
if (!customer.isActive) {
throw new ValidationError('Customer account is inactive', 'customerId', customerId);
}
return customer;
}
private async loadProducts(items: OrderItemInput[]): Promise<Map<string, Product>> {
const productIds = items.map(item => item.productId);
const products = await this.productRepo.findByIds(productIds);
const productMap = new Map<string, Product>();
for (const product of products) {
productMap.set(product.id, product);
}
// Verify all products were found
for (const item of items) {
if (!productMap.has(item.productId)) {
throw new NotFoundError('Product', item.productId);
}
}
return productMap;
}
private verifyPaymentMethod(customer: Customer): void {
if (!customer.paymentMethodId) {
throw new ValidationError(
'Customer has no payment method configured',
'paymentMethod',
null
);
}
}
private calculateTotal(items: OrderItemInput[], products: Map<string, Product>): number {
return items.reduce((total, item) => {
const product = products.get(item.productId)!; // Safe - already validated
return total + product.price * item.quantity;
}, 0);
}
private async verifyInventory(
items: OrderItemInput[],
products: Map<string, Product>
): Promise<void> {
for (const item of items) {
const product = products.get(item.productId)!;
if (product.stock < item.quantity) {
throw new ValidationError(
`Insufficient stock for product ${product.name}. Available: ${product.stock}, Requested: ${item.quantity}`,
'quantity',
item.quantity
);
}
}
}
private async processPayment(customer: Customer, amount: number): Promise<Payment> {
try {
return await this.paymentService.charge(customer.paymentMethodId, amount);
} catch (error) {
throw new PaymentError(
`Payment failed: ${error.message}`,
error.code || 'UNKNOWN'
);
}
}
private async createOrder(
input: CreateOrderInput,
customer: Customer,
total: number,
payment: Payment
): Promise<Order> {
return this.orderRepo.create({
customerId: customer.id,
items: input.items,
total,
paymentId: payment.id,
status: 'paid'
});
}
private async sendConfirmation(customer: Customer, order: Order): Promise<void> {
try {
await this.emailService.sendOrderConfirmation(customer.email, order);
} catch (error) {
// Log but don't fail - email is non-critical
this.logger.error('Failed to send order confirmation', { orderId: order.id, error });
}
}
}Why
1. Clear Error Messages: When validation fails, you know exactly what's wrong and where.
2. No Wasted Work: Invalid requests fail immediately, not after expensive operations.
3. Data Integrity: The database never enters an inconsistent state because we validate before mutating.
4. Debugging: Stack traces point to the actual problem, not to a downstream symptom.
5. Predictability: Either the operation fully succeeds or it cleanly fails with a clear reason.
6. Security: Invalid inputs are rejected at the boundary, not passed through the system.
7. Recovery: Callers can handle specific error types appropriately (retry, report, fallback).
KISS - Readability
Code is read far more often than it is written. Optimize for readability by using clear names, straightforward logic, and avoiding clever tricks that obscure intent.
Bad Example
// Anti-pattern: Clever but cryptic code
// One-liner that's hard to understand
const r = d.filter(x => x.s === 'a' && x.t > Date.now() - 864e5).reduce((a, x) => ({ ...a, [x.c]: (a[x.c] || 0) + x.v }), {});
// Nested ternaries
const status = x > 100 ? 'high' : x > 50 ? 'medium' : x > 20 ? 'low' : x > 0 ? 'minimal' : 'none';
// Bitwise operations for boolean logic
const isValid = !!(flags & 0x1) && !!(flags & 0x2) || !!(flags & 0x4);
// Regex that nobody can read
const isValidEmail = /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$/i.test(email);
// Clever use of || and && for control flow
user && user.isActive && user.hasPermission('admin') && doAdminStuff() || showError();
// Abusing array methods
const result = [...Array(10)].map((_, i) => i * 2).filter(Boolean).reduce((a, b) => a + b, 0);
// Short variable names that save typing but cost understanding
function p(d, o) {
return d.map(i => ({ ...i, t: i.t * o.r, s: o.s ? i.s + o.s : i.s })).filter(i => i.t > o.m);
}Good Example
// Correct approach: Clear, self-documenting code
// Break complex operations into named steps
function getActiveOrdersSummaryByCategory(orders: Order[]): Record<string, number> {
const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000;
const activeRecentOrders = orders.filter(order =>
order.status === 'active' && order.timestamp > oneDayAgo
);
const summaryByCategory: Record<string, number> = {};
for (const order of activeRecentOrders) {
const currentTotal = summaryByCategory[order.category] || 0;
summaryByCategory[order.category] = currentTotal + order.value;
}
return summaryByCategory;
}
// Use clear conditional logic
function getAlertLevel(value: number): AlertLevel {
if (value > 100) {
return 'high';
}
if (value > 50) {
return 'medium';
}
if (value > 20) {
return 'low';
}
if (value > 0) {
return 'minimal';
}
return 'none';
}
// Use named constants for flags
const UserFlags = {
IS_VERIFIED: 0x1,
IS_PREMIUM: 0x2,
IS_ADMIN: 0x4
} as const;
function hasUserFlag(flags: number, flag: number): boolean {
return (flags & flag) !== 0;
}
function isValidPremiumAdmin(flags: number): boolean {
const isVerified = hasUserFlag(flags, UserFlags.IS_VERIFIED);
const isPremium = hasUserFlag(flags, UserFlags.IS_PREMIUM);
const isAdmin = hasUserFlag(flags, UserFlags.IS_ADMIN);
return (isVerified && isPremium) || isAdmin;
}
// Use a simple email validation
function isValidEmail(email: string): boolean {
if (!email || email.length > 254) {
return false;
}
const atIndex = email.indexOf('@');
if (atIndex < 1) {
return false;
}
const domain = email.slice(atIndex + 1);
if (!domain || !domain.includes('.')) {
return false;
}
return true;
}
// Or use a well-tested library with clear intent
import { isEmail } from 'validator';
const isValidEmail = isEmail(email);
// Use explicit control flow
function handleUserAction(user: User | null): void {
if (!user) {
showError('User not found');
return;
}
if (!user.isActive) {
showError('User account is inactive');
return;
}
if (!user.hasPermission('admin')) {
showError('Admin permission required');
return;
}
doAdminStuff();
}
// Use descriptive variable and function names
function generateEvenNumbersSum(count: number): number {
const evenNumbers: number[] = [];
for (let i = 0; i < count; i++) {
evenNumbers.push(i * 2);
}
const sum = evenNumbers.reduce((total, num) => total + num, 0);
return sum;
}
// Use full, descriptive parameter names
interface PriceAdjustmentOptions {
rateMultiplier: number;
shippingSurcharge?: number;
minimumThreshold: number;
}
function adjustProductPrices(
products: Product[],
options: PriceAdjustmentOptions
): Product[] {
return products
.map(product => ({
...product,
totalPrice: product.basePrice * options.rateMultiplier,
shippingCost: options.shippingSurcharge
? product.shippingCost + options.shippingSurcharge
: product.shippingCost
}))
.filter(product => product.totalPrice > options.minimumThreshold);
}
// Usage is self-documenting
const adjustedProducts = adjustProductPrices(products, {
rateMultiplier: 1.1,
shippingSurcharge: 5,
minimumThreshold: 20
});Why
1. Comprehension Speed: Readable code is understood quickly. Clever code requires deciphering.
2. Fewer Bugs: When code clearly expresses intent, mistakes are obvious.
3. Onboarding: New team members can contribute faster with readable code.
4. Code Reviews: Reviewers can focus on logic, not translation.
5. Future You: Code you wrote 6 months ago might as well have been written by someone else.
6. Maintenance Cost: Most of a codebase's lifetime is spent in maintenance, not initial development.
7. Collaboration: Teams work better when everyone can understand everyone's code.
KISS Principle - Simplicity
Keep It Simple, Stupid. Choose the simplest solution that solves the problem. Avoid unnecessary complexity, over-engineering, and clever code that's hard to understand.
Bad Example
// Anti-pattern: Over-engineered solution for a simple problem
// Simple task: Check if a user can access a resource
// Over-engineered approach with unnecessary abstractions
interface AccessControlStrategy {
evaluate(context: AccessContext): AccessDecision;
}
interface AccessContext {
subject: Subject;
resource: Resource;
action: Action;
environment: Environment;
}
interface Subject {
id: string;
attributes: Map<string, AttributeValue>;
}
interface Resource {
id: string;
type: string;
attributes: Map<string, AttributeValue>;
}
interface Action {
id: string;
attributes: Map<string, AttributeValue>;
}
interface Environment {
currentTime: Date;
ipAddress: string;
attributes: Map<string, AttributeValue>;
}
type AttributeValue = string | number | boolean | string[];
interface AccessDecision {
decision: 'permit' | 'deny' | 'indeterminate' | 'not_applicable';
obligations?: Obligation[];
advice?: Advice[];
}
interface Obligation {
id: string;
fulfillOn: 'permit' | 'deny';
attributes: Map<string, AttributeValue>;
}
interface Advice {
id: string;
appliesTo: 'permit' | 'deny';
attributes: Map<string, AttributeValue>;
}
class AttributeBasedAccessControl {
private strategies: AccessControlStrategy[] = [];
private combiningAlgorithm: CombiningAlgorithm;
constructor(combiningAlgorithm: CombiningAlgorithm) {
this.combiningAlgorithm = combiningAlgorithm;
}
addStrategy(strategy: AccessControlStrategy): void {
this.strategies.push(strategy);
}
evaluate(context: AccessContext): AccessDecision {
const decisions = this.strategies.map(s => s.evaluate(context));
return this.combiningAlgorithm.combine(decisions);
}
}
// 500+ more lines of abstraction layers...
// Actually usage for a simple check:
const context: AccessContext = {
subject: {
id: user.id,
attributes: new Map([['role', user.role]])
},
resource: {
id: document.id,
type: 'document',
attributes: new Map([['ownerId', document.ownerId]])
},
action: {
id: 'read',
attributes: new Map()
},
environment: {
currentTime: new Date(),
ipAddress: request.ip,
attributes: new Map()
}
};
const decision = accessControl.evaluate(context);
if (decision.decision === 'permit') {
// Allow access
}Good Example
// Correct approach: Simple, direct solution
// Simple function that does what's needed
function canUserAccessDocument(user: User, document: Document, action: 'read' | 'write' | 'delete'): boolean {
// Admin can do anything
if (user.role === 'admin') {
return true;
}
// Owner can do anything with their document
if (document.ownerId === user.id) {
return true;
}
// Check explicit permissions
const permission = document.permissions.find(p => p.userId === user.id);
if (!permission) {
return false;
}
// Check if permission level is sufficient
switch (action) {
case 'read':
return ['read', 'write', 'admin'].includes(permission.level);
case 'write':
return ['write', 'admin'].includes(permission.level);
case 'delete':
return permission.level === 'admin';
default:
return false;
}
}
// Usage is straightforward
if (canUserAccessDocument(user, document, 'read')) {
// Allow access
}
// If requirements grow, evolve the solution incrementally
// Add time-based access when actually needed
function canUserAccessDocument(
user: User,
document: Document,
action: 'read' | 'write' | 'delete'
): boolean {
// Admin can do anything
if (user.role === 'admin') {
return true;
}
// Owner can do anything with their document
if (document.ownerId === user.id) {
return true;
}
// Check explicit permissions
const permission = document.permissions.find(p => p.userId === user.id);
if (!permission) {
return false;
}
// Check expiration (added when actually needed)
if (permission.expiresAt && permission.expiresAt < new Date()) {
return false;
}
// Check if permission level is sufficient
const requiredLevel = getRequiredLevel(action);
return hasRequiredLevel(permission.level, requiredLevel);
}
function getRequiredLevel(action: 'read' | 'write' | 'delete'): PermissionLevel {
const levels: Record<string, PermissionLevel> = {
read: 'read',
write: 'write',
delete: 'admin'
};
return levels[action];
}
function hasRequiredLevel(userLevel: PermissionLevel, required: PermissionLevel): boolean {
const hierarchy: PermissionLevel[] = ['read', 'write', 'admin'];
return hierarchy.indexOf(userLevel) >= hierarchy.indexOf(required);
}
// Still simple, still readable, handles new requirement
// For multiple resources, create focused helper
class DocumentAccessChecker {
canRead(user: User, document: Document): boolean {
return canUserAccessDocument(user, document, 'read');
}
canWrite(user: User, document: Document): boolean {
return canUserAccessDocument(user, document, 'write');
}
canDelete(user: User, document: Document): boolean {
return canUserAccessDocument(user, document, 'delete');
}
// Filter a list of documents to only accessible ones
filterReadable(user: User, documents: Document[]): Document[] {
return documents.filter(doc => this.canRead(user, doc));
}
}
// Usage remains simple
const checker = new DocumentAccessChecker();
if (checker.canRead(user, document)) {
// Show document
}
const accessibleDocs = checker.filterReadable(user, allDocuments);Why
1. Readability: Simple code can be understood in seconds. Complex abstractions require studying.
2. Maintainability: New developers can work with simple code immediately. Complex frameworks need training.
3. Debugging: When something breaks, simple code has obvious failure points.
4. Performance: Simple code is often faster - fewer layers, fewer allocations, less indirection.
5. Time to Market: Simple solutions are built and shipped faster.
6. YAGNI Alignment: The complex solution solves problems you don't have (and may never have).
7. Incremental Complexity: Start simple, add complexity only when real requirements demand it. The simple solution can evolve.
Law of Demeter
A method should only talk to its immediate friends, not to strangers. Don't reach through objects to access their internal structure. This reduces coupling and makes code more maintainable.
Bad Example
// Anti-pattern: Reaching through object chains
class Address {
street: string;
city: string;
country: Country;
}
class Country {
name: string;
code: string;
taxRules: TaxRules;
}
class TaxRules {
vatRate: number;
calculateTax(amount: number): number {
return amount * this.vatRate;
}
}
class Customer {
name: string;
address: Address;
wallet: Wallet;
}
class Wallet {
balance: number;
currency: Currency;
deduct(amount: number): void {
this.balance -= amount;
}
}
class Currency {
code: string;
exchangeRate: number;
}
class Order {
customer: Customer;
items: OrderItem[];
// Violation: Reaching deep into customer's structure
getCustomerCountry(): string {
return this.customer.address.country.name; // 4 levels deep!
}
// Violation: Reaching into customer's wallet
calculateTax(): number {
const amount = this.getTotal();
// Reaching through customer -> address -> country -> taxRules
return this.customer.address.country.taxRules.calculateTax(amount);
}
// Violation: Manipulating customer's wallet directly
processPayment(): void {
const total = this.calculateTax() + this.getTotal();
// Reaching into wallet to check and modify
if (this.customer.wallet.balance < total) {
throw new Error('Insufficient funds');
}
// Reaching into wallet's currency for conversion
const exchangeRate = this.customer.wallet.currency.exchangeRate;
const convertedAmount = total * exchangeRate;
this.customer.wallet.deduct(convertedAmount);
}
getTotal(): number {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
}
// Problems with this approach:
// 1. Order knows too much about Customer's internal structure
// 2. Changes to Address, Country, or Wallet break Order
// 3. Hard to test - must mock entire object graph
// 4. Tight coupling between unrelated classesGood Example
// Correct approach: Talk only to immediate friends
class Address {
private street: string;
private city: string;
private country: Country;
constructor(street: string, city: string, country: Country) {
this.street = street;
this.city = city;
this.country = country;
}
getCountryName(): string {
return this.country.getName();
}
calculateTax(amount: number): number {
return this.country.calculateTax(amount);
}
}
class Country {
private name: string;
private code: string;
private taxRules: TaxRules;
constructor(name: string, code: string, taxRules: TaxRules) {
this.name = name;
this.code = code;
this.taxRules = taxRules;
}
getName(): string {
return this.name;
}
getCode(): string {
return this.code;
}
calculateTax(amount: number): number {
return this.taxRules.calculate(amount);
}
}
class TaxRules {
private vatRate: number;
constructor(vatRate: number) {
this.vatRate = vatRate;
}
calculate(amount: number): number {
return amount * this.vatRate;
}
}
class Wallet {
private balance: number;
private currency: Currency;
constructor(balance: number, currency: Currency) {
this.balance = balance;
this.currency = currency;
}
canAfford(amount: number): boolean {
const convertedAmount = this.currency.convert(amount);
return this.balance >= convertedAmount;
}
pay(amount: number): PaymentResult {
const convertedAmount = this.currency.convert(amount);
if (!this.canAfford(amount)) {
return { success: false, error: 'Insufficient funds' };
}
this.balance -= convertedAmount;
return { success: true, amountPaid: convertedAmount };
}
getBalance(): number {
return this.balance;
}
}
class Currency {
private code: string;
private exchangeRate: number;
constructor(code: string, exchangeRate: number) {
this.code = code;
this.exchangeRate = exchangeRate;
}
convert(amount: number): number {
return amount * this.exchangeRate;
}
}
class Customer {
private name: string;
private address: Address;
private wallet: Wallet;
constructor(name: string, address: Address, wallet: Wallet) {
this.name = name;
this.address = address;
this.wallet = wallet;
}
getName(): string {
return this.name;
}
getCountryName(): string {
return this.address.getCountryName();
}
calculateTaxFor(amount: number): number {
return this.address.calculateTax(amount);
}
canAfford(amount: number): boolean {
return this.wallet.canAfford(amount);
}
pay(amount: number): PaymentResult {
return this.wallet.pay(amount);
}
}
class Order {
private customer: Customer;
private items: OrderItem[];
constructor(customer: Customer, items: OrderItem[]) {
this.customer = customer;
this.items = items;
}
// Only talks to immediate friend (customer)
getCustomerCountry(): string {
return this.customer.getCountryName();
}
getTotal(): number {
return this.items.reduce((sum, item) => sum + item.getPrice(), 0);
}
// Asks customer to calculate tax (customer knows how)
calculateTax(): number {
return this.customer.calculateTaxFor(this.getTotal());
}
getTotalWithTax(): number {
return this.getTotal() + this.calculateTax();
}
// Asks customer to pay (customer handles wallet)
processPayment(): PaymentResult {
const total = this.getTotalWithTax();
if (!this.customer.canAfford(total)) {
return { success: false, error: 'Insufficient funds' };
}
return this.customer.pay(total);
}
}
// Usage
const order = new Order(customer, items);
const result = order.processPayment();
if (result.success) {
console.log(`Payment successful: ${result.amountPaid}`);
} else {
console.log(`Payment failed: ${result.error}`);
}
// Testing is now easy - only mock the immediate friend
describe('Order', () => {
it('should process payment through customer', () => {
const mockCustomer: Customer = {
getCountryName: () => 'USA',
calculateTaxFor: (amount: number) => amount * 0.1,
canAfford: () => true,
pay: jest.fn().mockReturnValue({ success: true, amountPaid: 110 })
} as any;
const order = new Order(mockCustomer, [{ getPrice: () => 100 }]);
const result = order.processPayment();
expect(result.success).toBe(true);
expect(mockCustomer.pay).toHaveBeenCalledWith(110);
});
});Why
1. Reduced Coupling: Order only knows about Customer. Changes to Address, Country, or Wallet don't affect Order.
2. Encapsulation: Internal structure is hidden. Customer can change how it stores address without affecting clients.
3. Easier Testing: Mock only the immediate friend. No need to construct deep object graphs.
4. Better Abstraction: Customer is responsible for customer things. Order doesn't need to know about wallets.
5. Maintainability: When requirements change, changes are localized to the responsible class.
6. Readability: customer.calculateTaxFor(amount) is clearer than customer.address.country.taxRules.calculateTax(amount).
7. Flexibility: Can change internal implementations without affecting clients. Customer could switch from wallet to payment service.
YAGNI Principle - Abstractions
Don't create abstractions until you have concrete evidence they're needed. Premature abstraction leads to wrong abstractions that are worse than no abstraction.
Bad Example
// Anti-pattern: Creating abstractions before understanding the problem
// Task: Send an email notification
// Over-abstracted solution based on imagined future needs
// "We might need different notification channels someday"
interface NotificationChannel {
send(notification: Notification): Promise<void>;
getCapabilities(): ChannelCapabilities;
isAvailable(): Promise<boolean>;
}
// "We might need different notification types"
interface Notification {
id: string;
type: NotificationType;
priority: NotificationPriority;
payload: NotificationPayload;
metadata: NotificationMetadata;
}
// "We might need complex routing logic"
interface NotificationRouter {
route(notification: Notification): Promise<NotificationChannel[]>;
registerChannel(channel: NotificationChannel): void;
setRoutingRules(rules: RoutingRule[]): void;
}
// "We might need to transform notifications per channel"
interface NotificationTransformer {
transform(notification: Notification, channel: NotificationChannel): TransformedNotification;
}
// "We might need retry logic"
interface NotificationRetryPolicy {
shouldRetry(attempt: number, error: Error): boolean;
getDelay(attempt: number): number;
}
// "We might need to track delivery"
interface NotificationTracker {
trackSent(notification: Notification, channel: NotificationChannel): Promise<void>;
trackDelivered(notificationId: string): Promise<void>;
trackFailed(notificationId: string, error: Error): Promise<void>;
}
// "We might need a notification queue"
interface NotificationQueue {
enqueue(notification: Notification): Promise<void>;
process(): Promise<void>;
getStatus(notificationId: string): Promise<QueueStatus>;
}
// Orchestrator that ties it all together
class NotificationOrchestrator {
constructor(
private router: NotificationRouter,
private transformer: NotificationTransformer,
private retryPolicy: NotificationRetryPolicy,
private tracker: NotificationTracker,
private queue: NotificationQueue
) {}
async notify(notification: Notification): Promise<void> {
await this.queue.enqueue(notification);
// 200+ lines of orchestration logic
}
}
// But all we actually needed was:
// Send an email when a user registers
// Result: 1000+ lines of abstraction, weeks of work, for sending one emailGood Example
// Correct approach: Start concrete, abstract when patterns emerge
// Task: Send an email notification
// Simple, direct solution
interface EmailOptions {
to: string;
subject: string;
body: string;
}
class EmailService {
constructor(private smtpClient: SmtpClient) {}
async send(options: EmailOptions): Promise<void> {
await this.smtpClient.send({
from: 'noreply@example.com',
to: options.to,
subject: options.subject,
html: options.body
});
}
}
// Usage
const emailService = new EmailService(smtpClient);
await emailService.send({
to: user.email,
subject: 'Welcome!',
body: '<h1>Welcome to our app!</h1>'
});
// Later, when we actually need SMS (not "might need"):
class SmsService {
constructor(private twilioClient: TwilioClient) {}
async send(phone: string, message: string): Promise<void> {
await this.twilioClient.messages.create({
to: phone,
from: process.env.TWILIO_NUMBER,
body: message
});
}
}
// Now we have TWO concrete implementations
// We can see what they have in common
// Abstract AFTER seeing the pattern (Rule of Three)
// After email, SMS, and push notifications exist:
interface NotificationSender {
send(recipient: string, message: NotificationMessage): Promise<void>;
}
interface NotificationMessage {
subject?: string;
body: string;
}
class EmailNotificationSender implements NotificationSender {
constructor(private emailService: EmailService) {}
async send(recipient: string, message: NotificationMessage): Promise<void> {
await this.emailService.send({
to: recipient,
subject: message.subject || 'Notification',
body: message.body
});
}
}
class SmsNotificationSender implements NotificationSender {
constructor(private smsService: SmsService) {}
async send(recipient: string, message: NotificationMessage): Promise<void> {
// SMS doesn't support subject, so we just use body
await this.smsService.send(recipient, message.body);
}
}
class PushNotificationSender implements NotificationSender {
constructor(private pushService: PushService) {}
async send(recipient: string, message: NotificationMessage): Promise<void> {
await this.pushService.send(recipient, {
title: message.subject,
body: message.body
});
}
}
// Simple notification service that uses the abstraction
class NotificationService {
constructor(private senders: Map<string, NotificationSender>) {}
async notify(
channel: 'email' | 'sms' | 'push',
recipient: string,
message: NotificationMessage
): Promise<void> {
const sender = this.senders.get(channel);
if (!sender) {
throw new Error(`Unknown notification channel: ${channel}`);
}
await sender.send(recipient, message);
}
}
// The abstraction fits because it was derived from concrete implementations
// It's minimal - just what's needed, nothing speculative
// If we later need retry logic, we add it when we have concrete requirements:
class RetryingNotificationSender implements NotificationSender {
constructor(
private sender: NotificationSender,
private maxAttempts: number = 3
) {}
async send(recipient: string, message: NotificationMessage): Promise<void> {
let lastError: Error | undefined;
for (let attempt = 1; attempt <= this.maxAttempts; attempt++) {
try {
await this.sender.send(recipient, message);
return;
} catch (error) {
lastError = error as Error;
if (attempt < this.maxAttempts) {
await this.delay(attempt * 1000);
}
}
}
throw lastError;
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}Why
1. Wrong Abstractions Are Costly: Premature abstractions are often wrong because you don't understand the problem yet. Wrong abstractions are harder to change than no abstractions.
2. Rule of Three: Wait until you have three concrete examples before abstracting. Two is often coincidence; three reveals the pattern.
3. Duplication Is Cheaper Than Wrong Abstraction: It's easier to extract a correct abstraction from duplicated code than to fix a wrong abstraction.
4. Context Matters: Abstractions made with real requirements fit better than those made speculatively.
5. Simplicity: Concrete code is simpler to understand, debug, and modify.
6. Evolutionary Design: Let the design emerge from actual needs rather than imagined ones.
7. Time Value: The time spent on speculative abstractions could be spent on real features.
Related skills
How it compares
Pick clean-code-principles over generic lint skills when agents need architectural SOLID and pattern guidance, not just syntax or style violations.
FAQ
How many rules does clean-code-principles include?
clean-code-principles version 1.0.2 documents 23 rules—10 SOLID, 12 core principles including DRY and KISS, and design patterns—organized across seven priority categories.
Is clean-code-principles tied to one programming language?
clean-code-principles is language-agnostic, supplying SOLID guidelines, core principles, and design patterns with bad and good examples applicable during any stack's generation or review.
When should agents activate clean-code-principles?
Activate clean-code-principles during AI code generation, refactoring sessions, and architecture reviews when maintainable, scalable design must be enforced before merge.
Is Clean Code Principles safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.