Invert the semi-join and you have the report every operations team asks for first: orders with no payment, users with no login, events with no acknowledgement.
record Order(String ref, double amount) {}
record Payment(String orderRef, double paid) {}
var orders = List.of(new Order("A-1", 250.0), new Order("A-2", 90.0), new Order("A-3", 40.0));
var payments = List.of(new Payment("A-1", 250.0), new Payment("A-3", 40.0));
Set<String> settled = payments.stream().map(Payment::orderRef).collect(Collectors.toSet());
var unpaid = orders.stream().filter(o -> !settled.contains(o.ref())).toList();
System.out.println("unpaid: " + unpaid);
System.out.println("owed : " + unpaid.stream().mapToDouble(Order::amount).sum());
unpaid: [Order[ref=A-2, amount=90.0]]
owed : 90.0
Run this yourself in the Online Java Compiler, spin up a live REST API in the API Sandbox, or practise with Java interview questions.
Published 2026-09-20