Use when implementing new features, adding constants, creating types, or writing business logic. Use when you're about to hardcode values that might exist elsewhere...
Search BEFORE you implement. Every hardcoded value is a potential duplication.
DRY (Don't Repeat Yourself) violations aren't just about copy-pasted code. They include:
digraph dry_check {
"About to hardcode a value?" [shape=diamond];
"Creating array/constant?" [shape=diamond];
"Writing business logic?" [shape=diamond];
"SEARCH first" [shape=box, style=filled];
"Proceed" [shape=box];
"About to hardcode a value?" -> "SEARCH first" [label="yes"];
"About to hardcode a value?" -> "Creating array/constant?" [label="no"];
"Creating array/constant?" -> "SEARCH first" [label="yes"];
"Creating array/constant?" -> "Writing business logic?" [label="no"];
"Writing business logic?" -> "SEARCH first" [label="yes"];
"Writing business logic?" -> "Proceed" [label="no"];
}
Trigger symptoms:
Before implementing ANY of these, SEARCH the codebase:
| You're implementing | Search for |
|---|---|
| Array of types | Existing enums: export enum, *.enum.ts |
| Constant values | Existing constants: const.*=, *.constants.ts |
| Type definition | Existing types: interface, type.*=, domain/*.ts |
| Detection/calculation logic | Domain methods with similar names |
| String literals | Enum values that match |
# Find enums
grep -r "export enum" src/
# Find type/constant by concept name (example: payment types)
grep -ri "payment.*type\|PaymentType" src/
# Find domain methods
grep -r "isEligible\|calculate\|validate" src/
# Find all type definitions
grep -r "export type\|export interface" src/
// β DRY VIOLATION: Hardcoded array
const ORDER_STATUSES = ['pending', 'confirmed', 'shipped', 'delivered'];
// β
CORRECT: Reference existing enum
import { OrderStatus } from './domain/order-status.enum';
const statuses = Object.values(OrderStatus);
// β DRY VIOLATION: Local constant duplicating domain knowledge
const PREMIUM_TIERS = [UserTier.Gold, UserTier.Platinum, UserTier.Diamond];
// β
CORRECT: Use existing domain method or shared constant
const isPremium = user.isPremiumTier();
// Or use shared constant:
import { PREMIUM_TIERS } from './domain/user-tier-groups';
// β DRY VIOLATION: Creating duplicate type
interface ProductCategory {
name: string;
products: string[];
}
// β
CORRECT: Import existing definition
import { ProductCategory } from './domain/product-category';
// β DRY VIOLATION: Logic that belongs in domain
class DiscountCalculationService {
isEligibleForDiscount(orders: Order[]): boolean {
return orders.filter(o => o.status === 'completed').length >= 5;
}
}
// β
CORRECT: Domain method already exists (or should)
const isEligible = customer.isEligibleForDiscount();
// β DRY VIOLATION: String literal
if (payment.method === 'credit_card') { ... }
// β
CORRECT: Use enum
if (payment.method === PaymentMethod.CreditCard) { ... }
// β DRY VIOLATION: Validation duplicated in controller and service
// In controller:
if (!email.includes('@')) throw new BadRequest('Invalid email');
// In service:
if (!email.includes('@')) throw new Error('Invalid email');
// β
CORRECT: Centralized validation
import { validateEmail } from './domain/validators';
validateEmail(email); // Throws if invalid
| Concept | Search locations |
|---|---|
| Enums | src/*/domain/*.enum.ts, src/enums/, src/types/ |
| Types/Interfaces | src/*/domain/*.ts, src/common/, src/types/ |
| Constants | src/*/domain/*.ts, src/shared/, src/constants/ |
| Domain methods | src/*/domain/*.ts (look for classes with business logic) |
| Validators | src/common/validators/, src/*/domain/validators/ |
| DTOs | src/*/dto/*.ts |
| You're about to... | STOP and search for... |
|---|---|
| Define array of types | Existing enum |
| Create constant | Existing constant/enum |
| Write detection logic | Existing domain method |
| Define new interface | Existing type |
| Use string literal | Existing enum value |
| Create calculation service | Existing domain method |
| Add validation logic | Existing validators |
| Define error messages | Existing error constants |
When reviewing code for DRY violations:
| Violation | Impact |
|---|---|
| Duplicate constant | Changes must be made in multiple places |
| Duplicate enum values | Values drift out of sync |
| Duplicate logic | Bug fixes miss some copies |
| String literals vs enums | Typos cause runtime errors |
| Duplicate types | Incompatible interfaces |
| Duplicate validation | Inconsistent behavior across layers |
Even when using proper enum references, creating groupings can be a DRY violation.
Ask: Is this grouping a domain concept or a one-off filter?
| Situation | Action |
|---|---|
| Grouping appears in 2+ places | Centralize in domain |
| Grouping represents domain concept (e.g., "premium tiers") | Centralize in domain |
| Grouping is UI-specific filter, used once | OK to inline |
// β POTENTIAL DRY VIOLATION: Domain concept scattered
// In service A:
const refundableStatuses = [OrderStatus.Pending, OrderStatus.Confirmed];
// In service B:
const refundableStatuses = [OrderStatus.Pending, OrderStatus.Confirmed];
// β
CORRECT: Centralize domain concept
// In domain/order-status-groups.ts:
export const REFUNDABLE_STATUSES = [
OrderStatus.Pending,
OrderStatus.Confirmed
] as const;
// Usage everywhere:
import { REFUNDABLE_STATUSES } from './domain/order-status-groups';
Search before creating any grouping:
grep -ri "pending.*confirmed\|refundable" src/
When you need logic that operates on domain data, ask:
| Question | If YES β | If NO β |
|---|---|---|
| Does this logic operate on data inside a single entity? | Domain method | Service method |
| Would multiple services need this logic? | Domain method | Consider service |
| Is this a core business rule? | Domain method | Service method |
| Does it require external calls (DB, API)? | Service method | Domain method |
// β Service with logic that belongs in domain
class OrderService {
canBeCancelled(order: Order): boolean {
return ['pending', 'confirmed'].includes(order.status) &&
order.createdAt > Date.now() - 24 * 60 * 60 * 1000;
}
}
// β
Domain method
class Order {
canBeCancelled(): boolean {
return this.isInCancellableStatus() && this.isWithinCancellationWindow();
}
private isInCancellableStatus(): boolean {
return CANCELLABLE_STATUSES.includes(this.status);
}
private isWithinCancellationWindow(): boolean {
return this.createdAt > Date.now() - CANCELLATION_WINDOW_MS;
}
}
| Excuse | Reality |
|---|---|
| "It's faster to hardcode" | 30 seconds of searching prevents hours of debugging |
| "This is just a quick helper" | Quick helpers become permanent code |
| "I don't know if it exists" | That's why you search first |
| "This is a different context" | Same concept = same definition |
| "It's just a small array" | Small duplications multiply |
| "I'll refactor later" | Later never comes |
| "It's just 2 values" | Small groupings still represent domain concepts |
| "The other code is in a different module" | Shared concepts should be in shared location |
| "I need slightly different behavior" | Parameterize the existing code |
Document where different types of definitions live:
src/
βββ shared/
β βββ constants/ # App-wide constants
β βββ enums/ # Shared enums
β βββ types/ # Shared type definitions
βββ [module]/
β βββ domain/
β βββ *.enum.ts # Module-specific enums
β βββ *.ts # Domain entities with methods
β βββ groups/ # Conceptual groupings
Consistent naming makes searching easier:
| Pattern | Naming Convention |
|---|---|
| Enum | PascalCase ending with type: OrderStatus, PaymentMethod |
| Grouping constant | SCREAMING_SNAKE_CASE: REFUNDABLE_STATUSES, PREMIUM_TIERS |
| Domain method | Descriptive verb: canBeCancelled(), isEligible(), calculateTotal() |