@CreatedDate and @LastModifiedDate
Spring Data auditing fills these fields with the creation and last update time automatically, once auditing is enabled and the entity listener is registered.
@CreatedDate
@Column(updatable = false)
private Instant createdAt;
@LastModifiedDate
private Instant updatedAt;- Added in Spring Boot Spring Boot 4.1 (verified present)
Parameters
| Parameter | Type | Description |
|---|---|---|
$(field type) | Instant, LocalDateTime, other java.time types, Date, Long or long | The timestamp type. Instant is the safest choice for storage. |
$@EnableJpaAuditing(modifyOnCreate)optional | boolean= true | Whether @LastModifiedDate is also set on insert. |
$@EnableJpaAuditing(setDates)optional | boolean= true | Whether dates are set at all. |
$@EnableJpaAuditing(dateTimeProviderRef)optional | String= current system time | Bean name of a DateTimeProvider that supplies the current time, for example backed by a Clock. |
$@EnableJpaAuditing(auditorAwareRef)optional | String= the single AuditorAware bean, if any | Bean name of the AuditorAware used for @CreatedBy and @LastModifiedBy. |
Return value
n/aMarks fields that Spring Data's AuditingEntityListener sets to the current time when the entity is first persisted (both) and on every update (@LastModifiedDate).
Errors
No error when auditing is not enabled: the fields simply stay null, which then fails on a NOT NULL column.
Almost every table wants created_at and updated_at columns, and setting them by hand in every service method is easy to forget. Spring Data's auditing sets them for you: annotate the fields, register the listener, enable auditing.
The three parts
@CreatedDateand@LastModifiedDatefromorg.springframework.data.annotationon the fields.@EntityListeners(AuditingEntityListener.class)on the entity, or once on a@MappedSuperclassbase class. This is the JPA callback that does the work.@EnableJpaAuditingon a configuration class. Without it the listener has nothing to call.
Miss any one of them and the fields stay null with no error until a NOT NULL constraint fails. That silent failure is the most common auditing question.
Where to put @EnableJpaAuditing
Putting it on the main @SpringBootApplication class works, but then every test slice, including @WebMvcTest, tries to set up JPA auditing and fails without JPA infrastructure. Put it on a small @Configuration class instead, and @Import that class in @DataJpaTest tests that check the timestamps.
Behaviour worth knowing
- By default
@LastModifiedDateis also set on insert, so a new row has equal created and updated times. SetmodifyOnCreate = falseif you want itnulluntil the first change. - Add
@Column(updatable = false)to the created field. Then a detached entity merged from a DTO without the creation time cannot wipe it. - Use
Instant. It is unambiguous and maps totimestamp with time zonesemantics;LocalDateTimetakes the server's time zone, which changes when you move hosts. - For tests, supply a
DateTimeProviderbean backed by aClockand reference it withdateTimeProviderRef, so timestamps are predictable.
What auditing does not cover
The listener runs on JPA lifecycle events, so bulk @Modifying updates, JdbcClient statements and manual SQL do not change the timestamps. Set the column in those statements yourself. Also note the update time changes only when Hibernate actually issues an UPDATE; loading and saving an unchanged entity does not touch it.
Add @CreatedBy and @LastModifiedBy for who made the change; they need an AuditorAware bean that reads the current user, typically from Spring Security. Hibernate's own @CreationTimestamp and @UpdateTimestamp are an alternative that needs no setup but has no notion of the current user.
Example
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.MappedSuperclass;
import java.time.Instant;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
class AuditConfig { }
(AuditingEntityListener.class)
abstract class Audited {
(name = "created_at", nullable = false, updatable = false)
private Instant createdAt;
(name = "updated_at", nullable = false)
private Instant updatedAt;
public Instant getCreatedAt() { return createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
}
public class Customer extends Audited {
private Long id;
private String email;
protected Customer() { }
public Customer(String email) { this.email = email; }
public void changeEmail(String email) { this.email = email; } // bumps updatedAt at commit
}When to use it
- Showing when each support ticket was opened and last updated without touching every service method.
- Recording which signed-in user created or last edited an order with @CreatedBy and an AuditorAware bean.
- Sorting a list endpoint by most recently changed records using an indexed updated_at column.
Frequently asked questions
Why is my @CreatedDate field always null?
Auditing is not fully wired. Check that the entity (or its @MappedSuperclass) has @EntityListeners(AuditingEntityListener.class) and that a configuration class has @EnableJpaAuditing.
Should I use @CreatedDate or Hibernate's @CreationTimestamp?
Both set the time. Spring Data's annotations come with @CreatedBy and @LastModifiedBy, a pluggable DateTimeProvider and work the same across Spring Data modules. Hibernate's need no configuration. Pick one per project and stay consistent.
Why did updated_at not change after my bulk update?
Auditing runs on JPA lifecycle events for entities. A @Modifying query or plain SQL updates rows directly, so set updated_at in the statement yourself.
More examples
Recording who made the change
Fields annotated with @CreatedBy and @LastModifiedBy (type String here) are filled with the authenticated user's name. An anonymous request also reports isAuthenticated() as true, so it is filtered out rather than recorded as anonymousUser. For jobs that run without a user, the Optional is empty and the fields stay null, so allow null or return a fixed name such as 'system'.
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.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
@Configuration
@EnableJpaAuditing(auditorAwareRef = "currentUser")
public class AuditConfig {
@Bean
AuditorAware<String> currentUser() {
return () -> Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
.filter(Authentication::isAuthenticated)
.filter(auth -> !(auth instanceof AnonymousAuthenticationToken)) // anonymous counts as authenticated
.map(Authentication::getName);
}
}A fixed clock for predictable tests
In production the provider uses the system clock. A test can override the Clock bean with Clock.fixed(...), for example through @TestBean, and assert exact timestamps instead of 'roughly now'.
import java.time.Clock;
import java.time.Instant;
import java.util.Optional;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.auditing.DateTimeProvider;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@Configuration
@EnableJpaAuditing(dateTimeProviderRef = "auditClock")
public class AuditConfig {
@Bean
Clock clock() {
return Clock.systemUTC();
}
@Bean
DateTimeProvider auditClock(Clock clock) {
return () -> Optional.of(Instant.now(clock));
}
}Checking the timestamps with @DataJpaTest
@DataJpaTest does not pick up ordinary @Configuration classes, so the auditing configuration is imported explicitly. updatedAt equals createdAt because modifyOnCreate defaults to true.
import static org.assertj.core.api.Assertions.assertThat;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import java.time.Instant;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@Configuration
@EnableJpaAuditing
class AuditConfig { }
@Entity
@EntityListeners(AuditingEntityListener.class)
class Ticket {
@Id @GeneratedValue
Long id;
String subject;
@CreatedDate
Instant createdAt;
@LastModifiedDate
Instant updatedAt;
protected Ticket() { }
Ticket(String subject) { this.subject = subject; }
}
interface TicketRepository extends JpaRepository<Ticket, Long> { }
@DataJpaTest
@Import(AuditConfig.class)
class TicketAuditingTests {
@Autowired
TicketRepository tickets;
@Test
void setsBothTimestampsOnInsert() {
Ticket saved = tickets.saveAndFlush(new Ticket("Printer on fire"));
assertThat(saved.createdAt).isNotNull();
assertThat(saved.updatedAt).isEqualTo(saved.createdAt);
}
}
Discussion