A few days ago, I ran into a bug that didn’t look like a bug at all.
I was working on a fairly standard Spring Boot service, nothing unusual. A couple of entities, some relationships, and a small feature change. To keep things concise, I did what most of us do:
@Data
@Entity
public class User {
...
}
It felt natural, clean and efficient.
The application started fine. APIs were working. Everything looked normal.
Until I added a simple log statement.
The Bug That Didn’t Make Sense
log.info("User details: {}", user);
The moment this line executed, the application crashed with a StackOverflowError.
At first glance, it didn’t add up.
There was no recursion in my code. No complex logic. Just a log statement.
But as I traced through the stack, the pattern became clear.
When toString() Becomes a Trap
The User entity had a bidirectional relationship:
- A
Userhad a collection ofOrders - Each
Orderreferenced back to theUser
This is a very common JPA pattern.
What I hadn’t paid attention to was what Lombok’s @Data had generated for me.
It includes a toString() method that prints all fields, including relationships.
So when I logged the User:
-
User.toString()tried to printorders - Each
Ordertried to print itsuser - Which again tried to print
orders
And so on.
An infinite loop—triggered by logging.
The Subtle Performance Issue
While investigating, I noticed something else that was even more concerning.
In another part of the code, I had something like:
Set<User> users = new HashSet<>();
users.add(user);
Nothing unusual.
But it was unexpectedly slow.
The reason, again, traced back to @Data.
It generates equals() and hashCode() using all fields. In a JPA entity, that can be problematic especially when those fields include lazily loaded relationships.
To compute the hash, Hibernate ended up initializing those lazy fields, which meant:
- Additional database queries
- More data loaded than expected
- All happening implicitly
No explicit query. No clear signal in the code.
The Core Issue
The root of the problem is simple:
JPA entities are not plain Java objects.
They are managed by a persistence context, often proxied, and frequently only partially loaded.
Lombok, however, generates code as if everything is a simple POJO. That disconnect is where these issues originate.
What I Changed
I didn’t stop using Lombok. But I stopped using @Data on entities.
Instead, I now prefer:
@Getter
@Setter
@Entity
public class User {
...
}
For methods like toString(), equals(), and hashCode(), I either:
- Write them explicitly, or
- Use Lombok annotations selectively (e.g., excluding relationships)
This keeps behavior predictable and avoids unintended side effects.
Takeaway
@Data is incredibly useful in the right places.
But JPA entities are not one of them.
What looks like a small convenience can introduce:
- Infinite recursion
- Hidden database queries
- Hard-to-debug behavior
All without any obvious warning.
Final Thought
This wasn’t a complex failure. It didn’t require scale or load to surface.
It came from a perfectly normal development flow and that’s exactly why it’s worth paying attention to.
Sometimes the most dangerous problems aren’t the ones we struggle to write. They’re the ones we don’t realize we’ve already written for us.
📚 Further Reading
Vlad Mihalcea — How to implement equals and hashCode using the JPA entity identifier
https://vladmihalcea.com/hibernate-facts-equals-and-hashcode/
A must-read on why JPA entity equality should be based on identifiers rather than all fields, and how improper implementations can lead to subtle bugs.Project Lombok — @data Annotation Documentation
https://projectlombok.org/features/Data
Explains what@Dataactually generates under the hood, which helps understand why its default behavior can conflict with JPA entities.
Top comments (0)