--- id: core-dry title: Don't Repeat Yourself (DRY) category: core-principles priority: critical tags: [DRY, duplication, single-source-of-truth, maintainability] related: [core-dry-extraction, core-dry-single-source, solid-srp-class] --- # Don't Repeat Yourself (DRY) ## Why It Matters "Don't Repeat Yourself" means every piece of knowledge should have a single, authoritative representation. Duplication leads to inconsistencies, increases maintenance burden, and makes bugs harder to fix. When you change one copy but forget others, bugs creep in. ## Incorrect ```typescript // ❌ Duplicated validation logic class UserController { createUser(data) { if (!data.email || !data.email.includes('@')) { throw new Error('Invalid email'); } if (!data.password || data.password.length < 8) { throw new Error('Password too short'); } // create user... } updateUser(id, data) { if (!data.email || !data.email.includes('@')) { // Duplicated throw new Error('Invalid email'); } if (!data.password || data.password.length < 8) { // Duplicated throw new Error('Password too short'); } // update user... } } // ❌ Duplicated business rules function calculateOrderTotal(items) { let total = 0; for (const item of items) { total += item.price * item.quantity; } if (total > 100) { total = total * 0.9; // 10% discount over $100 } return total; } function calculateCartTotal(cartItems) { let total = 0; for (const item of cartItems) { total += item.price * item.quantity; // Duplicated } if (total > 100) { total = total * 0.9; // Duplicated discount logic } return total; } // ❌ Duplicated constants // file: checkout.ts const TAX_RATE = 0.08; const FREE_SHIPPING_THRESHOLD = 50; // file: cart.ts const TAX_RATE = 0.08; // Duplicated const FREE_SHIPPING_THRESHOLD = 50; // Duplicated ``` ## Correct ### Extract Shared Validation ```typescript // ✅ Single validation module // validators/user.ts export class UserValidator { static validateEmail(email: string): void { if (!email || !email.includes('@')) { throw new ValidationError('Invalid email address'); } } static validatePassword(password: string): void { if (!password || password.length < 8) { throw new ValidationError('Password must be at least 8 characters'); } } static validate(data: UserData): void { this.validateEmail(data.email); this.validatePassword(data.password); } } // controller.ts class UserController { createUser(data) { UserValidator.validate(data); // create user... } updateUser(id, data) { UserValidator.validate(data); // update user... } } ``` ### Extract Shared Business Logic ```typescript // ✅ Single source of truth for pricing // services/pricing.ts export class PricingService { private static readonly BULK_DISCOUNT_THRESHOLD = 100; private static readonly BULK_DISCOUNT_RATE = 0.1; static calculateSubtotal(items: LineItem[]): number { return items.reduce( (sum, item) => sum + item.price * item.quantity, 0 ); } static applyDiscount(subtotal: number): number { if (subtotal > this.BULK_DISCOUNT_THRESHOLD) { return subtotal * (1 - this.BULK_DISCOUNT_RATE); } return subtotal; } static calculateTotal(items: LineItem[]): number { const subtotal = this.calculateSubtotal(items); return this.applyDiscount(subtotal); } } // Both order and cart use the same logic const orderTotal = PricingService.calculateTotal(order.items); const cartTotal = PricingService.calculateTotal(cart.items); ``` ### Centralize Constants ```typescript // ✅ Single constants file // constants/pricing.ts export const PRICING = { TAX_RATE: 0.08, FREE_SHIPPING_THRESHOLD: 50, BULK_DISCOUNT_THRESHOLD: 100, BULK_DISCOUNT_RATE: 0.1, } as const; // Used everywhere import { PRICING } from '@/constants/pricing'; const tax = subtotal * PRICING.TAX_RATE; const freeShipping = total >= PRICING.FREE_SHIPPING_THRESHOLD; ``` ### Extract Shared Components ```tsx // ❌ Duplicated UI patterns function UserCard({ user }) { return (

{user.name}

{user.email}

); } function TeamMemberCard({ member }) { return (
{/* Same styles */}

{member.name}

{member.role}

); } // ✅ Reusable component function Card({ children, className }) { return (
{children}
); } function Avatar({ src, alt }) { return {alt}; } function UserCard({ user }) { return (

{user.name}

{user.email}

); } function TeamMemberCard({ member }) { return (

{member.name}

{member.role}

); } ``` ### Extract Shared Hooks ```typescript // ❌ Duplicated fetch logic function UserProfile() { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { fetch('/api/user') .then(res => res.json()) .then(setUser) .catch(setError) .finally(() => setLoading(false)); }, []); // ... } function ProductList() { const [products, setProducts] = useState(null); const [loading, setLoading] = useState(true); // Duplicated const [error, setError] = useState(null); // Duplicated useEffect(() => { fetch('/api/products') // Same pattern .then(res => res.json()) .then(setProducts) .catch(setError) .finally(() => setLoading(false)); }, []); // ... } // ✅ Custom hook (or use React Query) function useFetch(url: string) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { fetch(url) .then(res => res.json()) .then(setData) .catch(setError) .finally(() => setLoading(false)); }, [url]); return { data, loading, error }; } function UserProfile() { const { data: user, loading, error } = useFetch('/api/user'); // ... } function ProductList() { const { data: products, loading, error } = useFetch('/api/products'); // ... } ``` ## When NOT to DRY ```typescript // ⚠️ Don't extract coincidentally similar code // These might look similar but serve different purposes function validateUserAge(age: number) { return age >= 18; // Legal adult age } function validateMinimumOrderQuantity(quantity: number) { return quantity >= 18; // Business rule: minimum order } // These should remain separate even though both check >= 18 // Their reasons for change are different ``` ## Rule of Three ``` Wait until you see duplication three times before extracting. Two occurrences might be coincidental. Three indicates a pattern. ``` ## Benefits - Single source of truth - Fix bugs in one place - Consistent behavior across codebase - Easier refactoring - Reduced code size - Lower maintenance cost