design

Enum with Lambda (Strategy Pattern) in Java

Learn how to use Enum + Lambda as a practical Strategy Pattern in Java, with real-world examples and clean, interview-friendly code.

Reading Time: 14 min readAuthor: DeepTechHub
#java#design-patterns#strategy-pattern#enum#lambda#architecture
Enum with Lambda (Strategy Pattern) in Java

Enum with Lambda (Strategy Pattern) in Java

Enum + Lambda is one of the cleanest ways to implement Strategy Pattern in Java when the number of strategies is small to medium and known in advance.

It helps remove long if-else blocks, keeps behavior close to the type, and makes code easier to read and extend.


Why this pattern is useful

In many projects we write code like this:

if (method == PaymentMethod.CARD) {
    // logic 1
} else if (method == PaymentMethod.CASH) {
    // logic 2
} else if (method == PaymentMethod.DIGITAL_WALLET) {
    // logic 3
}

This becomes hard to maintain as the system grows.

With Enum + Lambda, each strategy is defined once and used directly.


Core idea

  • enum represents the available strategies
  • each enum constant stores a lambda function
  • client code calls one method and executes the selected strategy

You still get Strategy Pattern benefits, but with less boilerplate than separate classes.

Quick decision: Enum + Lambda vs Factory + Strategy classes

If you read both guides, use this practical rule:

  • Choose Enum + Lambda when strategies are finite, simple, and mostly stateless.
  • Choose Factory + Strategy classes when each strategy has richer logic, dependencies, or separate test setup.
SituationBetter choice
3-8 fixed business rules (fees, discounts, routing)Enum + Lambda
Each strategy calls multiple services (gateway, audit, retry, metrics)Factory + Strategy classes
You want very low boilerplate and fast readabilityEnum + Lambda
You need dependency injection per strategyFactory + Strategy classes
Strategies may be added dynamically via config/pluginFactory + Strategy classes

Think of Enum + Lambda as a lightweight strategy model. Once behavior becomes large, move to class-based strategies.


Small class diagram

The diagram below shows how enum constants hold behavior and client code invokes one strategy through the enum.


Basic implementation (Payment fee calculation)

Imagine a checkout flow where each payment method has a different processing fee.

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.function.Function;
 
public enum PaymentMethod {
    CARD(amount -> amount.multiply(BigDecimal.valueOf(0.02))),
    CASH(amount -> BigDecimal.ZERO),
    DIGITAL_WALLET(amount -> amount.multiply(BigDecimal.valueOf(0.01)));
 
    private final Function<BigDecimal, BigDecimal> feeCalculator;
 
    PaymentMethod(Function<BigDecimal, BigDecimal> feeCalculator) {
        this.feeCalculator = feeCalculator;
    }
 
    public BigDecimal fee(BigDecimal amount) {
        return feeCalculator.apply(amount).setScale(2, RoundingMode.HALF_UP);
    }
}

Usage:

import java.math.BigDecimal;
 
public class CheckoutService {
    public BigDecimal totalPayable(BigDecimal amount, PaymentMethod method) {
        BigDecimal fee = method.fee(amount);
        return amount.add(fee);
    }
 
    public static void main(String[] args) {
        CheckoutService service = new CheckoutService();
 
        BigDecimal amount = new BigDecimal("1000.00");
        System.out.println(service.totalPayable(amount, PaymentMethod.CARD));
        System.out.println(service.totalPayable(amount, PaymentMethod.CASH));
        System.out.println(service.totalPayable(amount, PaymentMethod.DIGITAL_WALLET));
    }
}

This is compact, readable, and avoids scattering strategy logic in multiple files.


Real-world example 1: Notification routing

In many systems you send alerts through multiple channels.

import java.util.function.BiConsumer;
 
public enum NotificationChannel {
    EMAIL(NotificationChannel::sendEmail),
    SMS(NotificationChannel::sendSms),
    PUSH(NotificationChannel::sendPush);
 
    private final BiConsumer<String, String> sender;
 
    NotificationChannel(BiConsumer<String, String> sender) {
        this.sender = sender;
    }
 
    public void send(String user, String message) {
        sender.accept(user, message);
    }
 
    private static void sendEmail(String user, String message) {
        String subject = "Action Required";
        // Real systems call an EmailClient here.
        System.out.println("EMAIL => user=" + user + ", subject=" + subject + ", body=" + message);
    }
 
    private static void sendSms(String user, String message) {
        String compact = message.length() > 140 ? message.substring(0, 140) : message;
        // Real systems call an SmsGateway here.
        System.out.println("SMS => user=" + user + ", text=" + compact);
    }
 
    private static void sendPush(String user, String message) {
        String title = "Notification";
        // Real systems call PushProvider/APNs/FCM here.
        System.out.println("PUSH => user=" + user + ", title=" + title + ", body=" + message);
    }
}

