Software Dev

Complete Java Refactoring Techniques: 20+ Patterns to Transform Legacy Code in 2026

Legacy Java codebases accumulate technical debt silently — until a simple feature takes weeks. This comprehensive guide covers 20+ refactoring techniques from Martin Fowler's catalog, each illustrated with Java & Spring Boot before/after examples so you can clean up real production code starting today.

Md Sanwar Hossain April 9, 2026 28 min read Java Refactoring
Java refactoring techniques for legacy code transformation in 2026

TL;DR — Key Takeaways

  • Refactoring ≠ rewriting — small, behaviour-preserving transformations backed by tests
  • Extract Method is the most-used refactoring; apply it whenever a method exceeds ~20 lines
  • Replace Conditional with Polymorphism eliminates brittle switch/if-else chains using the Strategy pattern
  • Introduce Parameter Object reduces long parameter lists into cohesive DTOs
  • Always cover code with tests before refactoring — IDE automated refactorings are your safety net

Table of Contents

  1. What is Refactoring?
  2. Composing Methods
  3. Moving Features Between Objects
  4. Organizing Data
  5. Simplifying Conditional Expressions
  6. Making Method Calls Simpler
  7. Dealing with Generalization
  8. Spring Boot Legacy Case Study
  9. IDE Tooling for Java Refactoring

1. What is Refactoring?

Martin Fowler defines refactoring as "a disciplined technique for restructuring an existing body of code, altering its internal structure without changing its external behaviour." The operative word is disciplined — not hacking, not rewriting, not fixing bugs. Each refactoring step is a small, verifiable transformation proven safe by an automated test suite.

Refactoring vs Rewriting

Dimension Refactoring Rewriting
Risk Low — incremental High — big-bang
Existing behaviour Preserved Often broken / re-discovered
Business continuity Feature delivery continues Delivery freezes
When to use Continuously, alongside features Only when architecture is fundamentally broken

When to Refactor — Code Smell Triggers

The Boy Scout Rule

Robert C. Martin popularised the Boy Scout Rule for software: "Always leave the code cleaner than you found it." You don't need a dedicated refactoring sprint. Every time you touch a file to add a feature or fix a bug, apply one small refactoring. Over months this transforms a codebase organically without stopping feature delivery.

Java refactoring techniques overview diagram — Extract Method, Move Method, Replace Conditional with Polymorphism
Java Refactoring Techniques — overview of 20+ patterns from Martin Fowler's catalog. Source: mdsanwarhossain.me

2. Composing Methods

Composing methods is about packaging code into well-named, single-purpose units. Most refactoring activity happens here. A well-composed method tells a story in domain language, delegating implementation details to descriptively named helper methods.

Extract Method

When: A code fragment can be grouped together and given a name that explains its intent. Pull it into a separate method. This is the single most valuable refactoring in Fowler's entire catalog.

// BEFORE — 40-line method, low cohesion
@Service
public class OrderService {
    public void processOrder(Order order) {
        // validate
        if (order.getItems() == null || order.getItems().isEmpty()) {
            throw new IllegalArgumentException("Order has no items");
        }
        if (order.getCustomerId() == null) {
            throw new IllegalArgumentException("Customer ID is required");
        }
        // calculate total
        BigDecimal total = BigDecimal.ZERO;
        for (OrderItem item : order.getItems()) {
            total = total.add(item.getPrice().multiply(
                BigDecimal.valueOf(item.getQuantity())));
        }
        if (order.getCouponCode() != null) {
            total = total.multiply(BigDecimal.valueOf(0.9));
        }
        order.setTotal(total);
        // persist and notify
        orderRepository.save(order);
        emailService.sendConfirmation(order);
    }
}
// AFTER — Extract Method applied three times
@Service
public class OrderService {
    public void processOrder(Order order) {
        validateOrder(order);
        order.setTotal(calculateTotal(order));
        orderRepository.save(order);
        emailService.sendConfirmation(order);
    }

    private void validateOrder(Order order) {
        if (order.getItems() == null || order.getItems().isEmpty()) {
            throw new IllegalArgumentException("Order has no items");
        }
        if (order.getCustomerId() == null) {
            throw new IllegalArgumentException("Customer ID is required");
        }
    }

