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.
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 six transformers, each adding a specific capability to any outer monad. It also provides MTL-style capability interfaces that let you write code against abstract capabilities rather than concrete transformer stacks.
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 │
│ │
│ "My operation needs to accumulate output or logs" │
│ └──▶ WriterT │
│ │
│ "I want stack-independent capability abstractions" │
│ └──▶ MTL Capabilities (MonadReader, MonadState, ...) │
│ │
└──────────────────────────────────────────────────────────┘
Available Transformers
| Transformer | Inner Effect | Use Case |
|---|---|---|
EitherT<F, E, A> | Typed error (Either<E, A>) | Async operations that fail with domain errors |
OptionalT<F, A> | Java Optional (Optional<A>) | Async operations that might return nothing |
MaybeT<F, A> | Optional value (Maybe<A>) | Same as OptionalT, for Higher-Kinded-J's Maybe |
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 |
WriterT<F, W, A> | Output (Pair<A, W>) | Logging, audit trails, diagnostic accumulation |
- Path or Transformer? – A short triage page that names the three signals you have outgrown the Effect Path API. Read this first if you are not yet sure whether the rest of the chapter applies to you.
- Quickstart – Three runnable transformer examples in about 150 lines:
EitherTfor async-plus-typed-error,OptionalTfor async lookup chains, and an MTL example for stack-independent code. The fastest path from zero to working code. - Transformers at a Glance – A one-page reference card listing every transformer, its factory methods, key operations, the equivalent Effect Path type, and the matching MTL capability.
- Migration Cookbook – Pattern-by-pattern translations from imperative Java (and from the Effect Path API) into raw transformers. Recipes for nested
thenCompose, manual config threading, manual log threading, and manual state threading. - 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
EitherTto get a singleflatMapthat handles both async sequencing and error propagation. - OptionalT – Lifts
java.util.Optionalinto 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
Maybetype. 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.
- WriterT – Accumulates output (logs, audit trails, diagnostics) alongside computation. Each step appends to the output via a
Monoid, andflatMapcombines outputs automatically. - MTL Capabilities –
MonadReader,MonadState, andMonadWriterabstract effect capabilities independently of the concrete transformer stack. Write polymorphic functions that declare what they need without specifying how it is assembled. - Common Compiler Errors – The six error messages developers hit most often when working with raw transformers, with the minimal trigger for each and the fix.
- Capstone: A Multi-Capability Workflow – A complete order-processing example that combines typed errors, configuration, audit, and async execution. Three side-by-side versions (imperative, MTL polymorphic, Effect Path) make the trade-offs concrete.
See also Capstone: Effects Meet Optics for a complete example combining effect paths with optics in a single pipeline.
Chapter Contents
- Path or Transformer? - The three signals that mean you have outgrown the Path API
- Quickstart - Three runnable transformer examples
- Transformers at a Glance - One-page reference card
- Migration Cookbook - Imperative and Path translations
- Stack Archetypes - Named patterns for the most common composition problems
- Monad Transformers - Why monads stack poorly and what transformers solve
- EitherT - Typed errors in any monadic context
- OptionalT - Java Optional lifting
- MaybeT - Maybe lifting
- ReaderT - Environment threading
- StateT - State management in effectful computation
- WriterT - Output accumulation and audit trails
- MTL Capabilities - Stack-independent capability abstractions
- MonadReader - Environment access and scoped modification
- MonadState - State threading and mutation
- MonadWriter - Output accumulation and log inspection
- Combining Capabilities - Multi-capability functions and concrete instances
- Common Compiler Errors - The six errors developers hit most often, with the fix for each
- Capstone: A Multi-Capability Workflow - End-to-end example combining typed errors, config, audit, and async
Next: Path or Transformer?