Back to Stream Practice: Employee Analysis
Q23Question 23 of 43

Find the top 3 departments by average salary

Key Concepts
  • groupingBy(dept, averagingDouble(salary)) computes the average salary per department in one pass
  • Sort the entrySet descending by value with comparingByValue().reversed() to rank departments
  • limit(3) takes the top 3 after sorting
  • Collect to LinkedHashMap to preserve the sorted order in the result map
  • For bottom 3, remove .reversed() to sort ascending instead
  • The pattern generalizes to top N by any metric: count, total, average, or max

Definition

Compute average salary per department with `groupingBy` and `averagingDouble`, then sort the entrySet descending by value and `limit(3)` for the top 3.
  • groupingBy(dept, averagingDouble(salary)) produces Map of per-department averages.
  • entrySet().stream().sorted(comparingByValue().reversed()) ranks departments highest-average first.
  • limit(3) takes the top three.
  • LinkedHashMap in the final toMap collector preserves the sorted order.
  • Same pattern works for top N by count, total, max, or any other aggregated metric.

The compensation committee at National Bank of India asks: 'Which 3 departments pay the most on average?' Priya computes averages, ranks them, and presents the top 3 to the committee.

Understanding the Concepts

What does finding the top 3 departments by average salary involve?

This is a two-step problem: first aggregate (compute the average salary per department), then rank and limit (sort descending by average and take the first three). The combination of groupingBy with averagingDouble handles the first step, and sorting the entrySet with limit handles the second.

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 Top3DeptsByAvgSalary {

    record Employee(String id, String name, String department, BigDecimal salary) {}

    public static void main(String[] args) {
        List<Employee> staff = List.of(
            new Employee("SAV2001", "Rajesh",  "TRADING",    new BigDecimal("1800000")),
            new Employee("SAV2002", "Priya",   "TRADING",    new BigDecimal("1200000")),
            new Employee("SAV2003", "Meera",   "TRADING",    new BigDecimal("1500000")),
            new Employee("SAV2004", "Deepak",  "RISK",       new BigDecimal("1500000")),
            new Employee("SAV2005", "Amit",    "RISK",       new BigDecimal("900000")),
            new Employee("SAV2006", "Vikram",  "COMPLIANCE", new BigDecimal("1300000")),
            new Employee("SAV2007", "Ananya",  "COMPLIANCE", new BigDecimal("800000")),
            new Employee("SAV2008", "Suresh",  "OPERATIONS", new BigDecimal("1100000")),
            new Employee("SAV2009", "Kavita",  "OPERATIONS", new BigDecimal("700000"))
        );

        // Step 1: Compute average salary per department
        Map<String, Double> avgByDept = staff.stream()
            .collect(Collectors.groupingBy(
                Employee::department,
                Collectors.averagingDouble(e -> e.salary().doubleValue())));

        // Step 2: Sort descending and take top 3
        LinkedHashMap<String, Double> top3 = avgByDept.entrySet().stream()
            .sorted(Map.Entry.<String, Double>comparingByValue().reversed())
            .limit(3)
            .collect(Collectors.toMap(
                Map.Entry::getKey, Map.Entry::getValue,
                (a, b) -> a, LinkedHashMap::new));

        int rank = 1;
        for (var entry : top3.entrySet()) {
            System.out.printf("%d. %s: Rs.%.0f%n", rank++, entry.getKey(), entry.getValue());
        }
    }
}
TEXT
1. TRADING: Rs.1500000
2. RISK: Rs.1200000
3. COMPLIANCE: Rs.1050000

How do you get the bottom 3 instead?

Remove .reversed() from the comparator to sort ascending.

JAVA
LinkedHashMap<String, Double> bottom3 = avgByDept.entrySet().stream()
    .sorted(Map.Entry.comparingByValue())  // ascending order
    .limit(3)
    .collect(Collectors.toMap(
        Map.Entry::getKey, Map.Entry::getValue,
        (a, b) -> a, LinkedHashMap::new));

How is this different from top 3 by total salary?

| Metric | Downstream collector | Meaning |
|---|---|---|
| Average salary | averagingDouble(salary) | Mean salary per department |
| Total salary | summingDouble(salary) | Sum of all salaries in department |
| Max salary | maxBy(comparing(salary)) | Highest individual salary |
| Employee count | counting() | Number of employees |

A department with few high earners may rank first by average but lower by total. The choice depends on the business question.

What are common mistakes?

Mistake 1: Forgetting to collect into LinkedHashMap and losing the sorted order.