    private BigDecimal calculateTotal(Order order) {
        BigDecimal total = order.getItems().stream()
            .map(i -> i.getPrice().multiply(BigDecimal.valueOf(i.getQuantity())))
            .reduce(BigDecimal.ZERO, BigDecimal::add);
        return order.getCouponCode() != null
            ? total.multiply(BigDecimal.valueOf(0.9))
            : total;
    }
}

Inline Method

When: A method body is just as clear as the method name itself, or the method is only called in one place and adds no clarity. Inline it to reduce indirection.

// BEFORE — unnecessary delegation
public boolean isEligibleForDiscount(Customer customer) {
    return moreThanFiveOrders(customer);
}
private boolean moreThanFiveOrders(Customer customer) {
    return customer.getOrderCount() > 5;
}

// AFTER — Inline Method
public boolean isEligibleForDiscount(Customer customer) {
    return customer.getOrderCount() > 5;
}

Extract Variable

When: A complex expression is hard to understand at a glance. Extract it into a named local variable that documents its purpose.

// BEFORE — opaque expression
if (order.getItems().size() > 0 && customer.getAge() >= 18
        && !customer.isBanned() && inventory.hasStock(order)) { ... }

// AFTER — Extract Variable
boolean hasItems          = order.getItems().size() > 0;
boolean isAdult           = customer.getAge() >= 18;
boolean isActiveCustomer  = !customer.isBanned();
boolean isInStock         = inventory.hasStock(order);

if (hasItems && isAdult && isActiveCustomer && isInStock) { ... }

Split Temporary Variable

When: A temporary variable is assigned more than once and serves two different purposes — it violates the single-responsibility principle for variables. Split it into two clearly named variables.

// BEFORE — 'temp' plays two roles
double temp = order.getBasePrice();
temp = temp * (1 - getDiscountRate(order));   // discount price
temp = temp * (1 + TAX_RATE);                  // taxed price

// AFTER — Split Temporary Variable
double discountedPrice = order.getBasePrice() * (1 - getDiscountRate(order));
double finalPrice      = discountedPrice * (1 + TAX_RATE);

3. Moving Features Between Objects

Good object-oriented design is about putting behaviour where the data lives. These refactorings move methods and fields to the classes that use them most, reducing coupling and increasing cohesion.

Move Method

When: A method uses more data from another class than from its own (Feature Envy smell). Move it to the class it "envies".

// BEFORE — OrderService computing shipping; it uses ShippingAddress data only
@Service
public class OrderService {
    public BigDecimal calculateShipping(Order order) {
        ShippingAddress addr = order.getShippingAddress();
        if ("EXPRESS".equals(addr.getShippingType())) return BigDecimal.valueOf(15.00);
        if (addr.getCountryCode().equals("BD"))       return BigDecimal.valueOf(2.50);
        return BigDecimal.valueOf(10.00);
    }
}

// AFTER — Move Method to ShippingAddress where it belongs
public class ShippingAddress {
    public BigDecimal calculateShippingCost() {
        if ("EXPRESS".equals(this.shippingType)) return BigDecimal.valueOf(15.00);
        if ("BD".equals(this.countryCode))        return BigDecimal.valueOf(2.50);
        return BigDecimal.valueOf(10.00);
    }
}
// OrderService now delegates:
BigDecimal shipping = order.getShippingAddress().calculateShippingCost();

Extract Class

When: One class does the work of two (God Class smell). Extract a cohesive subset of fields and methods into a new class.

// BEFORE — God Class mixing order logic and payment logic
public class OrderService {
    public void placeOrder(Order order) { /* order logic */ }
    public void refundOrder(Long orderId) { /* payment logic */ }
    public void chargeCreditCard(PaymentDetails pd) { /* payment logic */ }
    public void processPayPal(PayPalDetails pd) { /* payment logic */ }
}

// AFTER — Extract Class: PaymentService handles all payment concerns
@Service
public class OrderService {
    private final PaymentService paymentService;
    public void placeOrder(Order order) {
        // order logic only
        paymentService.charge(order.getPaymentDetails());
    }
}

