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:
auditorAwareRefnames 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) orLocalDateTimefor the date fields — both are supported.Instantavoids timezone ambiguity when your app runs across regions. Mark created columnsupdatable = falseso updates never overwrite them.
The Audit Annotations
| Annotation | Populated on | Meaning |
|---|---|---|
@CreatedDate | insert | Timestamp when the row was first persisted |
@LastModifiedDate | insert + update | Timestamp of the most recent change |
@CreatedBy | insert | Auditor (username) that created the row |
@LastModifiedBy | insert + update | Auditor 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(repositorysave, cascades, dirty checking). Bulk JPQLUPDATE/DELETEand 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.persistenceimports for@CreatedDateetc. They live inorg.springframework.data.annotation. - Expecting
@CreatedByto work without anAuditorAwarebean — without it the user fields stay null.
Related Topics
- Entity Mapping — how
@MappedSuperclasscolumns map into tables. - @Transactional — auditing fires within the persistence transaction.
- Spring Data Repositories — the
savecalls that trigger auditing. - Spring Security Basics — sourcing the current auditor.
- Java Annotations — how annotations like
@CreatedDateare processed.