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.