← Back to the section

A class hierarchy is a familiar tool in Java. But how do you carry it over into a relational database? Tables know nothing about inheritance, and Hibernate offers three mapping strategies plus a helper tool, @MappedSuperclass.

Why map a hierarchy

Suppose you have a payment system with three types of payments: CardPayment, BankTransferPayment, CryptoPayment. They all share common fields: id, amount, createdAt, status. The specifics differ: the card one has cardLast4, the bank one has ibanNumber, the crypto one has walletAddress.

Without special mapping you would have to either duplicate the common fields across three tables, or store everything mixed together in one, or do the JOINs by hand. Hibernate takes on this work for you — you only need to pick a strategy.

@Inheritance and the three strategies

The base class of the hierarchy is annotated with @Inheritance(strategy = ...). The subclasses are ordinary @Entity classes. We will look at all three strategies using the same payment example.

SINGLE_TABLE — one table with a discriminator column

All classes of the hierarchy are stored in a single table. Hibernate adds a discriminator column (dtype by default), which tells it which type of record to read.

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "payment_type")
public abstract class Payment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private BigDecimal amount;
    private LocalDateTime createdAt;
    private String status;
}

@Entity
@DiscriminatorValue("CARD")
public class CardPayment extends Payment {
    private String cardLast4;
}

@Entity
@DiscriminatorValue("BANK")
public class BankTransferPayment extends Payment {
    private String ibanNumber;
}

The payment table will hold the columns of all three types at once. For a CardPayment row, iban_number will be NULL, and vice versa.

Pros: maximum simplicity and performance — a single SELECT with no JOINs. A polymorphic query ("all payments") is trivial.

Cons: columns of the child types cannot be NOT NULL at the database level (for other types they are always NULL). With a large number of subtypes the table becomes wide and sparse. Data integrity is enforced only at the application level.

Short formula: SINGLE_TABLE is the default choice when there are few subtypes and no strict NOT NULL requirements.

JOINED — a separate table per type

Each class of the hierarchy gets its own table. The child tables contain only the specific fields plus a foreign key to the parent.

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Payment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private BigDecimal amount;
    private LocalDateTime createdAt;
    private String status;
}

@Entity
@Table(name = "card_payment")
public class CardPayment extends Payment {
    private String cardLast4;
}

@Entity
@Table(name = "bank_transfer_payment")
public class BankTransferPayment extends Payment {
    private String ibanNumber;
}

The schema in the database:

  • payment(id, amount, created_at, status, dtype)
  • card_payment(id, card_last4)id is both PK and FK to payment
  • bank_transfer_payment(id, iban_number) — the same

Pros: a normalized schema. Each field lives in its own table, and NOT NULL works as expected. Well suited when subtypes have many specific fields.

Cons: every query for an entity does a JOIN. A polymorphic query ("all payments") is a LEFT JOIN against every subtype. On large data volumes and a wide inheritance tree this can become noticeable.

TABLE_PER_CLASS — a separate table with the full set of fields

Each concrete class gets a table with all its fields — both its own and the inherited ones.

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Payment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private BigDecimal amount;
    private LocalDateTime createdAt;
    private String status;
}

@Entity
@Table(name = "card_payment")
public class CardPayment extends Payment {
    private String cardLast4;
}

The card_payment table contains: id, amount, created_at, status, card_last4. The common fields are duplicated.

Pros: reading a concrete type is a simple SELECT with no JOIN. There is no normalization, but queries for a specific type are fast.

Cons: a polymorphic query ("find all payments") turns into a UNION ALL across all tables — expensive and awkward. Auto-increment with GenerationType.IDENTITY does not work correctly; you need SEQUENCE. The strategy is almost never used in practice.

Short formula: TABLE_PER_CLASS — avoid it if you need polymorphic queries.

@MappedSuperclass — reusing fields without entity inheritance

@MappedSuperclass is a special case. It is not an inheritance strategy in the JPA sense: the superclass is not an entity, has no table of its own, and does not support polymorphic queries.

The point is to pull common fields into a base class so you do not duplicate them in every entity.

@MappedSuperclass
public abstract class BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @CreationTimestamp
    private LocalDateTime createdAt;

    @UpdateTimestamp
    private LocalDateTime updatedAt;
}

@Entity
@Table(name = "card_payment")
public class CardPayment extends BaseEntity {
    private BigDecimal amount;
    private String cardLast4;
}

@Entity
@Table(name = "user_account")
public class UserAccount extends BaseEntity {
    private String email;
}

CardPayment and UserAccount are different entities with different tables. They are not related by polymorphism. Hibernate simply "copies" the fields from BaseEntity into each table when generating the schema.

@MappedSuperclass is the right choice for common technical fields (id, createdAt, updatedAt, version). @Inheritance is for domain hierarchies where you need polymorphism.

What to choose

StrategySchemaPolymorphic queryWhen to use
SINGLE_TABLE1 tableSimple SELECTFew subtypes, no strict NOT NULL
JOINEDN tables (normalized)JOIN per subtypeMany specific fields, integrity needed in the DB
TABLE_PER_CLASSN tables (denormalized)UNION ALLPractically never used
@MappedSuperclassSame as the subclassesNot supportedCommon technical fields

Practical rule: start with SINGLE_TABLE. Move to JOINED when the number of subtypes is large, there are many specific fields, and data integrity at the schema level matters. TABLE_PER_CLASS and @MappedSuperclass are for special cases.

In short

  • Hibernate supports three strategies for mapping a hierarchy: SINGLE_TABLE, JOINED, TABLE_PER_CLASS.
  • SINGLE_TABLE — the simplest and most performant: one table, a discriminator column, no JOIN. The cost — you cannot use NOT NULL for subtype fields.
  • JOINED — a normalized schema: common fields in the parent table, specific ones in the children. Every query does a JOIN.
  • TABLE_PER_CLASS — each type in a separate table with all its fields. Polymorphic queries via UNION ALL — avoid it.
  • @MappedSuperclass — not an inheritance strategy but field reuse. Suitable for technical base classes (id, createdAt).
  • By default, choose SINGLE_TABLE; move to JOINED when you need a strict schema.
  • Entity Mapping — how the @Entity, @Table, @Column annotations describe the schema
  • Associations Between Entities — @OneToMany, @ManyToOne, @ManyToMany and their nuances
  • JPQL and Criteria API — how to write queries against hierarchies with polymorphism