Spring Data JPA

Auditing with @CreatedDate

18 min Lesson 8 of 13

Auditing with @CreatedDate

Every production database table benefits from knowing when a row was created and when it was last changed, and often who made those changes. Manually setting those columns in every service method is tedious and error-prone. Spring Data JPA's auditing feature takes that responsibility away from application code entirely and moves it into the framework infrastructure — so the timestamps are set consistently, automatically, and without cluttering your business logic.

How Spring Data Auditing Works

Spring Data auditing is built on top of JPA lifecycle callbacks (@PrePersist, @PreUpdate). When you enable auditing, Spring registers an AuditingEntityListener that intercepts these callbacks and populates fields you have annotated with @CreatedDate, @LastModifiedDate, @CreatedBy, or @LastModifiedBy. The net result is that those four fields are filled in for you, before every INSERT or UPDATE, without a single line of service-layer code.

Step 1: Enable JPA Auditing

Add @EnableJpaAuditing to your Spring Boot application class (or any @Configuration class):

import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @SpringBootApplication @EnableJpaAuditing public class StoreApplication { public static void main(String[] args) { SpringApplication.run(StoreApplication.class, args); } }

This single annotation tells Spring to activate the AuditingEntityListener globally. Without it, none of the audit annotations on your entities have any effect.

Step 2: Annotate Entity Fields

Apply the audit annotations to the fields you want populated automatically. You must also register the AuditingEntityListener on the entity, either directly with @EntityListeners or, more conveniently, on a shared @MappedSuperclass:

import jakarta.persistence.*; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import java.time.Instant; @MappedSuperclass @EntityListeners(AuditingEntityListener.class) public abstract class Auditable { @CreatedDate @Column(name = "created_at", nullable = false, updatable = false) private Instant createdAt; @LastModifiedDate @Column(name = "updated_at", nullable = false) private Instant updatedAt; // getters omitted for brevity }

Concrete entities then simply extend this base class:

import jakarta.persistence.*; @Entity @Table(name = "orders") public class Order extends Auditable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String status; // constructors, getters, setters }
Why Instant instead of LocalDateTime? Instant is a point on the UTC timeline with no timezone ambiguity. If your application ever runs in multiple time zones or across cloud regions, Instant ensures timestamps are always comparable. Use LocalDateTime only if you deliberately want to store wall-clock time in whatever zone the JVM happens to be in.

The updatable = false Constraint

The updatable = false attribute on createdAt is critical. It tells Hibernate to include this column in the INSERT statement but exclude it from every subsequent UPDATE. Without it, an accidental call to save() on an existing entity could overwrite the original creation timestamp — a data integrity bug that is very hard to catch in tests.

Step 3: Auditing the Author — @CreatedBy and @LastModifiedBy

To capture who performed an action, implement the AuditorAware interface and expose it as a bean. Spring Data calls getCurrentAuditor() just before each persist and update:

import org.springframework.data.domain.AuditorAware; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import java.util.Optional; @Component("auditorProvider") public class SpringSecurityAuditorAware implements AuditorAware<String> { @Override public Optional<String> getCurrentAuditor() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth == null || !auth.isAuthenticated()) { return Optional.of("system"); } return Optional.of(auth.getName()); } }

Tell Spring Data which bean to use by passing its name to @EnableJpaAuditing:

@EnableJpaAuditing(auditorAwareRef = "auditorProvider")

Then add the author fields to Auditable:

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

Testing Auditing Behaviour

A common gotcha: @DataJpaTest slices do not load @SpringBootApplication, so @EnableJpaAuditing is absent and auditing silently does nothing. The fix is to include a tiny configuration class in the test slice:

import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.context.annotation.Configuration; @DataJpaTest @Import(OrderRepositoryTest.TestConfig.class) class OrderRepositoryTest { @Configuration @EnableJpaAuditing static class TestConfig {} @Autowired private OrderRepository orderRepository; @Test void createdAt_isPopulatedOnSave() { Order order = new Order(); order.setStatus("PENDING"); Order saved = orderRepository.save(order); assertNotNull(saved.getCreatedAt()); assertNotNull(saved.getUpdatedAt()); } }
Clock manipulation in tests: If you need to assert that createdAt is exactly a certain value, inject a DateTimeProvider bean that returns a fixed Instant and pass it to @EnableJpaAuditing(dateTimeProviderRef = "fixedClock"). This makes time-sensitive tests deterministic.

Performance Considerations

Spring Data auditing uses JPA lifecycle callbacks, which means Hibernate must load the entity into the first-level cache before it can call @PreUpdate. For bulk updates executed via JPQL (@Modifying @Query) the callbacks are not invoked, so updatedAt will not be refreshed automatically. If audit accuracy matters on bulk paths, either update the timestamp manually in the JPQL statement or avoid bulk updates on audited entities.

Bulk updates bypass auditing. A statement like UPDATE Order o SET o.status = 'ARCHIVED' WHERE o.createdAt < :cutoff will silently skip the AuditingEntityListener. Either include SET o.updatedAt = :now in the query, or switch to a batch save approach if audit trails are mandatory.

Summary

Spring Data JPA auditing removes boilerplate timestamp management from your service layer. Enable it with @EnableJpaAuditing, register AuditingEntityListener on a @MappedSuperclass, and annotate fields with @CreatedDate and @LastModifiedDate. Use Instant for timezone safety and updatable = false to protect creation timestamps. For author tracking, implement AuditorAware. Remember that bulk JPQL updates bypass the listener and must handle updatedAt explicitly.