Production incident

The report that crashed on real data

40 minintermediate210 yrs

A real incident: symptom first, cause hidden, tradeoff at the end.

The incident

A daily sales report. It has full test coverage, it runs against a seeded fixture in staging every night, and it threw on its first morning in production — three times, in three different places. What the team found: 1. The staging fixture has one sale per region, every sale settled, and a promo code on every row. Production has none of those properties. 2. Each failure is a different exception from a different collector. 3. One of them is a NullPointerException on a value that a HashMap would have accepted without complaint. 4. Adding rows to the fixture makes the tests fail too — which is how the team eventually reproduced it. Every one of these is a collector's documented behaviour. Fix all three, and for each say which assumption the fixture was silently making.

What this teaches

  • Two-argument toMap asserts the key is unique; it does not merge
  • groupingBy creates only the keys it saw, so get(true) can return null
  • toMap rejects a null value because it is implemented with Map.merge
  • partitioningBy always returns both keys, which is what it is for
  • A fixture that avoids every edge case tests nothing about them

Starter

Starter.java
import java.util.*;
import java.util.function.*;
import java.util.stream.*;

/**
 * Production: the report that crashed on real data.
 *
 * A daily sales report. It passed every test, ran fine in staging against the
 * seeded fixture, and threw on the first morning in production.
 *
 * Three separate collector defects, each of which is invisible on tidy data.
 * Run it — three failures.
 */
public class Starter {

    record Sale(String region, String rep, boolean settled, String promoCode, int amount) { }

    /** The staging fixture: one sale per region, all settled, all promoted. */
    static List<Sale> fixture() {
        return List.of(
            new Sale("north", "Ana", true, "SPRING", 120),
            new Sale("south", "Bo", true, "SPRING", 95));
    }

    /** What actually arrived: repeated regions, nothing settled, missing promos. */
    static List<Sale> production() {
        return List.of(
            new Sale("north", "Ana", false, "SPRING", 120),
            new Sale("north", "Bo", false, null, 95),
            new Sale("south", "Cy", false, null, 70));
    }

    /** DEFECT 1: assumes one sale per region. */
    static Map<String, String> repByRegion(List<Sale> sales) {
        return sales.stream().collect(Collectors.toMap(Sale::region, Sale::rep));
    }

    /** DEFECT 2: groupingBy only creates the keys it saw. */
    static int settledCount(List<Sale> sales) {
        Map<Boolean, List<Sale>> split = sales.stream()
            .collect(Collectors.groupingBy(Sale::settled));
        return split.get(true).size();
    }

    /** DEFECT 3: toMap rejects a null value, unlike the map it builds. */
    static Map<String, String> promoByRep(List<Sale> sales) {
        return sales.stream().collect(Collectors.toMap(Sale::rep, Sale::promoCode));
    }

    public static void main(String[] args) {
        boolean ok = true;

        System.out.println("── against the staging fixture ──");
        System.out.println("  reps    : " + new TreeMap<>(repByRegion(fixture())));
        System.out.println("  settled : " + settledCount(fixture()));
        System.out.println("  promos  : " + new TreeMap<>(promoByRep(fixture())));
        System.out.println("  (all three pass here, which is the problem)");

        System.out.println();
        System.out.println("── against production data ──");

        ok &= check("the report handles more than one sale per region",
            attempt("reps", () -> new TreeMap<>(repByRegion(production())).toString()));

        ok &= check("the report handles a day with nothing settled",
            attempt("settled", () -> String.valueOf(settledCount(production()))));

        ok &= check("the report handles a sale with no promo code",
            attempt("promos", () -> new TreeMap<>(promoByRep(production())).toString()));

        System.out.println();
        System.out.println(ok ? "PASS" : "FAIL");
    }

    static boolean attempt(String label, Supplier<String> body) {
        try {
            System.out.println("  " + label + " : " + body.get());
            return true;
        } catch (RuntimeException e) {
            System.out.println("  " + label + " : " + e.getClass().getSimpleName()
                + (e.getMessage() == null ? "" : " — " + e.getMessage()));
            return false;
        }
    }

    static boolean check(String what, boolean passed) {
        System.out.println((passed ? "  ok    " : "  FAIL  ") + what);
        return passed;
    }
}

Run it locally:

cd exercises/java/java8/collectors-and-grouping/03-production
javac Starter.java -d /tmp/out && java -cp /tmp/out Starter

Hints

  1. Hint 1

    Read each exception type before changing anything. Three different collectors, three different messages.

  2. Hint 2

    For the region map, ask whether one region really has one rep — the data says no, so what should the value type be?

  3. Hint 3

    For the settled count, what does groupingBy do with a key it never encountered?

  4. Hint 4

    For the promo map, a plain put accepts the same null. Which method does toMap use internally?

