Spring Data JPA Auditing: @CreatedDate and @CreatedBy
Introduction
This tutorial explains auditing in Spring Data JPA: filling in who created an entity, who changed it last, and when, automatically on every save. We will see the four annotations@CreatedDate, @LastModifiedDate, @CreatedBy and @LastModifiedBy, put them in a reusable base class, and switch auditing on with @EnableJpaAuditing. We will supply the current user through an AuditorAware bean and the time through a DateTimeProvider, then create and update an article as two users and read the SQL. Lastly, we will compare this with Hibernate's own timestamp annotations.Why automatic auditing
Almost every business table ends up with columns likecreated_at and modified_by. They answer the first questions asked after something goes wrong: when did this row change, and who changed it? Setting them by hand in every service method is repetitive and easy to forget in one place. Spring Data fills them in from a JPA entity listener, a class JPA calls just before it inserts or updates an entity -
@CreatedDateand@CreatedByare set when the entity is first saved.@LastModifiedDateand@LastModifiedByare set on the first save and on every update.- The date comes from a
DateTimeProvider, which by default reads the system clock. The user comes from anAuditorAwarebean that you write, since only your application knows who is logged in.
The example
The audit fields live in an abstract@MappedSuperclass, so any entity gets them by extending it. A mapped superclass is not an entity and has no table of its own: its fields become columns of each subclass's table. The @EntityListeners(AuditingEntityListener.class) annotation registers Spring Data's listener for all subclasses.
Auditable.java - the four audit fields, in a mapped superclass
//Auditable.java
package com.javabrahman.data.audit;
import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import java.time.LocalDateTime;
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(updatable = false)
private LocalDateTime createdAt;
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedDate
private LocalDateTime modifiedAt;
@LastModifiedBy
private String modifiedBy;
public String auditInfo() {
return "created " + createdAt + " by " + createdBy + ", modified " + modifiedAt + " by " + modifiedBy;
}
}
Article.java - an entity that inherits the audit fields
//Article.java
package com.javabrahman.data.audit;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Article extends Auditable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
protected Article() {
}
public Article(String title) {
this.title = title;
}
public Long getId() { return id; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
}
ArticleRepository.java
//ArticleRepository.java
package com.javabrahman.data.audit;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ArticleRepository extends JpaRepository<Article, Long> {
}
@EnableJpaAuditing is placed on a configuration class. Its auditorAwareRef and dateTimeProviderRef name the beans for the user and the time. In a real application, AuditorAware would return the name of the logged-in user from Spring Security (module 6). Here, two static holders stand in for the user and the clock, so that the demo can switch users and the printed times are always the same. Without a dateTimeProviderRef, the current time is used.
AuditConfig.java - switching auditing on, with the current user and the clock
//AuditConfig.java
package com.javabrahman.data.audit;
import java.time.LocalDateTime;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.auditing.DateTimeProvider;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@Configuration
@EnableJpaAuditing(auditorAwareRef = "currentUser", dateTimeProviderRef = "clock")
public class AuditConfig {
// Stand-ins for the logged-in user and the system clock, so the demo can set them
public static final AtomicReference<String> USER = new AtomicReference<>();
public static final AtomicReference<LocalDateTime> NOW = new AtomicReference<>();
@Bean
AuditorAware<String> currentUser() {
return () -> Optional.ofNullable(USER.get());
}
@Bean
DateTimeProvider clock() {
return () -> Optional.of(NOW.get());
}
}
AuditApp.java - creating and changing an article as two users
//AuditApp.java
package com.javabrahman.data.audit;
import java.time.LocalDateTime;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class AuditApp {
@Bean
CommandLineRunner demo(ArticleRepository articles) {
return args -> {
AuditConfig.USER.set("alice");
AuditConfig.NOW.set(LocalDateTime.of(2026, 11, 2, 9, 30));
Article article = articles.save(new Article("Spring Data auditing"));
System.out.println("after create: " + articles.findById(article.getId()).orElseThrow().auditInfo());
AuditConfig.USER.set("bob");
AuditConfig.NOW.set(LocalDateTime.of(2026, 11, 5, 16, 45));
Article loaded = articles.findById(article.getId()).orElseThrow();
loaded.setTitle("Spring Data JPA auditing");
articles.save(loaded);
System.out.println("after update: " + articles.findById(article.getId()).orElseThrow().auditInfo());
AuditConfig.USER.set(null);
AuditConfig.NOW.set(LocalDateTime.of(2026, 11, 6, 8, 0));
Article system = articles.save(new Article("Written by a batch job"));
System.out.println("no user: " + articles.findById(system.getId()).orElseThrow().auditInfo());
};
}
public static void main(String[] args) {
SpringApplication.run(AuditApp.class, args);
}
}
Hibernate: drop table if exists article cascade Hibernate: create table article (created_at timestamp(6), id bigint generated by default as identity, modified_at timestamp(6), created_by varchar(255), modified_by varchar(255), title varchar(255), primary key (id)) Hibernate: insert into article (created_at,created_by,modified_at,modified_by,title,id) values (?,?,?,?,?,default) Hibernate: select a1_0.id,a1_0.created_at,a1_0.created_by,a1_0.modified_at,a1_0.modified_by,a1_0.title from article a1_0 where a1_0.id=? after create: created 2026-11-02T09:30 by alice, modified 2026-11-02T09:30 by alice Hibernate: select a1_0.id,a1_0.created_at,a1_0.created_by,a1_0.modified_at,a1_0.modified_by,a1_0.title from article a1_0 where a1_0.id=? Hibernate: select a1_0.id,a1_0.created_at,a1_0.created_by,a1_0.modified_at,a1_0.modified_by,a1_0.title from article a1_0 where a1_0.id=? Hibernate: update article set modified_at=?,modified_by=?,title=? where id=? Hibernate: select a1_0.id,a1_0.created_at,a1_0.created_by,a1_0.modified_at,a1_0.modified_by,a1_0.title from article a1_0 where a1_0.id=? after update: created 2026-11-02T09:30 by alice, modified 2026-11-05T16:45 by bob Hibernate: insert into article (created_at,created_by,modified_at,modified_by,title,id) values (?,?,?,?,?,default) Hibernate: select a1_0.id,a1_0.created_at,a1_0.created_by,a1_0.modified_at,a1_0.modified_by,a1_0.title from article a1_0 where a1_0.id=? no user: created 2026-11-06T08:00 by null, modified 2026-11-06T08:00 by null Hibernate: drop table if exists article cascade
Explanation of the output
- The table: the
create tablestatement showscreated_at,created_by,modified_atandmodified_byin thearticletable, inherited fromAuditable. - After create: Alice saved the article at 09:30 on 2 November. All four fields were filled: created and modified at the same time, by the same user. The application code never touched them.
- After update: Bob changed the title on 5 November. The created fields still say Alice at 09:30, and the modified fields now say Bob at 16:45. Look at the
updatestatement:set modified_at=?,modified_by=?,title=?. The created columns are not in it at all, because they are mapped with@Column(updatable = false). That guarantees that no update, however it is written in Java, can change who created the row. - No user: the batch job ran with no user set, so
AuditorAwarereturned an emptyOptional, and the user columns stayednull. The dates were still set. Many applications return a fixed name such as"system"in that case instead.
Details worth knowing
- Types: the date fields can be
LocalDateTime,Instant,ZonedDateTime,java.util.Dateorlong. PreferInstant, orLocalDateTimein UTC, to avoid time-zone confusion on servers in different regions. The user fields can be any type, such as aStringname or a user id;AuditorAware<T>must return the same type. - Only through JPA: the listener runs when JPA inserts or updates an entity. A bulk
@Modifyingupdate query or plain SQL bypasses it, as do changes made directly in the database. - Only the last change: auditing keeps the latest modification, not the history. For a full history of every change, use a separate audit table or Hibernate Envers, which records each version of an entity.
- Interfaces instead of annotations: an entity can implement Spring Data's
Auditableinterface, or extendAbstractAuditable, instead of using the annotations. The annotations on your own base class, as here, keep the entity free of Spring Data types.
Hibernate's @CreationTimestamp and @UpdateTimestamp
Hibernate has its own annotations,@CreationTimestamp and @UpdateTimestamp, that fill in dates without any Spring configuration. They cover only the dates, not the user, and they belong to Hibernate rather than to JPA or Spring Data. Spring Data auditing is the choice when you also need the user, or want to control the clock, as this example did. A controllable clock also makes tests of time-dependent code predictable.
Summary
In this tutorial we saw how Spring Data JPA fills in audit fields automatically. We put@CreatedDate, @CreatedBy, @LastModifiedDate and @LastModifiedBy in a mapped superclass with the AuditingEntityListener, switched auditing on with @EnableJpaAuditing, and supplied the user and the time through AuditorAware and DateTimeProvider beans. The output showed the created fields kept and the modified fields updated when a second user changed the article, and an update statement that leaves the created columns out. The next tutorial deals with two users changing the same row at the same time: optimistic and pessimistic lockingClick to read tutorial: Optimistic and Pessimistic Locking in Spring Data JPA.Spring Data Tutorials
Spring Data JPA Projections and DTOsClick to read tutorial: Spring Data JPA Projections and DTOs Optimistic and Pessimistic Locking in Spring Data JPAClick to read tutorial: Optimistic and Pessimistic Locking in Spring Data JPA Spring Data JPA Introduction: Entities and RepositoriesClick to read tutorial: Spring Data JPA Introduction: Entities and Repositories Spring Data JPA Derived Query Methods ExplainedClick to read tutorial: Spring Data JPA Derived Query Methods Explained Spring Data JPA @Query: JPQL and Native QueriesClick to read tutorial: Spring Data JPA @Query: JPQL and Native Queries JPA Entity Relationships: OneToOne, OneToMany, ManyToManyClick to read tutorial: JPA Entity Relationships: OneToOne, OneToMany, ManyToMany JPA Lazy Loading and the N+1 Problem in Spring BootClick to read tutorial: JPA Lazy Loading and the N+1 Problem in Spring Boot
Spring Data JPA Projections and DTOsClick to read tutorial: Spring Data JPA Projections and DTOs Optimistic and Pessimistic Locking in Spring Data JPAClick to read tutorial: Optimistic and Pessimistic Locking in Spring Data JPA Spring Data JPA Introduction: Entities and RepositoriesClick to read tutorial: Spring Data JPA Introduction: Entities and Repositories Spring Data JPA Derived Query Methods ExplainedClick to read tutorial: Spring Data JPA Derived Query Methods Explained Spring Data JPA @Query: JPQL and Native QueriesClick to read tutorial: Spring Data JPA @Query: JPQL and Native Queries JPA Entity Relationships: OneToOne, OneToMany, ManyToManyClick to read tutorial: JPA Entity Relationships: OneToOne, OneToMany, ManyToMany JPA Lazy Loading and the N+1 Problem in Spring BootClick to read tutorial: JPA Lazy Loading and the N+1 Problem in Spring Boot