Usage:

public class NotificationService {
    public void notify(String user, String message, NotificationChannel channel) {
        channel.send(user, message);
    }
}

Why this works well:

  • no channel-specific if-else
  • each channel behavior is centralized
  • adding a new channel is one enum constant

Real-world example 2: Discount policy engine

E-commerce promotions are often strategy driven.

import java.math.BigDecimal;
import java.util.function.UnaryOperator;
 
public enum DiscountPolicy {
    NO_DISCOUNT(amount -> amount),
    FESTIVAL_10(amount -> amount.multiply(BigDecimal.valueOf(0.90))),
    PREMIUM_15(amount -> amount.multiply(BigDecimal.valueOf(0.85)));
 
    private final UnaryOperator<BigDecimal> applyDiscount;
 
    DiscountPolicy(UnaryOperator<BigDecimal> applyDiscount) {
        this.applyDiscount = applyDiscount;
    }
 
    public BigDecimal apply(BigDecimal amount) {
        return applyDiscount.apply(amount);
    }
}

Service:

import java.math.BigDecimal;
 
public class PricingService {
    public BigDecimal finalPrice(BigDecimal basePrice, DiscountPolicy policy) {
        return policy.apply(basePrice);
    }
}

This pattern is clean for product pricing, shipping options, tax rules, or commission rules.


When Enum + Lambda is a great choice

Use it when:

  • strategy set is finite and stable
  • each strategy is simple to medium complexity
  • you want to reduce class explosion
  • you need fast readability for the team

When not to use it

Prefer classic Strategy classes when:

  • strategy logic is large or needs multiple helper dependencies
  • each strategy has complex state
  • strategies must be loaded dynamically (plugins/config/db)
  • teams prefer one-class-per-behavior for test isolation

A good practical rule:

  • simple behavior -> enum + lambda
  • complex behavior -> interface + classes

Common mistakes and fixes

Mistake 1: Putting too much code inside lambdas

If lambda blocks become large, move logic to private static methods.

import java.math.BigDecimal;
import java.util.function.Function;
 
public enum PaymentMethod {
    CARD(PaymentMethod::cardFee),
    CASH(PaymentMethod::cashFee),
    DIGITAL_WALLET(PaymentMethod::walletFee);
 
    private final Function<BigDecimal, BigDecimal> feeCalculator;
 
    PaymentMethod(Function<BigDecimal, BigDecimal> feeCalculator) {
        this.feeCalculator = feeCalculator;
    }
 
    private static BigDecimal cardFee(BigDecimal amount) {
        return amount.multiply(BigDecimal.valueOf(0.02));
    }
 
    private static BigDecimal cashFee(BigDecimal amount) {
        return BigDecimal.ZERO;
    }
 
    private static BigDecimal walletFee(BigDecimal amount) {
        return amount.multiply(BigDecimal.valueOf(0.01));
    }
}

Mistake 2: Using double for money

Always use BigDecimal for currency.

Mistake 3: Missing default behavior in API input mapping

When mapping external text values to enum, handle invalid values safely.

public static PaymentMethod from(String value) {
    for (PaymentMethod method : values()) {
        if (method.name().equalsIgnoreCase(value)) {
            return method;
        }
    }
    throw new IllegalArgumentException("Unsupported payment method: " + value);
}

Testing approach

Enum strategy testing is straightforward.

import static org.junit.jupiter.api.Assertions.assertEquals;
 
import java.math.BigDecimal;
import org.junit.jupiter.api.Test;
 
class PaymentMethodTest {
 
    @Test
    void cardFeeShouldBeTwoPercent() {
        BigDecimal fee = PaymentMethod.CARD.fee(new BigDecimal("1000.00"));
        assertEquals(new BigDecimal("20.00"), fee);
    }
 
    @Test
    void cashFeeShouldBeZero() {
        BigDecimal fee = PaymentMethod.CASH.fee(new BigDecimal("1000.00"));
        assertEquals(new BigDecimal("0.00"), fee);
    }
}

Comparison: classic Strategy vs Enum + Lambda

AspectClassic Strategy (classes)Enum + Lambda
BoilerplateHigherLower
Scalability for complex logicBetterLimited
Readability for small use casesMediumHigh
Dynamic runtime loadingBetterNot ideal
Dependency injection friendlinessBetterLimited

Final takeaway

Enum + Lambda is a very practical Strategy Pattern variant for Java teams.

It is especially useful in real-world modules such as:

  • payment and fee rules
  • pricing and discounts
  • notification routing
  • tax and commission policies

Start with Enum + Lambda for simple, known strategies. If behavior grows significantly, refactor to classic Strategy classes. That gives you both speed and long-term maintainability.

Enjoyed this article?

Check out more articles or share this with your network.