Skip to main content

Motivations & Core Concepts

Why We Created Sealed Monad​

We created Sealed Monad after observing patterns and challenges in real-world business logic. We noticed that well-designed business logic often follows certain principles, but traditional error handling approaches made implementation verbose and hard to read.

Let's first define some domain models we'll use in our examples:

import scala.concurrent.Future
import cats.Monad
import cats.instances.future._
import cats.data.OptionT

// Domain models
sealed trait Provider
object Provider {
case object EmailPass extends Provider
case object OAuth extends Provider
}

case class User(id: Long, email: String, archived: Boolean)
case class AuthMethod(userId: Long, provider: Provider)

// Result ADT for our login operation
sealed trait LoginResponse
object LoginResponse {
final case class LoggedIn(token: String) extends LoginResponse
case object InvalidCredentials extends LoginResponse
case object Deleted extends LoginResponse
case object ProviderAuthFailed extends LoginResponse
}

Key Observations​

1. Operation Results as ADTs​

Well-designed services represent operation results as Algebraic Data Types (ADTs), usually with a sealed trait and several case classes/objects. This approach models different business outcomes explicitly and comprehensively.

2. Methods as Self-Contained Units​

Service methods are designed as closed units of code, each returning one value from the result ADT:

def login(email: String,
findUser: String => Future[Option[User]],
findAuthMethod: (Long, Provider) => Future[Option[AuthMethod]],
issueTokenFor: User => String,
checkAuthMethod: AuthMethod => Boolean): Future[LoginResponse] = ???

3. No Arbitrary Error/Success Distinction​

We found that distinguishing between "errors" and "valid results" is often arbitrary in business logic. For example, when a user tries to log in with a deleted account, is "Deleted" an error or a legitimate response? With Sealed Monad, everything is simply a response.

4. Method-Local Error Handling​

Global or module-based error handling can be harmful to application architecture. Different operations need different error-handling strategies. Sealed Monad encourages handling business outcomes at the method level where context is clear.

5. For-Comprehension Friendly​

For-comprehensions provide a clean, sequential way to express business logic. Sealed Monad is designed to work seamlessly with for-comprehensions.

6. Linear vs. Branching Logic​

Traditional if-else or pattern-matching creates branching logic that becomes hard to follow. Sealed Monad aims to linearize the flow, making code more readable.

Core Concepts of Sealed Monad​

The Sealed Type​

The core type in Sealed Monad is Sealed[F[_], +A, +ADT] with three type parameters:

  • F[_]: The effect type (e.g., Future, IO, Id)
  • A: The intermediate value type (values you work with in the "happy path")
  • ADT: The final value or "result" type (typically a sealed trait hierarchy)

Conceptually, Sealed is like EitherT but oriented toward a workflow that:

  1. Works with intermediate values (A) through map/flatMap
  2. Can short-circuit to a final result (ADT) at any point
  3. Must ultimately evaluate to a final value of type ADT

The Execution Flow​

A typical Sealed Monad workflow:

  1. Start with values wrapped in effects (F[A], F[Option[A]], etc.)
  2. Process these values, potentially short-circuiting with an ADT value if validation fails
  3. Continue processing until reaching a final result
  4. Call .run to evaluate the computation to F[ADT]

Key Operations​

Sealed Monad provides several categories of operations:

Extraction Operations​

These help you work with Option and other container types:

// Extract a value from an Option or return a specified ADT
val user: Sealed[Future, User, LoginResponse] =
findUser(email).valueOr(LoginResponse.InvalidCredentials)

Validation Operations​

These let you validate values and short-circuit on failure:

// Ensure user is not archived, or return Deleted response
val activeUser: Sealed[Future, User, LoginResponse] =
user.ensure(!_.archived, LoginResponse.Deleted)

Transformation Operations​

These transform intermediate values:

// Map user to a token
val token: Sealed[Future, String, LoginResponse] =
user.map(u => issueTokenFor(u))

Side Effect Operations​

These let you perform side effects without affecting the computation:

// Log the current state
val loggedUser: Sealed[Future, User, LoginResponse] =
user.tap(u => println(s"Found user: ${u.email}"))

All these operations work together to create clean, linear business logic that's easy to read and maintain.

Comparing with Traditional Approaches​

To demonstrate the value of Sealed Monad, let's compare two implementations of the same login logic:

Without Sealed Monad​

def login(email: String,
findUser: String => Future[Option[User]],
findAuthMethod: (Long, Provider) => Future[Option[AuthMethod]],
issueTokenFor: User => String,
checkAuthMethod: AuthMethod => Boolean): Future[LoginResponse] = {
findUser(email).flatMap {
case None =>
Future.successful(LoginResponse.InvalidCredentials)
case Some(user) if user.archived =>
Future.successful(LoginResponse.Deleted)
case Some(user) =>
findAuthMethod(user.id, Provider.EmailPass).map {
case None =>
LoginResponse.ProviderAuthFailed
case Some(authMethod) if !checkAuthMethod(authMethod) =>
LoginResponse.InvalidCredentials
case Some(_) =>
LoginResponse.LoggedIn(issueTokenFor(user))
}
}
}

With Sealed Monad​

import pl.iterators.sealedmonad.syntax._

def login(email: String,
findUser: String => Future[Option[User]],
findAuthMethod: (Long, Provider) => Future[Option[AuthMethod]],
issueTokenFor: User => String,
checkAuthMethod: AuthMethod => Boolean): Future[LoginResponse] = {
(for {
user <- findUser(email)
.valueOr(LoginResponse.InvalidCredentials)
.ensure(!_.archived, LoginResponse.Deleted)

authMethod <- findAuthMethod(user.id, Provider.EmailPass)
.valueOr(LoginResponse.ProviderAuthFailed)
.ensure(checkAuthMethod, LoginResponse.InvalidCredentials)
} yield LoginResponse.LoggedIn(issueTokenFor(user))).run
}

The Sealed Monad version is more concise, easier to follow, and effectively communicates the business logic in a linear, step-by-step fashion.