What Inheritance Types Does JPA Support
The object-oriented paradigm in Java relies on polymorphism and class inheritance, while the relational database model operates on flat tables and foreign keys (the Object-Relat...
The object-oriented paradigm in Java relies on polymorphism and class inheritance, while the relational database model operates on flat tables and foreign keys (the Object-Relational Impedance Mismatch problem). The JPA specification defines three core inheritance mapping strategies (SINGLE_TABLE, JOINED, TABLE_PER_CLASS) using the @Inheritance annotation, alongside the non-polymorphic property-sharing pattern @MappedSuperclass.
🟢 Junior Level
The 3 JPA Inheritance Strategies
The inheritance strategy is declared using @Inheritance(strategy = InheritanceType...) on the root entity of the hierarchy:
3 INHERITANCE MAPPING STRATEGIES IN RELATIONAL DATABASES
1. SINGLE_TABLE (Default) 2. JOINED 3. TABLE_PER_CLASS
┌─────────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐
│ payments │ │ payments (Base) │ │ card_payments │
├─────────────────────────┤ ├──────────────────────┤ ├──────────────────────┤
│ id, dtype, amount, │ │ id, amount │ │ id, amount, card_num │
│ card_num, cash_date │ └──────────┬───────────┘ └──────────────────────┘
└─────────────────────────┘ │ PK/FK ┌──────────────────────┐
(All classes in 1 table) ┌──────────┴───────────┐ │ cash_payments │
│ card_payments │ ├──────────────────────┤
├──────────────────────┤ │ id, amount, cash_date│
│ id (FK), card_num │ └──────────────────────┘
└──────────────────────┘ (Isolated standalone tables)
Basic Configuration Example:
// Root base entity:
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "payment_type", discriminatorType = DiscriminatorType.STRING)
public abstract class Payment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private BigDecimal amount;
}
// Subclass 1: Credit card payment
@Entity
@DiscriminatorValue("CARD")
public class CardPayment extends Payment {
private String cardNumber;
}
// Subclass 2: Cash payment
@Entity
@DiscriminatorValue("CASH")
public class CashPayment extends Payment {
private String cashierName;
}
🟡 Middle Level
Detailed Strategy Analysis & Trade-offs
1. InheritanceType.SINGLE_TABLE (Single Table for Entire Hierarchy)
All fields of all classes across the hierarchy are stored in a single database table. A discriminator column (DTYPE by default) identifies the concrete subclass.
- Pros:
- Maximum read and write performance (zero SQL
JOINorUNIONoperations). - Polymorphic queries execute directly:
SELECT * FROM payments.
- Maximum read and write performance (zero SQL
- Cons:
- Table sparsity (many
NULLvalues for subclass-specific columns). - Cannot enforce database-level
NOT NULLconstraints on subclass fields (thecard_numbercolumn must allowNULLto permit insertingCashPaymentrows).
- Table sparsity (many
2. InheritanceType.JOINED (Normalized Table per Class)
The root class maps to a base table containing common columns, while each subclass maps to its own separate table containing specific columns, joined via primary/foreign key pairs.
- Pros:
- Fully normalized database schema (Third Normal Form - 3NF).
- Subclass columns fully support
NOT NULLdatabase integrity constraints.
- Cons:
- Reading polymorphic graphs requires executing an SQL
LEFT OUTER JOINacross all subclass tables. - Inserting a subclass record requires two sequential SQL
INSERTstatements (first into base, then into child table).
- Reading polymorphic graphs requires executing an SQL
3. InheritanceType.TABLE_PER_CLASS (Table per Concrete Class)
Each concrete subclass maps to a standalone database table containing both its own fields and duplicated columns inherited from the base class.
- Pros: Each table is self-contained without sparse
NULLs or joins when querying a known concrete subclass. - Cons: Polymorphic queries targeting the base class (
SELECT p FROM Payment p) generate heavyUNION ALLqueries that bypass index optimization.
Architectural Comparison Matrix
| Architectural Criterion | SINGLE_TABLE |
JOINED |
TABLE_PER_CLASS |
|---|---|---|---|
| Schema Normalization | ❌ Low (Sparse NULLs) |
✅ High (3NF) | ⚠️ Medium (Column duplication) |
NOT NULL Constraints |
❌ Impossible for subclass fields | ✅ Fully supported | ✅ Fully supported |
INSERT Statement Count |
⚡ 1 INSERT | 🐢 2+ INSERTs | ⚡ 1 INSERT |
| Polymorphic Query Cost | ⚡ 1 Query (No JOIN) | 🐢 Heavy LEFT OUTER JOIN |
🚨 Slow UNION ALL scan |
| Discriminator Column | Required (@DiscriminatorColumn) |
Optional | Not used |
| Recommendation | Default choice for 2–5 subclasses | Normalized enterprise systems | Rare (Avoid when polymorphism needed) |
🔴 Senior Level
@MappedSuperclass vs. Polymorphic Inheritance
A frequent conceptual confusion in technical interviews is treating @MappedSuperclass as an inheritance strategy:
// Base class for shared audit fields:
@MappedSuperclass
public abstract class BaseAuditableEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@CreationTimestamp
private Instant createdAt;
}
@Entity
public class User extends BaseAuditableEntity { ... }
@Entity
public class Order extends BaseAuditableEntity { ... }
Fundamental Invariants of @MappedSuperclass:
- A
@MappedSuperclassis NOT a JPA entity (@Entityis absent). - It has no dedicated database table.
- Does not support polymorphic queries:
SELECT b FROM BaseAuditableEntity bfails compilation because the class is not an entity known to Hibernate. - Other entities cannot define associations to it (e.g.,
@ManyToOne BaseAuditableEntity entityis illegal).
The Polymorphic Foreign Key Deadlock with TABLE_PER_CLASS
Consider an entity Order referencing an abstract payment:
@Entity
public class Order {
@Id
private Long id;
@OneToOne
@JoinColumn(name = "payment_id")
private Payment payment; // Payment uses TABLE_PER_CLASS
}
Why This Creates an Architectural Deadlock:
- The column
orders.payment_idmust enforce referential integrity. - However, the
paymentstable does not exist; only isolatedcard_paymentsandcash_paymentstables exist. - Relational database engines cannot declare a single Foreign Key constraint referencing multiple tables simultaneously!
- To load the associated payment, Hibernate must issue separate queries or complex outer unions against every possible subclass table.
- Senior Rule: Never map
@ManyToOneor@OneToOneassociations to an abstract base class configured withTABLE_PER_CLASS.
4 Tricky Questions
1. In SINGLE_TABLE, how do you enforce that a subclass field (e.g., cardNumber) is mandatory if the database column must remain NULLable?
Answer:
- At the database DDL level, the column must permit
NULLvalues to accommodate other subclasses. - Strategy 1 (Database Check Constraint): Add a conditional table-level check constraint:
ALTER TABLE payments ADD CONSTRAINT chk_card_number CHECK (payment_type != 'CARD' OR card_number IS NOT NULL); - Strategy 2 (Bean Validation): Place the
@NotNullannotation on thecardNumberproperty inside theCardPaymentsubclass. Hibernate Validator intercepts empty values during thePrePersistphase before SQL execution.
2. What happens when executing a paginated polymorphic query SELECT p FROM Payment p under TABLE_PER_CLASS?
Answer:
- Hibernate generates an SQL query wrapping all subclass tables in a
UNION ALL:SELECT p.id, p.amount, p.card_num, NULL AS cash_date, 1 AS clazz_ FROM card_payments p UNION ALL SELECT p.id, p.amount, NULL AS card_num, p.cash_date, 2 AS clazz_ FROM cash_payments p LIMIT 10 OFFSET 20; - The database engine must execute full scans of both tables, materialize them into an intermediate in-memory temporary table, and only then apply sorting and
LIMIT/OFFSET. - Query performance degrades severely as table sizes grow.
3. How do you query a specific subclass and perform downcasting in JPQL under JOINED or SINGLE_TABLE?
Answer:
- Use the
TYPE(e)expression in JPQL to filter by concrete subclass:List<Payment> cards = entityManager.createQuery( "SELECT p FROM Payment p WHERE TYPE(p) = CardPayment", Payment.class ).getResultList(); - Use the
TREAToperator for downcasting subclass properties in theWHEREorSELECTclauses:List<Payment> visa = entityManager.createQuery( "SELECT p FROM Payment p WHERE TREAT(p AS CardPayment).cardNumber LIKE '4%'", Payment.class ).getResultList();
4. If an entity is loaded via a lazy association order.getPayment(), will order.getPayment() instanceof CardPayment evaluate to true?
Answer:
- No, it will evaluate to
false! - When the association is lazy, Hibernate generates a ByteBuddy proxy based on the declared base class:
Payment$ByteBuddy$.... - Because the proxy is a direct subclass of
Paymentand does not inherit fromCardPayment, Java’s nativeinstanceof CardPaymentcheck returnsfalse, even if the underlying database row represents a card payment! - Solution: Unproxy the entity before checking:
Payment realPayment = Hibernate.unproxy(order.getPayment(), Payment.class); boolean isCard = realPayment instanceof CardPayment; // Or using Hibernate utility: boolean isCardType = Hibernate.getClass(order.getPayment()) == CardPayment.class;
🎯 Interview Cheat Sheet
30-Second Elevator Pitch
“JPA provides 3 inheritance strategies:
SINGLE_TABLE(all classes in one table with a discriminator column; highest performance, but requires nullable subclass columns),JOINED(normalized base table plus subclass tables; supportsNOT NULLconstraints, but requires joins), andTABLE_PER_CLASS(independent tables with duplicated fields; generates expensiveUNION ALLqueries).@MappedSuperclassis not an inheritance strategy, but a code reuse pattern for shared audit properties without an entity table or polymorphic querying. In roughly 80% of enterprise systems,SINGLE_TABLEremains the preferred standard due to superior query performance.”
Key Production Metrics & Indicators
- Execution Time of Polymorphic Root Queries: Monitored to detect degradation in
JOINEDandTABLE_PER_CLASShierarchies. - Table Column Count in
SINGLE_TABLE: Hierarchies exceeding 40–50 columns indicate schema bloat, signaling a need to transition toJOINEDor decompose into independent aggregates.
Red Flags (What NOT to Say)
- ❌ “@MappedSuperclass is a fourth JPA inheritance strategy.” — It is not an inheritance strategy; it creates no entity or table and does not support polymorphic queries.
- ❌ “SINGLE_TABLE supports database-level NOT NULL constraints on subclass-specific columns.” — False; columns must be nullable at the DB level for other subclasses to be inserted.
- ❌ “TABLE_PER_CLASS is ideal for
@ManyToOnepolymorphic associations.” — Relational databases cannot create a single foreign key pointing to multiple distinct tables. - ❌ “Java’s
instanceofoperator works reliably on lazy proxy associations.” — False; lazy ByteBuddy proxies of the base class failinstanceof SubClasschecks until unproxied.
Related Topics
- What is JPQL and How Does It Differ from SQL — Polymorphism in JPQL (
TYPE/TREAT) - What is Criteria API and When to Use It — Type-safe polymorphic queries
- How to Properly Use @OneToMany and @ManyToOne — Association mapping
- What Are the Peculiarities of Bidirectional Relationships — Association semantics