Spring Data JPA: Auditing with @CreatedDate & @LastModifiedBy

Most production entities need audit columns: when they were created, when they were last updated, and by whom. Spring Data JPA's auditing infrastructure fills these fields automatically on every save: no boilerplate, no interceptors to wire up manually. Add @EnableJpaAuditing, implement AuditorAware to supply the current user, and annotate your base entity.

Contents

Enable JPA Auditing

Enable the feature by adding the annotation shown below to your main application class or to a dedicated @Configuration class.

Java
@SpringBootApplication
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Alternatively, place @EnableJpaAuditing on a @Configuration class. The auditorAwareRef attribute names the bean that returns the current user: needed for @CreatedBy and @LastModifiedBy.

AuditorAware: Current User

The class below shows the implementation. Key points are highlighted in the inline comments.

Java
@Component("auditorProvider")
public class SecurityAuditorAware implements AuditorAware<String> {

    @Override
    public Optional<String> getCurrentAuditor() {
        // Pull the authenticated username from Spring Security
        return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
            .filter(Authentication::isAuthenticated)
            .map(Authentication::getName);
    }
}

For systems without Spring Security you can supply a different source: a request-scoped bean holding the tenant user, an MDC value, or a system account name for background jobs.

Java
// Simpler AuditorAware for non-security projects or batch jobs
@Component("auditorProvider")
public class StaticAuditorAware implements AuditorAware<String> {

    @Override
    public Optional<String> getCurrentAuditor() {
        // Could read from MDC, thread-local, or request context
        String user = RequestContextHolder.getRequestAttributes() != null
            ? ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
                .getRequest().getHeader("X-User-Id")
            : "system";
        return Optional.ofNullable(user).filter(s -> !s.isEmpty());
    }
}

Auditable Base Entity

Create a @MappedSuperclass that all audited entities extend. Spring Data will populate the annotated fields on every persist and merge.

Java
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class AuditableEntity {

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

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

    @CreatedBy
    @Column(name = "created_by", nullable = false, updatable = false, length = 100)
    private String createdBy;

    @LastModifiedBy
    @Column(name = "updated_by", nullable = false, length = 100)
    private String updatedBy;

    // Getters (no setters — Spring Data sets these internally)
    public Instant getCreatedAt()  { return createdAt; }
    public Instant getUpdatedAt()  { return updatedAt; }
    public String  getCreatedBy()  { return createdBy; }
    public String  getUpdatedBy()  { return updatedBy; }
}
Java
@Entity
@Table(name = "orders")
public class Order extends AuditableEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String customerId;
    private BigDecimal total;

    @Enumerated(EnumType.STRING)
    private OrderStatus status;

    // business fields — audit fields come from AuditableEntity
}
SQL
-- Corresponding migration (Flyway V3 or Liquibase changeSet)
ALTER TABLE orders
    ADD COLUMN created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    ADD COLUMN updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    ADD COLUMN created_by VARCHAR(100) NOT NULL DEFAULT 'system',
    ADD COLUMN updated_by VARCHAR(100) NOT NULL DEFAULT 'system';

@EntityListeners & Custom Callbacks

Beyond the built-in AuditingEntityListener, you can write custom listeners to run business logic on JPA lifecycle events.

Java
public class OrderAuditListener {

    private static final Logger log = LoggerFactory.getLogger(OrderAuditListener.class);

    @PrePersist
    public void onPrePersist(Order order) {
        log.info("Creating order for customer: {}", order.getCustomerId());
    }

    @PreUpdate
    public void onPreUpdate(Order order) {
        log.info("Updating order {} — new status: {}", order.getId(), order.getStatus());
    }

    @PostRemove
    public void onPostRemove(Order order) {
        log.warn("Order {} was deleted", order.getId());
    }
}
Java
@Entity
@Table(name = "orders")
@EntityListeners({ AuditingEntityListener.class, OrderAuditListener.class })
public class Order extends AuditableEntity {
    // ...
}
Entity listeners are plain POJOs: they are not Spring beans by default. To inject Spring beans into a listener, configure spring.jpa.properties.hibernate.ejb.interceptor or use a BeanFactoryAware lookup. Spring Data's own AuditingEntityListener handles this internally.

