@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 JOIN to 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 tableNoDepends on strategy
Polymorphic queriesNo — cannot query BaseEntityYes — SELECT e FROM Discount e
JPA entityNoYes
PurposeCode reuse (common fields)Modelling IS-A relationships
DatabaseEach subclass has own tableOne 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

  • @MappedSuperclass defines 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 @MappedSuperclass directly — it is not a JPA entity
  • Use @PrePersist / @PreUpdate in the base class to automate timestamp management
  • Combine with @Version to give all entities optimistic locking in one place
  • Use @AttributeOverride to 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.