Skip to content
Spring Boot sb data-jpa 3 min read

JPA Auditing

JPA auditing automatically fills in who created or changed a row and when, so you never write entity.setCreatedAt(Instant.now()) by hand again. Spring Data JPA ships an AuditingEntityListener that listens for persist and update events and populates four well-known fields. This page shows how to enable auditing, build a reusable base class, and wire in the current user.

Enabling Auditing

Auditing is opt-in. Add @EnableJpaAuditing to any @Configuration class (commonly your main application class), then attach the AuditingEntityListener to the entities you want audited.

import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
public class JpaAuditingConfig {
}

Note: auditorAwareRef names the bean that supplies the current user. Omit it if you only need timestamps (@CreatedDate / @LastModifiedDate); the date-time provider defaults to the system clock.

A Reusable Auditable Base Class

Rather than repeating four fields on every entity, define a @MappedSuperclass. Its columns are mapped into each subclass’s table, but it is not itself an entity.

import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import java.time.Instant;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Auditable {

    @CreatedDate
    @Column(name = "created_at", updatable = false)
    private Instant createdAt;

    @LastModifiedDate
    @Column(name = "updated_at")
    private Instant updatedAt;

    @CreatedBy
    @Column(name = "created_by", updatable = false)
    private String createdBy;

    @LastModifiedBy
    @Column(name = "updated_by")
    private String updatedBy;

    // getters and setters omitted for brevity
}

Tip: Use Instant (UTC) or LocalDateTime for the date fields — both are supported. Instant avoids timezone ambiguity when your app runs across regions. Mark created columns updatable = false so updates never overwrite them.

The Audit Annotations

AnnotationPopulated onMeaning
@CreatedDateinsertTimestamp when the row was first persisted
@LastModifiedDateinsert + updateTimestamp of the most recent change
@CreatedByinsertAuditor (username) that created the row
@LastModifiedByinsert + updateAuditor that last changed the row

These annotations come from org.springframework.data.annotation, not jakarta.persistence. The listener sets the *Date fields from a date-time provider and the *By fields from your AuditorAware bean.

Supplying the Current User

The *By fields stay null until you provide an AuditorAware<T> bean. Here T is String because we store the username. Pull it from the SecurityContextHolder with a sensible fallback when there is no authenticated principal (background jobs, tests, startup).

import java.util.Optional;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.AuditorAware;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;

@Configuration
public class AuditorConfig {

    @Bean
    public AuditorAware<String> auditorProvider() {
        return () -> {
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();
            if (auth == null || !auth.isAuthenticated()
                    || "anonymousUser".equals(auth.getPrincipal())) {
                return Optional.of("system");
            }
            return Optional.of(auth.getName());
        };
    }
}

The bean name auditorProvider matches the auditorAwareRef value set earlier. See Spring Security Basics for where that Authentication comes from.

Using It on an Entity

Any entity simply extends Auditable and inherits the four columns and the listener:

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class Product extends Auditable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    // getters and setters
}

Save it through a repository inside a transaction and the audit fields are filled automatically:

Product p = new Product();
p.setName("Keyboard");
productRepository.save(p);   // listener fires here

The generated insert carries the populated audit columns:

insert into product (created_at, created_by, name, updated_at, updated_by)
values ('2026-06-13T09:15:42Z', 'alice', 'Keyboard',
        '2026-06-13T09:15:42Z', 'alice')

A later update touches only the modified columns:

update product
set name=?, updated_at='2026-06-13T11:02:08Z', updated_by='bob'
where id=?
-- created_at and created_by remain unchanged (updatable = false)

Warning: Auditing fires on JPA lifecycle events, so it only runs when entities are persisted through the EntityManager (repository save, cascades, dirty checking). Bulk JPQL UPDATE/DELETE and native queries bypass the listener entirely — audit columns will not be touched.

Common Pitfalls

  • Forgetting @EnableJpaAuditing — the listener is registered but nothing populates the fields.
  • Placing @EntityListeners(AuditingEntityListener.class) on the entity and the base class, or omitting it from both. Put it once on the @MappedSuperclass.
  • Using jakarta.persistence imports for @CreatedDate etc. They live in org.springframework.data.annotation.
  • Expecting @CreatedBy to work without an AuditorAware bean — without it the user fields stay null.
Last updated June 13, 2026
Was this helpful?