Techniques to Map Subclasses in JPA Using @Inheritance(strategy = JOINED)
When we talk about mapping inheritance in JPA, we’re not just discussing a coding technique — we’re exploring how object-oriented design meets the relational world. The @Inheritance(strategy = JOINED) annotation is a bridge between a clean class ...

I am Tuanh.net. As of 2024, I have accumulated 8 years of experience in backend programming. I am delighted to connect and share my knowledge with everyone.
1. Understanding the Concept of JOINED Inheritance
@Inheritance(strategy = JOINED) strategy, allows developers to express inheritance at the database level by splitting data across multiple tables — one for the parent and one for each subclass.
1.1 How It Works Conceptually
User entity and two concrete subclasses: Admin and Customer. The User table holds common fields such as id, name, and email. The Admin table might store adminLevel, while Customer holds loyaltyPoints. When you query all users, JPA joins User with both subclass tables.
2. Example: Mapping Subclasses with @Inheritance(strategy = JOINED)
2.1 The Parent Entity: User
import jakarta.persistence.*;
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@Table(name = "users")
public abstract class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
// Constructors, Getters, Setters
public User() {}
public User(String name, String email) {
this.name = name;
this.email = email;
}
// toString for debug purposes
@Override
public String toString() {
return "User{id=" + id + ", name='" + name + "', email='" + email + "'}";
}
}
- The
@Inheritance(strategy = InheritanceType.JOINED)tells JPA that subclasses will have separate tables but linked through their primary keys. - The base table
usersstores shared attributes. - Each subclass table will reference
users.idas its foreign key.
2.2 The Subclass: Admin
@Entity
@Table(name = "admins")
public class Admin extends User {
private String adminLevel;
public Admin() {}
public Admin(String name, String email, String adminLevel) {
super(name, email);
this.adminLevel = adminLevel;
}
public String getAdminLevel() {
return adminLevel;
}
@Override
public String toString() {
return super.toString() + ", adminLevel='" + adminLevel + "'";
}
}
2.3 Another Subclass: Customer
@Entity
@Table(name = "customers")
public class Customer extends User {
private int loyaltyPoints;
public Customer() {}
public Customer(String name, String email, int loyaltyPoints) {
super(name, email);
this.loyaltyPoints = loyaltyPoints;
}
public int getLoyaltyPoints() {
return loyaltyPoints;
}
@Override
public String toString() {
return super.toString() + ", loyaltyPoints=" + loyaltyPoints;
}
}
3. Running the Example: Repository and Data Flow
3.1 Repository Interfaces
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<user, Long> {}
public interface AdminRepository extends JpaRepository<admin, Long> {}
public interface CustomerRepository extends JpaRepository<customer, Long> {}
UserRepository can be used to retrieve all types thanks to polymorphism.
3.2 Example Test Runner
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class DataLoader implements CommandLineRunner {
private final AdminRepository adminRepo;
private final CustomerRepository customerRepo;
private final UserRepository userRepo;
public DataLoader(AdminRepository adminRepo, CustomerRepository customerRepo, UserRepository userRepo) {
this.adminRepo = adminRepo;
this.customerRepo = customerRepo;
this.userRepo = userRepo;
}
@Override
public void run(String... args) {
adminRepo.save(new Admin("Alice", "alice@corp.com", "SUPER"));
customerRepo.save(new Customer("Bob", "bob@shop.com", 120));
System.out.println("All Users:");
userRepo.findAll().forEach(System.out::println);
}
}
4. Behind the Scenes: Database Structure
id column in each subclass table is both a primary key and a foreign key referencing users.id.When you query Admin, JPA executes something like:
SELECT u.id, u.name, u.email, a.admin_level
FROM users u
JOIN admins a ON u.id = a.id;
5. Why Use JOINED Strategy?
5.1 Advantages
- Normalized data: No redundant fields across tables.
- Type safety: Each subclass has its own logical boundaries.
- Clear relationships: Database mirrors object hierarchy cleanly.
5.2 Disadvantages
- Query performance: Requires joins for subclass queries.
- Complex migrations: Adding/removing fields means updating multiple tables.
5.3 When to Choose JOINED
- Your class hierarchy is stable and normalized.
- You want to avoid column duplication.
- You expect to query specific subclass types occasionally, not constantly.
SINGLE_TABLE. For full flexibility, TABLE_PER_CLASS might also fit certain analytical workloads.
6. Expanding Beyond the Basics
@DiscriminatorColumnto mark entity type.- DTO projections to flatten joined queries.
- Polymorphic queries: e.g.,
SELECT u FROM User u WHERE TYPE(u) = Admin. - MappedSuperclass: when you need shared fields but no actual table for the parent.
@Inheritance(strategy = JOINED) in JPA allows developers to map class hierarchies in a normalized, elegant way while maintaining strong type consistency in Java. It’s ideal for systems that model real-world hierarchies like employees, vehicles, or users — where shared attributes matter, but subclass-specific details need their own home.
Read more at : Techniques to Map Subclasses in JPA Using @Inheritance(strategy = JOINED)