@Service
public class PaymentService {
    public void charge(PaymentDetails pd) { /* ... */ }
    public void refund(Long orderId)      { /* ... */ }
    public void processPayPal(PayPalDetails pd) { /* ... */ }
}

Hide Delegate (Law of Demeter)

When: Client code calls into chains like order.getCustomer().getAddress().getCity(). Hide the chain by adding a delegation method to Order.

// BEFORE — violates Law of Demeter
String city = order.getCustomer().getAddress().getCity();

// AFTER — Hide Delegate on Order
public class Order {
    public String getCustomerCity() {
        return customer.getAddress().getCity();
    }
}
// Client:
String city = order.getCustomerCity();

4. Organizing Data

Data organisation refactorings turn raw primitives and unstructured collections into rich domain objects with proper encapsulation. They reduce Primitive Obsession and make domain rules explicit.

Replace Data Value with Object

When: A primitive type (String, int) carries domain rules — like a valid email format or a non-negative price. Replace it with a dedicated value class that enforces those rules at the boundary.

// BEFORE — String carries email semantics; validation is scattered
public class Customer {
    private String email;   // could be "not-an-email" with no guard
}

// AFTER — Replace Data Value with Object
public final class Email {
    private final String value;
    public Email(String value) {
        if (value == null || !value.matches("^[^@]+@[^@]+\\.[^@]+$"))
            throw new IllegalArgumentException("Invalid email: " + value);
        this.value = value.toLowerCase();
    }
    public String getValue() { return value; }
    @Override public String toString() { return value; }
}

public class Customer {
    private Email email;   // invariant: always a valid email
}

Encapsulate Field

When: A field is public. Make it private and provide accessor methods so the class controls all access to its state.

// BEFORE — public field, anyone can set any value
public class Product {
    public BigDecimal price;   // no guard possible
}

// AFTER — Encapsulate Field
public class Product {
    private BigDecimal price;
    public BigDecimal getPrice() { return price; }
    public void setPrice(BigDecimal price) {
        if (price.compareTo(BigDecimal.ZERO) < 0)
            throw new IllegalArgumentException("Price cannot be negative");
        this.price = price;
    }
}

Introduce Parameter Object

When: Multiple methods share the same group of parameters — a classic Long Parameter List smell. Group them into a data transfer object.

// BEFORE — 5-param list repeated in every method signature
public void registerCustomer(String name, String email,
                             String phone, String countryCode,
                             String referralCode) { ... }
public void updateCustomer(Long id, String name, String email,
                           String phone, String countryCode,
                           String referralCode) { ... }

// AFTER — Introduce Parameter Object: CustomerDto
public record CustomerDto(
    String name,
    String email,
    String phone,
    String countryCode,
    String referralCode
) {}

public void registerCustomer(CustomerDto dto) { ... }
public void updateCustomer(Long id, CustomerDto dto) { ... }

Replace Array with Object

When: You use an array (or Object[]) as a tuple where each index has a specific meaning. Replace it with a proper class.

// BEFORE — index 0 = name, index 1 = score (fragile)
String[] result = new String[]{"Alice", "95"};
System.out.println("Winner: " + result[0] + " Score: " + result[1]);

// AFTER — Replace Array with Object
public record QuizResult(String playerName, int score) {}

QuizResult result = new QuizResult("Alice", 95);
System.out.println("Winner: " + result.playerName() + " Score: " + result.score());

5. Simplifying Conditional Expressions

Complex conditionals are one of the top sources of bugs in Java codebases. These refactorings replace opaque boolean logic with expressive, polymorphic designs.

Decompose Conditional

When: The condition and the then/else branches each deserve their own named method.

// BEFORE — complex condition inline
if (date.before(SUMMER_START) || date.after(SUMMER_END)) {
    charge = quantity * winterRate + winterServiceCharge;
} else {
    charge = quantity * summerRate;
}

// AFTER — Decompose Conditional
if (isWinter(date)) {
    charge = winterCharge(quantity);
} else {
    charge = summerCharge(quantity);
}
private boolean isWinter(LocalDate d) { return d.isBefore(SUMMER_START) || d.isAfter(SUMMER_END); }
private BigDecimal winterCharge(int qty) { return qty * winterRate + winterServiceCharge; }
private BigDecimal summerCharge(int qty) { return qty * summerRate; }

