The Problem with Growing Switch Statements

We've all been there. You start with a simple switch block to handle a few different business rules. It's clean, it's intuitive, and it works. But then the project grows. Six months later, that switch statement has ballooned into a 200-line monster spread across multiple methods, and every time a new requirement comes in, you're terrified of breaking an existing case.

In my experience, this is where most developers start thinking about the Strategy Pattern. While the classic Gang of Four approach involves creating a separate interface and a dozen concrete implementation classes, that often feels like overkill for internal business logic. It litters your package with PaymentStrategyA, PaymentStrategyB, and so on. There's a more elegant, Java-centric way to handle this: Enum-based Strategies.

The Technique: Enums as Strategy Providers

Java enums are far more powerful than constants in other languages. They are full-blown classes. This means they can implement interfaces, hold state, and—most importantly—define abstract methods that each constant must implement. By shifting the logic into the enum itself, you encapsulate the behavior directly with the type, eliminating the need for external switch blocks entirely.

Real-World Scenario: Dynamic Discount Calculation

Imagine you're building an e-commerce engine. You have different discount tiers: NONE, PERCENTAGE, and FIXED_AMOUNT. Each has a different way of calculating the final price. Instead of a service class filled with if-else logic, we can bake the calculation directly into the discount type.

public interface DiscountStrategy {
    double applyDiscount(double originalPrice);
}

public enum DiscountType implements DiscountStrategy {
    // No discount applied
    NONE(price -> price),
    
    // Percentage-based discount (e.g., 0.2 for 20%)
    PERCENTAGE(price -> {
        double rate = 0.20; // In a real app, this might be passed via a constructor
        return price * (1 - rate);
    }),
    
    // Fixed amount reduction
    FIXED_AMOUNT(price -> {
        double amount = 10.00;
        return Math.max(0, price - amount);
    });

    private final java.util.function.UnaryOperator<Double> calculation;

    // Constructor takes a lambda to define the strategy
    DiscountType(java.util.function.UnaryOperator<Double> calculation) {
        this.calculation = calculation;
    }

    @Override
    public double applyDiscount(double originalPrice) {
        return calculation.apply(originalPrice);
    }
}

public class PricingService {
    public double calculateFinalPrice(double price, DiscountType type) {
        // No switch statement needed. The enum knows how to handle itself.
        return type.applyDiscount(price);
    }
}

Why This Approach Wins

I prefer this method over the traditional Strategy Pattern for several reasons:

  • Single Responsibility: The logic for how a discount is calculated lives exactly where the discount is defined. If you add a new discount type, you're forced to implement the logic right there, ensuring you don't forget to update a switch block elsewhere.
  • Type Safety: You can't pass an invalid strategy to the PricingService. The compiler guarantees that only valid DiscountType values are used.
  • Reduced Boilerplate: You avoid creating five different classes for five different strategies. Everything is contained within one readable file.
  • O(1) Complexity: Looking up a strategy via an enum is essentially a direct reference call, avoiding the overhead of conditional branching.
Pro Tip: If your strategies require complex dependencies (like a database repository), you can still use this pattern by passing those dependencies as arguments to the applyDiscount method rather than trying to inject them into the enum constants.

When to Stick to Classic Classes

While I use this trick often, it's not a silver bullet. If your strategies are massive—say, 500 lines of code each—putting them in an enum will make the file unreadable. In those cases, use the enum as a factory that returns a separate strategy class. This gives you the clean API of the enum with the scalability of separate classes.

By treating enums as behavioral objects rather than just labels, you can strip away a significant amount of conditional logic from your services, leading to code that is easier to test, easier to read, and far more resilient to change.