When we design a relational database, we usually rely on a single primary key to identify our records like an id field in our entity class. But sometimes, a single primary key just isn’t enough to uniquely identify a record.
When we build relationships between entity classes, especially many-to-many relationships. In such cases, JPA automatically creates a join table behind the scenes. This table uses a composite key, which is basically a combination of the primary keys from both related entities. This works just fine but the issue is we don’t really have any control over this table.
To make better use of this join table like adding extra fields such as timestamps or status we can create our own table with a composite key.
In this article, we will go through what composite keys are and how to implement them.
Composite Key
A composite key is a primary key made up of two or more columns that together uniquely identify a record in a table.
In a normal table, a primary key is always unique. But in the case of a composite key, the individual columns might not be unique on their own — but when combined, they ensure each row is unique.
In short, a composite key helps when a single column isn’t enough to guarantee uniqueness — but a combination of columns can.
This is especially useful in complex relationships like many-to-many. For example,Think of a university where students can enroll in many courses, and each course can have many students. So, both students and courses can show up multiple times in the enrollment table.
But a student should not be enrolled in the same course more than once.
To make sure of that, we use a combination of student_id and course_id to keep each enrollment unique. That’s where a composite key comes in.
How to Implement
Let’s build the composite key based on our earlier example.
In JPA, we have two options to define composite keys:
@IdClass@EmbeddedId(We will use this for this post.)
There are a few rules to follow when creating a composite key class:
- The composite primary key class must be public.
- It must have a no-arg constructor.
- It must define the equals() and hashCode() methods.
- It must be Serializable.
Define the Main Entities
Student Entity
@Entity
@Getter
@Setter
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// Other fields...
}
Course Entity
@Entity
@Getter
@Setter
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
// Other fields...
}
Now lets define the composite key class and make it embeddable.
@Getter
@Setter
@NoArgsConstructor
@Embeddable
public class StudentCourseId implements Serializable {
private Long studentId;
private Long courseId;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof StudentCourseId)) return false;
StudentCourseId that = (StudentCourseId) o;
return Objects.equals(studentId, that.studentId) &&
Objects.equals(courseId, that.courseId);
}
@Override
public int hashCode() {
return Objects.hash(studentId, courseId);
}
}
This class holds the combination of studentId and courseId.
Enrollment Entity (Join Table with Composite Key)
@Entity
@Getter
@Setter
public class Enrollment {
@EmbeddedId
private StudentCourseId id;
@ManyToOne
@MapsId("studentId")
@JoinColumn(name = "student_id")
private Student student;
@ManyToOne
@MapsId("courseId")
@JoinColumn(name = "course_id")
private Course course;
private String grade; // Additional field
private String enrolledDate; // Additional field
}
Annotation Used to create the composite Key
@Embeddable
This mark the class whose fields can be used into another entity. Here, StudentCourseId is embedded into the Enrollment entity.
@EmbeddedId
Its tells JPA that the annotated field is a composite primary key. We use it in Enrollment to indicate that StudentCourseId is the combined key.
@ManyToOne
Defines a many-to-one relationship between two entities. In the example many enrollments belong to one student and many enrollments belogs to one course.
@MapsId
Maps a part of the composite key to the ID of a related entity. It tells JPA: “Use the ID from this object (like student.getId()) to fill part of the composite key (studentId).”
Summary
Composite keys give us full control over join tables in JPA — especially useful when working with complex relationships and when we need to store additional metadata in the join table.
I wonder a question: “Why do I need to define the equals() and hashCode() methods? Doesn’t Spring handle this automatically?”
I will explore this in a future blog post, Insha’Allah. Stay tuned for that!
Until then — Happy Coding, and Happy Blogging! ✨
Oh, and one more thing if you’re curious about using @IdClass for defining composite keys instead of @EmbeddedId read here.


