SQL
JPA
Composite Primary Key
Database Design
Java Persistence

SQL JPA - Multiple columns as primary key

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

Composite primary keys are necessary when uniqueness comes from a combination of columns instead of a single identifier. In JPA, this is modeled using either @IdClass or @EmbeddedId, each with different ergonomics for queries and relationships. Correct mapping is mostly about consistency, especially around equality methods and foreign-key relations.

Core Sections

Decide if composite key is truly required

Before mapping, confirm business identity really needs multiple columns. Composite keys work well for join tables and natural-key domains, but they make entity APIs and relationships more complex than a single surrogate key.

If your table has a stable synthetic id and unique constraint on business columns, that design is often easier for long-term maintenance.

Mapping with @EmbeddedId

@EmbeddedId keeps key fields in a dedicated embeddable type and embeds it in the entity.

java
1@Embeddable
2public class EnrollmentId implements Serializable {
3    private Long studentId;
4    private Long courseId;
5
6    public EnrollmentId() {}
7
8    public EnrollmentId(Long studentId, Long courseId) {
9        this.studentId = studentId;
10        this.courseId = courseId;
11    }
12
13    @Override
14    public boolean equals(Object o) {
15        if (this == o) return true;
16        if (!(o instanceof EnrollmentId)) return false;
17        EnrollmentId that = (EnrollmentId) o;
18        return Objects.equals(studentId, that.studentId)
19            && Objects.equals(courseId, that.courseId);
20    }
21
22    @Override
23    public int hashCode() {
24        return Objects.hash(studentId, courseId);
25    }
26}
27
28@Entity
29public class Enrollment {
30    @EmbeddedId
31    private EnrollmentId id;
32
33    private LocalDate enrolledOn;
34}

This style keeps key logic grouped and usually reads better for complex keys.

Mapping with @IdClass

@IdClass keeps key fields directly in entity and uses external key class for identity contract.

java
1@IdClass(EnrollmentId.class)
2@Entity
3public class Enrollment {
4
5    @Id
6    private Long studentId;
7
8    @Id
9    private Long courseId;
10
11    private LocalDate enrolledOn;
12}

@IdClass can be convenient in legacy schemas because entity fields remain flat, but duplication between entity fields and key class is easier to misconfigure.

Model relationships with @MapsId

For composite keys involving foreign keys, @MapsId makes relations explicit and avoids manual synchronization bugs.

java
1@Entity
2public class Enrollment {
3
4    @EmbeddedId
5    private EnrollmentId id;
6
7    @ManyToOne(fetch = FetchType.LAZY)
8    @MapsId("studentId")
9    private Student student;
10
11    @ManyToOne(fetch = FetchType.LAZY)
12    @MapsId("courseId")
13    private Course course;
14}

This ties embedded key values to related entity identifiers cleanly.

Querying entities with composite keys

With @EmbeddedId, lookups use key object.

java
EnrollmentId id = new EnrollmentId(10L, 22L);
Enrollment e = entityManager.find(Enrollment.class, id);

For JPQL, key fields are accessed through key path:

java
TypedQuery<Enrollment> q = entityManager.createQuery(
    "select e from Enrollment e where e.id.studentId = :sid", Enrollment.class);
q.setParameter("sid", 10L);

Plan query style early so repository code remains consistent.

Equality and hash code requirements

Incorrect equals and hashCode in key class cause hard-to-debug cache and collection behavior issues. Use immutable semantics where possible and include all key fields.

Do not include non-key mutable fields in identity methods.

Migration and schema evolution considerations

Changing composite key fields later is expensive because all related foreign keys and application mappings must be updated. If you expect key structure to evolve, consider surrogate id plus unique constraint pattern.

For existing schemas, keep DDL and JPA mappings versioned together so environments do not drift.

Testing and validation strategy

Add tests for:

  • insert and find by composite key,
  • duplicate key constraint enforcement,
  • relationship loading with @MapsId.

Integration tests should run against the same database type used in production whenever possible.

Common Pitfalls

  • Forgetting Serializable on key class and breaking JPA requirements.
  • Implementing equals and hashCode inconsistently across key fields.
  • Mixing @IdClass and embedded-key assumptions in repository queries.
  • Omitting @MapsId and manually desynchronizing key and relation fields.
  • Choosing composite keys where a surrogate key plus unique constraint is simpler.

Summary

  • Use composite keys only when business identity truly requires multiple columns.
  • Choose @EmbeddedId for grouped key modeling or @IdClass for flatter legacy mappings.
  • Implement key equality methods correctly and include all key parts.
  • Use @MapsId for foreign-key-based composite identity relationships.
  • Cover key mapping behavior with integration tests to prevent persistence regressions.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track 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.

Practice system design

All Rights Reserved.