Why switch Fails You Over Time

We’ve all seen it:

switch (channel) {
    case EMAIL -> sendEmail();
    case SMS -> sendSms();
    case PUSH -> sendPush();
}

But this still centralizes logic and breaks the open-closed principle. Every time you add a new channel, you have to modify this block. It also makes unit testing harder and prevents your enum from being truly object-oriented.


A Better Way: Enum + Functional Interface

You can write cleaner, more extensible code by combining:

  • An enum where each constant implements its own logic
  • A functional interface to define behavior

Let’s look at how this improves structure and scalability.


Example 1: Notification Channels

We want to support different channels of notification:

  • EMAIL
  • SMS
  • PUSH

1. Functional Interface

A functional interface is an interface that has exactly one abstract method, making it eligible for lambda expressions or method references.

@FunctionalInterface
public interface NotificationStrategy {
    void send(NotificationContext context);
}

2. Context as a Record

public record NotificationContext(String recipient, String message) {

    public void sendEmail() {
        System.out.println("Email sent to %s: %s".formatted(recipient, message));
    }

    public void sendSms() {
        System.out.println("SMS sent to %s: %s".formatted(recipient, message));
    }

    public void sendPush() {
        System.out.println("Push sent to %s: %s".formatted(recipient, message));
    }
}

3. Enum Implements Strategy

public enum NotificationChannel implements NotificationStrategy {
    EMAIL {
        @Override
        public void send(NotificationContext context) {
            context.sendEmail();
        }
    },
    SMS {
        @Override
        public void send(NotificationContext context) {
            context.sendSms();
        }
    },
    PUSH {
        @Override
        public void send(NotificationContext context) {
            context.sendPush();
        }
    }
}

4. Usage

public class Main {
    public static void main(String[] args) {
        var channel = NotificationChannel.PUSH;
        var context = new NotificationContext("[email protected]", "You have a new task!");
        channel.send(context);
    }
}

…replacing the previous switch-case implementation:

public class MainSwitch {
    public static void main(String[] args) {
        var channel = NotificationChannel.PUSH;
        var context = new NotificationContext("[email protected]", "You have a new task!");
        switch (channel) {
            case EMAIL -> context.sendEmail();
            case SMS -> context.sendSms();
            case PUSH -> context.sendPush();
        }
    }
}

Example 2: HTTP Client ID Extraction

We want to extract a client ID from an HTTP request via two possible locations:

  • From an HTTP Header (e.g. X-Client-ID)
  • From a Query Parameter (e.g. client_id)

1. Functional Interface

@FunctionalInterface
public interface ClientIdExtractor {
    String extract(ServerHttpRequest request);
}

This interface defines a single method to extract the client ID from a request, allowing implementations using lambdas or method references.

2. Context Class (HTTP Request)

Here, the context is already provided by ServerHttpRequest (from Spring). So we don’t define a custom record, but rely on the framework-provided request object.

3. Enum Implements Strategy

public enum ClientIdLocation {
    HEADER {
        @Override
        public ClientIdExtractor createExtractor(String name) {
            return request -> request.getHeaders().getFirst(name);
        }
    },
    QUERY {
        @Override
        public ClientIdExtractor createExtractor(String name) {
            return request -> request.getQueryParams().getFirst(name);
        }
    };

    public abstract ClientIdExtractor createExtractor(String name);
}

Each enum constant provides its own extractor strategy using a lambda, based on where the client ID is located.

4. Usage

public class ClientIdResolver {
    public String resolve(ServerHttpRequest request, ClientIdLocation location, String name) {
        ClientIdExtractor extractor = location.createExtractor(name);
        return extractor.extract(request);
    }
}

…replacing the previous switch-case implementation:

public class ClientIdResolverSwitch {
    public String resolve(ServerHttpRequest request, ClientIdLocation location, String name) {
        switch (location) {
            case HEADER -> {
                return request.getHeaders().getFirst(name);
            }
            case QUERY -> {
                return request.getQueryParams().getFirst(name);
            }
        }
        return null;
    }
}

TLDR

Using enum + functional interfaces beats switch-case by offering:

  • Better encapsulation: Logic lives inside each enum constant
  • Open-closed principle: Add new behavior without touching existing code
  • Improved testability: Test enum constants independently
  • Cleaner code: More readable, modular, and maintainable
  • Object-oriented: Leverages polymorphism, not control flow

switch is fine for simple cases — but enums with behavior scale better.