Monad Transformers: Combining Effects

"You can't just bolt two things together and expect them to work."

– Richard Feynman


Every Java developer who has worked with CompletableFuture and Either separately knows they compose beautifully in isolation. CompletableFuture handles asynchronicity. Either handles typed errors. Each offers clean map and flatMap operations, each respects its own laws, and each is a pleasure to use alone.

Then you need both at once.

The result, CompletableFuture<Either<DomainError, Result>>, is technically correct in the same way that a car engine strapped to a bicycle is technically a vehicle. The types nest, the generics compile, and then you try to flatMap across the combined structure and discover that Java has no opinion about how these two contexts should interact. You write nested thenApply calls, peel back layers manually, and watch the indentation grow. The ergonomics deteriorate rapidly.

Monad transformers solve this by wrapping the nested structure in a new type that provides a single, unified monadic interface. EitherT<CompletableFutureKind.Witness, DomainError, Result> is still CompletableFuture<Either<DomainError, Result>> underneath, but it offers one flatMap that sequences both the async and error-handling layers together. The nesting is hidden; the composition is restored.

Path First, Stack Later

Most users do not need to read this chapter. The Effect Path API wraps these transformers into a fluent interface that handles composition for you. Start there. Come here when you need to build custom transformer stacks or understand what happens under the hood.

See Stack Archetypes for named patterns that cover the most common use cases without requiring raw transformer manipulation.

Higher-Kinded-J provides five transformers, each adding a specific capability to any outer monad:


The Stacking Concept

    ┌─────────────────────────────────────────────────────────────┐
    │  WITHOUT TRANSFORMER                                        │
    │                                                             │
    │    CompletableFuture<Either<Error, Result>>                 │
    │                                                             │
    │    future.thenApply(either ->                               │
    │        either.map(result ->                                 │
    │            either2.map(r2 ->                                │
    │                ...)))   // Nesting grows unboundedly        │
    └─────────────────────────────────────────────────────────────┘

    ┌─────────────────────────────────────────────────────────────┐
    │  WITH TRANSFORMER                                           │
    │                                                             │
    │    EitherT<FutureWitness, Error, Result>                    │
    │                                                             │
    │    eitherT                                                  │
    │        .flatMap(result -> operation1(result))               │
    │        .flatMap(r1 -> operation2(r1))                       │
    │        .map(r2 -> finalTransform(r2))  // Flat!             │
    └─────────────────────────────────────────────────────────────┘

Same semantics. Vastly different ergonomics.


Which Transformer Do I Need?

    ┌──────────────────────────────────────────────────────────┐
    │              WHICH TRANSFORMER DO I NEED?                │
    ├──────────────────────────────────────────────────────────┤
    │                                                          │
    │  "My operation might fail with a typed error"            │
    │    └──▶  EitherT                                         │
    │                                                          │
    │  "My operation might return nothing"                     │
    │    ├──▶  OptionalT  (java.util.Optional)                 │
    │    └──▶  MaybeT     (Higher-Kinded-J Maybe)              │
    │                                                          │
    │  "My operation needs shared configuration"               │
    │    └──▶  ReaderT                                         │
    │                                                          │
    │  "My operation needs to track changing state"            │
    │    └──▶  StateT                                          │
    │                                                          │
    └──────────────────────────────────────────────────────────┘

Available Transformers

TransformerInner EffectUse Case
EitherT<F, E, A>Typed error (Either<E, A>)Async operations that fail with domain errors
MaybeT<F, A>Optional value (Maybe<A>)Async operations that might return nothing
OptionalT<F, A>Java Optional (Optional<A>)Same as MaybeT, for java.util.Optional
ReaderT<F, R, A>Environment (Reader<R, A>)Dependency injection in effectful contexts
StateT<S, F, A>State (State<S, A>)Stateful computation within other effects

In This Chapter

  • Stack Archetypes – Seven named patterns (Service, Lookup, Validation, Context, Audit, Workflow, Safe Recursion) that cover the most common enterprise composition problems. Start here to find the right pattern for your use case.
  • The Problem – Monads don't compose naturally. A CompletableFuture<Either<E, A>> requires nested operations that become unwieldy. Transformers restore ergonomic composition.
  • EitherT – Adds typed error handling to any monad. Wrap your async operations with EitherT to get a single flatMap that handles both async sequencing and error propagation.
  • OptionalT – Lifts java.util.Optional into another monadic context. When your async operation might return nothing, OptionalT provides clean composition.
  • MaybeT – The same capability as OptionalT but for the library's Maybe type. Choose based on whether you're using Optional or Maybe elsewhere.
  • ReaderT – Threads environment dependencies through effectful computations. Combine dependency injection with async operations or error handling.
  • StateT – Manages state within effectful computations. Track state changes across async boundaries or error-handling paths.

See also Capstone: Effects Meet Optics for a complete example combining effect paths with optics in a single pipeline.


Chapter Contents

  1. Stack Archetypes - Named patterns for the most common composition problems
  2. Monad Transformers - Why monads stack poorly and what transformers solve
  3. EitherT - Typed errors in any monadic context
  4. OptionalT - Java Optional lifting
  5. MaybeT - Maybe lifting
  6. ReaderT - Environment threading
  7. StateT - State management in effectful computation

Next: Stack Archetypes