SSourav Saha
HomeExperienceSoftware DesignSystem DesignLearningBooksToolsContact
SSourav Saha

Building scalable backend systems, distributed infrastructure, and cloud-native applications.

Navigation

  • Home
  • Experience
  • Software Design
  • System Design
  • Learning

More

  • Books
  • Tools
  • Contact

Connect

  • LinkedIn
  • Email

© 2026 Sourav Saha. All rights reserved.

Built with using Next.js

Software DesignRefactoring Techniques That Actually Work
Software DesignRefactoring

Refactoring Techniques That Actually Work

Master extract method, extract class, replace conditionals, and introduce parameter objects — practical refactoring techniques with Java examples.

May 16, 20246 min read
clean-coderefactoringjava

Introduction

Refactoring is the process of changing the internal structure of code without changing its external behavior. It is not rewriting. It is not adding features. It is a disciplined, step-by-step improvement of existing code.

The golden rule: refactor only when you have tests. Without a safety net, you are just editing and praying.


1. Extract Method

The most common and most powerful refactoring technique. When a block of code can be grouped together and named, extract it.

Before

public void printInvoice(Invoice invoice) {
    System.out.println("====== INVOICE ======");
    System.out.println("Customer: " + invoice.getCustomerName());
    System.out.println("Date: " + invoice.getDate());

    double total = 0;
    for (LineItem item : invoice.getItems()) {
        double lineTotal = item.getPrice() * item.getQuantity();
        System.out.println(item.getName() + " x" + item.getQuantity() + " = " + lineTotal);
        total += lineTotal;
    }

    double tax = total * 0.18;
    double grandTotal = total + tax;
    System.out.println("Subtotal: " + total);
    System.out.println("Tax (18%): " + tax);
    System.out.println("Grand Total: " + grandTotal);
}

After

public void printInvoice(Invoice invoice) {
    printHeader(invoice);
    double subtotal = printLineItems(invoice.getItems());
    printTotals(subtotal);
}

private void printHeader(Invoice invoice) {
    System.out.println("====== INVOICE ======");
    System.out.println("Customer: " + invoice.getCustomerName());
    System.out.println("Date: " + invoice.getDate());
}

private double printLineItems(List<LineItem> items) {
    double total = 0;
    for (LineItem item : items) {
        double lineTotal = item.getPrice() * item.getQuantity();
        System.out.println(item.getName() + " x" + item.getQuantity() + " = " + lineTotal);
        total += lineTotal;
    }
    return total;
}

private void printTotals(double subtotal) {
    double tax = subtotal * 0.18;
    System.out.println("Subtotal: " + subtotal);
    System.out.println("Tax (18%): " + tax);
    System.out.println("Grand Total: " + (subtotal + tax));
}

The main method now reads like an outline. Each helper does exactly one thing.


2. Extract Class

When a class has too many responsibilities, split it into two or more focused classes.

Before

public class Employee {
    private String name;
    private String email;
    private String street;
    private String city;
    private String zipCode;
    private String country;
    private double salary;
    private String bankAccount;
    private String taxId;

    public String getFullAddress() {
        return street + ", " + city + " " + zipCode + ", " + country;
    }

    public double calculateNetSalary() {
        return salary - (salary * getTaxRate());
    }

    private double getTaxRate() { /* complex tax logic */ return 0.3; }
}

After

public class Employee {
    private String name;
    private String email;
    private Address address;
    private PayrollInfo payroll;
}

public class Address {
    private String street;
    private String city;
    private String zipCode;
    private String country;

    public String getFullAddress() {
        return street + ", " + city + " " + zipCode + ", " + country;
    }
}

public class PayrollInfo {
    private double salary;
    private String bankAccount;
    private String taxId;

    public double calculateNetSalary() {
        return salary - (salary * getTaxRate());
    }
}

3. Replace Conditionals with Polymorphism

