Back to Stream: Grouping & Partitioning
Q8Question 8 of 22

Sum salaries per department as BigDecimal using groupingBy with Collectors.reducing

Key Concepts
  • Collectors.reducing(identity, mapper, op) reduces each group to one value starting from an identity.
  • groupingBy(dept, reducing(ZERO, getSalary, BigDecimal::add)) totals salary per department exactly.
  • The result type is Map<String, BigDecimal>, with no floating-point error.
  • BigDecimal is mandatory for money because double cannot represent decimals like 0.1 exactly.
  • summingDouble exists but is wrong for currency because it accumulates rounding error.
  • The reducing operator must be associative so it stays correct under parallel streams.
  • Use BigDecimal.ZERO as the identity so an empty group totals to exactly zero.

Definition

`groupingBy(dept, reducing(BigDecimal.ZERO, Employee::getSalary, BigDecimal::add))` totals each department's salaries exactly, with no floating-point error.

Collectors.reducing is a downstream collector that folds a group into a single value.

  • reducing(identity, mapper, op) – starts from identity, maps each element with mapper, and combines running results with the associative operator op.
  • Nested in groupingBy – it totals a mapped field per group, here each employee's BigDecimal salary summed per department.
  • Result shape – Map of total salary per department.
  • BigDecimal for money – stores decimal digits exactly, so sums never drift.
  • Associative operator – BigDecimal::add is associative, so the result is correct even in parallel.

Rajesh at the National Bank of India sorts salary slips into department trays. Then, for each tray, he adds up the rupee amounts on a precise calculator that never rounds, which is BigDecimal. reducing is the rule add them all starting from zero applied inside each tray.

Understanding the Concepts

What is Collectors.reducing?

Collectors.reducing folds the elements of a group into a single value. The three-argument form reducing(identity, mapper, op) is the most useful as a downstream. It starts from the identity, applies the mapper to each element to get a value, and combines running results with the operator.

JAVA
package com.bank.finance;

import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class SalaryTotals {
    public static Map<String, BigDecimal> totalByDepartment(List<Employee> employees) {
        return employees.stream()
                .collect(Collectors.groupingBy(
                        Employee::getDepartment,
                        Collectors.reducing(BigDecimal.ZERO,
                                Employee::getSalary,
                                BigDecimal::add)));
    }
}

Why does reducing need an identity, a mapper, and an operator?

The identity is the starting value and the result for an empty group, so for addition it is BigDecimal.ZERO. The mapper extracts the value to accumulate, here the salary. The operator combines two accumulated values and must be associative so grouping and parallel evaluation stay correct.

JAVA
// identity = BigDecimal.ZERO  -> empty department totals to exactly 0
// mapper   = Employee::getSalary -> pull the BigDecimal salary
// operator = BigDecimal::add  -> associative, parallel-safe addition
Collectors.reducing(BigDecimal.ZERO, Employee::getSalary, BigDecimal::add);

How does reducing work internally as a downstream?

Inside groupingBy, each department gets its own running accumulator seeded with the identity. As each employee arrives in that group, reducing maps it to a salary and combines it into the accumulator with the operator. When the stream ends, each department's accumulator is the final total.

JAVA
package com.bank.finance;

import java.math.BigDecimal;

public class Employee {
    private final String name;
    private final String department;
    private final BigDecimal salary;

    public Employee(String name, String department, BigDecimal salary) {
        this.name = name;
        this.department = department;
        this.salary = salary;
    }

    public String getName() { return name; }
    public String getDepartment() { return department; }
    public BigDecimal getSalary() { return salary; }
}

How do you run a full example?

JAVA
package com.bank.finance;

import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class SalaryTotalsDemo {
    public static void main(String[] args) {
        List<Employee> employees = List.of(
                new Employee("Rajesh", "Trading", new BigDecimal("1200000")),
                new Employee("Priya", "Trading", new BigDecimal("1500000")),
                new Employee("Amit", "IT", new BigDecimal("900000")),
                new Employee("Pankaj", "IT", new BigDecimal("1100000")));

        Map<String, BigDecimal> totals = employees.stream()
                .collect(Collectors.groupingBy(
                        Employee::getDepartment,
                        Collectors.reducing(BigDecimal.ZERO,
                                Employee::getSalary,
                                BigDecimal::add)));

        totals.forEach((dept, total) ->
                System.out.println(dept + " -> " + total));
    }
}
BASH
javac com/bank/finance/Employee.java com/bank/finance/SalaryTotalsDemo.java
java com.bank.finance.SalaryTotalsDemo
TEXT
IT -> 2000000
Trading -> 2700000

