Back to Stream: Grouping & Partitioning
Q20Question 20 of 22

Convert a flat List<Employee> into a Map<String, Map<String, List<Employee>>> grouped by department then by city

Key Concepts
  • Nested groupingBy(dept, groupingBy(city)) builds a two-level map in a single collect pass
  • The outer classifier is department, the inner classifier (downstream) is city
  • The innermost default collector is toList(), producing List<Employee> at the leaves
  • Use TreeMap supplier at each level for sorted keys: groupingBy(dept, TreeMap::new, groupingBy(city, TreeMap::new, toList()))
  • Guard null department and city before grouping to avoid stray null keys
  • The pattern generalizes to any number of nesting levels by adding more groupingBy layers

Definition

`groupingBy(dept, groupingBy(city))` builds a two-level department-then-city map in a single collect pass.
  • Nested groupingBy composes two classifiers: the outer groups by department, the inner groups by city.
  • Result type is Map>> where 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 for counting(), 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?

JAVA
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() + ")"));
            });
        });
    }
}
TEXT
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.

JAVA
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.

JAVA
// 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.

JAVA
// 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.

JAVA
// 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() for counting() as the innermost collector.
  • "Can you do city-then-department instead?" Swap the two classifiers.
  • "How do you get a sorted result?" Use TreeMap::new as the map supplier at each level.

Visual Overview

DIAGRAM
  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.

---

Share
Use ← → arrow keys to navigate