JAVA
// WRONG: HashMap does not preserve insertion order
Map<String, Double> wrong = avgByDept.entrySet().stream()
    .sorted(Map.Entry.<String, Double>comparingByValue().reversed())
    .limit(3)
    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
// Iteration order is unpredictable

// CORRECT: LinkedHashMap preserves insertion (sorted) order
LinkedHashMap<String, Double> correct = avgByDept.entrySet().stream()
    .sorted(Map.Entry.<String, Double>comparingByValue().reversed())
    .limit(3)
    .collect(Collectors.toMap(
        Map.Entry::getKey, Map.Entry::getValue,
        (a, b) -> a, LinkedHashMap::new));

Mistake 2: Forgetting the type witness on comparingByValue().

JAVA
// WRONG: compiler cannot infer types
.sorted(Map.Entry.comparingByValue().reversed())  // does not compile

// CORRECT: explicit type witness
.sorted(Map.Entry.<String, Double>comparingByValue().reversed())

What about using BigDecimal for exact averages?

The averagingDouble collector uses double, which may lose precision for very large salary sums. For exact banking calculations, compute manually with BigDecimal.

JAVA
Map<String, BigDecimal> exactAvg = staff.stream()
    .collect(Collectors.groupingBy(
        Employee::department,
        Collectors.collectingAndThen(
            Collectors.toList(),
            list -> {
                BigDecimal sum = list.stream()
                    .map(Employee::salary)
                    .reduce(BigDecimal.ZERO, BigDecimal::add);
                return sum.divide(BigDecimal.valueOf(list.size()),
                    2, java.math.RoundingMode.HALF_UP);
            })));

How do you parameterize N?

Accept N as a method argument and pass it to limit.

JAVA
public static LinkedHashMap<String, Double> topN(
        List<Employee> staff, int n) {
    Map<String, Double> avgByDept = staff.stream()
        .collect(Collectors.groupingBy(
            Employee::department,
            Collectors.averagingDouble(e -> e.salary().doubleValue())));

    return avgByDept.entrySet().stream()
        .sorted(Map.Entry.<String, Double>comparingByValue().reversed())
        .limit(n)
        .collect(Collectors.toMap(
            Map.Entry::getKey, Map.Entry::getValue,
            (a, b) -> a, LinkedHashMap::new));
}

What are common interview follow-up questions?

  • "How do you get the bottom 3?" Remove .reversed() to sort ascending.
  • "How do you parameterize N?" Pass it as an argument to limit(n).
  • "What if two departments have the same average?" The sort is stable, so they appear in the order encountered. Add a secondary sort by department name for determinism.
  • "How do you include the employee count alongside the average?" Use teeing (Java 12) or collect into a summary record.

Visual Overview

DIAGRAM
  Step 1: Compute averages
  ┌──────────────────────────────────────┐
  │  TRADING    → avg(1.8M, 1.2M, 1.5M) │
  │             = Rs.1,500,000           │
  │  RISK       → avg(1.5M, 0.9M)       │
  │             = Rs.1,200,000           │
  │  COMPLIANCE → avg(1.3M, 0.8M)       │
  │             = Rs.1,050,000           │
  │  OPERATIONS → avg(1.1M, 0.7M)       │
  │             = Rs.900,000             │
  └──────────────────────────────────────┘
       │
       │  sorted(desc by value) → limit(3)
       ▼
  ┌──────────────────────────────────────┐
  │  1. TRADING:    Rs.1,500,000         │
  │  2. RISK:       Rs.1,200,000         │
  │  3. COMPLIANCE: Rs.1,050,000         │
  └──────────────────────────────────────┘

Key Takeaway

Finding the top 3 departments by average salary combines groupingBy with averagingDouble to compute per-department averages, then sorts the entrySet descending by value and limits to 3. The result is collected into a LinkedHashMap to preserve the sorted order.

At the National Bank of India, the compensation committee uses this pattern to identify which departments pay the most on average. Priya presents the ranked results, and when the committee asks for the bottom 3, she simply removes .reversed() from the comparator.

The same aggregate-then-sort-then-limit pattern works for any top-N query: swap averagingDouble for summingDouble to rank by total salary, counting to rank by headcount, or maxBy to rank by the highest individual salary. The key is choosing the right downstream collector for the business question and collecting into a LinkedHashMap so the sorted order survives.

Remember one sentence: aggregate per department, sort the entrySet by value descending, and limit(3) for the top N departments.

---

Share
Use ← → arrow keys to navigate