skip to content

JPA's @Inheritance annotation offers SINGLE_TABLE, JOINED and TABLE_PER_CLASS. Describe the table layout each one produces and the main trade-off of each.

level: middleimportance: must knowfreq 68%

answer

  1. SINGLE_TABLE = one table + DTYPE, subclass columns nullable
  2. JOINED = table per class, PK is also FK, join per level
  3. TABLE_PER_CLASS = duplicated columns, UNION ALL, no IDENTITY
  4. Default strategy is SINGLE_TABLE
  5. No FK can point at the base type under TABLE_PER_CLASS

basics

~20 s

SINGLE_TABLE puts the whole hierarchy in one table with a discriminator column; fastest, but subclass columns must be nullable. JOINED gives each class its own table joined by primary key; normalized with NOT NULL constraints, costs joins. TABLE_PER_CLASS gives each concrete class a full standalone table; polymorphic queries become UNIONs.

solid answer

~60 s

**SINGLE_TABLE** (the default): one table for the entire hierarchy, plus a discriminator column saying which subclass a row is. No joins ever, so reads and polymorphic queries are fastest — but every subclass-specific column must be **nullable**, so the database cannot enforce `NOT NULL` on them, and wide hierarchies produce sparse tables. **JOINED**: a table per class; the subclass table holds only its own columns and a primary key that is also a foreign key to the parent table. Fully normalized, `NOT NULL` works, no wasted space — at the cost of a join per level on every read and an extra INSERT per level on write. **TABLE_PER_CLASS**: one standalone table per concrete class, each repeating the inherited columns. Reads of a single concrete type are join-free, but a query on the base type becomes a `UNION ALL` over every table, and identifiers must be unique across the hierarchy so `IDENTITY` generation is unusable. Foreign keys to the base type are impossible. Default to SINGLE_TABLE; move to JOINED when subclasses add many columns or the constraints matter.

code

java · 25 lines
java
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "payment_type", discriminatorType = DiscriminatorType.STRING)
abstract class Payment {
    @Id @GeneratedValue private Long id;
    private BigDecimal amount;
}

@Entity @DiscriminatorValue("CARD")
class CardPayment extends Payment {
    private String cardNumber; // must be nullable in the shared table
}

// JOINED variant
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
abstract class Document {
    @Id @GeneratedValue private Long id;
}

@Entity
@PrimaryKeyJoinColumn(name = "document_id")
class Invoice extends Document {
    @Column(nullable = false) private String invoiceNumber; // NOT NULL is enforceable here
}

go deeper

for a junior

Name the three strategies and the table layout each produces, plus the fact that SINGLE_TABLE is the default.

for a middle

Give the SQL shape for each (no join / join per level / UNION ALL) and the concrete trade-off, including the nullable-column consequence of SINGLE_TABLE.

for a senior

Bring the operational consequences — multi-INSERT writes under JOINED, polymorphic query plans, CHECK-constraint workarounds, the identifier restriction under TABLE_PER_CLASS.

for a principal

Question whether the hierarchy should exist at all: composition, @MappedSuperclass, or a type column with distinct aggregates may model the domain better than JPA inheritance and its schema-migration burden.