The Trading total of 1,200,000 plus 1,500,000 is exactly 2,700,000 as a BigDecimal, with no rounding.

Why does BigDecimal matter for currency?

Money must be exact. A double cannot represent values like 0.1 precisely, so summing many monetary amounts as double accumulates rounding error that eventually shows up as a few paise off, which breaks reconciliation in banking. BigDecimal stores decimal digits exactly.

JAVA
// WRONG: double drifts when you add many decimal amounts.
double wrong = 0.0;
for (int i = 0; i < 10; i++) {
    wrong += 0.1;
}
System.out.println(wrong); // 0.9999999999999999, not 1.0

// CORRECT: BigDecimal stays exact.
BigDecimal right = BigDecimal.ZERO;
for (int i = 0; i < 10; i++) {
    right = right.add(new BigDecimal("0.1"));
}
System.out.println(right); // 1.0 exactly

How is reducing different from summingDouble?

summingDouble is simpler and exists for exactly this shape, but it uses double internally, so it is wrong for currency. For non-money metrics like latency or age it is fine.

| Aspect | reducing with BigDecimal | summingDouble |
|--------|--------------------------|---------------|
| Internal type | BigDecimal, exact | double, lossy |
| Correct for money | yes | no |
| Result type | BigDecimal | Double |
| Verbosity | more arguments | one mapper |
| Good for | currency, ledgers | age, latency, counts |

JAVA
// WRONG for money: summingDouble uses double.
Map<String, Double> lossy = employees.stream()
        .collect(Collectors.groupingBy(
                Employee::getDepartment,
                Collectors.summingDouble(e -> e.getSalary().doubleValue())));

// CORRECT for money: reducing with BigDecimal::add.
Map<String, BigDecimal> exact = employees.stream()
        .collect(Collectors.groupingBy(
                Employee::getDepartment,
                Collectors.reducing(BigDecimal.ZERO,
                        Employee::getSalary, BigDecimal::add)));

What are common mistakes?

A common mistake is using the one-argument reducing(op) which returns Optional and has no identity, making the downstream type awkward. Another is picking a non-associative operator. Another is using double anywhere in the chain.

JAVA
// WRONG: one-argument reducing yields Optional<BigDecimal> per group.
Map<String, java.util.Optional<BigDecimal>> awkward = employees.stream()
        .collect(Collectors.groupingBy(
                Employee::getDepartment,
                Collectors.reducing(BigDecimal::add)));

// CORRECT: three-argument reducing yields a plain BigDecimal total.
Map<String, BigDecimal> clean = employees.stream()
        .collect(Collectors.groupingBy(
                Employee::getDepartment,
                Collectors.reducing(BigDecimal.ZERO,
                        Employee::getSalary, BigDecimal::add)));

What are the edge cases?

An empty department does not appear in the map at all, because grouping only creates keys for elements that exist. Within a present group, the identity guarantees the sum starts at zero. A null salary would throw inside BigDecimal::add.

JAVA
// Edge case: guard a possibly-null salary before adding.
Map<String, BigDecimal> safe = employees.stream()
        .collect(Collectors.groupingBy(
                Employee::getDepartment,
                Collectors.reducing(BigDecimal.ZERO,
                        e -> e.getSalary() == null ? BigDecimal.ZERO : e.getSalary(),
                        BigDecimal::add)));

Why must the operator be associative?

Streams may be evaluated in parallel, splitting a group across threads and combining partial results. If the operator is not associative, the combined result depends on the order of combination and becomes wrong. BigDecimal::add is associative, so (a + b) + c equals a + (b + c).

JAVA
// Associative: order of combination does not change the total.
BigDecimal a = new BigDecimal("100");
BigDecimal b = new BigDecimal("200");
BigDecimal c = new BigDecimal("300");
assert a.add(b).add(c).equals(a.add(b.add(c))); // true, parallel-safe

