@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.

Signature
@CreatedDate
@Column(updatable = false)
private Instant createdAt;

@LastModifiedDate
private Instant updatedAt;
  • Added in Spring Boot Spring Boot 4.1 (verified present)

Parameters

ParameterTypeDescription
$(field type)Instant, LocalDateTime, other java.time types, Date, Long or longThe timestamp type. Instant is the safest choice for storage.
$@EnableJpaAuditing(modifyOnCreate)optionalboolean= trueWhether @LastModifiedDate is also set on insert.
$@EnableJpaAuditing(setDates)optionalboolean= trueWhether dates are set at all.
$@EnableJpaAuditing(dateTimeProviderRef)optionalString= current system timeBean name of a DateTimeProvider that supplies the current time, for example backed by a Clock.
$@EnableJpaAuditing(auditorAwareRef)optionalString= the single AuditorAware bean, if anyBean 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

  1. @CreatedDate and @LastModifiedDate from org.springframework.data.annotation on the fields.
  2. @EntityListeners(AuditingEntityListener.class) on the entity, or once on a @MappedSuperclass base class. This is the JPA callback that does the work.
  3. @EnableJpaAuditing on 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 @LastModifiedDate is also set on insert, so a new row has equal created and updated times. Set modifyOnCreate = false if you want it null until 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 to timestamp with time zone semantics; LocalDateTime takes the server's time zone, which changes when you move hosts.
  • For tests, supply a DateTimeProvider bean backed by a Clock and reference it with dateTimeProviderRef, 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

Example · java
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;

@Configuration
@EnableJpaAuditing
class AuditConfig { }

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
abstract class Audited {

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

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

    public Instant getCreatedAt() { return createdAt; }
    public Instant getUpdatedAt() { return updatedAt; }
}

@Entity
public class Customer extends Audited {
    @Id
    @GeneratedValue
    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'.

Example · java
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'.

Example · java
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.

Example · java
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

  • Be the first to comment on this lesson.