## The problem Java has inheritance; relational tables do not. `@Inheritance(strategy = ...)` on the root entity chooses how a class hierarchy is projected onto tables. Take `Payment` (base) with `CardPayment` and `BankTransfer` as subclasses. ## SINGLE_TABLE — one table, a discriminator ``` payment(id, amount, currency, payment_type, card_number, expiry, iban, bic) ``` Every row of every subclass lives here. A **discriminator column** (`payment_type` by default `DTYPE`, a `VARCHAR`) records the concrete type so Hibernate can instantiate the right class. *Reads*: `SELECT * FROM payment WHERE id = ?` — one statement, no joins, whether you asked for `Payment` or `CardPayment`. Polymorphic queries add at most a discriminator predicate. This is the fastest strategy, by a wide margin, and it is the **default** when you specify no strategy. *Writes*: a single INSERT. *The cost*: `card_number` is meaningless for a bank transfer, so it must be nullable. You therefore **lose database-level `NOT NULL`** on every subclass-specific column; the only enforcement left is Bean Validation in the application. With many subclasses the table grows wide and sparse. Column-name collisions between sibling subclasses must be resolved by hand. ## JOINED — a table per class, joined by primary key ``` payment(id, amount, currency) card_payment(id -> payment.id, card_number, expiry) bank_transfer(id -> payment.id, iban, bic) ``` The subclass table's primary key is also a foreign key to the root table. This is the textbook normalized mapping: every column sits where it belongs, `NOT NULL` and unique constraints work, no wasted space, and a foreign key can reference `payment(id)` polymorphically. *Reads*: loading a `CardPayment` needs a join to `payment`. A polymorphic `SELECT p FROM Payment p` produces an outer join to **every** subclass table (Hibernate 6 typically emits one query with left joins, older versions sometimes used a union of joins), so the cost grows with the number of subclasses — the classic "my query joins nine tables" complaint. *Writes*: an INSERT per level (root plus subclass), and deletes likewise. `@PrimaryKeyJoinColumn` renames the join column when you do not want the inherited name. ## TABLE_PER_CLASS — one standalone table per concrete class ``` card_payment(id, amount, currency, card_number, expiry) bank_transfer(id, amount, currency, iban, bic) ``` Inherited columns are **duplicated** into each table; the abstract root usually gets no table at all. *Reads of one concrete type*: a single table, no joins — as fast as SINGLE_TABLE. *Polymorphic reads*: `SELECT p FROM Payment p` becomes a `UNION ALL` across every concrete table (Hibernate builds a synthetic "union subclass" view, padding missing columns with NULL literals). Cost and plan quality degrade quickly with the number of subclasses, and the optimizer often cannot push predicates as well as you would like. *Structural limitations*: identifiers must be unique **across** the whole hierarchy, because two rows in different tables both claiming id 1 would be the same `Payment`. That rules out `GenerationType.IDENTITY`; use a shared sequence or `TABLE` generation, or assigned UUIDs. And there is no single table for the base type, so **no foreign key can reference `Payment`** — any association to the base type must be modelled as a join table or duplicated per subtype. JPA marks support for this strategy as optional, though Hibernate implements it. ## How to choose - Subclasses add a handful of columns, read performance matters, hierarchy is shallow → **SINGLE_TABLE**. This is right far more often than its reputation suggests. - Subclasses add many columns, or the mandatory-ness of those columns must be enforced by the database, or the table would be embarrassingly sparse → **JOINED**. - You need per-type isolation and essentially never query the base type polymorphically → **TABLE_PER_CLASS**; often a sign the hierarchy is not really a hierarchy, and `@MappedSuperclass` may be the better fit. ## Things interviewers probe next - The strategy is declared on the **root** entity and applies to the whole hierarchy; you cannot mix per branch (Hibernate historically allowed some mixing via `@SecondaryTable`-like tricks, but the portable answer is: one strategy per hierarchy). - Under SINGLE_TABLE you can add `@DiscriminatorFormula` for legacy schemas that encode the type in an expression rather than a dedicated column. - A **nullable subclass column that should be mandatory** is the single most common production regret with SINGLE_TABLE; teams either accept Bean Validation only, or add a database CHECK constraint conditioned on the discriminator. - Under JOINED, deleting a base row must delete the subclass row too — Hibernate does it in the right order, but hand-written SQL against the schema will not. The strongest answer names the layout, the SQL shape (no join / join / union), and the one thing you lose in each: constraints, join cost, and polymorphic query cost respectively.

  • Which strategy is the default when @Inheritance is present without a strategy, or absent entirely on an entity hierarchy?
    SINGLE_TABLE. If a mapped superclass-free entity hierarchy exists and no strategy is declared, JPA uses SINGLE_TABLE with a default discriminator column named DTYPE of type String. That default is also usually the right choice, so specifying it explicitly is documentation rather than a change.
  • Why can't you use GenerationType.IDENTITY with TABLE_PER_CLASS?
    Identifiers must be unique across the entire hierarchy, because a polymorphic reference to the base type carries only an id and Hibernate must be able to find exactly one row for it. IDENTITY columns are per-table counters, so two concrete tables would independently produce id 1. A shared sequence, TABLE generation, or assigned UUIDs solve it.
  • How do you keep a mandatory subclass column enforceable under SINGLE_TABLE?
    The database cannot use NOT NULL, because rows of sibling subclasses must leave the column null. The practical options are Bean Validation in the application layer, or a CHECK constraint conditioned on the discriminator such as CHECK (payment_type <> 'CARD' OR card_number IS NOT NULL). If that mandatory-ness is important across many columns, it is the signal to switch the hierarchy to JOINED.

SINGLE_TABLE is one big form where you leave irrelevant fields blank; JOINED is a common form plus a type-specific attachment stapled to it; TABLE_PER_CLASS is a separate complete form per type, so gathering everyone's answers means stacking piles of different forms together.

saying these in an interview costs you the question

  • "JOINED is always the correct, normalized choice" — ignoring its join and multi-INSERT cost
  • Thinking SINGLE_TABLE can keep NOT NULL on subclass-specific columns
  • Not knowing SINGLE_TABLE is the default
  • Claiming TABLE_PER_CLASS avoids joins in all cases — polymorphic queries become UNIONs
  • Believing you can choose a different strategy per subclass within one hierarchy

context