Auditing Annotations Reference

AnnotationFilled onTypeDescription
@CreatedDateFirst saveInstant, LocalDateTime, DateTimestamp when the entity was created
@LastModifiedDateEvery saveSame as aboveTimestamp of last update
@CreatedByFirst saveString, custom typeUser who created: from AuditorAware
@LastModifiedByEvery saveSame as aboveUser who last updated: from AuditorAware
@VersionEvery saveLong, IntegerOptimistic locking version counter (not auditing, but related)

Hibernate Envers: Full Revision History

When you need a full audit log (every historical value, not just the current created/updated timestamps), use Hibernate Envers. It creates a _AUD table per entity containing every version ever saved.

XML
<dependency>
  <groupId>org.hibernate.orm</groupId>
  <artifactId>hibernate-envers</artifactId>
</dependency>
Java
@Entity
@Audited                          // enables Envers for this entity
@Table(name = "orders")
public class Order extends AuditableEntity {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotAudited                   // exclude this field from revision history
    private String internalNote;

    private BigDecimal total;

    @Enumerated(EnumType.STRING)
    private OrderStatus status;
}
Java
// Querying revision history
@Repository
public class OrderAuditRepository {

    @PersistenceContext
    private EntityManager em;

    public List<Order> getOrderHistory(Long orderId) {
        AuditReader reader = AuditReaderFactory.get(em);
        return reader.createQuery()
            .forRevisionsOfEntity(Order.class, true, true)
            .add(AuditEntity.id().eq(orderId))
            .addOrder(AuditEntity.revisionNumber().asc())
            .getResultList();
    }

    public Order getOrderAtRevision(Long orderId, Number revision) {
        AuditReader reader = AuditReaderFactory.get(em);
        return reader.find(Order.class, orderId, revision);
    }

    public List<Number> getRevisionNumbers(Long orderId) {
        AuditReader reader = AuditReaderFactory.get(em);
        return reader.getRevisions(Order.class, orderId);
    }
}

Testing Auditing

The class below shows the implementation. Key points are highlighted in the inline comments.

Java
@DataJpaTest
@Import(SecurityAuditorAware.class)
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
class OrderAuditingTest {

    @Autowired
    private OrderRepository orderRepository;

    @Test
    void createdDateAndCreatedByAreSetOnFirstSave() {
        // Simulate an authenticated user
        SecurityContextHolder.getContext().setAuthentication(
            new UsernamePasswordAuthenticationToken("alice", null, List.of()));

        Order order = new Order();
        order.setCustomerId("CUST-1");
        order.setTotal(new BigDecimal("99.99"));
        order.setStatus(OrderStatus.PENDING);

        Order saved = orderRepository.save(order);

        assertThat(saved.getCreatedAt()).isNotNull();
        assertThat(saved.getUpdatedAt()).isNotNull();
        assertThat(saved.getCreatedBy()).isEqualTo("alice");
        assertThat(saved.getUpdatedBy()).isEqualTo("alice");

        SecurityContextHolder.clearContext();
    }

    @Test
    void updatedByChangesOnUpdate() {
        SecurityContextHolder.getContext().setAuthentication(
            new UsernamePasswordAuthenticationToken("alice", null, List.of()));
        Order saved = orderRepository.save(newOrder());
        SecurityContextHolder.clearContext();

        SecurityContextHolder.getContext().setAuthentication(
            new UsernamePasswordAuthenticationToken("bob", null, List.of()));
        saved.setStatus(OrderStatus.CONFIRMED);
        Order updated = orderRepository.save(saved);

        assertThat(updated.getCreatedBy()).isEqualTo("alice");  // unchanged
        assertThat(updated.getUpdatedBy()).isEqualTo("bob");    // updated

        SecurityContextHolder.clearContext();
    }
}