Java flatMap Explained with Examples

📘 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 Modules, and each module has a list of Topics.

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:

  1. First flatMap() flattens modules from all courses.
  2. 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 multiple
ifPresent() 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 be null — 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

Spring Boot 3 Paid Course Published for Free
on my Java Guides YouTube Channel

Subscribe to my YouTube Channel (165K+ subscribers):
Java Guides Channel

Top 10 My Udemy Courses with Huge Discount:
Udemy Courses - Ramesh Fadatare