A composite key combines multiple values into one key that identifies a map or set entry. It makes a relationship explicit when neither value alone is sufficient, and can replace a nested map whose structure hides the meaning of the grouping. section 4.2.2 section 4.3
Use a small immutable data carrier when the combined values naturally belong together. A named key such as AgeBmiKey communicates that an entry is counted by both age range and BMI category; Map<AgeBmiKey, Long> exposes that relationship directly, unlike Map<Age, Map<BmiCategory, Long>>. section 4.2.2 section 4.3
Counting BMI categories by age range
Create record AgeBmiKey(Age age, BmiCategory category) {}. For each physical profile, derive its ten-year age range and BMI category, construct an AgeBmiKey, and group profiles by that key. Each resulting entry contains the complete identity needed to calculate a BmiStat or find the age range with the most obese profiles. section 4.2.2
Listing 4.5 Refactored implementation with clear data modeling
public BmiGlobalResult computeBmiStats(List<PhysicalProfile> profiles) {
long totalUsers = profiles.size();
Map<AgeBmiKey, Long> countByAgeBmiKey = profiles.stream()
.collect(Collectors.groupingBy(
p -> new AgeBmiKey(
new Age((p.age().value() / 10) * 10),
calculateBmiCategory(p)
),
Collectors.counting()
));
List<BmiStat> stats = countByAgeBmiKey.entrySet().stream()
.map(entry -> new BmiStat(
entry.getKey().age(),
entry.getKey().category(),
new Percentage(totalUsers == 0 ? 0.0 :
100.0 * entry.getValue() / totalUsers)
))
.toList();
Age ageWithMostObese = countByAgeBmiKey.entrySet().stream()
.filter(entry -> entry.getKey().category() == BmiCategory.OBESE)
.max(Comparator.comparingLong(Map.Entry::getValue))
.map(Map.Entry::getKey)
.map(AgeBmiKey::age)
.orElse(null);
return new BmiGlobalResult(totalUsers, ageWithMostObese, stats);
}
This refactoring shows the composite key at the collection boundary: grouping produces one flat map, while later transformations read age and category from the named key. section 4.2.2
Model a composite key when it improves the reader’s ability to see the data’s identity:
- Name the carrier after the relationship it represents, rather than leaving the relationship implied by nested collection types. section 4.2.2 section 4.3
- Keep it small and immutable when it represents a stable combined identifier. section 4.2.2 section 4.3
- Flatten nested maps when the flatter key makes the grouping easier to reason about. section 4.3
- Do not add a type merely for ceremony; prefer the simpler model when extra structure adds little understanding. section 4.3
A composite key is not an automatic replacement for every nested structure. Introduce it when the pair has meaningful identity, reduces ambiguity, or makes transformations clearer; leave the data flatter only when that precision earns its added structure. section 4.3







