Contents

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

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

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

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

// 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()); } }

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

@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; } } @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 } -- 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';

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

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()); } } @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.
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)

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.

<dependency> <groupId>org.hibernate.orm</groupId> <artifactId>hibernate-envers</artifactId> </dependency> @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; } // 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); } }

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

@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(); } }