Are you manually writing entity.setCreatedAt(LocalDateTime.now()) in your Service layer every time you save an entity? If you forget, the DB column ends up null — a bug that’s easy to miss until much later.
Spring Data JPA has a feature called JPA Auditing that automatically records created and modified timestamps with just an annotation. You won’t need to touch your Service layer at all, and you can record the creator and modifier using the same mechanism.
Enabling JPA Auditing
First, check your dependencies. If you already have spring-boot-starter-data-jpa, no additional dependencies are needed.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
}
Next, enable @EnableJpaAuditing. The simplest approach is to add it to your main class, but to avoid issues during testing, it’s recommended in practice to extract it into a dedicated @Configuration class.
@Configuration
@EnableJpaAuditing
public class JpaAuditingConfig {
}
That’s all it takes to turn on the Auditing feature. Since this is easy to forget, make sure to check it first.
Creating a BaseEntity
Create a BaseEntity class to share the created and modified timestamp fields across entities.
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
@CreatedDate
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(nullable = false)
private LocalDateTime updatedAt;
// getters omitted
}
Three key points to keep in mind:
@MappedSuperclassmeans this class itself won’t become a table — only its fields are inherited by subclasses@EntityListeners(AuditingEntityListener.class)is required. Without it, Auditing won’t workupdatable = falsepreventscreatedAtfrom being overwritten on updates
If you want to use ZonedDateTime, verify that your DB column type supports TIMESTAMP WITH TIME ZONE. For single-timezone applications, LocalDateTime is sufficient.
Inheriting in Your Entity
All you need to do is extends BaseEntity.
@Entity
@Table(name = "articles")
public class Article extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String content;
}
Adding this single line is all it takes for created_at and updated_at to be recorded automatically. The same applies when adding it to existing entities. If you’re designing with entity relationship mapping as a reference, simply adding the inheritance is all you need.
Verifying the Behavior
You can verify with a lightweight test using @DataJpaTest.
@DataJpaTest
@Import(JpaAuditingConfig.class) // Required to enable Auditing
class ArticleRepositoryTest {
@Autowired
private ArticleRepository articleRepository;
@Test
void createdAtAndUpdatedAtAreAutoSetOnSave() {
Article saved = articleRepository.save(new Article("Test Article", "Body"));
assertThat(saved.getCreatedAt()).isNotNull();
assertThat(saved.getUpdatedAt()).isNotNull();
}
@Test
void onlyUpdatedAtChangesOnUpdate() {
Article article = articleRepository.save(new Article("Original Title", "Body"));
LocalDateTime createdAt = article.getCreatedAt();
article.setTitle("New Title");
Article updated = articleRepository.saveAndFlush(article);
assertThat(updated.getCreatedAt()).isEqualTo(createdAt);
assertThat(updated.getUpdatedAt()).isAfter(createdAt);
}
}
@DataJpaTest does not load @Configuration classes, so @Import(JpaAuditingConfig.class) is required. Forgetting this will cause timestamps to be null, so be careful.
Recording the Acting User with @CreatedBy and @LastModifiedBy
If you also want to record which user performed the action, implement the AuditorAware interface. First, add two fields to BaseEntity.
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String updatedBy;
Then implement AuditorAware and register it as a Bean. If you’re using Spring Security, you can retrieve the username from SecurityContextHolder.
@Component("auditorProvider")
public class SpringSecurityAuditorAware implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
return Optional.ofNullable(
SecurityContextHolder.getContext().getAuthentication()
)
.filter(auth -> auth.isAuthenticated()
&& !(auth instanceof AnonymousAuthenticationToken))
.map(Authentication::getName);
}
}
If you’re not using Spring Security, returning a fixed value like Optional.of("system") is perfectly fine. Finally, specify the Bean name in @EnableJpaAuditing.
@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
public class JpaAuditingConfig {
}
For Spring Security authentication configuration itself, refer to the JWT authentication article.
Common Pitfalls
When timestamps are null
This is almost always a missing @EnableJpaAuditing configuration or a missing @EntityListeners(AuditingEntityListener.class) annotation. If it doesn’t work in @DataJpaTest, also check your @Import.
When updated_at doesn’t change
You may be operating outside a transaction. Check the transaction management article as well.
When ZonedDateTime causes errors
Databases like MySQL require a column type equivalent to TIMESTAMP WITH TIME ZONE. If that’s difficult to support, switching to LocalDateTime is also an option.
Summary
JPA Auditing can be introduced in three steps:
- Enable
@EnableJpaAuditing - Define
@CreatedDateand@LastModifiedDateinBaseEntity - Use
extends BaseEntityin your entities
You can remove all timestamp-setting code from the Service layer, and prevent null bugs caused by forgotten configuration. Adding AuditorAware lets you manage acting user recording in one place as well — give it a try.
For more JPA-related articles, see how to use query methods and performance optimization.