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
@SpringBootApplication
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Alternatively, place @EnableJpaAuditing@ConfigurationauditorAwareRef@CreatedBy@LastModifiedBy
AuditorAware: Current User
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());
}
}
Auditable Base Entity
Create a @MappedSuperclass
@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';
@EntityListeners & Custom Callbacks
Beyond the built-in AuditingEntityListener
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 {
// ...
}
spring.jpa.properties.hibernate.ejb.interceptorBeanFactoryAwareAuditingEntityListenerAuditing Annotations Reference
| Annotation | Filled on | Type | Description |
|---|---|---|---|
@CreatedDate | First save | InstantLocalDateTimeDate | Timestamp when the entity was created |
@LastModifiedDate | Every save | Same as above | Timestamp of last update |
@CreatedBy | First save | String | User who created: from AuditorAware |
@LastModifiedBy | Every save | Same as above | User who last updated: from AuditorAware |
@Version | Every save | LongInteger | Optimistic 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
<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);
}
}
Testing Auditing
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();
}
}