A composite key is a primary key made of two or more fields that together identify a record uniquely, such as (student_id, course_id). It is used when no single column identifies a row, most often in join tables like enrollments, where the same student can appear many times and the same course can appear many times, but the pair appears only once. JPA maps the key fields into a separate key class, so the database rejects duplicate pairs and you can look records up by the full key. In this experiment, a StudentCourse enrollment entity uses a composite key to record which student is enrolled in which course, along with the enrollment date and grade.
Features
- The key class must be
Serializableand must overrideequals()andhashCode(). JpaRepository<StudentCourse, StudentCourseId>uses the key class as the ID type, sofindById,existsByIdanddeleteByIdall take the full key.@MapsIdreuses the key columns as the foreign keys to Student and Course, so no duplicate columns are created.- Derived queries can navigate into the key, for example
findByIdStudentId. - Extra columns (
enrolledDate,grade) can live on the same entity, which a plain@ManyToManycan’t do.
Composite Keys Types
1. @EmbeddedId (used in this example). The key fields live in a separate @Embeddable class, and the entity holds one field of that type marked @EmbeddedId. The key is accessed as one object (enrollment.getId().getStudentId()), and queries use the path id.studentId. It is the cleanest option and the most commonly used.
2. @IdClass. The key fields are declared directly in the entity, each marked @Id, and a separate plain class with the same field names is attached using @IdClass(StudentCourseId.class). The entity keeps its fields flat (enrollment.getStudentId()), and queries use studentId directly. It is useful when you don’t want a nested key object, but the field names must match exactly between the two classes.
3. Derived identity with @MapsId. This is not a separate key style but the way the key fields are filled from relationships. Here @MapsId("studentId") on a @ManyToOne fills the studentId part of the key from the related Student. It is combined with @EmbeddedId as in this example.
Hands-on Experiment: Creating a spring boot application with Composite Key
The following steps are involved in developing this hands-on experiment.
Step 1: Generate the Project
Step 2: Project Structure
Step 3: pom.xml
Step 4: application.properties
Step 5: Create Entity Class
Step 6: Create Repository Interface
Step 7: Create Service Class
Step 8: Create Controller Class
Step 9: Main Application Class
Step 10: Run the Main Application Class
Step 1: Configure the Project on Spring Initializr
Go to start.spring.io and set:
- Project: Maven
- Language: Java
- Spring Boot version: 3.2.x
- Group: com.example
- Artifact: compositekey
- Name: compositekey
- Package name: com.example.compositekey
- Packaging: Jar
- Java: 17
- Dependencies: Spring Web, Spring Data JPA, MySQL Driver
Click Generate to download the ZIP, then extract and import it into your IDE as a Maven project.
Step 2: Generated Project Structure

