📘 Premium Read: Access my best content on Medium member-only articles — deep dives into Java, Spring Boot, Microservices, backend architecture, interview preparation, career advice, and industry-standard best practices.
🎓 Top 15 Udemy Courses (80-90% Discount): My Udemy Courses - Ramesh Fadatare — All my Udemy courses are real-time and project oriented courses.
▶️ Subscribe to My YouTube Channel (176K+ subscribers): Java Guides on YouTube
▶️ For AI, ChatGPT, Web, Tech, and Generative AI, subscribe to another channel: Ramesh Fadatare on YouTube
If you’ve worked with Java Streams, chances are you’ve used map()
. It’s the go-to method for transforming data in a stream. But at some point, you’ll try to map a stream of items — and get stuck with a stream of streams.
That’s where flatMap()
comes in.
If map()
transforms data, then flatMap()
flattens it. It may sound technical, but in real-world Java code, flatMap()
solves very practical problems — especially when working with nested collections, optional values, or asynchronous data structures.
In this article, we’ll cover:
- What
flatMap()
is (in simple terms) - How it works with Java Streams and Optionals
- Real-world use cases with full code examples
- Why it matters for clean, modern Java
Let’s break it down step by step.
What is flatMap()
?
In Java, map()
takes each element of a stream and transforms it into something else.
flatMap()
goes one step further:
It transforms each element into a stream and then flattens all the resulting streams into one single stream.
A Quick Analogy
Imagine a list of envelopes. Each envelope contains a list of documents.
If you use map()
, you’ll still get a list of envelopes with documents inside (nested).
If you use flatMap()
, you open all envelopes and put all documents into one big pile — one flat list.
Basic Example: List of Lists
Let’s start with a simple case.
Scenario: You have a list of customers, and each customer has a list of phone numbers.
class Customer {
private String name;
private List<String> phoneNumbers;
// constructor, getters
}
Sample Data:
List<Customer> customers = List.of(
new Customer("Alice", List.of("123", "456")),
new Customer("Bob", List.of("789")),
new Customer("Charlie", List.of("101", "102", "103"))
);
❌ Using map()
Stream<List<String>> numbersStream = customers.stream()
.map(Customer::getPhoneNumbers);
This gives you a Stream<List<String>>
— a stream of lists, not a stream of strings.
✅ Using flatMap()
List<String> allNumbers = customers.stream()
.flatMap(c -> c.getPhoneNumbers().stream())
.collect(Collectors.toList());
Result:
[123, 456, 789, 101, 102, 103]
flatMap()
gives you one flattened stream of phone numbers — no nested lists.
Real-World Use Case: Search Across Nested Data
Let’s say you’re building a course management system. Each Course
has a list of Module
s, and each module has a list of Topic
s.
class Course {
private String name;
private List<Module> modules;
// constructor, getters
}
class Module {
private String title;
private List<String> topics;
// constructor, getters
}
Task: Get a list of all topic names from all courses.
✅ Using flatMap()
Twice
List<String> topics = courses.stream()
.flatMap(course -> course.getModules().stream())
.flatMap(module -> module.getTopics().stream())
.collect(Collectors.toList());
Here’s what’s happening:
- First
flatMap()
flattens modules from all courses. - Second
flatMap()
flattens topics from all modules.
🎯 Clean and concise — no for-loops, no temporary lists.
flatMap()
vs map()
in Simple Terms

🔄 Another Real-World Scenario: Optional Chaining
Say you have an Optional<User>
, and a user may or may not have an address:
Optional<User> userOpt = getUser(); // may be empty
class User {
private Optional<Address> address;
// getters
}
You want to get the user’s city, safely.
❌ map() leads to nested Optional
Optional<Optional<Address>> addressOpt = userOpt.map(User::getAddress);
Now you have an Optional<Optional<Address>>
. You’ll need extra calls to unwrap it.
✅ flatMap() avoids nesting
Optional<Address> address = userOpt.flatMap(User::getAddress);
Now you get an Optional<Address>
directly. Much cleaner.
You can keep chaining:
Optional<String> city = userOpt
.flatMap(User::getAddress)
.map(Address::getCity);
✅ Safe, null-free code
✅ No need for multipleifPresent()
checks
Use Case: Processing API Responses with Nested Lists
Suppose you consume a REST API that returns a list of blog posts. Each post has multiple tags.
You want to build a list of all unique tags across posts.
Sample Classes:
class BlogPost {
private String title;
private List<String> tags;
// constructor, getters
}
Sample Data:
List<BlogPost> posts = List.of(
new BlogPost("Intro to Java", List.of("java", "basics")),
new BlogPost("Advanced Streams", List.of("java", "streams")),
new BlogPost("Spring Boot Guide", List.of("spring", "java"))
);
✅ Solution with flatMap()
Set<String> uniqueTags = posts.stream()
.flatMap(post -> post.getTags().stream())
.collect(Collectors.toSet());
Output:
[java, basics, streams, spring]
Simple way to transform and flatten nested data
No extra data structures or loops
When Should You Use flatMap()
?
Use flatMap()
when:
✅ You have nested lists (e.g. List of Lists)
✅ You’re working with Optional<Optional<T>>
✅ You need to flatten complex object relationships
✅ You're dealing with tree or graph-like structures
✅ You want to chain asynchronous or reactive streams
Unit Testing with flatMap()
Let’s say you write a method:
public List<String> getAllTopics(List<Course> courses) {
return courses.stream()
.flatMap(course -> course.getModules().stream())
.flatMap(module -> module.getTopics().stream())
.collect(Collectors.toList());
}
Test it like any other method:
@Test
void testGetAllTopics() {
List<Course> courses = // mock or sample data
List<String> topics = myService.getAllTopics(courses);
assertTrue(topics.contains("OOP"));
assertEquals(5, topics.size());
}
flatMap()
doesn’t make code harder to test — in fact, it makes logic more predictable and isolated.
What About flatMap in Other Libraries?
If you’re using Project Reactor or RxJava, the concept of flatMap()
is the same — flattening nested structures.
Example in Reactor:
Mono<List<String>> mono = getMonoUser()
.flatMap(user -> getOrdersForUser(user.getId())); // returns Mono<List<String>>
Example in CompletableFuture:
CompletableFuture<List<String>> allTagsFuture = postFuture
.thenCompose(post -> getTagsForPost(post)); // thenCompose = flatMap
Even though the syntax changes, the concept of flattening nested results stays the same.
✅ Key Benefits of flatMap

🚫 When Not to Use flatMap()
Avoid flatMap()
when:
- You don’t need to flatten anything
- You’re returning simple values (use
map()
) - The stream inside
flatMap()
could benull
— it must always return a stream - You don’t fully understand the structure — always know what you’re flattening
Conclusion
The flatMap()
method is a must-know tool for any Java developer working with streams, optionals, or modern APIs.
It helps you:
- Cleanly process nested data
- Chain optional and stream operations
- Avoid deep nesting and boilerplate code
- Write functional-style, maintainable logic
If you’ve ever been frustrated with a Stream<Optional<T>>
or List<List<T>>
, learning how to use flatMap()
will unlock much cleaner solutions.
Java may be a verbose language, but with flatMap()
, your code can stay concise and clear.
Comments
Post a Comment
Leave Comment