Never put Lombok’s @Data on a JPA entity.
I learned this after my service started sending notifications twice. 💀
The code had worked for months. We checked duplicates in a HashSet. No race condition. No obvious bad code. But HashSet.contains() started returning false.

Set<Notification> pending = new HashSet<>();
pending.add(notification); // id is null here
repository.save(notification); // JPA assigns the id
pending.contains(notification); // false
Read that last line again. Same object. Same reference. contains() returned false.
A HashSet doesn’t scan like a list. It uses buckets:
add()callshashCode(), picks a bucket, and places the object there.contains()callshashCode()again, goes to that bucket, then checksequals().
The whole thing depends on one assumption: hashCode() returns the same number today and tomorrow.
@Data generates equals() and hashCode() over every field—including the database ID.
Before save → id is null → hashCode = X
After save → id is 4471 → hashCode = Y
We saved the object in bucket X but went looking in bucket Y. The object was in the set, but its hash code now pointed somewhere else.
The fix is to base equality on a value that never changes: a natural business key such as orderId, phone number, or reference number; or a UUID assigned before persistence.
No exception. No error log. The bug sat quietly for two weeks. 👻
A HashSet doesn’t store your object. It stores the bucket your object belongs to.