Replace Conditional with Polymorphism

When: You have a switch or if-else chain that varies behaviour based on a type code. Replace with a polymorphic class hierarchy (Strategy pattern).

// BEFORE — switch on OrderType grows with every new type
public BigDecimal getDiscount(Order order) {
    switch (order.getType()) {
        case "STANDARD":  return BigDecimal.ZERO;
        case "PREMIUM":   return order.getTotal().multiply(new BigDecimal("0.10"));
        case "VIP":       return order.getTotal().multiply(new BigDecimal("0.20"));
        default: throw new IllegalArgumentException("Unknown type: " + order.getType());
    }
}

// AFTER — Replace Conditional with Polymorphism (Strategy)
public interface DiscountStrategy {
    BigDecimal calculate(Order order);
}
public class StandardDiscount implements DiscountStrategy {
    public BigDecimal calculate(Order order) { return BigDecimal.ZERO; }
}
public class PremiumDiscount implements DiscountStrategy {
    public BigDecimal calculate(Order order) {
        return order.getTotal().multiply(new BigDecimal("0.10"));
    }
}
public class VipDiscount implements DiscountStrategy {
    public BigDecimal calculate(Order order) {
        return order.getTotal().multiply(new BigDecimal("0.20"));
    }
}
// OrderService
public BigDecimal getDiscount(Order order) {
    return order.getDiscountStrategy().calculate(order);
}

Introduce Null Object

When: You repeatedly check for null before calling a method. Introduce a Null Object that provides safe default behaviour, eliminating the checks entirely.

// BEFORE — null checks scattered everywhere
if (customer.getRewardProgram() != null) {
    customer.getRewardProgram().addPoints(order.getTotal());
}

// AFTER — Introduce Null Object
public class NullRewardProgram implements RewardProgram {
    @Override
    public void addPoints(BigDecimal amount) { /* no-op */ }
    @Override
    public int getPoints() { return 0; }
}
// Customer always returns a non-null RewardProgram:
public RewardProgram getRewardProgram() {
    return rewardProgram != null ? rewardProgram : new NullRewardProgram();
}
// Client — no null check needed:
customer.getRewardProgram().addPoints(order.getTotal());

Consolidate Conditional Expression

When: Multiple conditionals that produce the same result can be combined into a single expression — and then extracted into a named method.

// BEFORE — three separate ifs, same action
if (customer.isSuspended())    return BigDecimal.ZERO;
if (customer.isOnProbation())  return BigDecimal.ZERO;
if (!customer.isVerified())    return BigDecimal.ZERO;
return calculateFullDiscount(customer);

// AFTER — Consolidate Conditional Expression + Extract Method
if (isIneligibleForDiscount(customer)) return BigDecimal.ZERO;
return calculateFullDiscount(customer);

private boolean isIneligibleForDiscount(Customer c) {
    return c.isSuspended() || c.isOnProbation() || !c.isVerified();
}

6. Making Method Calls Simpler

These refactorings clean up method signatures and enforce the Command-Query Separation (CQS) principle: a method either changes state or returns data, but never both.

Rename Method

When: The method name doesn't clearly express what it does. Use IntelliJ's Shift+F6 for a safe, project-wide rename.

// BEFORE
public List<Order> getOrders(Long id) { ... }

// AFTER — name reveals intent
public List<Order> findOrdersByCustomerId(Long customerId) { ... }

Separate Query from Modifier (CQS)

When: A method both returns a value and changes object state. Split it into a separate query (no side effect) and a command (no return value).

// BEFORE — violates CQS: queries AND modifies
public Order finalizeAndGetOrder(Long id) {
    Order order = orderRepository.findById(id).orElseThrow();
    order.setStatus(OrderStatus.FINALIZED);   // side effect!
    orderRepository.save(order);
    return order;
}