When you see a switch statement or if-else chain that selects behavior based on type, replace it with polymorphism.

Before

public class NotificationService {
    public void send(String channel, String message, String recipient) {
        if ("EMAIL".equals(channel)) {
            EmailClient client = new EmailClient();
            client.sendEmail(recipient, "Notification", message);
        } else if ("SMS".equals(channel)) {
            SmsGateway gateway = new SmsGateway();
            gateway.sendSms(recipient, message);
        } else if ("PUSH".equals(channel)) {
            PushService push = new PushService();
            push.sendPush(recipient, message);
        } else if ("SLACK".equals(channel)) {
            SlackApi slack = new SlackApi();
            slack.postMessage(recipient, message);
        }
    }
}

After

public interface NotificationChannel {
    void send(String recipient, String message);
}

public class EmailNotification implements NotificationChannel {
    public void send(String recipient, String message) {
        new EmailClient().sendEmail(recipient, "Notification", message);
    }
}

public class SmsNotification implements NotificationChannel {
    public void send(String recipient, String message) {
        new SmsGateway().sendSms(recipient, message);
    }
}

// Adding Slack or Push = new class, no existing code changes
public class NotificationService {
    private final Map<String, NotificationChannel> channels;

    public void send(String channelName, String message, String recipient) {
        NotificationChannel channel = channels.get(channelName);
        if (channel == null) throw new IllegalArgumentException("Unknown channel: " + channelName);
        channel.send(recipient, message);
    }
}

4. Introduce Parameter Object

When multiple parameters travel together through method signatures, group them into an object.

Before

public List<Transaction> search(
    String accountId,
    LocalDate startDate,
    LocalDate endDate,
    String transactionType,
    double minAmount,
    double maxAmount,
    String sortBy,
    String sortOrder
) { ... }

After

public record TransactionSearchCriteria(
    String accountId,
    LocalDate startDate,
    LocalDate endDate,
    String transactionType,
    double minAmount,
    double maxAmount,
    String sortBy,
    String sortOrder
) {}

public List<Transaction> search(TransactionSearchCriteria criteria) { ... }

This also makes it easy to add defaults, validation, and builder patterns.


5. Inline Method

The opposite of Extract Method. When a method body is as clear as its name, inline it.

Before

public boolean isEligibleForDiscount(Customer customer) {
    return isAdult(customer);
}

private boolean isAdult(Customer customer) {
    return customer.getAge() >= 18;
}

After

public boolean isEligibleForDiscount(Customer customer) {
    return customer.getAge() >= 18;
}

Only inline when the extracted method adds no clarity. If the name communicates intent better than the code, keep it.


Interview Questions

  1. When should you refactor? Follow the Rule of Three: the first time you do something, just do it. The second time, note the duplication. The third time, refactor.

  2. What is the difference between refactoring and rewriting? Refactoring preserves behavior while improving structure. Rewriting discards the old code and starts fresh. Refactoring is incremental and safe; rewriting is risky.

  3. How do you refactor legacy code without tests? Start by writing "characterization tests" — tests that document the current behavior (even if buggy). Then refactor with confidence.


Common Mistakes

  • Big-bang refactoring: Trying to refactor everything at once. Always do it in small, tested steps.
  • Refactoring without a clear goal: Know why you are refactoring. "Make it cleaner" is too vague. "Extract the pricing logic so it can be unit tested" is specific.
  • Skipping the commit: Commit after each successful refactoring step. If something breaks, you can revert to a working state.

Summary

TechniqueWhen to Use
Extract MethodLong method, repeated code block
Extract ClassClass with too many responsibilities
Replace ConditionalsType-based if-else / switch chains
Introduce Parameter Object4+ parameters traveling together
Inline MethodWrapper adds no clarity

Related Articles

  • Clean Code Principles
  • Recognizing Bad Code
  • The SOLID Principles Explained
PreviousRecognizing Bad CodeNextThe SOLID Principles Explained with Java Examples