Nested groupingBy builds a map of maps, and nothing stops the inner classifier from reaching into a joined index. Both levels use TreeMap so the report is reproducible.
record Product(String sku, String category) {}
record Sale(String sku, String region, double amount) {}
var products = List.of(new Product("S1", "pens"), new Product("S2", "books"), new Product("S3", "pens"));
var sales = List.of(new Sale("S1", "east", 10.0), new Sale("S2", "east", 50.0),
new Sale("S3", "west", 20.0), new Sale("S1", "west", 5.0));
Map<String, String> categoryOf = products.stream()
.collect(Collectors.toMap(Product::sku, Product::category));
Map<String, Map<String, Double>> byRegionThenCategory = sales.stream()
.collect(Collectors.groupingBy(Sale::region, TreeMap::new,
Collectors.groupingBy(s -> categoryOf.get(s.sku()), TreeMap::new,
Collectors.summingDouble(Sale::amount))));
byRegionThenCategory.forEach((region, m) -> System.out.println(region + " " + m));
east {books=50.0, pens=10.0}
west {pens=25.0}
Run this yourself in the Online Java Compiler, spin up a live REST API in the API Sandbox, or practise with Java interview questions.
Published 2026-09-20