| name | dry-principle |
| description | Eliminate knowledge duplication via abstraction. Trigger: When same logic appears 3+ times or changing one place requires updating others. |
| license | Apache 2.0 |
| metadata | {"version":"1.0","type":"domain"} |
DRY Principle
Eliminate knowledge duplication through abstraction. DRY is about knowledge duplication, not code duplication—similar-looking code representing different concepts should NOT be merged.
When to Use
- Same logic appears in 3+ places (Rule of Three)
- Changing a rule requires updating multiple files
- Shared configuration or constants duplicated across modules
Don't use for:
- Code that looks similar but represents different business concepts
- Premature abstraction (apply after seeing duplication, not speculatively)
- Trivial one-off operations
Critical Patterns
✅ REQUIRED: Rule of Three — Extract After 3rd Occurrence
Apply DRY when logic appears in 3+ places. Below that, duplication may be acceptable.
const validateEmail = (email: string) => {
if (!email.includes('@')) return 'Invalid email';
if (email.length < 5) return 'Email too short';
return null;
};
export const validateEmail = (email: string): string | null => {
if (!email.includes('@')) return 'Invalid email';
if (email.length < 5) return 'Email too short';
return null;
};
✅ REQUIRED: Centralize Configuration
Single source of truth for constants, endpoints, and config values.
const BASE_URL = 'https://api.example.com/v1';
const API_URL = 'https://api.example.com/v1';
export const API_BASE_URL = 'https://api.example.com/v1';
export const API_ENDPOINTS = {
users: `${API_BASE_URL}/users`,
orders: `${API_BASE_URL}/orders`,
};
✅ REQUIRED: Shared Types
Define types once, import everywhere.
type User = { id: string; email: string; name: string };
type User = { id: string; email: string; role: string };
export interface User { id: string; email: string; name: string; role: string; }
✅ REQUIRED: React Custom Hooks as DRY Abstraction
Extract repeated fetch/state patterns into custom hooks — same Rule of Three applies.
function UserList() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => { fetch('/api/users').then(r => r.json()).then(setUsers).finally(() => setLoading(false)); }, []);
}
function ProductList() {
const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => { fetch('/api/products').then(r => r.json()).then(setProducts).finally(() => setLoading(false)); }, []);
}
function useFetch<T>(url: string) {
const [data, setData] = useState<T[]>([]);
const [loading, setLoading] = ();
( { (url).( r.()).(setData).( ()); }, [url]);
{ data, loading };
}
() { { : users, loading } = useFetch<>(); ... }
() { { : products, loading } = useFetch<>(); ... }
Rule: Apply only after seeing the same hook pattern in 3+ components — not speculatively.
❌ NEVER: Merge Code That Looks Similar but Isn't
DRY is about knowledge duplication, not code similarity.
function applyDiscount(price: number, percent: number) { return price * (1 - percent / 100); }
function applyLoyaltyDiscount(price: number, loyaltyPercent: number): number { ... }
function applyPromoDiscount(price: number, promoPercent: number): number { ... }
Decision Tree
Logic appears in 3+ places?
→ YES: Extract to shared function/utility/constant
→ NO (2 places): Consider if it's worth extracting; often OK to duplicate
Code looks similar but represents different business concepts?
→ Extract = premature abstraction → Keep separate
Configuration duplicated across files?
→ Move to centralized config module
Types defined multiple times with variations?
→ Define once in types/ directory, import everywhere
Abstraction would require many parameters or conditions?
→ Abstraction is fighting the code → may not be a real duplication
Example
export const ENDPOINTS = { users: '/api/users', orders: '/api/orders' };
export interface PaginatedResponse<T> { data: T[]; total: number; page: number; }
export async function fetchJson<T>(url: string): Promise<T> {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
import { ENDPOINTS } from '../config/api';
import { fetchJson } from '../utils/http';
import type { PaginatedResponse } from '../types/pagination';
import { } ;
= () => fetchJson<<>>(.);
Edge Cases
Forced parameter proliferation: If your shared function needs 5+ parameters to handle all callers, DRY may be wrong. The callers may represent genuinely different concerns.
Test helpers: Same test setup code in multiple tests — extract to beforeEach or test fixtures. This is legitimate DRY in tests.
Accidental coupling: Merging two functions because they look similar can accidentally couple unrelated business rules. If they change independently, keep them separate.
Resources