This content originally appeared on Level Up Coding - Medium and was authored by Baljeet Singh
A few months back, I was building an app that handles different types of user notifications. I had push notifications, email alerts, and SMS messages. My first attempt looked like this:
interface Notification {
fun send()
}
class PushNotification(val title: String) : Notification {
override fun send() {
println("Sending push: $title")
}
}
class EmailNotification(val subject: String) : Notification {
override fun send() {
println("Sending email: $subject")
}
}This worked fine until I needed to handle each notification type differently in my UI. I ended up with some missing checks everywhere. That's when I used sealed interfaces.
What Are Sealed Interfaces?
Think of sealed interfaces like a restricted club. Only specific classes can join, and you know exactly who all the members are at compile time.
Here’s the same notification system using sealed interfaces:
sealed interface Notification {
fun send()
}
data class PushNotification(val title: String, val body: String) : Notification {
override fun send() {
println("📱 Push: $title")
}
}
data class EmailNotification(val subject: String, val recipient: String) : Notification {
override fun send() {
println("📧 Email to $recipient: $subject")
}
}
data class SmsNotification(val message: String, val phoneNumber: String) : Notification {
override fun send() {
println("📱 SMS to $phoneNumber: $message")
}
}The magic happens when you use them with when expressions:
fun handleNotification(notification: Notification) {
when (notification) {
is PushNotification -> {
// Show in notification bar
showInNotificationBar(notification.title, notification.body)
}
is EmailNotification -> {
// Log email sent
logEmailSent(notification.recipient)
}
is SmsNotification -> {
// Track SMS cost
trackSmsCost(notification.phoneNumber)
}
// No else needed! Kotlin knows these are all possibilities
}
}Real Example: API Response Handling
Here’s where sealed interfaces really shine. I was working with an API that could return success, error, or loading states:
sealed interface ApiResponse<out T> {
data class Success<T>(val data: T) : ApiResponse<T>
data class Error(val message: String, val code: Int) : ApiResponse<Nothing>
object Loading : ApiResponse<Nothing>
}Now handling API responses is clean and safe:
fun displayUserProfile(response: ApiResponse<User>) {
when (response) {
is ApiResponse.Success -> {
val user = response.data
nameText.text = user.name
emailText.text = user.email
hideLoading()
}
is ApiResponse.Error -> {
showError("Failed to load profile: ${response.message}")
hideLoading()
}
is ApiResponse.Loading -> {
showLoading()
}
}
}The compiler forces you to handle every case. No more forgotten error states!
Payment Methods Example
Let me show you another real scenario. I was building a checkout flow with different payment methods:
sealed interface PaymentMethod {
data class CreditCard(
val number: String,
val expiryMonth: Int,
val expiryYear: Int,
val cvv: String
) : PaymentMethod
data class PayPal(val email: String) : PaymentMethod
data class BankTransfer(
val accountNumber: String,
val routingNumber: String
) : PaymentMethod
object ApplePay : PaymentMethod
}Processing payments becomes straightforward:
fun processPayment(method: PaymentMethod, amount: Double) {
when (method) {
is PaymentMethod.CreditCard -> {
chargeCreditCard(method.number, method.cvv, amount)
}
is PaymentMethod.PayPal -> {
initiatePayPalPayment(method.email, amount)
}
is PaymentMethod.BankTransfer -> {
processBankTransfer(method.accountNumber, method.routingNumber, amount)
}
is PaymentMethod.ApplePay -> {
startApplePayFlow(amount)
}
}
}Why Not Just Use Regular Interfaces?
You might ask, “Why not just use normal interfaces?” Here’s the problem:
With regular interfaces, anyone can implement them anywhere in your codebase. You lose control. With sealed interfaces, you define all implementations in the same file. The compiler knows the complete list.
This means:
- No missing cases in when expressions
- Better refactoring — add a new type and the compiler shows you everywhere you need to update
- Cleaner code — no defensive programming with else branches
File Upload States (Another Real Example)
Here’s how I handle file uploads in my apps:
sealed interface UploadState {
object Idle : UploadState
data class Uploading(val progress: Int) : UploadState
data class Success(val fileUrl: String) : UploadState
data class Failed(val error: String) : UploadState
}My UI updates automatically based on state:
fun updateUploadUI(state: UploadState) {
when (state) {
is UploadState.Idle -> {
uploadButton.isEnabled = true
progressBar.hide()
}
is UploadState.Uploading -> {
uploadButton.isEnabled = false
progressBar.show()
progressBar.progress = state.progress
}
is UploadState.Success -> {
uploadButton.isEnabled = true
progressBar.hide()
showSuccess("File uploaded: ${state.fileUrl}")
}
is UploadState.Failed -> {
uploadButton.isEnabled = true
progressBar.hide()
showError(state.error)
}
}
}Quick Tips
Do this:
sealed interface Result<out T> {
data class Success<T>(val data: T) : Result<T>
data class Error(val exception: Exception) : Result<Nothing>
}Not this:
// Regular interface - anyone can implement it
interface Result<out T> {
// ...
}
Use data classes for implementations that hold data. Use object for singleton states like Loading or Idle.
When to Use Sealed Interfaces
I use sealed interfaces when:
- I have a fixed set of related types
- I want exhaustive when expressions
- I’m modeling state machines (loading, success, error)
- I’m handling different data types from APIs
- I want the compiler to help me catch missing cases
Wrapping Up
Sealed interfaces changed how I write Kotlin code. They make impossible states impossible and help me write more reliable apps. The compiler becomes your friend, catching errors before they reach users.
Try replacing your next enum or interface with a sealed interface. Your future self will thank you when you add a new case and the compiler shows you exactly where to update your code.
What’s your experience with sealed interfaces? Have you found other creative ways to use them? I’d love to hear about it!
For any questions or queries, let’s connect on LinkedIn
https://www.linkedin.com/in/devbaljeet/
Why I Started Using Sealed Interfaces in Kotlin (And You Should Too) was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Level Up Coding - Medium and was authored by Baljeet Singh
Baljeet Singh | Sciencx (2025-10-01T14:46:12+00:00) Why I Started Using Sealed Interfaces in Kotlin (And You Should Too). Retrieved from https://www.scien.cx/2025/10/01/why-i-started-using-sealed-interfaces-in-kotlin-and-you-should-too/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.