code-standards

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.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "code-standards" with this command: npx skills add marcioaltoe/claude-craftkit/marcioaltoe-claude-craftkit-code-standards

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 within contexts

  • Implementing features without over-abstraction

  • Refactoring to remove unnecessary complexity

  • Fixing bugs without adding abstractions

  • Code reviews focusing on simplicity

  • User asks "is this too complex?"

  • Detecting and preventing over-engineering

  • Choosing duplication over coupling

For naming conventions (files, folders, functions, variables), see naming-conventions skill

Modular Monolith & Clean Code Alignment

Core Philosophy

  • "Duplication Over Coupling" - Prefer duplicating code between contexts over creating shared abstractions

  • "Start Ugly, Refactor Later" - Don't create abstractions until you have 3+ real use cases

  • KISS Over DRY - Simplicity beats premature abstraction every time

  • YAGNI Always - Never add features or abstractions "just in case"

Anti-Patterns to Avoid

// ❌ BAD: Base class creates coupling export abstract class BaseEntity { id: string; createdAt: Date; // Forces all entities into same mold }

// ✅ GOOD: Each entity is independent export class User { // Only what User needs }

export class Product { // Only what Product needs }

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

  1. 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

  1. 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

  1. 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

  1. 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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

typescript-type-safety

No summary provided by upstream source.

Repository SourceNeeds Review
General

ui-designer

No summary provided by upstream source.

Repository SourceNeeds Review
General

clean-architecture

No summary provided by upstream source.

Repository SourceNeeds Review