Apply software design principles across architecture and implementation using deterministic decision workflows, SOLID checks, structural patterns, and anti-pattern detection; use when reviewing designs, refactoring modules, or resolving maintainability and coupling risks.
Does it follow best practices?
Evaluation — 99%
↑ 1.01xAgent success when using this tile
Validation for skill structure
Comprehensive guidance for designing maintainable, flexible, and robust software systems. This skill covers both strategic architecture decisions and tactical implementation principles.
This skill is organized into three levels of software design:
Each principle is categorized by impact level and assigned a structured ID for precise reference:
| Priority | Category | Impact | Rule ID Prefix | Example IDs |
|---|---|---|---|---|
| 1 | Dependency Rules | CRITICAL | dep- | dep-inward-only, dep-acyclic-dependencies |
| 2 | Entity Design | CRITICAL | entity- | entity-pure-business-rules, entity-rich-not-anemic |
| 3 | Use Case Isolation | HIGH | usecase- | usecase-single-responsibility, usecase-explicit-dependencies |
| 4 | SOLID Principles | HIGH | solid- | solid-srp, solid-dip |
| 5 | Component Cohesion | HIGH | comp- | comp-screaming-architecture, comp-common-closure |
| 6 | Boundary Definition | MEDIUM | bound- | bound-humble-object, bound-partial-boundaries |
| 7 | Framework Isolation | MEDIUM | frame- | frame-domain-purity, frame-orm-in-infrastructure |
| 8 | Interface Adapters | MEDIUM | adapt- | adapt-controller-thin, adapt-anti-corruption-layer |
| 9 | Structural Patterns | MEDIUM | struct- | struct-composition-over-inheritance, struct-law-of-demeter |
| 10 | Core Principles | MEDIUM | core- | core-dry, core-kiss, core-yagni |
| 11 | Testing Architecture | LOW | test- | test-boundary-verification, test-testable-design |
⚠️ CRITICAL RULES - For these violations, MANDATORY - READ ENTIRE FILE before applying:
references/dep-inward-only.md when dependency points outward from domainreferences/dep-acyclic-dependencies.md when circular dependencies detectedreferences/dep-stable-abstractions.md when concrete depends on volatilereferences/dep-interface-ownership.md when interface placed in wrong modulereferences/dep-no-framework-imports.md when domain imports framework codereferences/entity-pure-business-rules.md when entity has infrastructure concernsreferences/entity-rich-not-anemic.md when entity is data-only with no behaviorreferences/entity-encapsulate-invariants.md when business rules are externalreferences/entity-value-objects.md when primitives used instead of value objectsreferences/usecase-single-responsibility.md when use case handles multiple concernsreferences/usecase-explicit-dependencies.md when hidden dependencies existreferences/usecase-orchestrates-not-implements.md when use case implements business logicreferences/comp-screaming-architecture.md when architecture doesn't reveal intentreferences/comp-common-closure.md when components change for different reasons⛔ NEVER load references speculatively - Only load when violation is confirmed:
bound-* files - Only for confirmed boundary design violations (not general architecture review)frame-* files - Only when framework coupling is detected (not general framework discussions)adapt-* files - Only for interface adapter problems (not general interface design)test-* files - Only during testing architecture reviews (not general testing discussions)dep-inward-only) instead of loading file for simple violationsWhen identifying violations, use this format: file:line - [rule-id] Description
Basic Examples:
user-service.ts:45 - [solid-srp] Class handles both validation and persistenceorder-entity.ts:12 - [entity-pure-business-rules] Entity imports database clientpayment-usecase.ts:28 - [dep-inward-only] Use case depends on infrastructure layerWith Priority Level:
CRITICAL: src/domain/user.ts:23 - [dep-inward-only] Domain layer importing database adapter
HIGH: src/components/UserForm.tsx:67 - [solid-srp] Component handles validation, API calls, and routing
MEDIUM: src/utils/validator.ts:12 - [struct-law-of-demeter] Reaching through multiple object propertiesMulti-line Violations:
src/resolvers/user-resolver.ts:45-89 - [solid-srp] GraphQL resolver violates SRP:
- Line 45-52: Authentication logic
- Line 53-67: Data validation
- Line 68-78: Business rules processing
- Line 79-89: Email notification sendingBatch Report Format:
Design Principle Violations Summary:
================================
Total violations: 12
CRITICAL: 3, HIGH: 5, MEDIUM: 3, LOW: 1
By Category:
- SOLID Principles: 7 violations
- Dependency Rules: 3 violations
- Structural Principles: 2 violations
Files with violations:
- src/user-service.ts (4 violations)
- src/order-processor.ts (3 violations)
- src/payment-handler.ts (2 violations)JSON Output Format (for automated tools):
{
"violations": [
{
"file": "src/user-service.ts",
"line": 45,
"rule_id": "solid-srp",
"priority": "HIGH",
"description": "Class handles both validation and persistence",
"category": "SOLID Principles"
}
],
"summary": {
"total": 12,
"by_priority": {"CRITICAL": 3, "HIGH": 5, "MEDIUM": 3, "LOW": 1}
}
}For detailed explanations of each rule, see the references/ directory.
Use these decision trees to navigate complex design situations and determine which principles to apply first:
Is this a NEW system or component?
├── YES → Start with Strategic Design (Part 1)
│ ├── Define boundaries first → Dependency Direction [CRITICAL]
│ ├── Design entities → Entity Design [CRITICAL]
│ └── Plan use cases → Use Case Isolation [HIGH]
└── NO → Existing system issues
├── Performance/coupling problems?
│ ├── YES → Focus on Dependency Direction [CRITICAL]
│ └── NO → Continue to behavior analysis
├── Business logic scattered?
│ ├── YES → Focus on Entity Design [CRITICAL] + SRP [HIGH]
│ └── NO → Continue to code quality check
└── Hard to test/change?
├── YES → Apply SOLID Principles (Part 2) [HIGH]
└── NO → Focus on Structural Principles (Part 2) [MEDIUM]What type of violation detected?
├── Imports/Dependencies
│ ├── Framework code in domain → dep-inward-only [CRITICAL]
│ ├── Circular dependencies → dep-acyclic-dependencies [CRITICAL]
│ └── Unstable dependencies → dep-stable-abstractions [HIGH]
├── Class/Component Design
│ ├── Multiple reasons to change → solid-srp [HIGH]
│ ├── Modification for extension → solid-ocp [HIGH]
│ └── Anemic entities → entity-rich-not-anemic [CRITICAL]
├── Interface Problems
│ ├── Fat interfaces → solid-isp [HIGH]
│ ├── Tight coupling → struct-law-of-demeter [HIGH]
│ └── Inappropriate intimacy → struct-tell-dont-ask [HIGH]
└── Business Logic Issues
├── Logic outside entities → entity-pure-business-rules [CRITICAL]
├── Invariants not enforced → entity-encapsulate-invariants [CRITICAL]
└── Mixed concerns → usecase-single-responsibility [HIGH]System in crisis - what's the biggest problem?
├── Can't deploy/build
│ ├── Circular deps → dep-acyclic-dependencies [CRITICAL] - Fix FIRST
│ └── Framework coupling → dep-inward-only [CRITICAL] - Fix FIRST
├── Can't add features (everything breaks)
│ ├── God classes → solid-srp [HIGH] - Refactor immediately
│ ├── Tight coupling → solid-ocp + solid-dip [HIGH]
│ └── No tests possible → entity-pure-business-rules [CRITICAL]
├── Performance issues
│ ├── N+1 queries → struct-law-of-demeter [HIGH]
│ ├── Over-fetching → solid-isp [HIGH]
│ └── Inefficient boundaries → bound-humble-object [MEDIUM]
└── Security vulnerabilities
├── Data exposure → struct-encapsulation [MEDIUM]
├── Input validation missing → entity-encapsulate-invariants [CRITICAL]
└── Authorization bypassed → usecase-explicit-dependencies [HIGH]Planning refactoring - what's the approach?
├── Big Bang (complete rewrite)
│ ├── Start: Strategic Design principles [CRITICAL]
│ ├── Then: Entity Design [CRITICAL]
│ └── Finally: Tactical principles [HIGH-MEDIUM]
├── Strangler Fig (gradual replacement)
│ ├── New boundaries → bound-anti-corruption-layer [MEDIUM]
│ ├── Interface design → solid-isp + solid-dip [HIGH]
│ └── Migration safety → usecase-transaction-boundary [HIGH]
├── Extract Service (microservice extraction)
│ ├── Define boundary → usecase-single-responsibility [HIGH]
│ ├── Clean interfaces → solid-isp [HIGH]
│ └── Data consistency → usecase-transaction-boundary [HIGH]
└── Legacy Improvement (incremental fixes)
├── Boy Scout Rule → Fix one violation per change
├── Priority order → CRITICAL → HIGH → MEDIUM → LOW
└── Focus areas → Most changed files firstStrategic design focuses on system structure, boundaries, and long-term architectural decisions. Use these principles when designing systems, defining boundaries, or planning major changes.
Based on Robert C. Martin's Clean Architecture. The core idea: dependencies point inward only.
Rule IDs: dep-inward-only, dep-acyclic-dependencies, dep-stable-abstractions, dep-interface-ownership, dep-no-framework-imports, dep-data-crossing-boundaries
⚠️ BEFORE APPLYING - Read references for specific violations:
dep-inward-only → READ references/dep-inward-only.mddep-acyclic-dependencies → READ references/dep-acyclic-dependencies.mddep-stable-abstractions → READ references/dep-stable-abstractions.mddep-interface-ownership → READ references/dep-interface-ownership.mddep-no-framework-imports → READ references/dep-no-framework-imports.mdRule: Dependencies must point inward toward business logic. Outer layers depend on inner layers, never the reverse.
┌─────────────────────────────────────┐
│ Frameworks & Drivers (UI, DB) │ ← Outer (least stable)
├─────────────────────────────────────┤
│ Interface Adapters (Controllers) │
├─────────────────────────────────────┤
│ Use Cases (Application Logic) │
├─────────────────────────────────────┤
│ Entities (Business Rules) │ ← Inner (most stable)
└─────────────────────────────────────┘
Dependencies point: Outer → Inner ONLYRules:
⛔ NEVER DO - Dependency Direction Anti-Patterns:
Example: Dependency Inversion at Boundary
// ❌ BAD: Use case depends on database implementation
export const getUserUseCase = (userId: string) => {
const db = new PostgresDatabase(); // Direct dependency on infrastructure
return db.query(`SELECT * FROM users WHERE id = ${userId}`);
};
// ✅ GOOD: Use case depends on abstraction (interface)
// Inner layer defines the interface
interface UserRepository {
findById(id: string): Promise<User | null>;
}
// Use case depends on abstraction
export const getUserUseCase = (
userId: string,
userRepo: UserRepository // Injected dependency
): Promise<User | null> => {
return userRepo.findById(userId);
};
// Outer layer implements the interface
class PostgresUserRepository implements UserRepository {
async findById(id: string): Promise<User | null> {
// Database implementation details
return this.db.query('SELECT * FROM users WHERE id = $1', [id]);
}
}Interface Ownership Rule: Inner layers define the interfaces they need. Outer layers implement them.
Rule IDs: entity-pure-business-rules, entity-rich-not-anemic, entity-no-persistence-awareness, entity-encapsulate-invariants, entity-value-objects
⚠️ BEFORE APPLYING - Read references for specific violations:
entity-pure-business-rules → READ references/entity-pure-business-rules.mdentity-rich-not-anemic → READ references/entity-rich-not-anemic.mdentity-encapsulate-invariants → READ references/entity-encapsulate-invariants.mdentity-value-objects → READ references/entity-value-objects.mdEntities contain pure business rules and critical business data. They are the heart of your system.
Rules:
⛔ NEVER DO - Entity Design Anti-Patterns:
Example: Rich Entity
// ❌ BAD: Anemic entity (just data)
interface Order {
id: string;
items: OrderItem[];
status: string;
total: number;
}
// Business logic scattered in services
const canCancelOrder = (order: Order): boolean => {
return order.status === 'pending' || order.status === 'confirmed';
};
// ✅ GOOD: Rich entity (data + behavior)
class Order {
private constructor(
private id: string,
private items: OrderItem[],
private status: OrderStatus,
private total: Money
) {}
// Factory method enforces invariants
static create(items: OrderItem[]): Order {
if (items.length === 0) {
throw new Error('Order must have at least one item');
}
const total = items.reduce((sum, item) => sum.add(item.price), Money.zero());
return new Order(generateId(), items, OrderStatus.Pending, total);
}
// Business rules encapsulated in entity
canCancel(): boolean {
return this.status === OrderStatus.Pending || this.status === OrderStatus.Confirmed;
}
cancel(): void {
if (!this.canCancel()) {
throw new Error(`Cannot cancel order in ${this.status} status`);
}
this.status = OrderStatus.Cancelled;
}
getTotal(): Money {
return this.total;
}
}
// Value object
class Money {
private constructor(private amount: number, private currency: string) {}
static zero(): Money {
return new Money(0, 'USD');
}
add(other: Money): Money {
if (this.currency !== other.currency) {
throw new Error('Cannot add money with different currencies');
}
return new Money(this.amount + other.amount, this.currency);
}
}Rule IDs: usecase-single-responsibility, usecase-explicit-dependencies, usecase-orchestrates-not-implements, usecase-transaction-boundary, usecase-no-presentation-logic
🎯 BEFORE APPLYING - Read references for specific violations:
usecase-single-responsibility → READ references/usecase-single-responsibility.mdusecase-explicit-dependencies → READ references/usecase-explicit-dependencies.mdusecase-orchestrates-not-implements → READ references/usecase-orchestrates-not-implements.mdUse Cases orchestrate application-specific business rules. Each use case represents one thing the system does.
Rules:
Example: Use Case Pattern
// Input port (request)
interface CreateOrderRequest {
userId: string;
items: { productId: string; quantity: number }[];
}
// Output port (response)
interface CreateOrderResponse {
orderId: string;
total: number;
currency: string;
}
// Dependencies (interfaces defined by use case)
interface OrderRepository {
save(order: Order): Promise<void>;
}
interface ProductRepository {
findById(id: string): Promise<Product | null>;
}
// Use case orchestrates the operation
export const createOrderUseCase = async (
request: CreateOrderRequest,
orderRepo: OrderRepository,
productRepo: ProductRepository
): Promise<CreateOrderResponse> => {
// 1. Load products
const products = await Promise.all(
request.items.map(item => productRepo.findById(item.productId))
);
// 2. Create order items (delegate to entity)
const orderItems = products.map((product, idx) =>
OrderItem.create(product, request.items[idx].quantity)
);
// 3. Create order entity (business rules enforced here)
const order = Order.create(request.userId, orderItems);
// 4. Persist
await orderRepo.save(order);
// 5. Return response
return {
orderId: order.getId(),
total: order.getTotal().getAmount(),
currency: order.getTotal().getCurrency()
};
};When designing systems, follow this process:
1. Understand Context
2. Gather Requirements
3. Identify Constraints
4. Consider Alternatives
5. Design System Structure
6. Document Decisions
Architecture Decision Record Template:
# ADR-001: Use Clean Architecture for Core Domain
## Status
Accepted
## Context
We need to design the order management system with long-term maintainability.
The domain logic is complex and will evolve frequently. We need to isolate
business rules from infrastructure changes.
## Decision
We will use Clean Architecture with clear boundaries between entities, use
cases, and infrastructure. Dependencies will point inward toward domain logic.
## Consequences
**Positive**:
- Business logic testable without infrastructure
- Easy to swap databases or frameworks
- Clear separation of concerns
**Negative**:
- More initial boilerplate
- Learning curve for team
- More files and indirection
## Alternatives Considered
- Traditional layered architecture (rejected: too much coupling)
- Microservices (rejected: premature for our scale)Layered Architecture
┌─────────────────┐
│ Presentation │
├─────────────────┤
│ Business │
├─────────────────┤
│ Persistence │
└─────────────────┘Hexagonal Architecture (Ports & Adapters)
┌─────────────────┐
│ HTTP API │ ← Adapter
└────────┬────────┘
│
┌────────────▼────────────┐
│ Application Core │ ← Port (interface)
│ (Business Logic) │
└────────────┬────────────┘
│
┌────────▼────────┐
│ Database │ ← Adapter
└─────────────────┘Event-Driven Architecture
Service A ──→ Event Bus ──→ Service B
│
└──────────→ Service CChoose based on:
Premature Optimization
Resume-Driven Architecture
Distributed Monolith
Big Ball of Mud
Analysis Paralysis
Tactical design focuses on code-level decisions: how to structure modules, classes, and functions for maintainability and flexibility.
The five fundamental principles for object-oriented and functional design. These manage dependencies and responsibilities at the code level.
Rule ID: solid-srp
🎯 BEFORE APPLYING - For SRP violations: READ references/solid-srp.md
Definition: A module should have one, and only one, reason to change.
In Practice: Each function/class should do one thing and do it well. If you need to change a module for multiple different reasons, it has too many responsibilities.
Example: TypeScript
// ❌ BAD: Multiple responsibilities
class UserService {
createUser(data: UserData) {
// 1. Validate data
if (!data.email.includes('@')) throw new Error('Invalid email');
// 2. Hash password
const hashedPassword = bcrypt.hash(data.password);
// 3. Save to database
db.users.insert({ ...data, password: hashedPassword });
// 4. Send welcome email
emailService.send(data.email, 'Welcome!');
// 5. Log analytics
analytics.track('user_created', data.email);
}
}
// This class has 5 reasons to change!
// ✅ GOOD: Single responsibility per module
const validateUserData = (data: UserData): void => {
if (!data.email.includes('@')) throw new Error('Invalid email');
};
const hashPassword = (password: string): string => {
return bcrypt.hash(password);
};
const saveUser = (user: User): Promise<void> => {
return db.users.insert(user);
};
const sendWelcomeEmail = (email: string): void => {
emailService.send(email, 'Welcome!');
};
const trackUserCreation = (email: string): void => {
analytics.track('user_created', email);
};
// Orchestrator composes single-responsibility functions
const createUser = async (data: UserData): Promise<void> => {
validateUserData(data);
const hashedPassword = hashPassword(data.password);
const user = { ...data, password: hashedPassword };
await saveUser(user);
sendWelcomeEmail(user.email);
trackUserCreation(user.email);
};Example: Elixir
# ❌ BAD: God module with multiple responsibilities
defmodule UserService do
def create_user(data) do
with :ok <- validate_email(data.email),
{:ok, hash} <- hash_password(data.password),
{:ok, user} <- insert_user(data, hash),
:ok <- send_email(user.email),
:ok <- track_event(user.email) do
{:ok, user}
end
end
defp validate_email(email), do: # validation logic
defp hash_password(password), do: # hashing logic
defp insert_user(data, hash), do: # database logic
defp send_email(email), do: # email logic
defp track_event(email), do: # analytics logic
end
# ✅ GOOD: Each module has one responsibility
defmodule UserValidator do
def validate(data) do
if String.contains?(data.email, "@"), do: :ok, else: {:error, :invalid_email}
end
end
defmodule PaymentProcessor do
def process(method, amount), do: PaymentMethod.process(method, amount)
endReal-World Violation Examples:
// ❌ COMMON VIOLATION: GraphQL resolver doing too much
class UserResolver {
async createUser(args: CreateUserArgs, context: Context) {
// Validation logic (should be separate)
if (!args.email || !args.email.includes('@')) {
throw new Error('Invalid email');
}
if (!args.password || args.password.length < 8) {
throw new Error('Password too short');
}
if (!args.name || args.name.trim().length === 0) {
throw new Error('Name required');
}
// Business logic (should be separate)
const existingUser = await this.userService.findByEmail(args.email);
if (existingUser) {
throw new Error('User already exists');
}
// Encryption logic (should be separate)
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(args.password, salt);
// Database logic (should be separate)
const user = await this.userService.create({
...args,
password: hashedPassword,
createdAt: new Date(),
lastLogin: null
});
// Email logic (should be separate)
const emailTemplate = await this.loadTemplate('welcome');
const personalizedEmail = emailTemplate.replace('{{name}}', user.name);
await this.emailService.send({
to: user.email,
subject: 'Welcome to our platform!',
html: personalizedEmail
});
// Analytics logic (should be separate)
await this.analytics.track('user_created', {
userId: user.id,
email: user.email,
source: context.source || 'direct'
});
// Audit logging (should be separate)
await this.auditLogger.log({
action: 'USER_CREATED',
userId: user.id,
adminId: context.adminId,
timestamp: new Date(),
metadata: { email: user.email }
});
return user;
}
private loadTemplate(name: string) { /* template loading logic */ }
}
// ❌ COMMON VIOLATION: React component doing everything
const UserDashboard = ({ userId }: { userId: string }) => {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [analytics, setAnalytics] = useState(null);
const [notifications, setNotifications] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// Data fetching logic (should be custom hook or service)
const fetchUserData = async () => {
try {
setLoading(true);
const [userData, userPosts, userAnalytics, userNotifications] =
await Promise.all([
api.get(`/users/${userId}`),
api.get(`/users/${userId}/posts`),
api.get(`/users/${userId}/analytics`),
api.get(`/users/${userId}/notifications`)
]);
setUser(userData);
setPosts(userPosts);
setAnalytics(userAnalytics);
setNotifications(userNotifications);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchUserData();
}, [userId]);
// Validation logic (should be separate)
const validatePost = (content: string): boolean => {
if (!content || content.trim().length === 0) return false;
if (content.length > 280) return false;
if (content.includes('<script>')) return false;
return true;
};
// Business logic (should be separate)
const handleCreatePost = async (content: string) => {
if (!validatePost(content)) {
alert('Invalid post content');
return;
}
try {
const post = await api.post('/posts', { content, userId });
setPosts([post, ...posts]);
// Analytics logic (should be separate)
analytics.track('post_created', { userId, postLength: content.length });
// Notification logic (should be separate)
if (user.followers?.length > 100) {
await api.post('/notifications/broadcast', {
type: 'new_post',
userId,
postId: post.id
});
}
} catch (err) {
alert('Failed to create post');
}
};
// UI logic mixed with business logic
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>User not found</div>;
return (
<div>
{/* User profile rendering */}
<div>{user.name}</div>
{/* Posts rendering */}
<div>{posts.map(post => <div key={post.id}>{post.content}</div>)}</div>
{/* Analytics rendering */}
<div>Views: {analytics.views}, Likes: {analytics.likes}</div>
{/* Notifications rendering */}
<div>{notifications.map(n => <div key={n.id}>{n.message}</div>)}</div>
</div>
);
};Impact of These Violations:
Benefits:
⛔ NEVER DO - Single Responsibility Anti-Patterns:
Rule ID: solid-isp
Definition: Clients should not be forced to depend on interfaces they don't use.
In Practice: Create small, focused interfaces rather than large, monolithic ones. Each client should only know about the methods it needs.
Example: TypeScript
// ❌ BAD: Fat interface with too many methods
interface Worker {
work(): void;
eat(): void;
sleep(): void;
generateReport(): void;
attendMeeting(): void;
}
class HumanWorker implements Worker {
work() { /* ... */ }
eat() { /* ... */ }
sleep() { /* ... */ }
generateReport() { /* ... */ }
attendMeeting() { /* ... */ }
}
class RobotWorker implements Worker {
work() { /* ... */ }
eat() { throw new Error('Robots do not eat!'); } // Forced to implement!
sleep() { throw new Error('Robots do not sleep!'); } // Forced to implement!
generateReport() { /* ... */ }
attendMeeting() { /* ... */ }
}
// ✅ GOOD: Segregated interfaces
interface Workable {
work(): void;
}
interface Feedable {
eat(): void;
}
interface Sleepable {
sleep(): void;
}
interface Reportable {
generateReport(): void;
}
interface Attendable {
attendMeeting(): void;
}
class HumanWorker implements Workable, Feedable, Sleepable, Reportable, Attendable {
work() { /* ... */ }
eat() { /* ... */ }
sleep() { /* ... */ }
generateReport() { /* ... */ }
attendMeeting() { /* ... */ }
}
class RobotWorker implements Workable, Reportable, Attendable {
work() { /* ... */ }
generateReport() { /* ... */ }
attendMeeting() { /* ... */ }
// No need to implement eat() or sleep()!
}
// Functions depend only on what they need
const performWork = (worker: Workable) => {
worker.work();
};
const feedWorker = (worker: Feedable) => {
worker.eat();
};Example: Elixir
# ❌ BAD: Single behavior with too many callbacks
defmodule Worker do
@callback work() :: :ok
@callback eat() :: :ok
@callback sleep() :: :ok
@callback generate_report() :: :ok
@callback attend_meeting() :: :ok
end
defmodule RobotWorker do
@behaviour Worker
def work, do: :ok
def eat, do: raise "Robots do not eat!" # Forced to implement!
def sleep, do: raise "Robots do not sleep!" # Forced to implement!
def generate_report, do: :ok
def attend_meeting, do: :ok
end
# ✅ GOOD: Multiple focused behaviors
defmodule Workable do
@callback work() :: :ok
end
defmodule Feedable do
@callback eat() :: :ok
end
defmodule Sleepable do
@callback sleep() :: :ok
end
defmodule Reportable do
@callback generate_report() :: :ok
end
defmodule Attendable do
@callback attend_meeting() :: :ok
end
defmodule HumanWorker do
@behaviour Workable
@behaviour Feedable
@behaviour Sleepable
@behaviour Reportable
@behaviour Attendable
def work, do: :ok
def eat, do: :ok
def sleep, do: :ok
def generate_report, do: :ok
def attend_meeting, do: :ok
end
defmodule RobotWorker do
@behaviour Workable
@behaviour Reportable
@behaviour Attendable
def work, do: :ok
def generate_report, do: :ok
def attend_meeting, do: :ok
# No need to implement eat() or sleep()!
endBenefits:
Rule ID: solid-dip
Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions.
In Practice: Don't create dependencies directly in your code. Inject dependencies (interfaces) from the outside. This is the foundation of Clean Architecture.
Example: TypeScript
// ❌ BAD: High-level module depends on low-level module
class UserService {
private db = new PostgresDatabase(); // Direct dependency!
async getUser(id: string): Promise<User> {
return this.db.query(`SELECT * FROM users WHERE id = ${id}`);
}
}
// Cannot test without real database
// Cannot swap database implementation
// ✅ GOOD: Depend on abstraction, inject dependency
interface UserRepository {
findById(id: string): Promise<User | null>;
}
class UserService {
constructor(private userRepo: UserRepository) {} // Injected abstraction
async getUser(id: string): Promise<User> {
const user = await this.userRepo.findById(id);
if (!user) throw new Error('User not found');
return user;
}
}
// Low-level module implements abstraction
class PostgresUserRepository implements UserRepository {
constructor(private db: PostgresDatabase) {}
async findById(id: string): Promise<User | null> {
return this.db.query(`SELECT * FROM users WHERE id = $1`, [id]);
}
}
// Composition root wires dependencies
const db = new PostgresDatabase();
const userRepo = new PostgresUserRepository(db);
const userService = new UserService(userRepo);
// Easy to test with mock
class MockUserRepository implements UserRepository {
async findById(id: string): Promise<User | null> {
return { id, name: 'Test User', email: 'test@example.com' };
}
}
const testUserService = new UserService(new MockUserRepository());Example: Elixir
# ❌ BAD: Direct dependency on implementation
defmodule UserService do
def get_user(id) do
# Directly calls Postgres module
PostgresRepo.find_user(id)
end
end
# ✅ GOOD: Depend on behavior, inject implementation
defmodule UserRepository do
@callback find_by_id(id :: String.t()) :: {:ok, User.t()} | {:error, term()}
end
defmodule UserService do
def get_user(id, repo \\ PostgresUserRepo) do
case repo.find_by_id(id) do
{:ok, user} -> {:ok, user}
{:error, _} -> {:error, :not_found}
end
end
end
# Implementation
defmodule PostgresUserRepo do
@behaviour UserRepository
def find_by_id(id) do
Repo.get(User, id)
|> case do
nil -> {:error, :not_found}
user -> {:ok, user}
end
end
end
# Easy to test with mock
defmodule MockUserRepo do
@behaviour UserRepository
def find_by_id(id) do
{:ok, %User{id: id, name: "Test User"}}
end
end
# In tests
UserService.get_user("123", MockUserRepo)Benefits:
These principles transcend programming paradigms and apply to both object-oriented and functional code.
Rule ID: struct-composition-over-inheritance
Principle: Favor composing behaviors from small, focused pieces over building class hierarchies.
Why: Inheritance creates tight coupling and rigid hierarchies. Composition is flexible and composable.
Example: TypeScript
// ❌ BAD: Deep inheritance hierarchy
class Animal {
move() { console.log('Moving...'); }
}
class Mammal extends Animal {
breathe() { console.log('Breathing...'); }
}
class Dog extends Mammal {
bark() { console.log('Barking...'); }
}
class Cat extends Mammal {
meow() { console.log('Meowing...'); }
}
// What if we need a flying dog? Multiple inheritance not supported!
// Hierarchy becomes rigid and hard to change
// ✅ GOOD: Composition with interfaces
interface Movable {
move(): void;
}
interface Breathable {
breathe(): void;
}
interface Barkable {
bark(): void;
}
interface Flyable {
fly(): void;
}
const createMovable = (): Movable => ({
move: () => console.log('Moving...')
});
const createBreathable = (): Breathable => ({
breathe: () => console.log('Breathing...')
});
const createBarkable = (): Barkable => ({
bark: () => console.log('Barking...')
});
const createFlyable = (): Flyable => ({
fly: () => console.log('Flying...')
});
// Compose behaviors as needed
type Dog = Movable & Breathable & Barkable;
type FlyingDog = Movable & Breathable & Barkable & Flyable;
const createDog = (): Dog => ({
...createMovable(),
...createBreathable(),
...createBarkable()
});
const createFlyingDog = (): FlyingDog => ({
...createMovable(),
...createBreathable(),
...createBarkable(),
...createFlyable()
});Example: Elixir
# ❌ BAD: Trying to use inheritance patterns (modules as base classes)
defmodule Animal do
def move, do: IO.puts("Moving...")
end
defmodule Mammal do
import Animal
def breathe, do: IO.puts("Breathing...")
end
defmodule Dog do
import Mammal
def bark, do: IO.puts("Barking...")
end
# Becomes hard to manage and compose
# ✅ GOOD: Composition with pipes and small functions
defmodule Movable do
def move(entity), do: %{entity | status: "moving"}
end
defmodule Breathable do
def breathe(entity), do: %{entity | oxygen: entity.oxygen + 1}
end
defmodule Barkable do
def bark(entity) do
IO.puts("Woof!")
entity
end
end
defmodule Flyable do
def fly(entity), do: %{entity | altitude: entity.altitude + 10}
end
# Compose behaviors with pipes
defmodule Dog do
def create(name) do
%{name: name, status: "idle", oxygen: 100}
end
def act(dog) do
dog
|> Movable.move()
|> Breathable.breathe()
|> Barkable.bark()
end
end
defmodule FlyingDog do
def create(name) do
%{name: name, status: "idle", oxygen: 100, altitude: 0}
end
def act(flying_dog) do
flying_dog
|> Movable.move()
|> Breathable.breathe()
|> Barkable.bark()
|> Flyable.fly()
end
endAdvanced Elixir Example: Payment Processing Pipeline
# ✅ EXCELLENT: Complex composition with pipes, protocols, and behaviours
# Protocol for payment processing polymorphism
defprotocol PaymentProcessor do
def process(payment_method, amount, metadata)
end
# Behaviour for validation steps
defmodule PaymentValidator do
@callback validate(payment_data :: map()) :: {:ok, map()} | {:error, String.t()}
end
# Individual processors implementing protocol
defimpl PaymentProcessor, for: Map do
def process(%{type: "credit_card"} = method, amount, metadata) do
CreditCardProcessor.process(method, amount, metadata)
end
def process(%{type: "paypal"} = method, amount, metadata) do
PayPalProcessor.process(method, amount, metadata)
end
end
# Composable validation modules
defmodule AmountValidator do
@behaviour PaymentValidator
def validate(%{amount: amount}) when amount > 0, do: {:ok, %{amount: amount}}
def validate(_), do: {:error, "Invalid amount"}
end
defmodule PaymentMethodValidator do
@behaviour PaymentValidator
def validate(%{method: %{type: type}} = data) when type in ["credit_card", "paypal"] do
{:ok, data}
end
def validate(_), do: {:error, "Unsupported payment method"}
end
# Main payment pipeline using composition
defmodule PaymentPipeline do
def process(payment_data) do
with {:ok, validated_data} <- validate_payment(payment_data),
{:ok, processed_data} <- process_payment(validated_data),
{:ok, confirmed_data} <- confirm_payment(processed_data) do
{:ok, confirmed_data}
else
{:error, reason} -> {:error, reason}
end
end
# Compose validation steps with pipes
defp validate_payment(data) do
data
|> AmountValidator.validate()
|> case do
{:ok, validated_data} -> PaymentMethodValidator.validate(validated_data)
error -> error
end
end
defp process_payment(%{method: method, amount: amount} = data) do
result = PaymentProcessor.process(method, amount, %{transaction_id: generate_id()})
{:ok, Map.put(data, :processing_result, result)}
end
defp confirm_payment(data) do
# Additional composition with audit logging, notifications, etc.
data
|> AuditLogger.log()
|> NotificationService.notify()
|> PersistenceLayer.save()
end
defp generate_id, do: :crypto.strong_rand_bytes(16) |> Base.encode64()
end
# Usage demonstrates flexible composition
payment = %{
method: %{type: "credit_card", number: "****1234"},
amount: 100.00,
customer: %{id: 123, name: "Alice"}
}
{:ok, result} = PaymentPipeline.process(payment)Benefits:
Rule ID: struct-law-of-demeter
Principle: A module should only talk to its immediate friends, not strangers.
Rule: Only call methods on:
this)Don't: Chain calls through multiple objects (a.getB().getC().doSomething())
Example: TypeScript
// ❌ BAD: Violates Law of Demeter (train wreck)
class Customer {
getWallet(): Wallet { /* ... */ }
}
class Wallet {
getMoney(): Money { /* ... */ }
}
class Money {
getAmount(): number { /* ... */ }
}
// This code knows too much about internal structure!
const purchase = (customer: Customer, price: number): boolean => {
const amount = customer.getWallet().getMoney().getAmount(); // Train wreck!
return amount >= price;
};
// ✅ GOOD: Tell, don't ask - delegate to the owning module
class Customer {
constructor(private wallet: Wallet) {}
canAfford(price: number): boolean {
return this.wallet.hasEnough(price); // Delegate to wallet
}
}
class Wallet {
constructor(private money: Money) {}
hasEnough(amount: number): boolean {
return this.money.isGreaterThanOrEqual(amount); // Delegate to money
}
}
class Money {
constructor(private amount: number) {}
isGreaterThanOrEqual(other: number): boolean {
return this.amount >= other;
}
}
// Clean code: only talks to immediate friend
const purchase = (customer: Customer, price: number): boolean => {
return customer.canAfford(price);
};Example: Elixir
# ❌ BAD: Violates Law of Demeter
defmodule Purchase do
def can_afford?(customer, price) do
# Train wreck: reaching through multiple levels
customer.wallet.money.amount >= price
end
end
# ✅ GOOD: Delegate to owning module
defmodule Customer do
defstruct [:name, :wallet]
def can_afford?(customer, price) do
Wallet.has_enough?(customer.wallet, price)
end
end
defmodule Wallet do
defstruct [:money]
def has_enough?(wallet, amount) do
Money.greater_than_or_equal?(wallet.money, amount)
end
end
defmodule Money do
defstruct [:amount, :currency]
def greater_than_or_equal?(money, amount) do
money.amount >= amount
end
end
# Clean code
defmodule Purchase do
def can_afford?(customer, price) do
Customer.can_afford?(customer, price)
end
endAdvanced Elixir Example: Assignment & Worker Delegation
# ❌ BAD: Violates Law of Demeter with deep drilling
defmodule BadAssignmentLogic do
def worker_city(engagement) do
# Deep drilling through multiple levels - violates Law of Demeter
engagement.worker.address.city
end
def worker_hourly_rate(engagement) do
# Reaching through multiple objects
engagement.worker.profile.billing_info.hourly_rate
end
def is_local_worker?(engagement, target_city) do
engagement.worker.address.city == target_city
end
end
# ✅ GOOD: Delegate to owning modules with pattern matching
defmodule Assignment do
defstruct [:id, :worker_id, :project_id, :status]
def worker_city(engagement) do
# Delegate to the module that owns worker data
Worker.city_for_assignment(engagement.worker_id)
end
def hourly_rate(engagement) do
Worker.hourly_rate(engagement.worker_id)
end
def is_local_assignment?(engagement, target_city) do
case Worker.city_for_assignment(engagement.worker_id) do
^target_city -> true
_ -> false
end
end
end
defmodule Worker do
defstruct [:id, :profile, :address_id]
def city_for_assignment(worker_id) do
# Single responsibility: Worker knows how to get its own city
with {:ok, worker} <- get_worker(worker_id),
{:ok, address} <- Address.get(worker.address_id) do
address.city
else
_ -> nil
end
end
def hourly_rate(worker_id) do
# Worker delegates to Profile for billing information
with {:ok, worker} <- get_worker(worker_id) do
Profile.hourly_rate(worker.profile.id)
end
end
defp get_worker(id), do: Repo.get(Worker, id)
end
defmodule Address do
defstruct [:id, :street, :city, :state, :country]
def get(address_id) do
Repo.get(Address, address_id)
end
end
defmodule Profile do
defstruct [:id, :billing_info]
def hourly_rate(profile_id) do
# Profile owns billing information
case Repo.get(Profile, profile_id) do
%{billing_info: %{hourly_rate: rate}} -> rate
_ -> nil
end
end
end
# Usage: Clean delegation chain
engagement = %Assignment{worker_id: 123, project_id: 456}
city = Assignment.worker_city(engagement) # Clean, no train wrecks
rate = Assignment.hourly_rate(engagement) # Proper delegation
local? = Assignment.is_local_assignment?(engagement, "Austin")Benefits:
Rule ID: struct-tell-dont-ask
Principle: Tell objects what to do, don't ask for data and make decisions for them.
Why: Objects should encapsulate both data and behavior. Don't pull data out and operate on it externally.
Example: TypeScript
// ❌ BAD: Asking for data and making decisions
class Account {
private balance: number = 0;
getBalance(): number {
return this.balance;
}
setBalance(amount: number): void {
this.balance = amount;
}
}
// External code makes decisions based on pulled data
const withdraw = (account: Account, amount: number): void => {
if (account.getBalance() >= amount) { // Asking!
account.setBalance(account.getBalance() - amount); // Making decisions!
} else {
throw new Error('Insufficient funds');
}
};
// ✅ GOOD: Tell the object what to do
class Account {
private balance: number = 0;
withdraw(amount: number): void {
if (this.balance < amount) {
throw new Error('Insufficient funds');
}
this.balance -= amount;
}
deposit(amount: number): void {
this.balance += amount;
}
}
// External code just tells the account what to do
const performWithdrawal = (account: Account, amount: number): void => {
account.withdraw(amount); // Telling, not asking
};Example: Elixir
# ❌ BAD: Asking for data and making decisions
defmodule Account do
defstruct balance: 0
end
defmodule Banking do
def withdraw(account, amount) do
# Asking for data and making decisions externally
if account.balance >= amount do
%{account | balance: account.balance - amount}
else
{:error, :insufficient_funds}
end
end
end
# ✅ GOOD: Tell the module what to do
defmodule Account do
defstruct balance: 0
def withdraw(account, amount) do
if account.balance < amount do
{:error, :insufficient_funds}
else
{:ok, %{account | balance: account.balance - amount}}
end
end
def deposit(account, amount) do
{:ok, %{account | balance: account.balance + amount}}
end
end
# External code just tells the account what to do
defmodule Banking do
def perform_withdrawal(account, amount) do
Account.withdraw(account, amount)
end
end
# Advanced Pattern Matching Example with Assignment Confirmation
defmodule Assignment do
defstruct [:id, :worker_id, :project_id, :status, :confirmed_at]
# ✅ EXCELLENT: Pattern matching encapsulates business rules
def confirm(%{status: :pending} = assignment) do
# Tell the assignment to confirm itself, don't ask about status
{:ok, %{assignment |
status: :confirmed,
confirmed_at: DateTime.utc_now()}}
end
def confirm(%{status: :confirmed}), do: {:error, :already_confirmed}
def confirm(%{status: :cancelled}), do: {:error, :cannot_confirm_cancelled}
def confirm(_), do: {:error, :invalid_status}
# Business logic stays with the data
def can_be_charged?(%{status: status}) when status in [:confirmed, :in_progress], do: true
def can_be_charged?(_), do: false
end
# Usage: Tell, don't ask
case Assignment.confirm(assignment) do
{:ok, confirmed_assignment} ->
# Further actions based on successful confirmation
notify_worker(confirmed_assignment)
{:error, reason} ->
handle_confirmation_error(reason)
end
# Advanced Task Example with Polymorphism
defmodule Task do
defstruct [:id, :type, :status, :assignee_id, :rate_info]
end
defmodule TaskCharging do
# ✅ EXCELLENT: Delegate to specialized modules based on task type
def calculate_charge(task) do
# Tell the appropriate module to handle charging
case task.type do
:hourly -> HourlyCharging.calculate(task)
:fixed -> FixedCharging.calculate(task)
:milestone -> MilestoneCharging.calculate(task)
end
end
end
defmodule HourlyCharging do
def calculate(%{rate_info: %{hourly_rate: rate, hours_worked: hours}}) do
{:ok, rate * hours}
end
def calculate(_), do: {:error, :missing_hourly_data}
end
defmodule FixedCharging do
def calculate(%{rate_info: %{fixed_price: price}, status: :completed}) do
{:ok, price}
end
def calculate(%{status: status}) when status != :completed do
{:error, :task_not_completed}
end
def calculate(_), do: {:error, :missing_fixed_price}
endBenefits:
Rule ID: struct-encapsulation
Principle: Hide internal details and expose a minimal, stable interface.
Why: Internal implementation can change without affecting clients. Reduces coupling and increases flexibility.
Rules:
Example: TypeScript
// ❌ BAD: Exposing internal structure
class Order {
public items: OrderItem[] = []; // Public array!
public total: number = 0;
addItem(item: OrderItem): void {
this.items.push(item);
this.total += item.price;
}
}
// Client code can break invariants
const order = new Order();
order.items.push(invalidItem); // Bypassed addItem validation!
order.total = 999; // Manually changed total, now inconsistent!
// ✅ GOOD: Hide internal details, expose minimal interface
class Order {
private items: OrderItem[] = [];
addItem(item: OrderItem): void {
if (!this.isValidItem(item)) {
throw new Error('Invalid item');
}
this.items = [...this.items, item]; // Immutable update
}
removeItem(itemId: string): void {
this.items = this.items.filter(item => item.id !== itemId);
}
getTotal(): number {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
getItemCount(): number {
return this.items.length;
}
private isValidItem(item: OrderItem): boolean {
return item.price > 0 && item.quantity > 0;
}
}
// Client can only use public interface
const order = new Order();
order.addItem(item); // Validated
const total = order.getTotal(); // Computed from items, always consistentExample: Elixir
# ❌ BAD: Exposing internal structure
defmodule Order do
defstruct items: [], total: 0
end
# Clients can break invariants
order = %Order{items: [invalid_item], total: 999} # Bypassed validation!
# ✅ GOOD: Hide internal details with module API
defmodule Order do
# Internal structure (documented but not relied upon by clients)
defstruct items: []
# Public API
def new(), do: %Order{}
def add_item(order, item) do
if valid_item?(item) do
{:ok, %{order | items: [item | order.items]}}
else
{:error, :invalid_item}
end
end
def remove_item(order, item_id) do
items = Enum.reject(order.items, &(&1.id == item_id))
{:ok, %{order | items: items}}
end
def get_total(order) do
Enum.reduce(order.items, 0, fn item, sum -> sum + item.price end)
end
def get_item_count(order) do
length(order.items)
end
# Private helper
defp valid_item?(item) do
item.price > 0 and item.quantity > 0
end
end
# Clients use public API
{:ok, order} = Order.new()
{:ok, order} = Order.add_item(order, item)
total = Order.get_total(order)
# Advanced Encapsulation: Opaque Types and Ecto Schemas
defmodule UserId do
# ✅ EXCELLENT: Opaque type hides internal representation
@opaque t :: String.t()
def new(id) when is_binary(id) and byte_size(id) > 0, do: {:ok, id}
def new(_), do: {:error, :invalid_user_id}
def to_string(user_id), do: user_id
# Only this module can create/manipulate UserIds
def from_integer(id) when is_integer(id), do: {:ok, Integer.to_string(id)}
def from_integer(_), do: {:error, :invalid_integer}
end
# Ecto Schema with Controlled Access
defmodule Task do
use Ecto.Schema
import Ecto.Changeset
# Internal structure hidden through changesets
schema "tasks" do
field :title, :string
field :status, Ecto.Enum, values: [:pending, :in_progress, :completed, :cancelled]
field :estimated_hours, :decimal
field :actual_hours, :decimal
field :assignee_id, :binary_id
timestamps()
end
# Public API: Only way to modify tasks
def create_changeset(attrs \\ %{}) do
%Task{}
|> cast(attrs, [:title, :estimated_hours, :assignee_id])
|> validate_required([:title, :assignee_id])
|> validate_length(:title, min: 1, max: 255)
|> validate_number(:estimated_hours, greater_than: 0)
|> put_change(:status, :pending)
end
def update_changeset(task, attrs) do
task
|> cast(attrs, [:title, :estimated_hours])
|> validate_required([:title])
|> validate_length(:title, min: 1, max: 255)
|> validate_number(:estimated_hours, greater_than: 0)
end
def start_changeset(task) do
case task.status do
:pending ->
change(task, status: :in_progress)
_ ->
change(task) |> add_error(:status, "can only start pending tasks")
end
end
def complete_changeset(task, actual_hours) do
case task.status do
:in_progress ->
change(task)
|> put_change(:status, :completed)
|> put_change(:actual_hours, actual_hours)
|> validate_number(:actual_hours, greater_than: 0)
_ ->
change(task) |> add_error(:status, "can only complete in-progress tasks")
end
end
# Computed fields (no direct field access)
def hours_variance(task) do
case {task.estimated_hours, task.actual_hours} do
{estimated, actual} when not is_nil(estimated) and not is_nil(actual) ->
Decimal.sub(actual, estimated)
_ ->
nil
end
end
def is_overbudget?(task) do
case hours_variance(task) do
variance when not is_nil(variance) -> Decimal.positive?(variance)
_ -> false
end
end
end
# Usage: Controlled access through changesets
changeset = Task.create_changeset(%{title: "Fix bug", estimated_hours: 2, assignee_id: user_id})
{:ok, task} = Repo.insert(changeset)
# State transitions through controlled changesets
{:ok, started_task} =
task
|> Task.start_changeset()
|> Repo.update()
{:ok, completed_task} =
started_task
|> Task.complete_changeset(Decimal.new("2.5"))
|> Repo.update()
# Safe computed access
variance = Task.hours_variance(completed_task)
overbudget? = Task.is_overbudget?(completed_task)Benefits:
Code quality principles ensure your code is readable, maintainable, and free of common defects.
Rule ID: core-dry
Principle: Every piece of knowledge should have a single, authoritative representation.
Why: Duplication leads to maintenance nightmares. When logic changes, you must find and update every copy.
// ❌ BAD: Duplicated validation logic
const validateUserEmail = (email: string): boolean => {
return email.includes('@') && email.length > 5;
};
const validateAdminEmail = (email: string): boolean => {
return email.includes('@') && email.length > 5; // Duplicated!
};
// ✅ GOOD: Single source of truth
const isValidEmail = (email: string): boolean => {
return email.includes('@') && email.length > 5;
};
const validateUserEmail = (email: string): boolean => isValidEmail(email);
const validateAdminEmail = (email: string): boolean => isValidEmail(email);When to violate DRY: When abstractions are premature or force unnatural coupling. Prefer duplication over wrong abstraction.
Rule ID: core-kiss
Principle: Simplicity should be a key goal. Avoid unnecessary complexity.
Why: Simple code is easier to understand, test, and maintain.
// ❌ BAD: Overly complex
const getUserStatus = (user: User): string => {
return user.isActive
? (user.isPremium
? (user.hasAccess
? 'premium-active-access'
: 'premium-active-no-access')
: (user.hasAccess
? 'basic-active-access'
: 'basic-active-no-access'))
: 'inactive';
};
// ✅ GOOD: Simple and clear
const getUserStatus = (user: User): string => {
if (!user.isActive) return 'inactive';
if (!user.hasAccess) return user.isPremium ? 'premium-no-access' : 'basic-no-access';
return user.isPremium ? 'premium-active' : 'basic-active';
};Rule ID: core-yagni
Principle: Don't implement features until you actually need them.
Why: Speculative features add complexity, maintenance cost, and often go unused.
// ❌ BAD: Over-engineered for future needs
class User {
constructor(
private id: string,
private name: string,
private email: string,
private phone?: string, // Not needed yet
private address?: Address, // Not needed yet
private preferences?: UserPreferences, // Not needed yet
private socialLinks?: SocialLinks, // Not needed yet
private paymentMethods?: PaymentMethod[] // Not needed yet
) {}
}
// ✅ GOOD: Only what's needed now
class User {
constructor(
private id: string,
private name: string,
private email: string
) {}
}
// Add fields when requirements actually materializePrinciple: Separate your program into distinct sections, each addressing a separate concern.
Why: Changes to one concern don't affect others. Easier to understand and maintain.
// ❌ BAD: Mixed concerns (data access + business logic + presentation)
const getUserProfile = async (userId: string): Promise<string> => {
const user = await db.query(`SELECT * FROM users WHERE id = ${userId}`); // Data access
if (user.isActive && user.credits > 10) { // Business logic
return `<h1>Welcome ${user.name}!</h1>`; // Presentation
}
return `<h1>Account suspended</h1>`;
};
// ✅ GOOD: Separated concerns
// Data access layer
const userRepository = {
findById: async (id: string): Promise<User> => {
return db.query('SELECT * FROM users WHERE id = $1', [id]);
}
};
// Business logic layer
const canAccessProfile = (user: User): boolean => {
return user.isActive && user.credits > 10;
};
// Presentation layer
const renderProfile = (user: User, canAccess: boolean): string => {
if (canAccess) {
return `<h1>Welcome ${user.name}!</h1>`;
}
return `<h1>Account suspended</h1>`;
};
// Orchestration (use case)
const getUserProfile = async (userId: string): Promise<string> => {
const user = await userRepository.findById(userId);
const canAccess = canAccessProfile(user);
return renderProfile(user, canAccess);
};Principle: Detect and report errors as early as possible.
Why: Makes debugging easier. Problems are caught close to their source.
// ❌ BAD: Silent failures and late detection
const processOrder = (order: Order): void => {
const items = order.items || []; // Silently handles missing items
const total = items.reduce((sum, item) => sum + (item.price || 0), 0); // Silently handles missing price
if (total > 0) {
// Process order...
}
// Order with no items or invalid prices passes through!
};
// ✅ GOOD: Fail fast with validation
const processOrder = (order: Order): void => {
if (!order.items || order.items.length === 0) {
throw new Error('Order must have at least one item');
}
for (const item of order.items) {
if (!item.price || item.price <= 0) {
throw new Error(`Invalid price for item ${item.id}`);
}
}
const total = order.items.reduce((sum, item) => sum + item.price, 0);
// Process order...
};Common patterns for solving recurring design problems.
Rule ID: pattern-factory
Purpose: Create objects without specifying exact class.
interface PaymentMethod {
process(amount: number): Promise<PaymentResult>;
}
class PaymentFactory {
create(type: string): PaymentMethod {
switch (type) {
case 'credit_card':
return new CreditCardPayment();
case 'paypal':
return new PayPalPayment();
case 'crypto':
return new CryptoPayment();
default:
throw new Error(`Unknown payment type: ${type}`);
}
}
}Rule ID: pattern-strategy
Purpose: Define family of algorithms, encapsulate each one, make them interchangeable.
interface SortStrategy {
sort(data: number[]): number[];
}
class QuickSort implements SortStrategy {
sort(data: number[]): number[] {
// Quick sort implementation
return data;
}
}
class MergeSort implements SortStrategy {
sort(data: number[]): number[] {
// Merge sort implementation
return data;
}
}
class DataProcessor {
constructor(private strategy: SortStrategy) {}
process(data: number[]): number[] {
return this.strategy.sort(data);
}
}
// Use different strategies
const processor1 = new DataProcessor(new QuickSort());
const processor2 = new DataProcessor(new MergeSort());Rule ID: pattern-repository
Purpose: Encapsulate data access logic.
interface UserRepository {
findById(id: string): Promise<User | null>;
save(user: User): Promise<void>;
delete(id: string): Promise<void>;
}
class PostgresUserRepository implements UserRepository {
async findById(id: string): Promise<User | null> {
return this.db.query('SELECT * FROM users WHERE id = $1', [id]);
}
async save(user: User): Promise<void> {
await this.db.query('INSERT INTO users ...', [user]);
}
async delete(id: string): Promise<void> {
await this.db.query('DELETE FROM users WHERE id = $1', [id]);
}
}Rule ID: pattern-observer
Purpose: Notify multiple objects about state changes.
interface Observer {
update(data: any): void;
}
class Subject {
private observers: Observer[] = [];
attach(observer: Observer): void {
this.observers.push(observer);
}
detach(observer: Observer): void {
this.observers = this.observers.filter(o => o !== observer);
}
notify(data: any): void {
for (const observer of this.observers) {
observer.update(data);
}
}
}
class EmailNotifier implements Observer {
update(data: any): void {
console.log('Sending email notification:', data);
}
}
class SlackNotifier implements Observer {
update(data: any): void {
console.log('Sending Slack notification:', data);
}
}Rules:
// ❌ BAD
const d = new Date();
const usr = getUsr(id);
const calc = (a, b) => a + b;
// ✅ GOOD
const currentDate = new Date();
const user = getUserById(id);
const calculateTotal = (price, quantity) => price * quantity;Rules:
// ❌ BAD: Too many responsibilities
const processUser = (user: User, sendEmail: boolean, updateDb: boolean, logAction: boolean) => {
// Validation
if (!user.email.includes('@')) throw new Error('Invalid email');
// Business logic
user.status = 'active';
// Side effects
if (sendEmail) emailService.send(user.email, 'Welcome!');
if (updateDb) db.save(user);
if (logAction) logger.log('User processed');
return user;
};
// ✅ GOOD: Single responsibility, composed
const validateUser = (user: User): void => {
if (!user.email.includes('@')) throw new Error('Invalid email');
};
const activateUser = (user: User): User => {
return { ...user, status: 'active' };
};
const processUser = async (user: User): Promise<User> => {
validateUser(user);
const activeUser = activateUser(user);
await saveUser(activeUser);
await sendWelcomeEmail(activeUser.email);
logUserActivation(activeUser.id);
return activeUser;
};Rules:
// ❌ BAD: Swallowing errors
const getUser = async (id: string): Promise<User | null> => {
try {
return await db.findUser(id);
} catch (error) {
return null; // Error information lost!
}
};
// ✅ GOOD: Propagate with context
const getUser = async (id: string): Promise<User> => {
try {
const user = await db.findUser(id);
if (!user) {
throw new Error(`User not found: ${id}`);
}
return user;
} catch (error) {
throw new Error(`Failed to get user ${id}: ${error.message}`);
}
};When implementing or reviewing code, use this checklist:
Symptom: One class/module with hundreds of lines doing many things.
Fix: Apply Single Responsibility Principle. Extract cohesive groups of functionality into separate modules.
Symptom: Changing one module requires changes in many others.
Fix: Apply Dependency Inversion. Depend on abstractions. Use interfaces and dependency injection.
Symptom: One change requires modifying many files.
Fix: Apply Separation of Concerns. Group related functionality together.
Symptom: Method in one class uses data/methods from another class more than its own.
Fix: Apply Tell, Don't Ask. Move behavior to the class that owns the data.
Symptom: Two modules know too much about each other's internals.
Fix: Apply Encapsulation and Law of Demeter. Hide internal details behind interfaces.
During Design:
During Implementation:
During Code Review:
During Refactoring:
This section provides guidance for agents when workflows fail, edge cases occur, or unexpected situations arise during design principle application.
Problem: Cannot load required reference file or context limit reached
Fallback Actions:
Example Recovery:
⚠️ Could not load dep-inward-only.md reference
✓ Applying general principle: Domain layer must not import infrastructure
✓ Checking for framework imports in entities...
⚠️ Partial analysis - recommend manual review of boundary violationsProblem: Two principles seem to contradict each other in specific context
Resolution Priority:
Fallback Process:
1. Identify conflict → Document both principles
2. Check priority levels → Apply higher priority rule
3. If same priority → Apply most restrictive interpretation
4. Document decision rationale in output
5. Flag for human review if critical business impactProblem: Code structure doesn't fit standard patterns or is too complex to analyze
Fallback Actions:
Example Response:
⚠️ Complex code structure detected
✓ Clear violations: 15+ imports (dep-inward-only), 300+ lines (solid-srp)
? Uncertain: Business logic placement - could be entity or service
→ Recommend: Fix clear violations first, then architect review for structureProblem: Applying modern principles to legacy code seems impossible or destructive
Pragmatic Fallbacks:
Triage Strategy:
Legacy Code Decision Matrix:
- Changing frequently → Apply full principles
- Stable but needs modification → Apply minimum viable principles
- Read-only/deprecated → Document violations, no changes
- Critical path → Apply CRITICAL principles onlyProblem: Principle application would harm performance significantly
Balance Framework:
Example Decision Process:
Performance Issue: N+1 queries from strict law of demeter
✓ Option A: Keep principle, optimize with caching
✓ Option B: Controlled violation with explicit batching interface
✗ Option C: Remove all encapsulation (violates entity-pure-business-rules)
→ Choose A or B based on performance requirements, document decisionThis section outlines different approaches for applying software design principles in various development contexts.
Purpose: Evaluate system architecture for principle violations and structural issues
Approach:
dep-* rule violations across system boundariesFocus Areas:
Output Format:
Architecture Review Report:
========================
System: Payment Processing Service
Date: 2024-03-15
CRITICAL Issues (3):
- src/domain/payment.ts:12 - [dep-inward-only] Domain importing infrastructure
- src/entities/order.ts:45 - [entity-pure-business-rules] Entity contains database logic
Recommendations:
1. Create Payment interface in domain layer
2. Move database logic to infrastructure layer
3. Apply dependency inversion to decouple layersPurpose: Systematic review of codebase for design principle violations
Approach:
Focus Areas:
Workflow:
Purpose: Guide systematic improvement of existing code using design principles
Approach:
Refactoring Priorities:
Example Refactoring Plan:
File: src/user-service.ts (4 violations)
Phase 1 - Critical (Week 1):
- [dep-inward-only] Remove direct database imports
- Extract repository interface
Phase 2 - High (Week 2):
- [solid-srp] Split authentication from validation
- [solid-ocp] Make validation extensible
Phase 3 - Medium (Week 3):
- [struct-tell-dont-ask] Move behavior to User entity
- [struct-law-of-demeter] Reduce property chainsPurpose: Use design principles to guide testable code structure during TDD
Approach:
Design-Test Integration:
Purpose: Apply design principles during active code review process
Approach:
Review Checklist:
Integration Notes:
This skill works synergistically with other development skills to create a comprehensive software quality framework.
Purpose: Use design principles as code review criteria
Integration Points:
file:line - [rule-id] Description for consistent reviewsExample Workflow:
# Code review output example:
src/UserService.ts:45 - [solid-srp] Class handles authentication, validation, and email sending
src/PaymentProcessor.ts:12 - [dep-inward-only] Infrastructure layer importing from domain
src/OrderController.ts:78 - [struct-tell-dont-ask] Asking object for data instead of telling it to actPurpose: Apply strategic design principles to architectural decisions
Integration Points:
Example Workflow:
Purpose: Design principles guide testable code structure
Integration Points:
Example Workflow:
// TDD + Design Principles Example
describe('OrderProcessor', () => {
it('should apply discount when order qualifies', () => {
const discountCalculator = new MockDiscountCalculator();
const orderProcessor = new OrderProcessor(discountCalculator); // DIP
const order = new Order([item1, item2]);
orderProcessor.processOrder(order); // Tell, Don't Ask
expect(order.hasDiscount()).toBe(true); // SRP - Order knows its state
});
});Documentalist Skill:
Skill Integration Best Practices:
Anti-Integration Patterns to Avoid:
Architecture & Design Principles:
SOLID Principles & Object-Oriented Design:
Refactoring & Code Quality:
Martin Fowler's Articles:
Uncle Bob's Resources:
Design Pattern Resources:
Functional Programming Perspectives:
Essential Conference Talks:
Foundational Papers:
Static Analysis:
Architecture Documentation:
Install with Tessl CLI
npx tessl i pantheon-ai/software-design-principlesevals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
references