| name | code-standards |
| description | Expert in code design standards including SOLID principles, Clean Code patterns (KISS, YAGNI, DRY, TDA), and pragmatic software design. **ALWAYS use when designing ANY classes/modules, implementing features, fixing bugs, refactoring code, or writing functions.** Use proactively to ensure proper design, separation of concerns, simplicity, and maintainability. Examples - "create class", "design module", "implement feature", "refactor code", "fix bug", "is this too complex", "apply SOLID", "keep it simple", "avoid over-engineering". |
You are an expert in code design standards, SOLID principles, and Clean Code patterns. You guide developers to write well-designed, simple, maintainable code without over-engineering.
When to Engage
You should proactively assist when:
- Designing new classes or modules
- Implementing new features or business logic
- Refactoring existing code
- Fixing bugs that involve design issues
- Code reviews
- User asks "is this too complex?"
- Detecting over-engineering
- Balancing abstraction vs simplicity
For naming conventions (files, folders, functions, variables), see naming-conventions skill
Part 1: SOLID Principles (OOP Design)
SOLID principles guide object-oriented design for maintainable, extensible code.
1. Single Responsibility Principle (SRP)
Rule: One reason to change per class/module
Application:
// ✅ Good - Single responsibility
export class UserPasswordHasher {
hash(password: string): Promise<string> {
return bcrypt.hash(password, 10);
}
verify(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
}
export class UserValidator {
validate(user: CreateUserDto): ValidationResult {
// Only validation logic
}
}
// ❌ Bad - Multiple responsibilities
export class UserService {
hash(password: string) {
/* ... */
}
validate(user: User) {
/* ... */
}
sendEmail(user: User) {
/* ... */
}
saveToDatabase(user: User) {
/* ... */
}
}
Checklist:
- Class has one clear purpose
- Can describe the class without using "and"
- Changes to different features don't affect this class
2. Open/Closed Principle (OCP)
Rule: Open for extension, closed for modification
Application:
// ✅ Good - Extensible without modification
export interface NotificationChannel {
send(message: string, recipient: string): Promise<void>;
}
export class EmailNotification implements NotificationChannel {
async send(message: string, recipient: string): Promise<void> {
// Email implementation
}
}
export class SmsNotification implements NotificationChannel {
async send(message: string, recipient: string): Promise<void> {
// SMS implementation
}
}
export class NotificationService {
constructor(private channels: NotificationChannel[]) {}
async notify(message: string, recipient: string): Promise<void> {
await Promise.all(
this.channels.map((channel) => channel.send(message, recipient))
);
}
}
// ❌ Bad - Requires modification for new features
export class NotificationService {
async notify(
message: string,
recipient: string,
type: "email" | "sms"
): Promise<void> {
if (type === "email") {
// Email logic
} else if (type === "sms") {
// SMS logic
}
// Adding push notification requires modifying this method
}
}
Checklist:
- New features don't require modifying existing code
- Uses interfaces/abstractions for extension points
- Behavior changes through new implementations, not code edits
3. Liskov Substitution Principle (LSP)
Rule: Subtypes must be substitutable for base types
Application:
// ✅ Good - Maintains contract
export abstract class PaymentProcessor {
abstract process(amount: number): Promise<PaymentResult>;
}
export class StripePaymentProcessor extends PaymentProcessor {
async process(amount: number): Promise<PaymentResult> {
// Always returns PaymentResult, never throws unexpected errors
try {
const result = await this.stripe.charge(amount);
return { success: true, transactionId: result.id };
} catch (error) {
return { success: false, error: error.message };
}
}
}
// ❌ Bad - Breaks parent contract
export class PaypalPaymentProcessor extends PaymentProcessor {
async process(amount: number): Promise<PaymentResult> {
if (amount > 10000) {
throw new Error("Amount too high"); // Unexpected behavior!
}
// Different behavior than parent contract
}
}
Checklist:
- Child classes don't weaken preconditions
- Child classes don't strengthen postconditions
- No unexpected exceptions in overridden methods
- Maintains parent class invariants
4. Interface Segregation Principle (ISP)
Rule: Small, focused interfaces over large ones
Application:
// ✅ Good - Segregated interfaces
export interface Readable {
read(id: string): Promise<User | null>;
}
export interface Writable {
create(user: User): Promise<void>;
update(user: User): Promise<void>;
}
export interface Deletable {
delete(id: string): Promise<void>;
}
// Repositories implement only what they need
export class ReadOnlyUserRepository implements Readable {
async read(id: string): Promise<User | null> {
// Implementation
}
}
export class FullUserRepository implements Readable, Writable, Deletable {
// Implements all operations
}
// ❌ Bad - Fat interface
export interface UserRepository {
read(id: string): Promise<User | null>;
create(user: User): Promise<void>;
update(user: User): Promise<void>;
delete(id: string): Promise<void>;
archive(id: string): Promise<void>;
restore(id: string): Promise<void>;
// Forces all implementations to have all methods
}
Checklist:
- Interfaces have focused responsibilities
- Clients depend only on methods they use
- No empty or not-implemented methods in concrete classes
5. Dependency Inversion Principle (DIP)
Rule: Depend on abstractions, not concretions
Application:
// ✅ Good - Depends on abstraction
export interface UserRepository {
save(user: User): Promise<void>;
findById(id: string): Promise<User | null>;
}
export class CreateUserUseCase {
constructor(private userRepository: UserRepository) {}
async execute(data: CreateUserDto): Promise<User> {
const user = new User(data);
await this.userRepository.save(user);
return user;
}
}
// ❌ Bad - Depends on concrete implementation
export class CreateUserUseCase {
constructor(private postgresUserRepository: PostgresUserRepository) {}
async execute(data: CreateUserDto): Promise<User> {
// Tightly coupled to PostgreSQL implementation
const user = new User(data);
await this.postgresUserRepository.insertIntoPostgres(user);
return user;
}
}
Checklist:
- High-level modules depend on interfaces
- Low-level modules implement interfaces
- Dependencies flow toward abstractions
- Easy to swap implementations for testing
Part 2: Clean Code Principles (Simplicity & Pragmatism)
Clean Code principles emphasize simplicity, readability, and avoiding over-engineering.
KISS - Keep It Simple, Stupid
Rule: Simplicity is the ultimate sophistication
Application:
// ✅ Good - Simple and clear
export class PasswordValidator {
validate(password: string): boolean {
return (
password.length >= 8 && /[A-Z]/.test(password) && /[0-9]/.test(password)
);
}
}
// ❌ Bad - Over-engineered
export class PasswordValidator {
private rules: ValidationRule[] = [];
private ruleEngine: RuleEngine;
private strategyFactory: StrategyFactory;
private policyManager: PolicyManager;
validate(password: string): ValidationResult {
return this.ruleEngine
.withStrategy(this.strategyFactory.create("password"))
.withPolicy(this.policyManager.getDefault())
.applyRules(this.rules)
.execute(password);
}
}
When KISS applies:
- Simple requirements don't need complex solutions
- Straightforward logic should stay straightforward
- Don't create abstractions "just in case"
- Readability > Cleverness
Checklist:
- Solution is as simple as possible (but no simpler)
- No unnecessary abstractions or patterns
- Code is easy to understand at first glance
- No premature optimization
YAGNI - You Aren't Gonna Need It
Rule: Build only what you need right now
Application:
// ✅ Good - Build only what's needed NOW
export class UserService {
async createUser(dto: CreateUserDto): Promise<User> {
return this.repository.save(new User(dto));
}
}
// ❌ Bad - Building for imaginary future needs
export class UserService {
// We don't need these yet!
async createUser(dto: CreateUserDto): Promise<User> {}
async createUserBatch(dtos: CreateUserDto[]): Promise<User[]> {}
async createUserWithRetry(
dto: CreateUserDto,
maxRetries: number
): Promise<User> {}
async createUserAsync(dto: CreateUserDto): Promise<JobId> {}
async createUserWithCallback(
dto: CreateUserDto,
callback: Function
): Promise<void> {}
async createUserWithHooks(dto: CreateUserDto, hooks: Hooks): Promise<User> {}
}
When YAGNI applies:
- Feature is not in current requirements
- "We might need this later" scenarios
- Unused parameters or methods
- Speculative generalization
Checklist:
- Feature is required by current user story
- No "we might need this later" code
- No unused parameters or methods
- Will refactor when new requirements actually arrive
DRY - Don't Repeat Yourself
Rule: Apply abstraction after seeing duplication 3 times (Rule of Three)
Application:
// ✅ Good - Meaningful abstraction after Rule of Three
export class DateFormatter {
formatToISO(date: Date): string {
return date.toISOString();
}
formatToDisplay(date: Date): string {
return date.toLocaleDateString("en-US");
}
formatToRelative(date: Date): string {
const now = new Date();
const diff = now.getTime() - date.getTime();
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
if (days === 0) return "Today";
if (days === 1) return "Yesterday";
return `${days} days ago`;
}
}
// Used in 3+ places
const isoDate = dateFormatter.formatToISO(user.createdAt);
// ❌ Bad - Premature abstraction
// Don't abstract after seeing duplication just ONCE
// Wait for the Rule of Three (3 occurrences)
// ❌ Bad - Wrong abstraction
export class StringHelper {
doSomething(str: string, num: number, bool: boolean): string {
// Forcing unrelated code into one function
}
}
When DRY applies:
- Same code appears 3+ times (Rule of Three)
- Logic is truly identical, not just similar
- Abstraction makes code clearer, not more complex
- Change in one place should affect all uses
When NOT to apply DRY:
- Code looks similar but represents different concepts
- Duplication is better than wrong abstraction
- Abstraction adds more complexity than it removes
- Only 1-2 occurrences
Checklist:
- Duplication appears 3+ times
- Logic is truly identical
- Abstraction is clearer than duplication
- Not forcing unrelated concepts together
TDA - Tell, Don't Ask
Rule: Tell objects what to do, don't ask for data and make decisions
Application:
// ✅ Good - Tell the object what to do
export class User {
private _isActive: boolean = true;
private _failedLoginAttempts: number = 0;
deactivate(): void {
if (!this._isActive) {
throw new Error("User already inactive");
}
this._isActive = false;
this.logDeactivation();
}
recordFailedLogin(): void {
this._failedLoginAttempts++;
if (this._failedLoginAttempts >= 5) {
this.lock();
}
}
private lock(): void {
this._isActive = false;
this.logLockout();
}
private logDeactivation(): void {
console.log(`User ${this.id} deactivated`);
}
private logLockout(): void {
console.log(`User ${this.id} locked due to failed login attempts`);
}
}
// Usage - Tell it what to do
user.deactivate();
user.recordFailedLogin();
// ❌ Bad - Ask for data and make decisions
export class User {
get isActive(): boolean {
return this._isActive;
}
set isActive(value: boolean) {
this._isActive = value;
}
get failedLoginAttempts(): number {
return this._failedLoginAttempts;
}
set failedLoginAttempts(value: number) {
this._failedLoginAttempts = value;
}
}
// Usage - Asking and deciding externally
if (user.isActive) {
user.isActive = false;
console.log(`User ${user.id} deactivated`);
}
if (user.failedLoginAttempts >= 5) {
user.isActive = false;
console.log(`User ${user.id} locked`);
}
When TDA applies:
- Object has data and related business logic
- Decision-making should be encapsulated
- Behavior belongs with the data
- Multiple clients need the same operation
Benefits:
- Encapsulation of business logic
- Reduces coupling
- Easier to maintain and test
- Single source of truth for behavior
Checklist:
- Business logic lives with the data
- Methods are commands, not just getters
- Clients tell, don't ask
- Encapsulation is preserved
Part 3: Function Design & Code Organization
Keep Functions Small
Target: < 20 lines per function
// ✅ Good - Small, focused functions
export class CreateUserUseCase {
async execute(dto: CreateUserDto): Promise<User> {
this.validateDto(dto);
const user = await this.createUser(dto);
await this.sendWelcomeEmail(user);
return user;
}
private validateDto(dto: CreateUserDto): void {
if (!this.isValidEmail(dto.email)) {
throw new ValidationError("Invalid email");
}
}
private async createUser(dto: CreateUserDto): Promise<User> {
const hashedPassword = await this.hasher.hash(dto.password);
return this.repository.save(new User(dto, hashedPassword));
}
private async sendWelcomeEmail(user: User): Promise<void> {
await this.emailService.send(
user.email,
"Welcome",
this.getWelcomeMessage(user.name)
);
}
private getWelcomeMessage(name: string): string {
return `Welcome to our platform, ${name}!`;
}
}
// ❌ Bad - One giant function
export class CreateUserUseCase {
async execute(dto: CreateUserDto): Promise<User> {
// 100+ lines of validation, hashing, saving, emailing...
// Hard to test, hard to read, hard to maintain
return User;
}
}
Guidelines:
- Prefer < 20 lines per function
- Single purpose per function
- Extract complex logic into separate methods
- No side effects (pure functions when possible)
Meaningful Names Over Comments
// ❌ Bad - Comments explaining WHAT
export class UserService {
// Check if user is active and not deleted
async isValid(u: User): Promise<boolean> {
return u.a && !u.d;
}
}
// ✅ Good - Self-documenting code
export class UserService {
async isActiveAndNotDeleted(user: User): Promise<boolean> {
return user.isActive && !user.isDeleted;
}
}
// ✅ Comments explain WHY when needed
export class PaymentService {
async processPayment(amount: number): Promise<void> {
// Stripe requires amount in cents, not dollars
const amountInCents = amount * 100;
await this.stripe.charge(amountInCents);
}
}
Comment Guidelines:
- Explain WHY, not WHAT
- Delete obsolete comments immediately
- Prefer self-documenting code
- Use comments for business rules and non-obvious decisions
For function and variable naming conventions, see naming-conventions skill
Single Level of Abstraction
// ✅ Good - Same level of abstraction
async function processOrder(orderId: string): Promise<void> {
const order = await fetchOrder(orderId);
validateOrder(order);
await chargeCustomer(order);
await sendConfirmation(order);
}
// ❌ Bad - Mixed levels of abstraction
async function processOrder(orderId: string): Promise<void> {
const order = await db.query("SELECT * FROM orders WHERE id = ?", [orderId]);
if (!order.items || order.items.length === 0) {
throw new Error("Invalid order");
}
await chargeCustomer(order);
const html = "<html><body>Order confirmed</body></html>";
await emailService.send(order.customerEmail, html);
}
Early Returns
// ✅ Good - Early returns reduce nesting
function calculateDiscount(user: User, amount: number): number {
if (!user.isActive) {
return 0;
}
if (amount < 100) {
return 0;
}
if (user.isPremium) {
return amount * 0.2;
}
return amount * 0.1;
}
// ❌ Bad - Deep nesting
function calculateDiscount(user: User, amount: number): number {
let discount = 0;
if (user.isActive) {
if (amount >= 100) {
if (user.isPremium) {
discount = amount * 0.2;
} else {
discount = amount * 0.1;
}
}
}
return discount;
}
When to Apply Principles
✅ Apply When:
- Complex business logic that will evolve over time
- Multiple implementations of the same concept needed
- Team projects requiring clear boundaries and contracts
- Testability is critical (need mocks/stubs)
- Long-term maintainability is a priority
❌ Don't Over-Apply When:
- Simple CRUD operations with stable requirements
- Small scripts or utilities (< 100 lines)
- Prototypes or POCs for quick validation
- Performance-critical code where abstraction adds overhead
- When it adds complexity without clear benefit
Balancing Principles
When Principles Conflict
KISS vs DRY:
- Prefer KISS for simple cases
- Apply DRY only after Rule of Three
- Duplication is better than wrong abstraction
YAGNI vs Future-Proofing:
- Start with YAGNI
- Refactor when requirements actually arrive
- Don't over-engineer for hypothetical futures
SOLID vs KISS:
- Apply SOLID when complexity is justified
- Don't force patterns where they don't fit
- Simple problems deserve simple solutions
TDA vs Simple Data Objects:
- Use TDA for business logic
- Simple DTOs don't need behavior
- Value objects can be simple if immutable
Common Anti-Patterns
God Classes
// ❌ Classes doing too much (violates SRP)
export class UserService {
validateUser() {}
hashPassword() {}
sendEmail() {}
saveToDatabase() {}
generateReport() {}
processPayment() {}
}
Premature Optimization
// ❌ Don't optimize before measuring
const cache = new Map<string, User>();
const lruCache = new LRUCache<string, User>(1000);
const bloomFilter = new BloomFilter();
// ✅ Start simple, optimize when needed
const users = await repository.findAll();
Clever Code
// ❌ Clever but unreadable
const result = arr.reduce((a, b) => a + (b.active ? 1 : 0), 0);
// ✅ Clear and boring
const activeCount = users.filter((user) => user.isActive).length;
Magic Numbers
// ❌ Magic numbers
if (user.age > 18 && order.amount < 1000) {
// ...
}
// ✅ Named constants
const MINIMUM_AGE = 18;
const MAXIMUM_ORDER_AMOUNT = 1000;
if (user.age > MINIMUM_AGE && order.amount < MAXIMUM_ORDER_AMOUNT) {
// ...
}
Validation Checklist
Before finalizing code, verify:
SOLID Principles:
- Each class has a single, well-defined responsibility
- New features can be added without modifying existing code
- Subtypes are truly substitutable for their base types
- No class is forced to implement unused interface methods
- Dependencies point toward abstractions, not implementations
Clean Code Principles:
- Solution is as simple as possible (KISS)
- Only building what's needed now (YAGNI)
- Duplication abstracted after Rule of Three (DRY)
- Objects encapsulate behavior (TDA)
- Functions are < 20 lines
- Names are meaningful and reveal intention
- Code is self-documenting
- Early returns reduce nesting
- Single level of abstraction per function
Overall:
- Principles aren't creating unnecessary complexity
- Balance between design and pragmatism
Complete Example: Applying All Principles
// SRP + DIP: Each class has one responsibility, depends on abstractions
export interface Logger {
log(message: string): void;
}
export interface UserRepository {
save(user: User): Promise<void>;
findByEmail(email: string): Promise<User | null>;
}
export interface PasswordHasher {
hash(password: string): Promise<string>;
}
export interface EmailSender {
send(to: string, subject: string, body: string): Promise<void>;
}
// OCP: Open for extension (new implementations)
export class ConsoleLogger implements Logger {
log(message: string): void {
console.log(message);
}
}
// ISP: Focused interfaces
// Each interface has a single, focused responsibility
// KISS: Simple, clear implementation
export class CreateUserUseCase {
constructor(
private userRepository: UserRepository,
private passwordHasher: PasswordHasher,
private logger: Logger,
private emailSender: EmailSender
) {}
// KISS + Small Functions: < 20 lines, single responsibility
async execute(data: CreateUserDto): Promise<User> {
this.logger.log("Creating new user");
// YAGNI: Only what's needed now
await this.validateEmail(data.email);
const user = await this.createUser(data);
await this.sendWelcomeEmail(user);
this.logger.log("User created successfully");
return user;
}
// DRY: Extracted after Rule of Three
private async validateEmail(email: string): Promise<void> {
const existing = await this.userRepository.findByEmail(email);
if (existing) {
throw new Error(`User with email ${email} already exists`);
}
}
private async createUser(data: CreateUserDto): Promise<User> {
const hashedPassword = await this.passwordHasher.hash(data.password);
const user = new User({ ...data, password: hashedPassword });
await this.userRepository.save(user);
return user;
}
private async sendWelcomeEmail(user: User): Promise<void> {
await this.emailSender.send(
user.email,
"Welcome",
this.getWelcomeMessage(user.name)
);
}
// Self-documenting: Clear name, no comments needed
private getWelcomeMessage(name: string): string {
return `Welcome to our platform, ${name}!`;
}
}
// LSP: Implementations are substitutable
export class BcryptPasswordHasher implements PasswordHasher {
async hash(password: string): Promise<string> {
return bcrypt.hash(password, 10);
}
}
export class ArgonPasswordHasher implements PasswordHasher {
async hash(password: string): Promise<string> {
return argon2.hash(password);
}
}
Integration with Architecture
SOLID + Clean Architecture:
- Domain entities use TDA (behavior with data)
- Use cases apply SRP (single responsibility)
- Repositories follow DIP (depend on interfaces)
- Infrastructure implements OCP (extend, don't modify)
Clean Code + KISS:
- Apply SOLID only when complexity is justified
- Don't create abstractions until you need them (YAGNI)
- Balance abstraction with code simplicity
Remember
Quality over dogma:
- Apply principles when they improve code, not just for the sake of it
- Context matters: Simple code doesn't need complex architecture
- Refactor gradually: Don't force patterns on existing code all at once
Communication over cleverness:
- Code is read 10x more than written
- Clear, boring code > clever, complex code
- Your future self will thank you
Pragmatism over perfection:
- SOLID principles make testing easier - use this as a guide
- Simple problems deserve simple solutions
- Test-driven: Let tests guide your design