Can someone explain mappedBy in JPA and Hibernate?
System Design practice on Codemia
Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.
In JPA and Hibernate, mappedBy declares the inverse (non-owning) side of a bidirectional relationship. It tells the persistence provider "the foreign key lives on the other entity, in the field named by this string." Without mappedBy, Hibernate assumes both sides own the relationship and creates a redundant join table. The entity that does not have mappedBy (the one with @JoinColumn) is the owning side and is the only side that controls inserts and updates to the foreign key column.
The Owning Side vs. the Inverse Side
Every bidirectional relationship in JPA has exactly two sides:
Owning side: the entity whose table contains the foreign key column. This side has @JoinColumn (or is the default owner in @ManyToMany). Changes to this side's collection or reference are written to the database.
Inverse side: the entity that declares mappedBy. This side is read-only from the persistence provider's perspective. Setting or adding entities on the inverse side alone does not produce SQL INSERT or UPDATE statements for the foreign key.
This distinction is the single most important concept for understanding mappedBy. If you set both sides correctly, Hibernate generates efficient SQL. If you only set the inverse side, the relationship is silently not persisted.
One-to-Many / Many-to-One
This is the most common relationship type. The "many" side always owns the relationship because that is where the foreign key naturally lives.
The mappedBy = "department" string refers to the department field in the Employee class. Hibernate reads this to understand that Employee.department_id is the foreign key column and Department.employees is a mirror that does not produce its own SQL.
What SQL Hibernate Generates
No join table is created. The foreign key lives on the employee table, exactly where you want it.
What Happens Without mappedBy
If you remove mappedBy from @OneToMany:
Hibernate treats the Department.employees collection as a unidirectional @OneToMany and creates a join table:
This join table is almost never what you want for a one-to-many relationship. It adds unnecessary storage, complicates queries, and degrades performance.
One-to-One
In a one-to-one relationship, either side can own the relationship. The side with @JoinColumn is the owner.
The foreign key user_id lives in the user_profile table. User.profile is the inverse side.
Lazy Loading Caveat for @OneToOne
On the inverse side (User.profile), Hibernate cannot determine whether the associated entity is null or not without querying the database. This means FetchType.LAZY on the inverse @OneToOne side is often not honored. Hibernate issues an eager query to check if a profile exists. The owning side (UserProfile.user) supports lazy loading correctly because Hibernate can check the user_id column value.
If lazy loading on the inverse side is critical for performance, consider using @MapsId to share the primary key:
Many-to-Many
In a many-to-many relationship, a join table is always required. One side must declare mappedBy to prevent Hibernate from creating two join tables.
Student is the owning side. Adding or removing entries in student.getCourses() triggers inserts/deletes in the student_course table. Modifying course.getStudents() alone does nothing in the database.
The mappedBy String Must Match the Field Name Exactly
The mappedBy value is a string that must exactly match the Java field name on the owning side. It is case-sensitive and refers to the Java property, not the database column.
If the string is wrong, Hibernate throws a MappingException at startup:
Synchronizing Both Sides
Because Hibernate only persists changes from the owning side, you must set both sides when working within the same persistence context (same transaction or same session). Otherwise the in-memory object graph is inconsistent.
The recommended pattern is to use helper methods on the parent entity that maintain both sides, as shown in the Department.addEmployee() example above.
Quick Reference Table
| Relationship | Owning side (has @JoinColumn) | Inverse side (has mappedBy) | Join table created |
@OneToMany / @ManyToOne | @ManyToOne side | @OneToMany side | No (FK on "many" table) |
@OneToOne | Side with @JoinColumn | Other side | No (FK on owner's table) |
@ManyToMany | Side with @JoinTable | Other side | Yes (one join table) |
@OneToMany without mappedBy | Both sides treated as owners | N/A | Yes (unintended join table) |
Common Pitfalls
Only modifying the inverse side. This is the most frequent mistake. Adding an entity to the mappedBy collection without setting the owning side's reference means the foreign key is never written. The entity appears in memory but is not persisted. Always set the owning side.
Typo in the mappedBy string. The value must match the Java field name exactly, including case. A mismatch causes a startup MappingException. This error is easy to introduce during refactoring when you rename a field but forget to update the mappedBy string. IDEs like IntelliJ flag this, but not all refactoring tools catch it.
Missing mappedBy on @OneToMany. Without mappedBy, Hibernate creates an unnecessary join table. This is valid JPA behavior (it models a unidirectional @OneToMany), but it is almost never the intended design for a bidirectional relationship.
N+1 query problem with eager fetching. Using FetchType.EAGER on the inverse side (the default for @ManyToOne and @OneToOne) loads associated entities immediately with each query. For collections, this produces N+1 queries. Use FetchType.LAZY and fetch explicitly with JOIN FETCH or @EntityGraph when you need the data.
Circular toString() and equals(). Bidirectional relationships create circular references. If both entities include the other in toString(), equals(), or hashCode(), you get a StackOverflowError. Exclude the inverse side from these methods or use only the entity's own fields (typically the primary key).
Summary
mappedBy declares the inverse side of a bidirectional JPA relationship. The owning side (the one without mappedBy, holding @JoinColumn or @JoinTable) controls all foreign key inserts and updates. The inverse side is read-only from the persistence provider's perspective. Always set the owning side when persisting relationships, use helper methods to synchronize both sides for in-memory consistency, and make sure the mappedBy string exactly matches the Java field name on the owning entity. For @OneToMany, omitting mappedBy causes Hibernate to generate an unnecessary join table.
Related reading
- Can SQL reads return stale data?
- Can table columns with a Foreign Key be NULL?
- Can the DynamoDB single table design play nicely with a Microservices architecture?
- Can we have more than 1024 nodes in Couchbase?
- Can Spring Boot application handle multiple requests simultaneously?
- Can Spring Boot be used with OSGi? If not, any plans to have an OSGi Spring Boot?
- Can we make 2 phase commit protocol to be non-blocking if assumptions of 3PC used on it?
- Can we restore to same dynamodb table from backup

System Design Fundamentals
Build a strong foundation in designing scalable, reliable distributed systems.
View the courseTrack what you have practised
A free account saves your progress, solutions and study plan across every problem on Codemia.
System Design practice on Codemia
Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.