Understanding JPA JOINED Inheritance: From Java Subtypes to Database Tables
The Domain Model
An employee enters a building every day using an access card. A visitor might enter the same building using a temporary credential.
Both people have a name, an identity, and authentication information. However, they do not have the same business data. An employee has an employee number and company email. A visitor has a visitor number and an external company.
Java represents this naturally with inheritance:
AccessUser
├── Employee
└── Visitor
But relational databases do not understand classes, inheritance, or subtypes. They understand tables, columns, primary keys, foreign keys, and joins.
This creates an important question:
How does Hibernate store one Java subtype when part of its data comes from a parent class and the rest comes from the subtype?
JPA’s JOINED inheritance strategy answers this.
This note focuses exclusively on InheritanceType.JOINED. JPA also supports SINGLE_TABLE and TABLE_PER_CLASS, while @MappedSuperclass is commonly discussed as another inheritance-mapping approach. We will leave those alternatives for separate articles.
Consider a simplified access-control system.
Every person who receives access credentials is represented as an AccessUser. Employees and Visitors are specialized types of access users.
The parent class contains information shared by every subtype:
public abstract class AccessUser {
protected Long id;
protected String firstName;
protected String lastName;
protected String credential;
}
An employee adds employee-specific information:
public class Employee extends AccessUser {
private String employeeId;
private String email;
}
A visitor adds visitor-specific information:
public class Visitor extends AccessUser {
private String visitorId;
private String company;
}
The entity hierarchy looks like this:
This hierarchy represents a genuine subtype relationship:
An Employee is an AccessUser.
A Visitor is an AccessUser.
The next challenge is mapping that object model to relational tables.
Mapping the Hierarchy with JOINED
The inheritance strategy is declared on the root entity:
@Entity
@Table(name = "access_users")
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class AccessUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
protected Long id;
@Column(nullable = false)
protected String firstName;
@Column(nullable = false)
protected String lastName;
protected String credential;
protected AccessUser() {
}
}
The key annotation is:
@Inheritance(strategy = InheritanceType.JOINED)
It tells the JPA provider to:
Use one table for the root entity.
Use a separate table for each managed subtype.
Store common fields in the root table.
Store subtype-specific fields in the subtype table.
Connect the parent and subtype rows using the same primary key.
Declare the strategy only on the root of the hierarchy. It does not need to be repeated on every subtype.
The concrete subtypes still need to be JPA entities:
@Entity
@Table(name = "employees")
public class Employee extends AccessUser {
@Column(nullable = false, unique = true)
private String employeeId;
@Column(nullable = false)
private String email;
protected Employee() {
}
}
@Entity
@Table(name = "visitors")
public class Visitor extends AccessUser {
@Column(nullable = false, unique = true)
private String visitorId;
private String company;
protected Visitor() {
}
}
Each subtype uses extends AccessUser for Java inheritance and @Entity for JPA persistence.
The Resulting Database Tables
The mapping produces one parent table and one table for each subtype:
A simplified PostgreSQL schema could look like this:
CREATE TABLE access_users (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
credential TEXT
);
CREATE TABLE employees (
id BIGINT PRIMARY KEY,
employee_id TEXT NOT NULL UNIQUE,
email TEXT NOT NULL,
CONSTRAINT fk_employees_access_user
FOREIGN KEY (id) REFERENCES access_users (id)
);
CREATE TABLE visitors (
id BIGINT PRIMARY KEY,
visitor_id TEXT NOT NULL UNIQUE,
company TEXT,
CONSTRAINT fk_visitors_access_user
FOREIGN KEY (id) REFERENCES access_users (id)
);
The most important part of this structure is the subtype ID:
employees.id → access_users.id
visitors.id → access_users.id
The subtype table’s id is both:
its primary key;
a foreign key to
access_user.id
Suppose both tables contain a row with ID 42:
access_users
────────────────────────────────────────────
id first_name last_name credential
42 Ethan Zay CARD-1007
visitors
────────────────────────────────────
id visitor_id company
42 V99881 Example Ltd
These are not two independent business objects. Together, they represent one Visitor:
access_users row 42
+
visitors row 42
↓
one Visitor entity
This is the central idea behind JOINED inheritance.
What Happens When Hibernate Saves a Visitor?
Consider the following object:
Visitor visitor = new Visitor();
visitor.setFirstName("Ethan");
visitor.setLastName("Zay");
visitor.setCredential("CARD-1007");
visitor.setVisitorId("V99881");
visitor.setCompany("Example Ltd");
visitorRepository.save(visitor);
Hibernate needs to persist both the inherited state and the visitor-specific state.
It first inserts the common fields into access_users:
INSERT INTO access_users (
first_name,
last_name,
credential
)
VALUES (
'Ethan',
'Zay',
'CARD-1007'
)
RETURNING id;
Assume that the database generates ID 42. Hibernate then uses that ID when inserting the subtype fields:
INSERT INTO visitors (
id,
visitor_id,
company
)
VALUES (
42,
'V99881',
'Example Ltd'
);
One Java object therefore produces two database rows:
Visitor object
│
├── common state ────────> access_users row 42
│
└── visitor state ───────> visitors row 42
Both inserts normally execute within the same transaction. If one insert fails, the transaction should roll back instead of leaving an incomplete entity behind.
What Happens When Hibernate Loads a Visitor?
When the application loads visitor 42, Hibernate needs information from both tables.
The exact generated SQL depends on the Hibernate version and database dialect, but its logical form is:
SELECT
au.id,
au.first_name,
au.last_name,
au.credential,
v.visitor_id,
v.company
FROM access_users au
JOIN visitors v ON v.id = au.id
WHERE au.id = 42;
Hibernate combines the columns into one Java object:
Visitor {
id = 42,
firstName = "Ethan",
lastName = "Zay",
credential = "CARD-1007",
visitorId = "V99881",
company = "Example Ltd"
}
The application works with one object even though its state is stored across two tables.
This join is both the mechanism and the principal cost of JOINED inheritance. Every subtype read requires Hibernate to join the subtype table with its parent table.
Because the root is also an entity, a query against AccessUser can return both Employee and Visitor instances. Hibernate may need additional joins to determine the concrete subtype, so parent-level queries become more expensive as a hierarchy grows.
Benefits of JOINED Inheritance
The JOINED strategy provides several useful properties.
Normalized tables
Common fields are stored once in the parent table instead of being repeated in every subtype table.
Strong subtype constraints
Subtype-specific columns can have appropriate database constraints:
visitor_id TEXT NOT NULL
employee_id TEXT NOT NULL UNIQUE
This is harder to enforce when unrelated subtype columns share one large table.
Shared identity
Every employee and visitor has a common AccessUser identity. Other parts of the system can reference that shared identity when the domain requires it.
Polymorphic entity model
Application code can operate on the parent type when it does not need subtype-specific behavior.
The Jakarta Persistence specification describes JOINED as a well-normalized strategy with support for polymorphic relationships.
Trade-Offs to Consider
JOINED is not automatically the best strategy for every hierarchy.
Additional joins
Loading a subtype requires at least one join with its parent table.
Queries across the hierarchy
Queries against the parent entity may need to inspect several subtype tables. A hierarchy with many subtypes can generate large SQL statements.
Multi-table writes
Creating or deleting a subtype affects more than one table. Updating fields can also target different tables:
Changing firstName → UPDATE access_users
Changing company → UPDATE visitors
More complex schema changes
The database must maintain parent-child foreign-key integrity. Migrations and manual data operations need to respect that relationship.
The Java model may look simple, but developers should still inspect the SQL generated for important production queries.
Common implementation mistakes
Missing @Entity on a subtype
This is incomplete:
@Table(name = "visitors")
public class Visitor extends AccessUser {
}
@Table specifies a table mapping, but it does not register the class as an entity.
The correct mapping is:
@Entity
@Table(name = "visitors")
public class Visitor extends AccessUser {
}
Declaring a Separate ID in the subtype
The subtype should inherit the parent ID. It should not introduce an unrelated primary key:
@Entity
public class Visitor extends AccessUser {
// Do not declare another @Id here.
}
The shared ID is what connects the parent and subtype rows.
Using inheritance only to reuse fields
Two entities sharing createdAt and updatedAt does not necessarily mean they belong to the same entity hierarchy.
Use inheritance when the subtype truly represents the parent concept:
Visitor is an AccessUser → valid subtype
When JOINED is a good fit?
Consider JOINED when:
the classes have a genuine “is-a” relationship;
the subtypes share meaningful business identity;
normalized tables are important;
subtype-specific constraints matter;
polymorphic behavior is required;
the hierarchy contains a controlled number of subtypes;
the cost of joins is acceptable.
Reconsider it when:
the classes only share technical fields;
the hierarchy contains many levels or subtypes;
most queries process the entire hierarchy at high volume;
query latency is more important than normalization;
the database structure would be simpler without entity inheritance.
Key takeaway
JPA JOINED inheritance maps one Java entity across a parent table and a subtype table.
The parent table stores shared state. The subtype table stores specialized state. The same primary key connects both rows:
AccessUser row + Employee row = one Employee entity
AccessUser row + Visitor row = one Visitor entity
This produces a normalized schema and a clean Java hierarchy, but it also introduces joins and multi-table persistence operations.
Before choosing JOINED, draw three things:
The Java class hierarchy.
The resulting database tables.
The SQL required to save and load a subtype.
If those three views accurately represent the business domain and the query cost is acceptable JOINED can be a clear and production-ready inheritance strategy.