// AFTER — Separate Query from Modifier
public void finalizeOrder(Long id) {
    Order order = orderRepository.findById(id).orElseThrow();
    order.setStatus(OrderStatus.FINALIZED);
    orderRepository.save(order);
}
public Order getOrder(Long id) {
    return orderRepository.findById(id).orElseThrow();
}

Parameterize Method

When: Several methods do similar things but with different literal values. Combine them into one method with a parameter.

// BEFORE — three near-identical methods
public void applyTenPercentDiscount(Order o)    { o.setDiscount(0.10); }
public void applyFifteenPercentDiscount(Order o) { o.setDiscount(0.15); }
public void applyTwentyPercentDiscount(Order o)  { o.setDiscount(0.20); }

// AFTER — Parameterize Method
public void applyDiscount(Order order, double rate) {
    order.setDiscount(rate);
}

7. Dealing with Generalization

Inheritance hierarchies tend to get messy over time. These refactorings reorganise class hierarchies so behaviour lives at the right level of abstraction.

Pull Up Method / Pull Up Field

When: Identical or very similar methods exist in sibling subclasses. Move them to the common superclass to eliminate duplication.

// BEFORE — same getName() in both subclasses
public class DomesticShipment extends Shipment {
    public String getName() { return "Domestic Shipment"; }
}
public class InternationalShipment extends Shipment {
    public String getName() { return "International Shipment"; }
}

// AFTER — Pull Up Field (using abstract method for variation)
public abstract class Shipment {
    public abstract String getName();   // pull up as abstract method
}
// Or if the method is truly identical, pull up as a concrete method in Shipment.

Push Down Method

When: A method on a superclass is only relevant to one subclass. Push it down to that subclass to keep the superclass clean.

// BEFORE — generateCustomsForm only applies to InternationalShipment
public abstract class Shipment {
    public CustomsForm generateCustomsForm() { /* only used by International */ }
}

// AFTER — Push Down Method
public class InternationalShipment extends Shipment {
    public CustomsForm generateCustomsForm() { /* ... */ }
}
// Superclass Shipment no longer has this method.

Extract Interface

When: Clients only use a subset of a class's methods. Extract an interface representing that subset to decouple clients from the full implementation.

// BEFORE — OrderController depends on the full OrderService
@RestController
public class OrderController {
    private final OrderService orderService;
    // only calls findById and findAll — doesn't use processOrder, cancel, refund...
}

// AFTER — Extract Interface
public interface OrderQueryPort {
    Order findById(Long id);
    List<Order> findAll(Pageable pageable);
}
@Service
public class OrderService implements OrderQueryPort { /* ... */ }

@RestController
public class OrderController {
    private final OrderQueryPort orderQuery;   // minimal dependency
}

Replace Inheritance with Delegation

When: A subclass uses only a small part of the superclass interface, or the inheritance relationship isn't truly an IS-A. Switch to composition.

// BEFORE — PriorityQueue extends LinkedList, but ISN'T a LinkedList
public class PriorityQueue extends LinkedList<Task> {
    public Task poll() { /* custom priority logic */ }
}

// AFTER — Replace Inheritance with Delegation
public class PriorityQueue {
    private final LinkedList<Task> store = new LinkedList<>();
    public void add(Task t)  { store.add(t); store.sort(Comparator.comparing(Task::getPriority)); }
    public Task poll()       { return store.pollFirst(); }
    public boolean isEmpty() { return store.isEmpty(); }
}

8. Refactoring in Practice: A Spring Boot Legacy Case Study

Let's walk through a real-world scenario: a monolithic OrderController that has accumulated 500 lines of business logic directly in the controller. This is a common anti-pattern in Spring Boot codebases that grew organically over years. We'll apply 5 refactoring techniques step by step.

The Starting Point — Problematic OrderController

// BEFORE — 500-line God Controller (representative excerpt)
@RestController
@RequestMapping("/api/orders")
public class OrderController {

    @Autowired private OrderRepository orderRepository;
    @Autowired private ProductRepository productRepository;
    @Autowired private CustomerRepository customerRepository;
    @Autowired private EmailService emailService;
    @Autowired private PaymentGateway paymentGateway;