// Parallel total still correct because add is associative.
Map<String, BigDecimal> parallelTotals = employees.parallelStream()
        .collect(Collectors.groupingBy(
                Employee::getDepartment,
                Collectors.reducing(BigDecimal.ZERO,
                        Employee::getSalary, BigDecimal::add)));

When should you use reducing, and when not?

Use reducing with BigDecimal::add whenever you total money per group, because it is exact. Do not use it for non-money numeric metrics where a double is acceptable and summingDouble or summingInt reads more simply. Do not use the one-argument form when you want a plain total rather than an Optional.

JAVA
// Use reducing with BigDecimal: exact money totals.
Map<String, BigDecimal> money = employees.stream()
        .collect(Collectors.groupingBy(Employee::getDepartment,
                Collectors.reducing(BigDecimal.ZERO,
                        Employee::getSalary, BigDecimal::add)));

// Do NOT use reducing here: counting is clearer for sizes.
Map<String, Long> headcount = employees.stream()
        .collect(Collectors.groupingBy(Employee::getDepartment,
                Collectors.counting()));

What are common interview follow-up questions?

Interviewers ask why money is never a double, how reducing differs from summingDouble and from Stream.reduce, why the operator must be associative, what the identity is for, and how to handle empty or null values. A senior answer notes that for very large sums you may prefer a custom collector that sets a target scale, and that Collectors.summingDouble should be reserved strictly for non-monetary metrics.

JAVA
// Follow-up: combine grouping total with a formatted display scale.
import java.math.RoundingMode;

Map<String, BigDecimal> displayTotals = employees.stream()
        .collect(Collectors.groupingBy(
                Employee::getDepartment,
                Collectors.collectingAndThen(
                        Collectors.reducing(BigDecimal.ZERO,
                                Employee::getSalary, BigDecimal::add),
                        total -> total.setScale(2, RoundingMode.HALF_EVEN))));

Visual Overview

DIAGRAM
  List<Employee>
       │
       │  groupingBy(dept, reducing(ZERO, getSalary, add))
       ▼
  ┌──────────────────────────────────────────────────┐
  │  per department: start acc = BigDecimal.ZERO        │
  │                                                     │
  │  Trading: 0 +1,200,000 +1,500,000 = 2,700,000       │
  │  IT:      0   +900,000 +1,100,000 = 2,000,000       │
  └──────────────────────────────────────────────────┘
       │
       ▼
  Map<String, BigDecimal>
DIAGRAM
  Why BigDecimal, not double:

  double add 0.1 ten times
  ┌──────────────────────────────┐
  │ 0.1+0.1+... = 0.9999999999999 │  drift -> reconciliation fails
  └──────────────────────────────┘

  BigDecimal add "0.1" ten times
  ┌──────────────────────────────┐
  │ 0.1+0.1+... = 1.0  (exact)    │  safe for the ledger
  └──────────────────────────────┘

Key Takeaway

To total a mapped field per group, Rajesh at the National Bank of India uses groupingBy with a reducing downstream collector. The three-argument reducing takes an identity, a mapper, and an associative operator: it starts from the identity, maps each element to a partial value, and combines them with the operator. For salaries he uses BigDecimal.ZERO as the identity, Employee::getSalary as the mapper, and BigDecimal::add as the operator, producing a Map of department to total salary.

The finance team needs the exact salary cost per department for the monthly budget, and exactness is non-negotiable because even a one-paisa drift across thousands of employees breaks reconciliation. That is why every salary is a BigDecimal and the totals are summed with BigDecimal::add, not with summingDouble, which uses double and would accumulate floating-point rounding error.

The senior point to always stress is that money must never be a double or float, because those cannot represent many decimal fractions exactly, so repeated addition drifts. BigDecimal stores decimal digits precisely. The demonstration is simple: adding 0.1 ten times as a double gives 0.9999999999999999, while adding "0.1" ten times as a BigDecimal gives exactly 1.0.

Keeping the reducing operator associative matters so the computation stays correct if the stream is ever parallelized, and BigDecimal::add satisfies that. For currency, prefer reducing with BigDecimal::add over any double-based collector, guard null salaries, and set an explicit display scale with collectingAndThen when the total feeds a report.

Remember one sentence: to total money per group, nest reducing with BigDecimal.ZERO and BigDecimal::add inside groupingBy, and never let a double touch the amount.

---

Share
Use ← → arrow keys to navigate