Definition
- Nested groupingBy composes two classifiers: the outer groups by department, the inner groups by city.
- Result type is
Mapwhere department maps to a city-to-employees map.>> - Single pass through the stream builds the entire two-level hierarchy without nested loops.
- TreeMap supplier at each level gives sorted department and city keys.
- Innermost collector defaults to
toList()but can be swapped forcounting(),mapping(), or any other collector.
At the National Bank of India, Priya first sorts employees into department drawers, then inside each drawer she sorts them into city folders. The result is a filing cabinet where opening a department drawer and then a city folder shows exactly the employees in that department and city.
Understanding the Concepts
What is a two-level grouping?
A two-level grouping organizes data into a hierarchy with two classification levels. The first level groups by one attribute (department), and within each first-level group the second level groups by another attribute (city). The result is a nested map: Map.
This replaces nested loops with a single declarative collect call.
How do you implement it with streams?
package com.bank.hr;
import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;
public class TwoLevelGrouping {
record Employee(String id, String name, String department,
String city, BigDecimal salary) {}
public static void main(String[] args) {
List<Employee> staff = List.of(
new Employee("SAV2001", "Rajesh", "TRADING", "Mumbai", new BigDecimal("1800000")),
new Employee("SAV2002", "Priya", "TRADING", "Delhi", new BigDecimal("1200000")),
new Employee("SAV2003", "Amit", "RISK", "Mumbai", new BigDecimal("900000")),
new Employee("SAV2004", "Kavita", "RISK", "Mumbai", new BigDecimal("1100000")),
new Employee("SAV2005", "Meera", "TRADING", "Mumbai", new BigDecimal("1500000")),
new Employee("SAV2006", "Ananya", "COMPLIANCE", "Delhi", new BigDecimal("1050000"))
);
Map<String, Map<String, List<Employee>>> deptThenCity = staff.stream()
.filter(e -> e.department() != null && e.city() != null)
.collect(Collectors.groupingBy(
Employee::department,
Collectors.groupingBy(Employee::city)));
deptThenCity.forEach((dept, cityMap) -> {
System.out.println(dept + ":");
cityMap.forEach((city, employees) -> {
System.out.println(" " + city + ":");
employees.forEach(e ->
System.out.println(" " + e.name() + " (Rs." + e.salary() + ")"));
});
});
}
}
TRADING:
Mumbai:
Rajesh (Rs.1800000)
Meera (Rs.1500000)
Delhi:
Priya (Rs.1200000)
RISK:
Mumbai:
Amit (Rs.900000)
Kavita (Rs.1100000)
COMPLIANCE:
Delhi:
Ananya (Rs.1050000)
How do you add sorted keys at each level?
Use the three-argument groupingBy with a TreeMap supplier to sort keys alphabetically.
Map<String, Map<String, List<Employee>>> sorted = staff.stream()
.filter(e -> e.department() != null && e.city() != null)
.collect(Collectors.groupingBy(
Employee::department,
TreeMap::new,
Collectors.groupingBy(
Employee::city,
TreeMap::new,
Collectors.toList())));
// Both department and city keys iterate in alphabetical order
How do you change the leaf collector?
Swap toList() for any other collector to change what appears at the leaves.
// Count employees per department-city combination
Map<String, Map<String, Long>> counts = staff.stream()
.collect(Collectors.groupingBy(
Employee::department,
Collectors.groupingBy(
Employee::city,
Collectors.counting())));
// Collect just names per department-city
Map<String, Map<String, List<String>>> names = staff.stream()
.collect(Collectors.groupingBy(
Employee::department,
Collectors.groupingBy(
Employee::city,
Collectors.mapping(Employee::name, Collectors.toList()))));
How is this different from a flat groupingBy?
| Aspect | Flat groupingBy(dept) | Nested groupingBy(dept, groupingBy(city)) |
|---|---|---|
| Result type | Map | Map |
| Levels | One | Two (or more) |
| Access pattern | map.get("TRADING") | map.get("TRADING").get("Mumbai") |
| Use case | Single dimension | Hierarchy / drill-down |
What are common mistakes?
Mistake: Not guarding null keys, which creates stray null entries in the map.
// WRONG: null department or city creates null keys
staff.stream().collect(Collectors.groupingBy(
Employee::department,
Collectors.groupingBy(Employee::city))); // NPE or null key
// CORRECT: filter nulls before grouping
staff.stream()
.filter(e -> e.department() != null && e.city() != null)
.collect(Collectors.groupingBy(
Employee::department,
Collectors.groupingBy(Employee::city)));
What about three or more levels?
The pattern extends to any depth by nesting further.
// Three levels: department -> city -> grade
Map<String, Map<String, Map<String, List<Employee>>>> threeLevels = staff.stream()
.collect(Collectors.groupingBy(
Employee::department,
Collectors.groupingBy(
Employee::city,
Collectors.groupingBy(e -> e.salary().compareTo(new BigDecimal("1200000")) > 0
? "Senior" : "Junior"))));
What should a senior engineer know?
A senior engineer knows that nested groupingBy is composable: each level is just a classifier, and the innermost is just a downstream collector. This makes the pattern declarative and extensible. The same mechanism that builds two levels builds three or more by nesting further. A senior also watches for null keys at every level and chooses the leaf collector based on what the consumer needs: the full employees, their names, or counts.
What are common interview follow-up questions?
- "How do you flatten the result back?" Iterate both levels and collect into a single list.
- "How do you count employees per department-city?" Swap
toList()forcounting()as the innermost collector. - "Can you do city-then-department instead?" Swap the two classifiers.
- "How do you get a sorted result?" Use
TreeMap::newas the map supplier at each level.
Visual Overview
Input: flat List<Employee>
│
│ groupingBy(department)
▼
┌────────────────────────────────────────┐
│ TRADING → [Rajesh, Priya, Meera] │
│ RISK → [Amit, Kavita] │
│ COMPLIANCE → [Ananya] │
└────────────────────────────────────────┘
│
│ downstream: groupingBy(city)
▼
┌────────────────────────────────────────────────┐
│ TRADING │
│ ├── Mumbai → [Rajesh, Meera] │
│ └── Delhi → [Priya] │
│ RISK │
│ └── Mumbai → [Amit, Kavita] │
│ COMPLIANCE │
│ └── Delhi → [Ananya] │
└────────────────────────────────────────────────┘
Key Takeaway
To build a two-level grouping by department and then by city, you nest two groupingBy collectors. The outer classifier is the department and the inner classifier, supplied as the downstream collector, is the city. The result is a map from department to a map from city to the employees in that department and city, all built in a single pass through the stream.
At the National Bank of India, this pattern backs an org browser that navigates employees first by department and then by city. Priya uses a TreeMap supplier at each level so the keys come out alphabetically, and she swaps the innermost collector for counting() when the dashboard only needs headcounts rather than full employee lists.
Nesting collectors is how you express a hierarchy declaratively without writing nested loops. Each level is just a classifier, the innermost defaults to collecting into a list, and the same mechanism generalizes to any number of levels by nesting further. A senior engineer guards null department and city so missing values do not create stray keys, and chooses the leaf collector deliberately based on what the consumer needs.
Remember one sentence: groupingBy(dept, groupingBy(city)) builds a two-level hierarchy in a single declarative collect.
---