Definition
JPA provides two mechanisms for class hierarchies, and they serve different purposes.
- @MappedSuperclass – a non-entity base class that holds shared mapping information (columns, audit fields) which subclass entities inherit as their own columns, but it has no table, no identity of its own, and cannot be queried or referenced polymorphically.
- @Inheritance – marks a base class that is itself an @Entity, creating a real type hierarchy where the base and all subclasses are entities that support polymorphic queries and polymorphic associations.
- Polymorphic query – a JPQL query against the base type that returns all subtypes, such as
SELECT a FROM Account areturning both SavingsAccount and CurrentAccount rows. - @PrePersist / @PreUpdate – lifecycle callbacks that a @MappedSuperclass can define once and all subclasses inherit, commonly used for automatic audit stamping.
At National Bank of India, every form -- whether it is a savings form, a current-account form, or a loan form -- has a standard header block with the date, the branch code, and the officer's name. That header is like @MappedSuperclass: it is copied onto every form, but you cannot walk into the records room and ask "show me all headers" because a header is not a real form on its own. Now consider "Account" itself: Savings Account and Current Account are real kinds of Account, and you can ask the records room "give me all accounts" and receive both. That is @Inheritance: Account genuinely exists as a queryable category.
Understanding the Concepts
What is @MappedSuperclass?
@MappedSuperclass is a non-entity base class that holds shared mapping information. You put common fields -- an id strategy, audit columns like createdAt and updatedAt, a @Version column -- on this base class, and each @Entity subclass inherits those fields as columns in its own table. The base class is not an entity: there is no table for it, you cannot write em.find(Auditable.class, id), you cannot run SELECT a FROM Auditable a, and no relationship can target it.
package com.bank.model;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@MappedSuperclass
public abstract class Auditable {
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@Column(name = "created_by")
private String createdBy;
@Version
private Long version;
@PrePersist
protected void onCreate() {
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
this.createdBy = "SYSTEM";
}
@PreUpdate
protected void onUpdate() {
this.updatedAt = LocalDateTime.now();
}
// getters
}
Every entity that extends Auditable gets created_at, updated_at, created_by, and version columns in its own table, plus automatic stamping via the lifecycle callbacks.
package com.bank.model;
import jakarta.persistence.*;
import java.math.BigDecimal;
@Entity
@Table(name = "accounts")
public class Account extends Auditable {
@Id
@Column(name = "account_id")
private String accountId; // SAV2001
@Column(name = "holder_name")
private String holderName; // Priya
private BigDecimal balance;
}
@Entity
@Table(name = "loan_applications")
public class LoanApplication extends Auditable {
@Id
@Column(name = "loan_id")
private String loanId;
@Column(name = "applicant_name")
private String applicantName; // Rajesh
@Column(name = "loan_amount")
private BigDecimal loanAmount;
}
accounts table: loan_applications table:
account_id | holder_name loan_id | applicant_name | loan_amount
balance | created_at | created_at | updated_at
updated_at | created_by | created_by | version
version | |
Both tables have audit columns, but there is no "auditable" table.
What is @Inheritance?
@Inheritance creates a genuine polymorphic entity hierarchy. The base class is itself an @Entity and carries @Inheritance(strategy = ...). All subclasses are entities. The base type is queryable: SELECT a FROM Account a returns SavingsAccount and CurrentAccount rows together. An association like @ManyToOne Account account can hold either subtype.
package com.bank.model;
import jakarta.persistence.*;
import java.math.BigDecimal;
@Entity
@Table(name = "accounts")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "account_type")
public abstract class Account {
@Id
@Column(name = "account_id")
protected String accountId;
@Column(name = "holder_name")
protected String holderName;
protected BigDecimal balance;
}
@Entity
@DiscriminatorValue("SAVINGS")
public class SavingsAccount extends Account {
@Column(name = "interest_rate")
private BigDecimal interestRate;
}
@Entity
@DiscriminatorValue("CURRENT")
public class CurrentAccount extends Account {
@Column(name = "overdraft_limit")
private BigDecimal overdraftLimit;
}
package com.bank.service;
import com.bank.model.Account;
import jakarta.persistence.EntityManager;
import java.util.List;
public class AccountService {
private final EntityManager em;
public AccountService(EntityManager em) { this.em = em; }
public List<Account> findAllAccounts() {
// Polymorphic query -- returns both SavingsAccount and CurrentAccount
return em.createQuery(
"SELECT a FROM Account a", Account.class)
.getResultList();
}
}
Expected output:
[SavingsAccount{SAV2001, Priya, 50000.00, rate=6.5},
CurrentAccount{CUR3001, Rajesh, 120000.00, limit=50000.00}]
Why does JPA need both mechanisms?
The two mechanisms solve different problems. @MappedSuperclass is purely a code-reuse mechanism for mappings. You want to avoid copy-pasting createdAt, updatedAt, and version across 30 entities, so you pull them into one base class. The subtypes are not related domain concepts -- an Account and a LoanApplication have nothing in common except audit fields.
@Inheritance models a real domain relationship. SavingsAccount and CurrentAccount are genuinely different kinds of Account. Your business logic needs to treat them uniformly sometimes ("list all accounts") and specifically other times ("calculate interest only for savings"). That requires polymorphic queries, polymorphic associations, and runtime type dispatch -- all of which only @Inheritance provides.
// @MappedSuperclass use case -- unrelated entities sharing audit fields
// Account and LoanApplication are not "kinds of Auditable"
// You never query "all auditables"
// @Inheritance use case -- related domain types
// SavingsAccount IS an Account
// You DO query "all accounts" and get both types back
How is @MappedSuperclass different from @Inheritance?
This comparison table captures every important difference.
| Feature | @MappedSuperclass | @Inheritance |
|---|---|---|
| Base class is an entity? | No | Yes |
| Base class has a table? | No | Yes (depends on strategy) |
| Polymorphic queries? | Not possible | Yes -- query base, get all subtypes |
| Polymorphic associations? | Not possible | Yes -- @ManyToOne to base type |
| em.find(Base.class, id)? | Not possible | Works, returns correct subtype |
| Purpose | Share column mappings | Model a real type hierarchy |
| Subclass tables | Each fully independent | Linked by strategy (single, joined, table-per-class) |
| Lifecycle callbacks? | Yes, inherited by subclasses | Yes, inherited by subclasses |
| @AttributeOverride? | Yes, subclass can override columns | Not directly supported |
| Strategy choice needed? | No | Yes -- SINGLE_TABLE, JOINED, TABLE_PER_CLASS |
// Trying to query a @MappedSuperclass -- FAILS
// em.createQuery("SELECT a FROM Auditable a"); // compile error or runtime error
// Querying an @Inheritance base -- WORKS
List<Account> all = em.createQuery(
"SELECT a FROM Account a", Account.class)
.getResultList();
// Returns SavingsAccount and CurrentAccount rows together
What are the rules and constraints?
Rules for @MappedSuperclass:
- The class must be annotated with
@MappedSuperclass, not with@Entity. - It cannot have
@Tablebecause it has no table. - It can define
@Id,@Version,@Column, lifecycle callbacks, and even@ManyToOnerelationships that subclasses inherit. - Subclasses can override inherited column names with
@AttributeOverride. - Each subclass is a completely independent entity at the database level.
Rules for @Inheritance:
- The base class must be annotated with both
@Entityand@Inheritance. - You must choose a strategy:
SINGLE_TABLE(default),JOINED, orTABLE_PER_CLASS. SINGLE_TABLErequires a@DiscriminatorColumn(auto-generated if omitted).- All subclasses must also be
@Entity. - The base class must have an
@Id.
// Rules demonstration
@MappedSuperclass
public abstract class Auditable {
// Can define @Id here
// Can define @Version here
// Can define lifecycle callbacks
// Cannot have @Table
// Cannot be queried
}
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "account_type")
public abstract class Account {
@Id private String accountId;
// Must have @Id
// Must choose strategy
// Can be queried polymorphically
}
What are common mistakes?
Mistake 1: Trying to query a @MappedSuperclass.
// WRONG -- Auditable is not an entity, cannot be queried
@MappedSuperclass
public abstract class Auditable {
@Column(name = "created_at")
private LocalDateTime createdAt;
}
// This throws an exception at runtime:
// em.createQuery("SELECT a FROM Auditable a");
// IllegalArgumentException: Auditable is not a mapped entity
// CORRECT -- query the concrete entity subclass instead
List<Account> accounts = em.createQuery(
"SELECT a FROM Account a WHERE a.createdAt > :date", Account.class)
.setParameter("date", LocalDateTime.now().minusDays(30))
.getResultList();
Mistake 2: Using @Inheritance when you only need shared fields.
// WRONG -- Account and LoanApplication are unrelated domain concepts
// Do not force them into one inheritance hierarchy
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public abstract class Auditable {
@Id private Long id;
private LocalDateTime createdAt;
}
@Entity public class Account extends Auditable { ... }
@Entity public class LoanApplication extends Auditable { ... }
// Now Account and LoanApplication share one table with many nullable columns!
// CORRECT -- use @MappedSuperclass for shared fields
@MappedSuperclass
public abstract class Auditable {
@Column(name = "created_at")
private LocalDateTime createdAt;
}
@Entity @Table(name = "accounts")
public class Account extends Auditable { @Id private String accountId; }
@Entity @Table(name = "loan_applications")
public class LoanApplication extends Auditable { @Id private String loanId; }
// Each has its own clean table with audit columns
Mistake 3: Forgetting that @ManyToOne cannot reference a @MappedSuperclass.
// WRONG -- cannot reference a @MappedSuperclass in a relationship
@Entity
public class Transaction {
@Id private Long id;
@ManyToOne
private Auditable relatedEntity; // Auditable is not an entity!
}
// CORRECT -- reference a concrete entity
@Entity
public class Transaction {
@Id private Long id;
@ManyToOne
private Account account; // Account is an @Entity
}
Can you combine @MappedSuperclass and @Inheritance?
Yes, and this is the clean answer for most banking applications. Use @MappedSuperclass for audit fields and @Inheritance for real domain hierarchies.
package com.bank.model;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
// Layer 1: shared audit fields (not an entity)
@MappedSuperclass
public abstract class Auditable {
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@Version
private Long version;
@PrePersist
protected void onCreate() {
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
this.updatedAt = LocalDateTime.now();
}
}
// Layer 2: real entity hierarchy (IS an entity)
@Entity
@Table(name = "accounts")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "account_type")
public abstract class Account extends Auditable {
@Id
@Column(name = "account_id")
protected String accountId;
@Column(name = "holder_name")
protected String holderName;
protected BigDecimal balance;
}
@Entity
@DiscriminatorValue("SAVINGS")
public class SavingsAccount extends Account {
@Column(name = "interest_rate")
private BigDecimal interestRate;
}
@Entity
@DiscriminatorValue("CURRENT")
public class CurrentAccount extends Account {
@Column(name = "overdraft_limit")
private BigDecimal overdraftLimit;
}
Now every account entity inherits audit columns and lifecycle callbacks from Auditable, and you can still query Account polymorphically to get both savings and current accounts.
When should you use @MappedSuperclass?
Use @MappedSuperclass when:
- You have common columns (audit fields, version, soft-delete flag) repeated across many unrelated entities.
- You never need to query the base type directly.
- You never need a relationship pointing to the base type.
- You want each entity to have its own independent, clean table.
At National Bank of India, Pankaj uses a BaseEntity mapped superclass with createdAt, updatedAt, createdBy, and version fields. Every entity in the system extends it, getting automatic audit stamping with zero overhead.
package com.bank.model;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@MappedSuperclass
public abstract class BaseEntity {
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void prePersist() {
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void preUpdate() {
this.updatedAt = LocalDateTime.now();
}
}
When should you NOT use @MappedSuperclass?
Do not use @MappedSuperclass when:
- The subtypes are genuinely different kinds of the same domain concept (SavingsAccount and CurrentAccount are kinds of Account).
- You need polymorphic queries ("find all accounts regardless of type").
- You need polymorphic associations (
@ManyToOne Accountthat can hold any subtype). - You need runtime type dispatch in business logic.
In all these cases, use @Inheritance instead.
// You need to list all accounts regardless of type
// MappedSuperclass CANNOT do this
// Inheritance CAN do this
List<Account> allAccounts = em.createQuery(
"SELECT a FROM Account a WHERE a.balance > :min", Account.class)
.setParameter("min", new BigDecimal("10000.00"))
.getResultList();
What should a senior engineer know?
- @MappedSuperclass and Spring Data JPA. Spring Data's
JpaRepositoryworks with entities, not with mapped superclasses. You cannot createJpaRepository. - Combining layers. The common production pattern is @MappedSuperclass for audit/base fields at the top, then @Inheritance hierarchies where the domain requires polymorphism.
- @AttributeOverride on inherited fields. A subclass of @MappedSuperclass can use
@AttributeOverrideto rename inherited columns. This is useful when two subclasses need different column names for the same field. - Performance. @MappedSuperclass adds zero runtime overhead. It is purely a compile-time mapping mechanism. @Inheritance has performance implications depending on the strategy chosen.
- Testing. When writing integration tests, you cannot persist or query the @MappedSuperclass directly. Always test through concrete entity subclasses.
package com.bank.service;
import com.bank.model.Account;
import com.bank.model.SavingsAccount;
import com.bank.model.CurrentAccount;
import jakarta.persistence.EntityManager;
import java.math.BigDecimal;
import java.util.List;
public class AccountReportService {
private final EntityManager em;
public AccountReportService(EntityManager em) { this.em = em; }
public void generateReport() {
// Polymorphic query -- only possible with @Inheritance
List<Account> accounts = em.createQuery(
"SELECT a FROM Account a", Account.class)
.getResultList();
for (Account acc : accounts) {
if (acc instanceof SavingsAccount sa) {
System.out.println("Savings: " + sa.getAccountId()
+ " rate=" + sa.getInterestRate());
} else if (acc instanceof CurrentAccount ca) {
System.out.println("Current: " + ca.getAccountId()
+ " limit=" + ca.getOverdraftLimit());
}
}
}
}
Expected output:
Savings: SAV2001 rate=6.50
Current: CUR3001 limit=50000.00
Visual Overview
@MappedSuperclass (not an entity, no table)
Auditable
├── createdAt
├── updatedAt
└── version
│ inherits columns
├──────────────────────┐
▼ ▼
┌─────────────┐ ┌──────────────────┐
│ accounts │ │ loan_applications│
│ table │ │ table │
├─────────────┤ ├──────────────────┤
│ account_id │ │ loan_id │
│ holder_name │ │ applicant_name │
│ balance │ │ loan_amount │
│ created_at │ │ created_at │
│ updated_at │ │ updated_at │
│ version │ │ version │
└─────────────┘ └──────────────────┘
(independent tables, no shared base table)
@Inheritance (base IS an entity, shared hierarchy)
Account (entity, table = accounts)
├── account_id (PK)
├── holder_name
├── balance
├── account_type (discriminator)
│
├── SAVINGS ──▶ SavingsAccount
│ └── interest_rate
│
└── CURRENT ──▶ CurrentAccount
└── overdraft_limit
SELECT a FROM Account a
→ returns BOTH SavingsAccount and CurrentAccount rows
Combined pattern (production):
@MappedSuperclass Auditable
├── createdAt, updatedAt, version
│
▼
@Entity @Inheritance Account
├── accountId, holderName, balance
│
├── SavingsAccount (interestRate)
└── CurrentAccount (overdraftLimit)
Audit fields inherited + polymorphic queries work
Key Takeaway
A @MappedSuperclass is a non-entity base class that holds shared mapping information like audit columns, a version field, and lifecycle callbacks. Each entity subclass inherits those fields as columns in its own table, but the base class has no table, no identity, and cannot be queried or referenced in relationships. It is purely a code-reuse mechanism to avoid duplicating common mappings across unrelated entities like Account, LoanApplication, and Customer.
@Inheritance creates a real entity hierarchy where the base class is itself an entity. SavingsAccount and CurrentAccount are genuinely different kinds of Account, and you can write a JPQL query against Account and get both subtypes back in one result. You can also have a @ManyToOne reference to Account that holds either subtype at runtime. This is what you need when the subtypes are real variants of the same domain concept that you sometimes treat uniformly and sometimes specifically.
At National Bank of India, Pankaj combines both mechanisms. A @MappedSuperclass called BaseEntity holds createdAt, updatedAt, and version with @PrePersist callbacks that stamp them automatically, and every entity in the system extends it. Separately, Account is a real @Inheritance hierarchy with SavingsAccount and CurrentAccount subtypes, because reporting needs to list all accounts together and transactions need to reference Account polymorphically. The two mechanisms combine naturally: BaseEntity for audit fields at the top, and Account for the real domain hierarchy below it.
Remember one sentence: @MappedSuperclass shares columns without being an entity; @Inheritance creates a real entity hierarchy you can query and associate polymorphically.
---