Loading content...
Refactoring patterns are named, repeatable transformations that improve code structure while preserving behavior. Having a shared vocabulary of these patterns enables precise communication during code reviews, pair programming sessions, and LLD interviews.
Martin Fowler's Refactoring catalog documents over 60 patterns, but most real-world refactoring draws from a smaller set of frequently used transformations. This page covers the essential patterns that address the most common code smells and structural problems.
Each pattern follows a predictable structure:
By the end of this page, you will have internalized the most important refactoring patterns, understand their mechanical application, and be able to identify which pattern addresses which code smell. This knowledge is directly applicable in LLD interviews where demonstrating clean code practices carries significant weight.
Extract Method is the single most important and frequently used refactoring pattern. It takes a code fragment and transforms it into a method whose name explains its purpose.
Motivation:
The fundamental principle: A method should do one thing at one level of abstraction. When you can name a code fragment, you should extract it into a method with that name.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
// BEFORE: Long method with mixed abstraction levelsclass OrderProcessor { processOrder(order: Order): void { // Validate order if (!order.items || order.items.length === 0) { throw new Error('Order must have at least one item'); } if (!order.customer || !order.customer.id) { throw new Error('Order must have a valid customer'); } for (const item of order.items) { if (!item.productId || item.quantity <= 0) { throw new Error('Invalid order item'); } } // Calculate total let subtotal = 0; for (const item of order.items) { subtotal += item.price * item.quantity; } const tax = subtotal * 0.08; const shipping = subtotal > 100 ? 0 : 10; const total = subtotal + tax + shipping; // Create invoice const invoice = { orderId: order.id, customerId: order.customer.id, items: order.items, subtotal, tax, shipping, total, createdAt: new Date() }; this.invoiceRepository.save(invoice); this.emailService.sendInvoice(order.customer.email, invoice); }} // AFTER: Extracted methods with clear responsibilitiesclass OrderProcessor { processOrder(order: Order): void { this.validateOrder(order); const totals = this.calculateTotals(order); const invoice = this.createInvoice(order, totals); this.deliverInvoice(order.customer, invoice); } private validateOrder(order: Order): void { this.validateOrderHasItems(order); this.validateOrderHasCustomer(order); this.validateOrderItems(order.items); } private validateOrderHasItems(order: Order): void { if (!order.items || order.items.length === 0) { throw new Error('Order must have at least one item'); } } private validateOrderHasCustomer(order: Order): void { if (!order.customer || !order.customer.id) { throw new Error('Order must have a valid customer'); } } private validateOrderItems(items: OrderItem[]): void { for (const item of items) { if (!item.productId || item.quantity <= 0) { throw new Error('Invalid order item'); } } } private calculateTotals(order: Order): OrderTotals { const subtotal = this.calculateSubtotal(order.items); const tax = this.calculateTax(subtotal); const shipping = this.calculateShipping(subtotal); return { subtotal, tax, shipping, total: subtotal + tax + shipping }; } private calculateSubtotal(items: OrderItem[]): number { return items.reduce((sum, item) => sum + item.price * item.quantity, 0); } private calculateTax(subtotal: number): number { return subtotal * this.TAX_RATE; } private calculateShipping(subtotal: number): number { return subtotal > this.FREE_SHIPPING_THRESHOLD ? 0 : this.SHIPPING_COST; } private createInvoice(order: Order, totals: OrderTotals): Invoice { return { orderId: order.id, customerId: order.customer.id, items: order.items, ...totals, createdAt: new Date() }; } private deliverInvoice(customer: Customer, invoice: Invoice): void { this.invoiceRepository.save(invoice); this.emailService.sendInvoice(customer.email, invoice); }}Extract Class creates a new class by moving a subset of fields and methods from an existing class. This is the primary remedy for the Large Class smell and violations of the Single Responsibility Principle.
Motivation:
Signs you need Extract Class:
billingAddress, billingCity, billingZip)123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
// BEFORE: Class with multiple responsibilitiesclass Employee { private id: string; private name: string; private email: string; private department: string; // Address-related fields - cohesive group private street: string; private city: string; private state: string; private zipCode: string; private country: string; // Phone-related fields - another cohesive group private areaCode: string; private phoneNumber: string; private extension: string; getFullAddress(): string { return `${this.street}\n${this.city}, ${this.state} ${this.zipCode}\n${this.country}`; } getFormattedPhone(): string { let result = `(${this.areaCode}) ${this.phoneNumber}`; if (this.extension) { result += ` x${this.extension}`; } return result; } validateAddress(): boolean { return this.street?.length > 0 && this.city?.length > 0 && this.state?.length === 2 && this.zipCode?.match(/^\d{5}(-\d{4})?$/) !== null; } // ... many more methods mixing employee logic with address/phone logic} // AFTER: Cohesive classes with single responsibilitiesclass Address { constructor( private street: string, private city: string, private state: string, private zipCode: string, private country: string ) {} getFullAddress(): string { return `${this.street}\n${this.city}, ${this.state} ${this.zipCode}\n${this.country}`; } validate(): boolean { return this.street?.length > 0 && this.city?.length > 0 && this.state?.length === 2 && this.zipCode?.match(/^\d{5}(-\d{4})?$/) !== null; } // Address can now evolve independently getMailingLabel(): string { /* ... */ } distanceTo(other: Address): number { /* ... */ }} class PhoneNumber { constructor( private areaCode: string, private number: string, private extension?: string ) {} format(): string { let result = `(${this.areaCode}) ${this.number}`; if (this.extension) { result += ` x${this.extension}`; } return result; } validate(): boolean { return this.areaCode?.match(/^\d{3}$/) !== null && this.number?.match(/^\d{3}-\d{4}$/) !== null; } // PhoneNumber can now evolve independently isInternational(): boolean { /* ... */ }} class Employee { constructor( private id: string, private name: string, private email: string, private department: string, private address: Address, private phone: PhoneNumber ) {} // Employee is now focused on employee-specific behavior getContactInfo(): string { return `${this.name}\n${this.address.getFullAddress()}\n${this.phone.format()}`; } isContactValid(): boolean { return this.address.validate() && this.phone.validate(); }}Look for fields that change together, methods that use the same subset of fields, and data that has meaning independent of the original class. Address and PhoneNumber are meaningful concepts on their own—they don't require Employee to make sense. This semantic independence signals extraction opportunity.
Move Method and Move Field relocate functionality to the class where it belongs. These patterns address Feature Envy—when a method uses more data from another class than from its own.
Motivation:
The guiding principle: Things that change for the same reason should be together. Things that change for different reasons should be apart.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
// BEFORE: Method envies data from another classclass Account { private balance: number; private overdraftLimit: number; private accountType: AccountType; getBalance(): number { return this.balance; } getOverdraftLimit(): number { return this.overdraftLimit; } getAccountType(): AccountType { return this.accountType; }} class BankReportService { // This method uses Account data more than its own calculateOverdraftFee(account: Account, amount: number): number { if (account.getAccountType() === AccountType.PREMIUM) { return 0; // Premium accounts have no overdraft fees } const overdraftAmount = amount - account.getBalance(); if (overdraftAmount <= 0) { return 0; // Not actually overdrawing } if (overdraftAmount > account.getOverdraftLimit()) { return 35.00; // Over limit: maximum fee } return overdraftAmount * 0.05; // 5% of overdraft amount }} // AFTER: Method moved to where the data livesclass Account { private balance: number; private overdraftLimit: number; private accountType: AccountType; // Method now lives with its data calculateOverdraftFee(withdrawalAmount: number): number { if (this.accountType === AccountType.PREMIUM) { return 0; } const overdraftAmount = withdrawalAmount - this.balance; if (overdraftAmount <= 0) { return 0; } if (overdraftAmount > this.overdraftLimit) { return Account.MAXIMUM_OVERDRAFT_FEE; } return overdraftAmount * Account.OVERDRAFT_FEE_RATE; } private static readonly MAXIMUM_OVERDRAFT_FEE = 35.00; private static readonly OVERDRAFT_FEE_RATE = 0.05;} class BankReportService { // Caller becomes simpler, delegating to Account generateOverdraftReport(account: Account, amount: number): Report { const fee = account.calculateOverdraftFee(amount); // ... reporting logic that is actually this service's concern }}Move Field mechanics:
Move Field follows similar logic but for data rather than behavior:
Fields should live where their methods live. If you've moved methods to a class, the fields they use often should follow.
Move Method often transforms code from 'Ask' style (querying data then computing) to 'Tell' style (telling an object to do something). The latter typically results in better encapsulation and clearer responsibilities.
Renaming is often underestimated as a refactoring technique, but it's one of the most powerful. Good names reveal intent, reduce the need for comments, and make code self-documenting.
Motivation:
The core principle: Code is read far more often than it's written. Investing in good names pays dividends on every future read.
| Before | After | Why Better |
|---|---|---|
data | userPreferences | Reveals what data represents |
process() | validateAndEnrichOrder() | Describes what processing does |
flag | isEligibleForDiscount | Boolean reads naturally in conditionals |
tmp | remainingBalance | Domain term instead of implementation detail |
Manager | OrderRepository | Specific responsibility instead of vague role |
doIt() | submitPayment() | Action is clear from name |
list | pendingOrders | Contents and status are visible |
x, y | latitude, longitude | Domain semantics, not arbitrary labels |
123456789101112131415161718192021222324252627282930313233343536373839
// BEFORE: Cryptic names require mental translationfunction calc(a: number[], b: number): number { let r = 0; for (let i = 0; i < a.length; i++) { if (a[i] > b) { r += a[i]; } } return r;} // AFTER: Names reveal intentfunction sumValuesAboveThreshold(values: number[], threshold: number): number { let total = 0; for (const value of values) { if (value > threshold) { total += value; } } return total;} // EVEN BETTER: Using modern constructs that name the patternfunction sumValuesAboveThreshold(values: number[], threshold: number): number { return values .filter(value => value > threshold) .reduce((sum, value) => sum + value, 0);} // CLASS RENAMING EXAMPLE// BEFORE: What does this class do?class Manager { handle(input: unknown): unknown { /* ... */ }} // AFTER: Clear responsibilityclass OrderValidationService { validateOrder(order: Order): ValidationResult { /* ... */ }}Methods: Use verb phrases that describe the action (validate, calculate, fetch, send). Classes: Use nouns that describe the abstraction (Order, Customer, PaymentGateway). Variables: Use nouns describing content (pendingOrders, currentUser). Booleans: Use 'is/has/can/should' prefixes (isValid, hasPermission, canRetry).
Replace Conditional with Polymorphism transforms complex switch statements or type-checking conditionals into polymorphic method calls. This is one of the most powerful object-oriented refactorings.
Motivation:
The principle: When behavior varies by type, let the type itself define the behavior through polymorphism.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
// BEFORE: Switch statement on type codeenum EmployeeType { ENGINEER = 'ENGINEER', MANAGER = 'MANAGER', SALESPERSON = 'SALESPERSON'} class Employee { type: EmployeeType; baseSalary: number; commissionRate?: number; teamSize?: number; calculatePay(): number { switch (this.type) { case EmployeeType.ENGINEER: return this.baseSalary; case EmployeeType.MANAGER: return this.baseSalary + (this.teamSize! * 100); case EmployeeType.SALESPERSON: return this.baseSalary * (1 + this.commissionRate!); default: throw new Error(`Unknown employee type: ${this.type}`); } } getTitle(): string { switch (this.type) { case EmployeeType.ENGINEER: return 'Software Engineer'; case EmployeeType.MANAGER: return 'Engineering Manager'; case EmployeeType.SALESPERSON: return 'Sales Representative'; default: throw new Error(`Unknown employee type: ${this.type}`); } } // Every new employee type requires changes to EVERY switch statement} // AFTER: Polymorphic classesabstract class Employee { constructor(protected baseSalary: number) {} abstract calculatePay(): number; abstract getTitle(): string;} class Engineer extends Employee { calculatePay(): number { return this.baseSalary; } getTitle(): string { return 'Software Engineer'; }} class Manager extends Employee { constructor(baseSalary: number, private teamSize: number) { super(baseSalary); } calculatePay(): number { return this.baseSalary + this.teamSize * Manager.TEAM_BONUS_PER_MEMBER; } getTitle(): string { return 'Engineering Manager'; } private static readonly TEAM_BONUS_PER_MEMBER = 100;} class Salesperson extends Employee { constructor(baseSalary: number, private commissionRate: number) { super(baseSalary); } calculatePay(): number { return this.baseSalary * (1 + this.commissionRate); } getTitle(): string { return 'Sales Representative'; }} // Adding a new employee type: just add a new classclass Intern extends Employee { calculatePay(): number { return this.baseSalary * 0.5; // Interns get half } getTitle(): string { return 'Engineering Intern'; }} // Factory creates the right typeclass EmployeeFactory { static create(type: EmployeeType, ...args: any[]): Employee { switch (type) { case EmployeeType.ENGINEER: return new Engineer(args[0]); case EmployeeType.MANAGER: return new Manager(args[0], args[1]); case EmployeeType.SALESPERSON: return new Salesperson(args[0], args[1]); } }}Not all switch statements should become polymorphism. Keep conditionals when: (1) the behavior doesn't justify a class hierarchy, (2) the types are external and can't be extended, (3) there's only one conditional location, or (4) the domain is truly procedural. Apply polymorphism when the same conditional appears in multiple places.
Introduce Parameter Object replaces a long parameter list with a single object that encapsulates those parameters. This pattern reduces parameter coupling and often reveals new abstractions.
Motivation:
The insight: If the same parameters always travel together, they probably represent a concept that deserves its own name.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
// BEFORE: Long parameter lists repeated across methodsclass ReportGenerator { generateSalesReport( startDate: Date, endDate: Date, region: string, includeReturns: boolean, groupByCategory: boolean, format: ReportFormat ): Report { // ... } generateInventoryReport( startDate: Date, endDate: Date, region: string, includeInTransit: boolean, groupByWarehouse: boolean, format: ReportFormat ): Report { // ... } // What does a call look like? // generator.generateSalesReport(start, end, 'NA', true, false, 'PDF') // Which boolean is which? Easy to get wrong.} // AFTER: Parameter object encapsulates related datainterface DateRange { startDate: Date; endDate: Date;} interface SalesReportOptions { dateRange: DateRange; region: string; includeReturns: boolean; groupByCategory: boolean; format: ReportFormat;} interface InventoryReportOptions { dateRange: DateRange; region: string; includeInTransit: boolean; groupByWarehouse: boolean; format: ReportFormat;} class ReportGenerator { generateSalesReport(options: SalesReportOptions): Report { // Clearer access: options.dateRange.startDate // Easy to add new options without changing signature } generateInventoryReport(options: InventoryReportOptions): Report { // ... }} // Call sites are now self-documentingconst report = generator.generateSalesReport({ dateRange: { startDate: lastMonth, endDate: today }, region: 'NA', includeReturns: true, groupByCategory: false, format: 'PDF'}); // DateRange can grow behavior of its ownclass DateRange { constructor( public readonly startDate: Date, public readonly endDate: Date ) { this.validate(); } private validate(): void { if (this.startDate > this.endDate) { throw new Error('Start date must be before end date'); } } getDurationInDays(): number { const diffTime = this.endDate.getTime() - this.startDate.getTime(); return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); } contains(date: Date): boolean { return date >= this.startDate && date <= this.endDate; } overlaps(other: DateRange): boolean { return this.startDate <= other.endDate && other.startDate <= this.endDate; }}Often, introducing a parameter object reveals a missing domain concept. 'DateRange' is not just a convenience—it's a genuine business concept that deserves its own abstraction. The refactoring surfaces implicit concepts and makes them explicit.
Extract Interface creates a new interface from the existing public methods of a class. This enables polymorphism without inheritance and supports dependency inversion.
Motivation:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
// BEFORE: Concrete class makes testing difficultclass PaymentGateway { async chargeCard( cardNumber: string, amount: number, currency: string ): Promise<PaymentResult> { // Real HTTP calls to payment provider const response = await fetch('https://payment.provider.com/charge', { method: 'POST', body: JSON.stringify({ cardNumber, amount, currency }), headers: { 'Authorization': `Bearer ${this.apiKey}` } }); return response.json(); } async refund(transactionId: string, amount: number): Promise<RefundResult> { // Real HTTP calls to payment provider }} class OrderService { constructor(private gateway: PaymentGateway) {} // Hard dependency async completeOrder(order: Order): Promise<void> { // This is hard to test - always makes real API calls const result = await this.gateway.chargeCard( order.paymentCardNumber, order.total, 'USD' ); // ... }} // AFTER: Interface allows substitutioninterface PaymentProcessor { chargeCard( cardNumber: string, amount: number, currency: string ): Promise<PaymentResult>; refund(transactionId: string, amount: number): Promise<RefundResult>;} // Real implementationclass StripePaymentGateway implements PaymentProcessor { constructor(private apiKey: string) {} async chargeCard( cardNumber: string, amount: number, currency: string ): Promise<PaymentResult> { // Real Stripe API calls } async refund(transactionId: string, amount: number): Promise<RefundResult> { // Real Stripe API calls }} // Test implementationclass FakePaymentGateway implements PaymentProcessor { private charges: { cardNumber: string; amount: number }[] = []; async chargeCard( cardNumber: string, amount: number, currency: string ): Promise<PaymentResult> { this.charges.push({ cardNumber, amount }); return { success: true, transactionId: 'fake-txn-123' }; } async refund(transactionId: string, amount: number): Promise<RefundResult> { return { success: true }; } // Test helper methods getCharges() { return this.charges; } simulateFailure() { /* ... */ }} class OrderService { constructor(private paymentProcessor: PaymentProcessor) {} // Depends on interface async completeOrder(order: Order): Promise<void> { const result = await this.paymentProcessor.chargeCard( order.paymentCardNumber, order.total, 'USD' ); // Now testable without real API calls }} // In tests:const fakeGateway = new FakePaymentGateway();const orderService = new OrderService(fakeGateway); await orderService.completeOrder(testOrder); expect(fakeGateway.getCharges()).toHaveLength(1);expect(fakeGateway.getCharges()[0].amount).toBe(testOrder.total);When extracting interfaces, include only the methods that clients actually need. If different clients use different subsets of a class's methods, create separate, focused interfaces. This aligns with the Interface Segregation Principle.
Real-world refactoring typically involves composing multiple patterns into a coherent transformation. Each pattern is a small, safe step; together they achieve significant improvements.
Common compositions:
| Problem | Composed Solution | Patterns Used |
|---|---|---|
| God Class with mixed responsibilities | Split into focused classes | Extract Method → Extract Class → Move Field → Move Method |
| Repeated conditionals on type | Polymorphic hierarchy | Extract Interface → Extract Class (per type) → Move Method → Replace Conditional with Polymorphism |
| Untestable class with dependencies | Testable design | Extract Interface → Inject Dependencies → Extract Methods (for complex logic) |
| Data class with getters everywhere | Rich domain object | Move Method (logic to data class) → Encapsulate Collection → Rename (intention-revealing) |
| Primitive obsession | Value objects | Extract Class → Introduce Parameter Object → Replace Primitive with Object |
The key principle: Each individual step should leave the code in a working, tested state. Never be more than a few minutes away from green tests. If you make a mistake, you can easily revert to the last working state.
This discipline—small steps with frequent verification—is what distinguishes refactoring from 'hacking until it works'. The former is controlled and safe; the latter is risky and often incomplete.
You now have a working vocabulary of essential refactoring patterns: Extract Method, Extract Class, Move Method, Rename, Replace Conditional with Polymorphism, Introduce Parameter Object, Extract Interface, and how to compose them. Next, we'll examine how to maintain behavior correctness throughout the refactoring process.