Better Return Types in Java with Sealed Interfaces
In many real-world applications, a function doesn’t return just one thing.
I bet this is common for you: you query user data by ID, and three things can happen:
- ✅ You get the data
- 🔒 The user exists but restricts access (whatever really)
- ❌ The user doesn’t exist at all
The Problem with Typical Return Structures
Instead of using clean types, people often end up writing brittle logic like this:
if (result.isError() && result.reason == "validation") {
// handle validation error
} else if (!result.isError()) {
// process valid result
} else if (result.errorType == NOT_FOUND) {
// handle not found
}
This kind of code becomes unmaintainable and easy to break.
Sealed Interfaces to the Rescue
Here’s how I rewrote it using Java 17+ features:
public sealed interface UserDataResult permits UserData, UserNotFound, UserRestricted {}
public record UserData(Data data) implements UserDataResult {}
public record UserNotFound(String userId) implements UserDataResult {}
public record UserRestricted(String userId) implements UserDataResult {}
Now your function can return one of these types:
public UserDataResult getDataForUser(String userId) { ... }
And handling the result becomes explicit and safe:
switch (result) {
case UserData data -> show(data.data());
case UserNotFound nf -> log("User not found: " + nf.userId());
case UserRestricted ur -> warn("Access denied for user: " + ur.userId());
}
Why I Prefer This Style
✅ All return paths are modeled explicitly
✅ The compiler forces you to handle each case
✅ No flag juggling, no null, no error-wrapping
✅ More readable and future-proof