Done when

  • All three report sections run against the production data
  • The region map reflects that a region has several reps
  • A day with nothing settled returns zero rather than throwing
  • A missing promo code is an absent entry, decided deliberately
  • A comment names the fixture assumption behind each defect

Solution

Show the solution — try it yourself first
Solution.java
import java.util.*;
import java.util.function.*;
import java.util.stream.*;

/**
 * Solution: the report that crashed on real data.
 *
 * Three defects, one theme: every collector has a failure mode, and the tidy
 * fixture exercised none of them. The staging data had one sale per region,
 * everything settled and every promo present — so the three assumptions were
 * never tested, only assumed.
 *
 *   Defect 1 — two-argument toMap on a key that repeats. It throws
 *   IllegalStateException naming both values. The two-argument form is not the
 *   simple version; it is an assertion that the key is unique. Where it is
 *   not, the three-argument form makes the collision rule explicit — and here
 *   a region genuinely has several reps, so the value should have been a
 *   collection in the first place.
 *
 *   Defect 2 — groupingBy on a boolean, then get(true). groupingBy only
 *   creates keys it actually encountered, so a day with nothing settled leaves
 *   get(true) returning null. partitioningBy always returns both keys, which
 *   is exactly what it exists for.
 *
 *   Defect 3 — toMap with a value that can be null. toMap is implemented with
 *   Map.merge, whose contract rejects null values, so it throws where a plain
 *   put would have accepted it. The fix is to decide what a missing promo
 *   means rather than to let a null through: here, filter it out.
 */
public class Solution {

    record Sale(String region, String rep, boolean settled, String promoCode, int amount) { }

    static List<Sale> fixture() {
        return List.of(
            new Sale("north", "Ana", true, "SPRING", 120),
            new Sale("south", "Bo", true, "SPRING", 95));
    }

    static List<Sale> production() {
        return List.of(
            new Sale("north", "Ana", false, "SPRING", 120),
            new Sale("north", "Bo", false, null, 95),
            new Sale("south", "Cy", false, null, 70));
    }

    /** FIX 1: a region has many reps, so the value is a collection. */
    static Map<String, List<String>> repByRegion(List<Sale> sales) {
        return sales.stream().collect(Collectors.groupingBy(Sale::region,
            Collectors.mapping(Sale::rep, Collectors.toList())));
    }

    /** FIX 2: partitioningBy always returns both keys. */
    static int settledCount(List<Sale> sales) {
        Map<Boolean, List<Sale>> split = sales.stream()
            .collect(Collectors.partitioningBy(Sale::settled));
        return split.get(true).size();
    }

    /** FIX 3: a missing promo is an absent entry, not a null value. */
    static Map<String, String> promoByRep(List<Sale> sales) {
        return sales.stream()
            .filter(s -> s.promoCode() != null)
            .collect(Collectors.toMap(Sale::rep, Sale::promoCode, (a, b) -> a));
    }

    public static void main(String[] args) {
        boolean ok = true;

        System.out.println("── against the staging fixture ──");
        System.out.println("  reps    : " + new TreeMap<>(repByRegion(fixture())));
        System.out.println("  settled : " + settledCount(fixture()));
        System.out.println("  promos  : " + new TreeMap<>(promoByRep(fixture())));

        System.out.println();
        System.out.println("── against production data ──");

        ok &= check("the report handles more than one sale per region",
            attempt("reps", () -> new TreeMap<>(repByRegion(production())).toString()));

        ok &= check("the report handles a day with nothing settled",
            attempt("settled", () -> String.valueOf(settledCount(production()))));

        ok &= check("the report handles a sale with no promo code",
            attempt("promos", () -> new TreeMap<>(promoByRep(production())).toString()));

        System.out.println();
        System.out.println(ok ? "PASS" : "FAIL");
    }

    static boolean attempt(String label, Supplier<String> body) {
        try {
            System.out.println("  " + label + " : " + body.get());
            return true;
        } catch (RuntimeException e) {
            System.out.println("  " + label + " : " + e.getClass().getSimpleName()
                + (e.getMessage() == null ? "" : " — " + e.getMessage()));
            return false;
        }
    }

    static boolean check(String what, boolean passed) {
        System.out.println((passed ? "  ok    " : "  FAIL  ") + what);
        return passed;
    }
}

Stretch

The fixture is the root cause of all three. Write the property that each defect violates as a test — several sales per region, nothing settled, a null promo — and say where those cases should come from so the next assumption is caught too. Then argue whether the report should fail loudly or degrade when the data is malformed, and who decides.

← Back to How do Collectors.groupingBy and toMap differ in failure modes?