import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import Transaction.Category;
public class Runner {
public static void main(String[] args) {
var txns = List.of(
new Transaction("alice", Category.GROCERIES, new BigDecimal("42.50"), LocalDate.of(2024, 1, 3)),
new Transaction("alice", Category.RENT, new BigDecimal("1200.00"), LocalDate.of(2024, 1, 1)),
new Transaction("bob", Category.GROCERIES, new BigDecimal("18.99"), LocalDate.of(2024, 1, 4)),
new Transaction("bob", Category.TRANSPORT, new BigDecimal("7.25"), LocalDate.of(2024, 1, 5)),
new Transaction("alice", Category.GROCERIES, new BigDecimal("63.10"), LocalDate.of(2024, 1, 7)));
var report = new TransactionReport();
System.out.println("Total by category: " + report.totalByCategory(txns));
System.out.println("Count by account: " + report.countByAccount(txns));
System.out.println("Category summary: " + report.categorySummary(txns));
report.largestPerAccount(txns).forEach((account, top) ->
top.ifPresent(t -> System.out.printf("%s largest: %s%n", account, t.amount())));
}
}
import java.math.BigDecimal;
import java.time.LocalDate;
public record Transaction(
String account,
Category category,
BigDecimal amount,
LocalDate timestamp) {
public enum Category {
GROCERIES, RENT, ENTERTAINMENT, TRANSPORT, INCOME
}
public Transaction {
if (amount == null) {
throw new IllegalArgumentException("amount is required");
}
}
public record CategoryStat(BigDecimal total, long count) {
}
}
import java.math.BigDecimal;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static java.util.stream.Collectors.counting;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.maxBy;
import static java.util.stream.Collectors.reducing;
import static java.util.stream.Collectors.teeing;
import Transaction.Category;
import Transaction.CategoryStat;
public final class TransactionReport {
public Map<Category, BigDecimal> totalByCategory(List<Transaction> txns) {
return txns.stream().collect(groupingBy(
Transaction::category,
reducing(BigDecimal.ZERO, Transaction::amount, BigDecimal::add)));
}
public Map<String, Long> countByAccount(List<Transaction> txns) {
return txns.stream().collect(groupingBy(Transaction::account, counting()));
}
public Map<String, Map<Category, BigDecimal>> topCategoriesByAccount(List<Transaction> txns) {
return txns.stream().collect(groupingBy(
Transaction::account,
groupingBy(
Transaction::category,
reducing(BigDecimal.ZERO, Transaction::amount, BigDecimal::add))));
}
public Map<Category, CategoryStat> categorySummary(List<Transaction> txns) {
return txns.stream().collect(groupingBy(
Transaction::category,
teeing(
reducing(BigDecimal.ZERO, Transaction::amount, BigDecimal::add),
counting(),
CategoryStat::new)));
}
public Map<String, Optional<Transaction>> largestPerAccount(List<Transaction> txns) {
return txns.stream().collect(groupingBy(
Transaction::account,
maxBy(Comparator.comparing(Transaction::amount))));
}
}
This snippet demonstrates how to turn a flat list of financial transactions into a structured report using the java.util.stream.Collectors API, avoiding manual loops and mutable accumulators. The domain is modeled in Transaction as an immutable record carrying an account, a category, an amount as BigDecimal, and a timestamp. Using BigDecimal rather than double is deliberate: monetary sums must not accumulate floating-point error, which rules out the primitive summingDouble collectors in favor of a reducing approach.
The reporting logic lives in TransactionReport, where each method composes a different collector. totalByCategory uses groupingBy with a downstream collector built from Collectors.reducing, seeded with BigDecimal.ZERO and folding amounts with BigDecimal::add; this keeps the sum exact across the whole group. countByAccount shows the simplest downstream, Collectors.counting, producing a Map<String, Long>. topCategoriesByAccount nests two groupingBy calls to build a Map<String, Map<Category, BigDecimal>>, proving that downstream collectors compose arbitrarily deep.
categorySummary combines several statistics in one pass with Collectors.teeing, merging a total and a count into a CategoryStat record. Because teeing runs both downstream collectors over the same elements and merges their results, the stream is traversed once rather than repeatedly filtered. largestPerAccount uses maxBy with a comparator on the amount, wrapping the winner in an Optional to handle empty groups safely.
The key idea is that collectors are values that can be nested and combined, so a complex aggregation is expressed declaratively instead of as imperative bookkeeping. This scales better than hand-rolled maps: there is no null-checking computeIfAbsent, no accidental mutation, and the intent reads directly from the method names. A pitfall worth noting is that groupingBy returns a HashMap with no ordering guarantee; when ordered output matters a TreeMap supplier or LinkedHashMap should be supplied explicitly. The Runner tab wires sample data together and prints the reports, showing how the pieces fit in a small program.
Related snips
class Money
include Comparable
attr_reader :amount, :currency
def initialize(amount, currency = 'USD')
Value objects for domain modeling
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: String!
GraphQL API with Spring Boot
package com.example.starter.config;
import com.example.starter.properties.CustomProperties;
import com.example.starter.service.CustomService;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
Custom Spring Boot starters
package com.example.demo.config;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;
Messaging with Apache Kafka
class ReportQuery
SQL = <<~SQL.freeze
SELECT date_trunc('day', events.created_at) AS day,
count(*) AS total,
count(*) FILTER (WHERE events.kind = 'purchase') AS purchases
FROM events
Safe Raw SQL with exec_query + Binds
class Tag < ApplicationRecord
has_many :taggings, dependent: :destroy
scope :top, ->(limit = 20) {
joins(:taggings)
.group(Arel.sql("tags.id"))
Memory-Safe “top tags” aggregation with pluck + group
Share this code
Here's the card — post it anywhere.