    @PostMapping
    public ResponseEntity<?> createOrder(@RequestBody Map<String, Object> payload,
                                          HttpServletRequest request) {
        // Extract and validate fields inline
        String customerId = (String) payload.get("customerId");
        if (customerId == null) return ResponseEntity.badRequest().body("Missing customerId");
        Customer customer = customerRepository.findById(Long.parseLong(customerId)).orElse(null);
        if (customer == null) return ResponseEntity.badRequest().body("Customer not found");

        List<Map<String, Object>> items = (List) payload.get("items");
        if (items == null || items.isEmpty()) return ResponseEntity.badRequest().body("No items");

        // Calculate total inline
        BigDecimal total = BigDecimal.ZERO;
        for (Map<String, Object> item : items) {
            Long productId = Long.parseLong((String) item.get("productId"));
            int qty = Integer.parseInt((String) item.get("quantity"));
            Product p = productRepository.findById(productId).orElseThrow();
            total = total.add(p.getPrice().multiply(BigDecimal.valueOf(qty)));
        }
        String coupon = (String) payload.get("couponCode");
        if ("SAVE10".equals(coupon)) total = total.multiply(new BigDecimal("0.9"));
        if ("SAVE20".equals(coupon)) total = total.multiply(new BigDecimal("0.8"));

        // Payment
        String cardNumber = (String) payload.get("cardNumber");
        if (cardNumber == null) return ResponseEntity.badRequest().body("Missing card");
        boolean charged = paymentGateway.charge(cardNumber, total);
        if (!charged) return ResponseEntity.status(402).body("Payment failed");

        // Persist
        Order order = new Order();
        order.setCustomer(customer);
        order.setTotal(total);
        order.setStatus("PLACED");
        orderRepository.save(order);

        // Notify
        emailService.sendOrderConfirmation(customer.getEmail(), order.getId(), total);
        return ResponseEntity.ok(Map.of("orderId", order.getId()));
    }
    // ... 450 more lines below
}

Step 1 — Extract Method: isolate logical blocks

// Within OrderController, extract validateCustomer, calculateOrderTotal, processPayment
private Customer validateCustomer(String customerId) {
    if (customerId == null) throw new BadRequestException("Missing customerId");
    return customerRepository.findById(Long.parseLong(customerId))
        .orElseThrow(() -> new NotFoundException("Customer not found"));
}
private BigDecimal calculateOrderTotal(List<Map<String, Object>> items, String coupon) {
    BigDecimal total = items.stream()
        .map(item -> {
            Product p = productRepository.findById(Long.parseLong((String)item.get("productId"))).orElseThrow();
            return p.getPrice().multiply(BigDecimal.valueOf(Integer.parseInt((String)item.get("quantity"))));
        })
        .reduce(BigDecimal.ZERO, BigDecimal::add);
    return applyCoupon(total, coupon);
}

Step 2 — Move Method: move business logic to OrderService

// Move calculateOrderTotal and applyCoupon to OrderService
@Service
public class OrderService {
    public BigDecimal calculateTotal(List<OrderItemDto> items, String coupon) {
        BigDecimal total = items.stream()
            .map(i -> productRepository.findById(i.productId()).orElseThrow().getPrice()
                        .multiply(BigDecimal.valueOf(i.quantity())))
            .reduce(BigDecimal.ZERO, BigDecimal::add);
        return couponService.apply(total, coupon);
    }
    public Order placeOrder(CreateOrderRequest req) {
        Customer customer  = customerService.getById(req.customerId());
        BigDecimal total   = calculateTotal(req.items(), req.couponCode());
        paymentService.charge(req.cardNumber(), total);
        return orderRepository.save(Order.of(customer, total));
    }
}

Step 3 — Extract Class: split PaymentService out

// Extract payment concerns into dedicated PaymentService
@Service
public class PaymentService {
    private final PaymentGateway gateway;
    public void charge(String cardNumber, BigDecimal amount) {
        boolean success = gateway.charge(cardNumber, amount);
        if (!success) throw new PaymentFailedException("Card charge failed for amount: " + amount);
    }
}

Step 4 — Introduce Parameter Object: replace Map payload with typed DTO

