@MappedSuperclass: Sharing Fields Without Inheritance Tables
Introduction
Almost every entity in a real application shares a few common fields: id, createdAt, updatedAt, and perhaps version. Writing these in every entity class is repetitive and error-prone. @MappedSuperclass lets you define them once in a base class that all entities extend — without creating a parent table or any inheritance mapping in the database.
What Is @MappedSuperclass?
@MappedSuperclass marks a class whose field mappings are inherited by subclass entities. The class itself:
- Is NOT an entity — it has no table of its own
- Cannot be queried directly
- Does not participate in JPA polymorphism (no
JOINto a parent table) - Is purely a Java construct for code reuse
Each subclass entity has its fields mapped to its own table, as if the parent’s fields were declared directly in the subclass.
Creating a Base Entity
package com.devopsmonk.jpademo.domain;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
@MappedSuperclass
@Getter
@Setter
public abstract class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
}
All entities extending BaseEntity automatically get id, createdAt, and updatedAt mapped to their own tables.
Using the Base Entity
@Entity
@Table(name = "categories")
public class Category extends BaseEntity {
@Column(nullable = false, length = 100)
private String name;
@Column(nullable = false, length = 100, unique = true)
private String slug;
// id, createdAt, updatedAt inherited from BaseEntity
}
@Entity
@Table(name = "products")
public class Product extends BaseEntity {
@Column(nullable = false, length = 255)
private String name;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal price;
// id, createdAt, updatedAt inherited from BaseEntity
}
Each entity’s table includes the inherited columns:
CREATE TABLE categories (
id BIGINT NOT NULL AUTO_INCREMENT,
created_at DATETIME(6) NOT NULL,
updated_at DATETIME(6) NOT NULL,
name VARCHAR(100) NOT NULL,
slug VARCHAR(100) NOT NULL UNIQUE,
PRIMARY KEY (id)
);
CREATE TABLE products (
id BIGINT NOT NULL AUTO_INCREMENT,
created_at DATETIME(6) NOT NULL,
updated_at DATETIME(6) NOT NULL,
name VARCHAR(255) NOT NULL,
price DECIMAL(10,2) NOT NULL,
PRIMARY KEY (id)
);
No base_entity table is created. The columns are in each entity’s table.
Populating Timestamps Automatically
Use JPA lifecycle callbacks (covered in detail in a later article) to set timestamps automatically:
@MappedSuperclass
@Getter
@Setter
public abstract class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
}
Now you never need to set createdAt or updatedAt manually:
Product product = new Product();
product.setName("Laptop");
product.setPrice(new BigDecimal("999.99"));
productRepository.save(product);
// createdAt and updatedAt set automatically via @PrePersist
Adding Optimistic Locking to the Base
A @Version field for optimistic locking (covered in Article 29) belongs in the base class too:
@MappedSuperclass
@Getter
@Setter
public abstract class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Version
private Long version;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
}
Every entity now has optimistic locking without any per-entity configuration.
@MappedSuperclass vs. @Inheritance
@MappedSuperclass | @Inheritance | |
|---|---|---|
| Parent table | No | Depends on strategy |
| Polymorphic queries | No — cannot query BaseEntity | Yes — SELECT e FROM Discount e |
| JPA entity | No | Yes |
| Purpose | Code reuse (common fields) | Modelling IS-A relationships |
| Database | Each subclass has own table | One table (SINGLE_TABLE) or joined tables |
Use @MappedSuperclass when you just want to share fields. Use @Inheritance when you have a true IS-A relationship and want to query polymorphically.
Auditing Base with Spring Data JPA
Spring Data JPA’s @EnableJpaAuditing provides an even cleaner way to handle timestamps and auditor tracking. This is covered in full in Article 28. A preview:
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
@Setter
public abstract class AuditableEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
@CreatedBy
@Column(name = "created_by", updatable = false, length = 100)
private String createdBy;
@LastModifiedBy
@Column(name = "updated_by", length = 100)
private String updatedBy;
}
Overriding Inherited Column Mappings
If a specific entity needs different column names, use @AttributeOverride:
@Entity
@Table(name = "audit_logs")
@AttributeOverrides({
@AttributeOverride(name = "createdAt", column = @Column(name = "log_time"))
})
public class AuditLog extends BaseEntity {
private String action;
private String entityType;
}
The Full Base Entity for This Series
package com.devopsmonk.jpademo.domain;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
@MappedSuperclass
@Getter
@Setter
public abstract class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Version
private Long version;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
LocalDateTime now = LocalDateTime.now();
createdAt = now;
updatedAt = now;
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof BaseEntity)) return false;
BaseEntity other = (BaseEntity) o;
return id != null && id.equals(other.id);
}
@Override
public int hashCode() {
return getClass().hashCode();
}
}
All entities in this series extend this base class from now on, eliminating the id, version, createdAt, and updatedAt boilerplate.
Key Takeaways
@MappedSuperclassdefines shared field mappings without creating a parent table- Each subclass entity has its own table with the inherited columns included
- Perfect for
id,createdAt,updatedAt,version— fields every entity needs - Cannot query a
@MappedSuperclassdirectly — it is not a JPA entity - Use
@PrePersist/@PreUpdatein the base class to automate timestamp management - Combine with
@Versionto give all entities optimistic locking in one place - Use
@AttributeOverrideto rename an inherited column in a specific entity
What’s Next
Article 16 starts Part 5 — Spring Data JPA Repositories. You will learn how the repository abstraction works, the hierarchy of repository interfaces, and what methods each provides out of the box.