Monads in Java | Baeldung (original) (raw)
eBook – Guide Spring Cloud – NPI EA (cat=Spring Cloud)
eBook – Mockito – NPI EA (tag = Mockito)
![]()
Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.
Get started with mocking and improve your application tests using our Mockito guide:
eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
![]()
Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.
Get started with understanding multi-threaded applications with our Java Concurrency guide:
eBook – Reactive – NPI EA (cat=Reactive)
![]()
Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:
>> Join Pro and download the eBook
eBook – Java Streams – NPI EA (cat=Java Streams)
![]()
Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.
But these can also be overused and fall into some common pitfalls.
To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:
>> Join Pro and download the eBook
eBook – Jackson – NPI EA (cat=Jackson)
eBook – HTTP Client – NPI EA (cat=Http Client-Side)
eBook – Maven – NPI EA (cat = Maven)
eBook – Persistence – NPI EA (cat=Persistence)
eBook – RwS – NPI EA (cat=Spring MVC)
Course – LS – NPI EA (cat=Jackson)
![]()
Get started with Spring and Spring Boot, through the Learn Spring course:
Course – RWSB – NPI EA (cat=REST)
Course – LSS – NPI EA (cat=Spring Security)
![]()
Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.
I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.
You can explore the course here:
Course – LSD – NPI EA (tag=Spring Data JPA)
![]()
Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.
Get started with Spring Data JPA through the guided reference course:
Partner – Moderne – NPI EA (cat=Spring Boot)
![]()
Refactor Java code safely — and automatically — with OpenRewrite.
Refactoring big codebases by hand is slow, risky, and easy to put off. That’s where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.
Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions — one for newcomers and one for experienced users. You’ll see how recipes work, how to apply them across projects, and how to modernize code with confidence.
Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.
Course – LJB – NPI EA (cat = Core Java)
eBook – Java Streams – NPI (cat=Java Streams)
![]()
Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.
But these can also be overused and fall into some common pitfalls.
To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:
>> Join Pro and download the eBook
1. Overview
In this tutorial, we’ll learn about monads, and how they can help us deal with effects. We’ll learn about the essential methods enabling us to chain monads and operations: map() and flatMap(). Throughout the article, we’ll explore the APIs of a few popular monads from the Java ecosystem, focusing on their practical applications.
2. Effects
In functional programming, “effects” typically refer to operations that cause changes beyond the scope of the function or component.
To apply the functional programming paradigm while dealing with these effects, we can wrap our operation or data inside a container. We can think of monads as containers that allow us to handle the effects outside the function’s scope, preserving the function’s purity.
For instance, let’s say we have a function that divides two integer numbers:
double divide(int dividend, int divisor) {
return dividend / divisor;
}Although it looks like a pure function, when we pass zero as the value for the divisor parameter, the function produces a side effect by throwing an ArithmeticException. However, we can use a monad to wrap the function’s result and contain its effect.
Let’s change the function and make it return an Optional instead:
Optional<Double> divide(int dividend, int divisor) {
if (divisor == 0) {
return Optional.empty();
}
return Optional.of(dividend / divisor);
}As we can see, the function is no longer producing side effects when we try to divide by zero_._
Here are a few other popular Java examples of monads that help us deal with various effects:
- Optional<> – dealing with nullability
- List<>, Stream<> – managing collections of data
- Mono<>, CompletableFuture<> – dealing with concurrency and I/O
- Try<>, Result<> – dealing with errors
- Either<> – dealing with duality
3. Functors
When we create a monad, we need to allow it to change its encapsulated object or operation, while keeping the same container type.
Let’s take Java Streams as an example. If in the “real world”, an instance of type Long can be converted to an Instant by calling the method Instant.ofEpochSeconds(), this relation must be preserved in the world of _Stream_s.
To achieve this, the Stream API exposes a higher-order function that “lifts” the original relationship. This concept is also known as a “functor” and the method that allows transforming the encapsulated type is typically named “_map_“:
Stream<Long> longs = Stream.of(1712000000L, 1713000000L, 1714000000L);
Stream<Instant> instants = longs.map(Instant::ofEpochSecond);
Although “_map_” is the typical term for this function type, the specific method name itself isn’t essential for an object to qualify as a functor. For example, the CompletableFuture monad provides a method called thenApply() instead:
CompletableFuture<Long> timestamp = CompletableFuture.completedFuture(1713000000L);
CompletableFuture<Instant> instant = timestamp.thenApply(Instant::ofEpochSecond);
As we can see, both Stream and CompletableFuture containers expose methods that enable us to apply all the operations supported by the encapsulated data:
4. Binding
Binding is a key characteristic of a monad that allows us to chain multiple computations in a monadic context. In other words, we can avoid double nesting by replacing map() with binding.
4.1. Nested Monads
If we solely rely on functors to sequence the operations, we’ll eventually end up with nested containers. Let’s use Project Reactor‘s Mono monad for this example.
Let’s assume we have two methods that allow us to fetch Author and Book entities reactively:
Mono<Author> findAuthorByName(String name) { /* ... */ }
Mono<Book> findLatestBookByAuthorId(Long authorId) { /* ... */ }Now, if we start with the author’s name, we can use the first method and fetch his details. The result is a Mono:
void findLatestBookOfAuthor(String authorName) {
Mono<Author> author = findAuthorByName(authorName);
// ...
}After that, we may be tempted to use the map() method to change the content of thecontainer from an Author to his latest Book:
Mono<Mono<Book>> book = author.map(it -> findLatestBookByAuthorId(it.authorId());
But, as we can see, this results in a nested Mono container. This happens because findLatestBookByAuthorId() returns a Mono while map() wraps the result yet another time.
4.2. flatMap()
However, if we use binding instead, we eliminate the extra container and flatten the structure. The name “flatMap” has been commonly adopted for the bind method, although there are a few exceptions where it’s called differently:
void findLatestBookOfAuthor(String authorName) {
Mono<Author> author = findAuthorByName(authorName);
Mono<Book> book = author.flatMap(it -> findLatestBookByAuthorId(it.authorId()));
// ...
}We can now simplify the code a bit by in-lining the operations, and introducing an intermediate map() that translates from the Author to its authorId:
void findLatestBookOfAuthor(String authorName) {
Mono<Book> book = findAuthorByName(authorName)
.map(Author::authorId)
.flatMap(this::findLatestBookByAuthorId));
// ...
}As we can see, combining map() and flatMap() is an efficient way of working with monads, allowing us to define a sequence of transformations in a declarative fashion.
5. Practical Use-Cases
As we have seen in the previous code examples, monads help us deal with effects by offering an extra layer of abstraction. Most of the time, they enable us to focus on the main scenario and handle the corner cases outside of the main logic.
5.1. The “Railroad” Pattern
Binding monads like this is also known as the “railroad” pattern. We can visualize the main flow by imagining a railroad going into a straight line. Additionally, if something unexpected happens, we’ll switch from the main railroad to a secondary, parallel one.
Let’s think about validating a Book object. We start by validating the book’s ISBN, then check the authorId and, finally, we validate the book’s genre:
void validateBook(Book book) {
if (!validIsbn(book.getIsbn())) {
throw new IllegalArgumentException("Invalid ISBN");
}
Author author = authorRepository.findById(book.getAuthorId());
if (author == null) {
throw new AuthorNotFoundException("Author not found");
}
if (!author.genres().contains(book.genre())) {
throw new IllegalArgumentException("Author does not write in this genre");
}
}We can use vavr’s Try monad and apply the railroad pattern to chain these validations together:
void validateBook(Book bookToValidate) {
Try.ofSupplier(() -> bookToValidate)
.andThen(book -> validateIsbn(book.getIsbn()))
.map(book -> fetchAuthor(book.getAuthorId()))
.andThen(author -> validateBookGenre(bookToValidate.genre(), author))
.get();
}
void validateIsbn(String isbn) { /* ... */ }
Author fetchAuthor(Long authorId) { /* ... */ }
void validateBookGenre(String genre, Author author) { /* ... */ }As we can see, the API exposes methods like andThen(), useful for functions where we don’t need their response. Their purpose is to check for failure, and, if required, to switch to the secondary channel. On the other hand, methods such as map() and flatMap() are meant to move the flow further, creating a new Try<> monad that wraps the response of the function, in this case, the Author object:
5.2. Recovering
In some cases, the APIs allow us to recover from the secondary channel back to the main one. Most of the time, this requires us to supply a fallback value. For instance, when using the API of Try<>, we can use the method recover() to switch from the “failure” channel back into the main one:
5.3. Other Examples
Now that we’ve learned how monads work and how to bind them using the railroad pattern, we understand that the actual names of the various methods are irrelevant. Instead, we should focus on their purpose. Most of the methods from a monad’s API:
- transform the underlying data
- if needed, switch between the channels
For example, the Optional monad uses map() and flatMap() to transform its data, respectively filter() and or() to potentially switch between “empty” and “present” states.
On the other hand, CompletableFuture uses methods like thenApply() and thenCombine() instead of map() and flatMap(), and allows us to recover from the failure channel via exceptionally().
6. Conclusion
In this article, we discussed about monads and their main characteristics. We used practical examples to understand how they can help us deal with effects such as managing collections, concurrency, nullability, exceptions etc. After that, we learned how to apply the “railroad” pattern to bind monads and push all these effects outside our component’s scope.
The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.