// Replace raw Map<String,Object> with a validated record
public record CreateOrderRequest(
    @NotNull Long customerId,
    @NotEmpty List<OrderItemDto> items,
    String couponCode,
    @NotBlank String cardNumber
) {}

public record OrderItemDto(
    @NotNull Long productId,
    @Min(1) int quantity
) {}

// Controller becomes thin:
@PostMapping
public ResponseEntity<OrderResponse> createOrder(
        @Valid @RequestBody CreateOrderRequest request) {
    Order order = orderService.placeOrder(request);
    return ResponseEntity.ok(OrderResponse.from(order));
}

Step 5 — Replace Conditional with Polymorphism: coupon strategy

// Replace if/else coupon chain with Strategy
public interface CouponStrategy {
    BigDecimal apply(BigDecimal amount);
    String code();
}
@Component
public class Save10Coupon implements CouponStrategy {
    public BigDecimal apply(BigDecimal amount) { return amount.multiply(new BigDecimal("0.90")); }
    public String code() { return "SAVE10"; }
}
@Component
public class Save20Coupon implements CouponStrategy {
    public BigDecimal apply(BigDecimal amount) { return amount.multiply(new BigDecimal("0.80")); }
    public String code() { return "SAVE20"; }
}
@Service
public class CouponService {
    private final Map<String, CouponStrategy> strategies;
    public CouponService(List<CouponStrategy> list) {
        this.strategies = list.stream()
            .collect(Collectors.toMap(CouponStrategy::code, Function.identity()));
    }
    public BigDecimal apply(BigDecimal amount, String code) {
        return strategies.getOrDefault(code, a -> a).apply(amount);
    }
}

Before vs After — Metrics

Metric Before After
OrderController lines 500+ ~40
Cyclomatic complexity (createOrder) 18 3
Unit test coverage 12% 91%
Time to add a new discount type ~2 hours (touch 3 files, risk regression) ~15 min (add a new CouponStrategy @Component)

9. IDE Tooling for Java Refactoring

Manual refactoring is error-prone. Modern Java IDEs automate the transformation steps safely, letting you focus on design decisions rather than mechanical edits. Always prefer IDE-automated refactorings over manual text editing — they update all references simultaneously.

IntelliJ IDEA — Essential Shortcuts

Refactoring Shortcut (macOS) Shortcut (Windows/Linux)
Extract Method ⌥⌘M Ctrl+Alt+M
Extract Variable ⌥⌘V Ctrl+Alt+V
Rename ⇧F6 Shift+F6
Move Class / Method F6 F6
Extract Interface Refactor menu → Extract Interface Refactor menu → Extract Interface
Introduce Parameter Object Refactor → Introduce Parameter Object Refactor → Introduce Parameter Object
Inline Method / Variable ⌥⌘N Ctrl+Alt+N
Change Signature ⌘F6 Ctrl+F6

Eclipse & VS Code

Static Analysis Integration — SonarQube & SpotBugs

// ArchUnit test: controllers must not call repositories directly
@AnalyzeClasses(packages = "com.example.app")
class ArchitectureTest {
    @ArchTest
    static final ArchRule noDirectRepoAccessFromController =
        noClasses().that().resideInAPackage("..controller..")
            .should().dependOnClassesThat()
            .resideInAPackage("..repository..");
}

Refactoring Workflow — The Safe Sequence

  1. Identify a code smell (SonarQube, code review, or reading the code).
  2. Write or verify tests cover the code you're about to change.
  3. Apply one small, IDE-automated refactoring.
  4. Run the full test suite — all green.
  5. Commit the refactoring as a standalone commit (separate from feature changes).
  6. Repeat — do not batch multiple refactorings into one commit.

Refactoring is a professional discipline, not a luxury. The teams that ship fast and stay sane in large Java codebases are those that refactor continuously, commit to clean code as a team norm, and use their IDE automation rather than trying to be clever with manual edits. Apply one technique per day from this catalog and your codebase will look unrecognisable — in the best possible way — within a quarter.

Leave a Comment

Related Posts

Md Sanwar Hossain - Software Engineer
Md Sanwar Hossain

Software Engineer · Java · Spring Boot · Microservices · AI/LLM Systems

All Posts
Last updated: April 9, 2026