Introduction
In the previous articles, we explored Collectors.groupingBy() and discovered how it can transform large collections into meaningful reports, dashboards, and analytical summaries.
groupingBy() is ideal when data needs to be classified into multiple categories.
Examples include:
- Employees by department
- Customers by city
- Transactions by branch
- Orders by status
But what happens when there are only two possible outcomes?
Business systems constantly ask binary questions:
- Is the customer active?
- Is the loan approved?
- Is the account blocked?
- Is the transaction fraudulent?
- Is the order shipped?
- Is the employee eligible for promotion?
Although groupingBy() can solve these problems, Java provides a more specialized and expressive collector:
Collectors.partitioningBy()
This collector divides data into exactly two partitions based on a boolean condition.
It is one of the simplest collectors in the Stream API, yet it appears frequently in enterprise systems where business rules naturally evaluate to true or false.
Learning Objectives
By the end of this article, you will be able to:
- Understand how
partitioningBy()works. - Compare
partitioningBy()withgroupingBy(). - Build binary classifications.
- Combine partitioning with downstream collectors.
- Apply partitioning in Spring Boot microservices.
- Recognize when
partitioningBy()is the better design choice.
Why Another Collector?
Suppose we want to separate active and inactive employees.
A developer might write:
Map<Boolean, List<Employee>> result =
employees.stream()
.collect(Collectors.groupingBy(Employee::isActive));
This works.
However, the intent is clearer with:
Map<Boolean, List<Employee>> result =
employees.stream()
.collect(Collectors.partitioningBy(Employee::isActive));
Both return a Map<Boolean, List<Employee>>, but partitioningBy() communicates that there are exactly two logical groups.
How partitioningBy() Works
The simplest form is:
Collectors.partitioningBy(predicate)
The predicate decides which partition receives each element.
Example:
Map<Boolean, List<Employee>> employeesByStatus =
employees.stream()
.collect(Collectors.partitioningBy(Employee::isActive));
The resulting map contains two keys:
true
false
Unlike groupingBy(), these two partitions are always represented in the result, even if one partition ends up empty.
Visualizing the Result
Input:
Alice Active
Bob Active
Carol Inactive
David Active
Output:
true
Alice
Bob
David
false
Carol
Enterprise Example 1 – Loan Approval
Business Requirement:
Separate approved and rejected loan applications.
Map<Boolean, List<LoanApplication>> loans =
applications.stream()
.collect(Collectors.partitioningBy(
LoanApplication::isApproved));
This result can be used to populate approval dashboards.
Enterprise Example 2 – Fraud Detection
Business Requirement:
Separate suspicious transactions.
Map<Boolean, List<Transaction>> result =
transactions.stream()
.collect(Collectors.partitioningBy(
transaction ->
transaction.getAmount()
.compareTo(new BigDecimal("1000000")) > 0));
Output:
true
High-value transactions
false
Normal transactions
This pattern frequently appears in fraud detection systems.
Enterprise Example 3 – Feature Flags
Map<Boolean, List<Customer>> rollout =
customers.stream()
.collect(Collectors.partitioningBy(
Customer::isFeatureEnabled));
Useful for:
- Canary releases
- A/B testing
- Progressive deployments
Downstream Partitioning
Just like groupingBy(), partitioningBy() supports downstream collectors.
Example:
Count active and inactive employees.
Map<Boolean, Long> counts =
employees.stream()
.collect(Collectors.partitioningBy(
Employee::isActive,
Collectors.counting()));
Output:
true
182
false
24
Summing Values Inside Partitions
Business Requirement:
Calculate total account balances.
Map<Boolean, BigDecimal> totals =
accounts.stream()
.collect(Collectors.partitioningBy(
Account::isPremium,
Collectors.reducing(
BigDecimal.ZERO,
Account::getBalance,
BigDecimal::add)));
Each partition now contains an aggregated financial value.
Mapping Inside Partitions
Map<Boolean, List<String>> names =
employees.stream()
.collect(Collectors.partitioningBy(
Employee::isActive,
Collectors.mapping(
Employee::getName,
Collectors.toList())));
Output:
true
Alice
Bob
false
Carol
partitioningBy() vs groupingBy()
| Requirement | Recommended Collector |
|---|---|
| Two logical groups | partitioningBy() |
| Multiple categories | groupingBy() |
| Department | groupingBy() |
| City | groupingBy() |
| Status (Approved/Rejected) | partitioningBy() |
| Premium/Standard | partitioningBy() |
| Country | groupingBy() |
Choosing the appropriate collector makes code easier to understand.
Spring Boot Integration
Suppose an HR system needs to return active and inactive employees separately.
Service
@Service
public class EmployeeService {
public Map<Boolean, List<Employee>> employeesByStatus() {
return repository.findAll()
.stream()
.collect(Collectors.partitioningBy(
Employee::isActive));
}
}
REST Controller
@RestController
@RequestMapping("/employees")
public class EmployeeController {
@GetMapping("/partition")
public Map<Boolean, List<Employee>> partition() {
return employeeService.employeesByStatus();
}
}
Migration from Java 7
Java 7
List<Employee> active = new ArrayList<>();
List<Employee> inactive = new ArrayList<>();
for (Employee employee : employees) {
if (employee.isActive()) {
active.add(employee);
} else {
inactive.add(employee);
}
}
Java 8+
Map<Boolean, List<Employee>> result =
employees.stream()
.collect(Collectors.partitioningBy(Employee::isActive));
The Stream version is shorter, less error-prone, and scales naturally when combined with downstream collectors.
Performance Considerations
- Use
partitioningBy()only when the outcome is naturally boolean. - Avoid converting non-boolean classifications into boolean simply to use this collector.
- Like other collectors, it processes the Stream in a single traversal.
- For large datasets stored in a database, consider whether filtering or aggregation should occur in SQL before data reaches the application.
Common Mistakes
Using partitioningBy() for Multiple Categories
Bad example:
Department
IT
HR
Finance
This is not a binary classification.
Use groupingBy() instead.
Ignoring Downstream Collectors
Many developers first partition data and then iterate again to count or sum values.
Instead:
Collectors.partitioningBy(
Employee::isActive,
Collectors.counting())
performs both operations in one pass.
Best Practices
- Use
partitioningBy()only for true/false classifications. - Combine it with downstream collectors whenever additional aggregation is required.
- Keep predicates simple and business-focused.
- Prefer meaningful predicate methods such as
isApproved()orisPremium()over complex inline expressions when possible.
Interview Questions
What is the difference between partitioningBy() and groupingBy()?
partitioningBy() always creates two boolean partitions (true and false). groupingBy() groups elements by any key type and may produce any number of groups.
Does partitioningBy() always return both keys?
Yes. The result map contains entries for both true and false, even if one partition is empty.
Can partitioningBy() use downstream collectors?
Yes. It supports downstream collectors such as counting(), mapping(), reducing(), and many others.
Hands-On Exercise
Build a Spring Boot API that:
- Partitions customers into premium and non-premium.
- Counts customers in each partition.
- Calculates the total balance for each partition.
- Returns customer names in each partition.
- Exposes the result through a REST endpoint.
Then compare the implementation with an equivalent Java 7 solution using two separate lists.
Summary
Collectors.partitioningBy() is a specialized collector designed for one of the most common business scenarios in enterprise software—binary classification. It provides a clear and expressive way to separate data into true and false groups while supporting the same powerful downstream aggregation capabilities available with groupingBy().
Whenever your business rule naturally answers a yes/no question, partitioningBy() is often the clearest and most maintainable choice.
Coming Up Next
Part 11 – Custom Collectors: Building Your Own Aggregation Engine
We’ll go beneath the surface of the Stream API and learn how to implement custom collectors using the Collector interface. You’ll understand the roles of the Supplier, Accumulator, Combiner, Finisher, and Characteristics, and build production-ready collectors for enterprise reporting, financial calculations, and domain-specific aggregation.