Step 3: Explore pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.5</version>
<relativePath/>
</parent>
<groupId>com.javaplanet</groupId>
<artifactId>student-course-compositekey</artifactId>
<version>1.0.0</version>
<name>student-course-compositekey</name>
<description>Composite Keys - StudentCourse Enrollment</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>Step 4: application.properties
spring.application.name=student-course-compositekey
server.port=8082
spring.datasource.url=jdbc:mysql://localhost:3306/composite_db?createDatabaseIfNotExist=true&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
spring.datasource.username=root
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialectCode language: JavaScript (javascript)
Step 5: Create Entity Class
package com.javaplanet.compositekey.entity;
import jakarta.persistence.*;
@Entity
@Table(name = "courses")
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
private int credits;
public Course() {}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public int getCredits() { return credits; }
public void setCredits(int credits) { this.credits = credits; }
}package com.javaplanet.compositekey.entity;
import jakarta.persistence.*;
@Entity
@Table(name = "students")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false, unique = true)
private String email;
public Student() {}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}package com.javaplanet.compositekey.entity;
import jakarta.persistence.*;
import java.time.LocalDate;
@Entity
@Table(name = "student_course")
public class StudentCourse {
// the primary key is the PAIR (student_id, course_id)
@EmbeddedId
private StudentCourseId id;
// @MapsId reuses the key columns as the foreign keys
@ManyToOne
@MapsId("studentId")
@JoinColumn(name = "student_id")
private Student student;
@ManyToOne
@MapsId("courseId")
@JoinColumn(name = "course_id")
private Course course;
private LocalDate enrolledDate;
private String grade;
public StudentCourse() {}
public StudentCourseId getId() { return id; }
public void setId(StudentCourseId id) { this.id = id; }
public Student getStudent() { return student; }
public void setStudent(Student student) { this.student = student; }
public Course getCourse() { return course; }
public void setCourse(Course course) { this.course = course; }
public LocalDate getEnrolledDate() { return enrolledDate; }
public void setEnrolledDate(LocalDate enrolledDate) { this.enrolledDate = enrolledDate; }
public String getGrade() { return grade; }
public void setGrade(String grade) { this.grade = grade; }
}
package com.javaplanet.compositekey.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import java.io.Serializable;
import java.util.Objects;
// Composite primary key: must be Serializable and implement equals() and hashCode()
@Embeddable
public class StudentCourseId implements Serializable {
@Column(name = "student_id")
private Long studentId;
@Column(name = "course_id")
private Long courseId;
public StudentCourseId() {}
public StudentCourseId(Long studentId, Long courseId) {
this.studentId = studentId;
this.courseId = courseId;
}
public Long getStudentId() { return studentId; }
public void setStudentId(Long studentId) { this.studentId = studentId; }
public Long getCourseId() { return courseId; }
public void setCourseId(Long courseId) { this.courseId = courseId; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof StudentCourseId that)) return false;
return Objects.equals(studentId, that.studentId) && Objects.equals(courseId, that.courseId);
}
@Override
public int hashCode() {
return Objects.hash(studentId, courseId);
}
}
Step 6: Create below Repository Interface
package com.javaplanet.compositekey.repository;
import com.javaplanet.compositekey.entity.Course;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface CourseRepository extends JpaRepository<Course, Long> {
}
package com.javaplanet.compositekey.repository;
import com.javaplanet.compositekey.entity.StudentCourse;
import com.javaplanet.compositekey.entity.StudentCourseId;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
// the ID type is the composite key class
@Repository
public interface StudentCourseRepository extends JpaRepository<StudentCourse, StudentCourseId> {
// navigates id.studentId
List<StudentCourse> findByIdStudentId(Long studentId);
// navigates id.courseId
List<StudentCourse> findByIdCourseId(Long courseId);
}
package com.javaplanet.compositekey.repository;
import com.javaplanet.compositekey.entity.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {
}
Step 7: Create below Service class
package com.javaplanet.compositekey.service;
import com.javaplanet.compositekey.entity.Course;
import com.javaplanet.compositekey.repository.CourseRepository;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class CourseService {
private final CourseRepository courseRepository;
public CourseService(CourseRepository courseRepository) {
this.courseRepository = courseRepository;
}
public Course createCourse(Course course) {
return courseRepository.save(course);
}
public List<Course> getAllCourses() {
return courseRepository.findAll();
}
}
package com.javaplanet.compositekey.service;
import com.javaplanet.compositekey.entity.Course;
import com.javaplanet.compositekey.entity.Student;
import com.javaplanet.compositekey.entity.StudentCourse;
import com.javaplanet.compositekey.entity.StudentCourseId;
import com.javaplanet.compositekey.repository.CourseRepository;
import com.javaplanet.compositekey.repository.StudentCourseRepository;
import com.javaplanet.compositekey.repository.StudentRepository;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDate;
import java.util.List;
@Service
public class StudentCourseService {
private final StudentCourseRepository enrollmentRepository;
private final StudentRepository studentRepository;
private final CourseRepository courseRepository;
public StudentCourseService(StudentCourseRepository enrollmentRepository,
StudentRepository studentRepository,
CourseRepository courseRepository) {
this.enrollmentRepository = enrollmentRepository;
this.studentRepository = studentRepository;
this.courseRepository = courseRepository;
}
// request body carries student.id, course.id and grade
public StudentCourse enroll(StudentCourse request) {
if (request.getStudent() == null || request.getStudent().getId() == null
|| request.getCourse() == null || request.getCourse().getId() == null) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "student.id and course.id are required");
}
Long studentId = request.getStudent().getId();
Long courseId = request.getCourse().getId();
Student student = studentRepository.findById(studentId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Student not found: " + studentId));
Course course = courseRepository.findById(courseId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Course not found: " + courseId));
StudentCourseId key = new StudentCourseId(studentId, courseId);
if (enrollmentRepository.existsById(key)) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "Student is already enrolled in this course");
}
StudentCourse enrollment = new StudentCourse();
enrollment.setId(key);
enrollment.setStudent(student);
enrollment.setCourse(course);
enrollment.setEnrolledDate(LocalDate.now());
enrollment.setGrade(request.getGrade());
return enrollmentRepository.save(enrollment);
}
public List<StudentCourse> getAll() {
return enrollmentRepository.findAll();
}
public List<StudentCourse> getByStudent(Long studentId) {
return enrollmentRepository.findByIdStudentId(studentId);
}
public List<StudentCourse> getByCourse(Long courseId) {
return enrollmentRepository.findByIdCourseId(courseId);
}
public StudentCourse getOne(Long studentId, Long courseId) {
return enrollmentRepository.findById(new StudentCourseId(studentId, courseId))
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Enrollment not found"));
}
public StudentCourse updateGrade(Long studentId, Long courseId, String grade) {
StudentCourse enrollment = getOne(studentId, courseId);
enrollment.setGrade(grade);
return enrollmentRepository.save(enrollment);
}
public void delete(Long studentId, Long courseId) {
StudentCourse enrollment = getOne(studentId, courseId);
enrollmentRepository.delete(enrollment);
}
}
package com.javaplanet.compositekey.service;
import com.javaplanet.compositekey.entity.Student;
import com.javaplanet.compositekey.repository.StudentRepository;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class StudentService {
private final StudentRepository studentRepository;
public StudentService(StudentRepository studentRepository) {
this.studentRepository = studentRepository;
}
public Student createStudent(Student student) {
return studentRepository.save(student);
}
public List<Student> getAllStudents() {
return studentRepository.findAll();
}
}
Step 8: Create Controller class
package com.javaplanet.compositekey.controller;
import com.javaplanet.compositekey.entity.Course;
import com.javaplanet.compositekey.service.CourseService;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/courses")
public class CourseController {
private final CourseService courseService;
public CourseController(CourseService courseService) {
this.courseService = courseService;
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Course create(@RequestBody Course course) {
return courseService.createCourse(course);
}
@GetMapping
public List<Course> getAll() {
return courseService.getAllCourses();
}
}
package com.javaplanet.compositekey.controller;
import com.javaplanet.compositekey.entity.StudentCourse;
import com.javaplanet.compositekey.service.StudentCourseService;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/enrollments")
public class EnrollmentController {
private final StudentCourseService enrollmentService;
public EnrollmentController(StudentCourseService enrollmentService) {
this.enrollmentService = enrollmentService;
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public StudentCourse enroll(@RequestBody StudentCourse request) {
return enrollmentService.enroll(request);
}
@GetMapping
public List<StudentCourse> getAll() {
return enrollmentService.getAll();
}
@GetMapping("/student/{studentId}")
public List<StudentCourse> getByStudent(@PathVariable Long studentId) {
return enrollmentService.getByStudent(studentId);
}
@GetMapping("/course/{courseId}")
public List<StudentCourse> getByCourse(@PathVariable Long courseId) {
return enrollmentService.getByCourse(courseId);
}
@GetMapping("/student/{studentId}/course/{courseId}")
public StudentCourse getOne(@PathVariable Long studentId, @PathVariable Long courseId) {
return enrollmentService.getOne(studentId, courseId);
}
@PutMapping("/student/{studentId}/course/{courseId}")
public StudentCourse updateGrade(@PathVariable Long studentId, @PathVariable Long courseId,
@RequestBody StudentCourse request) {
return enrollmentService.updateGrade(studentId, courseId, request.getGrade());
}
@DeleteMapping("/student/{studentId}/course/{courseId}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long studentId, @PathVariable Long courseId) {
enrollmentService.delete(studentId, courseId);
}
}
package com.javaplanet.compositekey.controller;
import com.javaplanet.compositekey.entity.Student;
import com.javaplanet.compositekey.service.StudentService;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/students")
public class StudentController {
private final StudentService studentService;
public StudentController(StudentService studentService) {
this.studentService = studentService;
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Student create(@RequestBody Student student) {
return studentService.createStudent(student);
}
@GetMapping
public List<Student> getAll() {
return studentService.getAllStudents();
}
}
Step 9: Main Application
package com.javaplanet.compositekey;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CompositeKeyApplication {
public static void main(String[] args) {
SpringApplication.run(CompositeKeyApplication.class, args);
}
}
Step 10: Run the CompositeKeyApplication

