Database · Lesson 65 of 95
Auditing with @CreatedDate
Auditing with @CreatedDate in Spring Boot: fill created and updated times and the user automatically with @EnableJpaAuditing, plus a runnable demo.
A courier company gets a complaint: "My parcel was booked on Monday, but your system says Wednesday." Who is right? If the system never recorded when each parcel was created, and when it was last changed, nobody can prove anything. Good software keeps this diary automatically, so people do not forget to write it.
In Spring Boot, this diary is called auditing. Let's learn how @CreatedDate and its friends fill it for you, without a single line of date code in your services.
What is auditing with @CreatedDate?
Think of a library register. Every time a book is issued, the librarian stamps the date and writes a name. Nobody asks the reader to write the date, because readers forget or lie. The stamp is applied by the system.
The main annotations are:
| Annotation | Filled with | When |
|---|---|---|
@CreatedDate | Time of creation | Once, on first save |
@LastModifiedDate | Time of the latest change | Every update |
@CreatedBy | The user who created it | Once, on first save |
@LastModifiedBy | The user who changed it last | Every update |
Why is it used?
- Proof. You can answer "when was this order placed?" for every row.
- Debugging. When data looks wrong, the update time tells you when it changed.
- Reports. "Tickets created this week" becomes a simple query.
- No forgotten code. If each developer set dates by hand, some would forget. With auditing, every entity that opts in gets the same behaviour.
How it works
Auditing has three parts: a switch, a listener and marked fields. You switch it on with @EnableJpaAuditing. You attach the listener AuditingEntityListener to your entity. You mark fields with @CreatedDate and friends.
textrepository.save(ticket) | v Hibernate: about to insert | v AuditingEntityListener runs | v createdAt = now updatedAt = now | v INSERT with both dates
On the first save, the listener sets both the creation and the update time. Your code never touches them.
textchange a field, save again | v Hibernate: about to update | v AuditingEntityListener runs | v updatedAt = now createdAt stays the same | v UPDATE row
On a later update, only the update time moves. To make sure the creation time never changes, mark its column updatable = false.
For @CreatedBy and @LastModifiedBy, Spring needs to know who the current user is. You provide one bean that implements AuditorAware, and Spring calls it at save time. In a real app it reads the logged-in user from Spring Security. In our demo, it returns a fixed name.
Spring Data auditing or Hibernate timestamps?
Hibernate offers its own @CreationTimestamp and @UpdateTimestamp. Both approaches work, but they differ.
| Feature | Spring Data auditing | Hibernate timestamps |
|---|---|---|
| Creation and update time | Yes | Yes |
| User who made the change | Yes, with AuditorAware | No |
| Works beyond JPA | Yes, other Spring Data stores | No |
| Needs an enabling switch | Yes | No |
If you only need two dates, Hibernate's annotations are shorter. If you also need the user, or you may change the database later, Spring Data auditing is the better choice. In a school-sized project you can start with either, but stay consistent across all entities.
Real-Life Example
A hospital keeps a file for every patient. The reception desk stamps "Opened: 3 March" on the cover once. Every time a nurse adds a note, she writes "Last updated: date, name" on the same cover. The opening date never changes, and the update line changes on every visit. @CreatedDate is the opening stamp. @LastModifiedDate is the update line.
Code Example
Let's build the help desk of CampusCab, a shuttle booking service. A SupportTicket records when it was created, when it was last changed, and who did it. To avoid repeating those fields, we put them in a base class marked @MappedSuperclass.
textcampuscab/ ├─ pom.xml └─ src/main/ ├─ java/com/campuscab/helpdesk/ │ ├─ HelpdeskApplication.java │ ├─ AuditConfig.java │ ├─ AuditedEntity.java │ ├─ SupportTicket.java │ └─ TicketRepository.java └─ resources/ └─ application.properties
File: pom.xml
xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.1.1</version> <relativePath/> </parent> <groupId>com.campuscab</groupId> <artifactId>helpdesk</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>21</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
File: application.properties in src/main/resources
propertiesspring.main.banner-mode=off logging.level.root=warn
File: AuditConfig.java in package com.campuscab.helpdesk
javapackage com.campuscab.helpdesk; 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; @Configuration @EnableJpaAuditing public class AuditConfig { @Bean AuditorAware<String> auditorProvider() { return () -> Optional.of("desk-agent"); } }
File: AuditedEntity.java in package com.campuscab.helpdesk
javapackage com.campuscab.helpdesk; import java.time.Instant; import jakarta.persistence.Column; import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedBy; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @MappedSuperclass @EntityListeners(AuditingEntityListener.class) public abstract class AuditedEntity { @CreatedDate @Column(nullable = false, updatable = false) private Instant createdAt; @LastModifiedDate private Instant updatedAt; @CreatedBy @Column(updatable = false) private String createdBy; @LastModifiedBy private String updatedBy; public Instant getCreatedAt() { return createdAt; } public Instant getUpdatedAt() { return updatedAt; } public String getCreatedBy() { return createdBy; } public String getUpdatedBy() { return updatedBy; } }
File: SupportTicket.java in package com.campuscab.helpdesk
javapackage com.campuscab.helpdesk; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @Entity public class SupportTicket extends AuditedEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String subject; private String status = "OPEN"; protected SupportTicket() { } public SupportTicket(String subject) { this.subject = subject; } public String getSubject() { return subject; } public String getStatus() { return status; } public void setStatus(String status) { this.status = status; } }
File: TicketRepository.java in package com.campuscab.helpdesk
javapackage com.campuscab.helpdesk; import org.springframework.data.jpa.repository.JpaRepository; public interface TicketRepository extends JpaRepository<SupportTicket, Long> { }
File: HelpdeskApplication.java in package com.campuscab.helpdesk
javapackage com.campuscab.helpdesk; import java.time.Instant; import java.time.temporal.ChronoUnit; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @SpringBootApplication public class HelpdeskApplication { public static void main(String[] args) { SpringApplication.run(HelpdeskApplication.class, args); } @Bean CommandLineRunner demo(TicketRepository tickets) { return args -> { SupportTicket ticket = tickets.save(new SupportTicket("Driver was late")); System.out.println("Created by: " + ticket.getCreatedBy()); System.out.println("Created at: " + secs(ticket.getCreatedAt())); System.out.println("Updated at: " + secs(ticket.getUpdatedAt())); Thread.sleep(1000); ticket.setStatus("RESOLVED"); SupportTicket saved = tickets.saveAndFlush(ticket); System.out.println("--- after the update"); System.out.println("Created at: " + secs(saved.getCreatedAt())); System.out.println("Updated at: " + secs(saved.getUpdatedAt())); System.out.println("Same creation time: " + saved.getCreatedAt().equals(ticket.getCreatedAt())); }; } private static String secs(Instant time) { return time.truncatedTo(ChronoUnit.SECONDS).toString(); } }
Run it:
bashmvn spring-boot:run
Output:
textCreated by: desk-agent Created at: 2026-09-26T21:28:06Z Updated at: 2026-09-26T21:28:06Z --- after the update Created at: 2026-09-26T21:28:06Z Updated at: 2026-09-26T21:28:08Z Same creation time: true
The exact times will differ on your machine, and we print them rounded to seconds so they fit on a small screen. What matters is the pattern: on the first save, both times are equal. After the update, the creation time stays and only the update time moves forward.
Code Explained
@EnableJpaAuditingswitches the feature on. Without it, the annotated fields staynull.AuditorAware<String>tells Spring who the current user is. Here it always answersdesk-agent. In a secured app it would read the login name.@MappedSuperclasslets many entities share the same audit fields without a table of its own.@EntityListenersnamingAuditingEntityListenerattaches the listener that fills the fields.updatable = falseon the creation columns stops Hibernate from ever writing them again.- We use
Instant, a point on the timeline in UTC. It avoids time zone confusion.LocalDateTimeandDatealso work.
Common Mistakes
- Missing `@EntityListeners`. The switch is on, but the entity does not listen. Put the listener on the base class.
- Setting dates by hand. If you also write
setCreatedAt(now), you now have two sources of truth. Let the listener do it. - Bulk updates. A JPQL
updatestatement skips entity listeners, soupdatedAtis not changed. Set it in the query if you need it. - No `AuditorAware` bean.
@CreatedBythen staysnull. Add the bean, and return an emptyOptionalwhen nobody is logged in. - Old tutorials with `@Temporal`. With
InstantorLocalDateTimeyou do not need it.
Interview Questions
What is JPA auditing?
Ans:It is a Spring Data feature that fills creation and modification fields automatically when an entity is saved.
Which steps enable it?
Ans:Add @EnableJpaAuditing to a configuration class, add @EntityListeners naming AuditingEntityListener to the entity or its base class, and mark the fields with @CreatedDate and @LastModifiedDate.
How does Spring know the current user for `@CreatedBy`?
Ans:Through a bean of type AuditorAware, whose getCurrentAuditor() method returns the user name.
What does `@MappedSuperclass` do?
Ans:It lets a class share its fields with child entities. Those fields become columns in each child's own table, and the base class has no table.
Key Points to Remember
- Auditing fills date and user fields automatically on save.
- You need three things:
@EnableJpaAuditing, the entity listener and marked fields. @CreatedDateis set once.@LastModifiedDatechanges on every update.- Put audit fields in a
@MappedSuperclassand reuse them everywhere. - Use
Instantfor times, andAuditorAwarefor the user.
Frequently Asked Questions
What is the difference between @CreatedDate and @LastModifiedDate?
@CreatedDate is written once, when the row is first inserted. @LastModifiedDate is written on the first insert and again on every update.
Does auditing with @CreatedDate work with Hibernate's own annotations?
Hibernate has @CreationTimestamp and @UpdateTimestamp, which do a similar job. Spring's auditing adds the user fields and works across Spring Data stores, so many teams prefer it.
Can I audit only some entities?
Yes. Only entities that carry the listener and the annotated fields are audited. Others are untouched.
Why is my createdAt null?
Almost always because @EnableJpaAuditing is missing, or the entity does not have @EntityListeners naming AuditingEntityListener.
Related Topics
- JPA Entity and @Entity: revisit how a class becomes a table.
- JpaRepository: the save method that triggers auditing.
- Soft Delete: keep history by marking rows instead of removing them.
- Spring Security Basics: the source of the current user.
Practice Problems
Try each problem on your own first. Both use the H2 in-memory database, so nothing needs to be installed.
Easy: Recipe Post Dates
FoodNest publishes recipes. Give the Recipe entity a creation time and a last update time that Spring fills automatically. Save a recipe, then change its title and save again. Print whether the two times were equal after the first save, and whether the creation time survived the update.
Show answerHide answer
Recipe entity itself. No AuditorAware is needed, because we audit only dates.File: pom.xml
xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.1.1</version> <relativePath/> </parent> <groupId>com.foodnest</groupId> <artifactId>recipes</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>21</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
File: application.properties in src/main/resources
propertiesspring.main.banner-mode=off logging.level.root=warn
File: Recipe.java in package com.foodnest.recipes
javapackage com.foodnest.recipes; import java.time.Instant; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EntityListeners; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @Entity @EntityListeners(AuditingEntityListener.class) public class Recipe { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String title; @CreatedDate @Column(updatable = false) private Instant createdAt; @LastModifiedDate private Instant updatedAt; protected Recipe() { } public Recipe(String title) { this.title = title; } public void setTitle(String title) { this.title = title; } public Instant getCreatedAt() { return createdAt; } public Instant getUpdatedAt() { return updatedAt; } }
File: RecipeRepository.java in package com.foodnest.recipes
javapackage com.foodnest.recipes; import org.springframework.data.jpa.repository.JpaRepository; public interface RecipeRepository extends JpaRepository<Recipe, Long> { }
File: RecipesApplication.java in package com.foodnest.recipes
javapackage com.foodnest.recipes; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @SpringBootApplication @EnableJpaAuditing public class RecipesApplication { public static void main(String[] args) { SpringApplication.run(RecipesApplication.class, args); } @Bean CommandLineRunner demo(RecipeRepository recipes) { return args -> { Recipe recipe = recipes.save(new Recipe("Paneer Wrap")); boolean equalAtStart = recipe.getCreatedAt().equals(recipe.getUpdatedAt()); System.out.println("Equal after first save: " + equalAtStart); Thread.sleep(500); recipe.setTitle("Paneer Kathi Roll"); Recipe saved = recipes.saveAndFlush(recipe); System.out.println("Created unchanged: " + saved.getCreatedAt().equals(recipe.getCreatedAt())); System.out.println("Updated later: " + saved.getUpdatedAt().isAfter(saved.getCreatedAt())); }; } }
Running the app prints:
textEqual after first save: true Created unchanged: true Updated later: true
Medium: Who Changed the Booking
A hotel app must record who created a booking and who last changed it. Use an AuditorAware<String> that reads the name from a small CurrentUser bean. Create a booking as priya, then switch the current user to kabir and change it. Print createdBy and updatedBy.
Show answerHide answer
priya. The second save changes only updatedBy.File: pom.xml
xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.1.1</version> <relativePath/> </parent> <groupId>com.stayeasy</groupId> <artifactId>bookings</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>21</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
File: application.properties in src/main/resources
propertiesspring.main.banner-mode=off logging.level.root=warn
File: CurrentUser.java in package com.stayeasy.bookings
javapackage com.stayeasy.bookings; import org.springframework.stereotype.Component; @Component public class CurrentUser { private volatile String name; public String getName() { return name; } public void setName(String name) { this.name = name; } }
File: AuditConfig.java in package com.stayeasy.bookings
javapackage com.stayeasy.bookings; 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; @Configuration @EnableJpaAuditing public class AuditConfig { @Bean AuditorAware<String> auditor(CurrentUser currentUser) { return () -> Optional.ofNullable(currentUser.getName()); } }
File: RoomBooking.java in package com.stayeasy.bookings
javapackage com.stayeasy.bookings; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EntityListeners; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.LastModifiedBy; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @Entity @EntityListeners(AuditingEntityListener.class) public class RoomBooking { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private int nights; @CreatedBy @Column(updatable = false) private String createdBy; @LastModifiedBy private String updatedBy; protected RoomBooking() { } public RoomBooking(int nights) { this.nights = nights; } public void setNights(int nights) { this.nights = nights; } public String getCreatedBy() { return createdBy; } public String getUpdatedBy() { return updatedBy; } }
File: BookingRepository.java in package com.stayeasy.bookings
javapackage com.stayeasy.bookings; import org.springframework.data.jpa.repository.JpaRepository; public interface BookingRepository extends JpaRepository<RoomBooking, Long> { }
File: BookingsApplication.java in package com.stayeasy.bookings
javapackage com.stayeasy.bookings; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @SpringBootApplication public class BookingsApplication { public static void main(String[] args) { SpringApplication.run(BookingsApplication.class, args); } @Bean CommandLineRunner demo(BookingRepository bookings, CurrentUser user) { return args -> { user.setName("priya"); RoomBooking booking = bookings.save(new RoomBooking(2)); user.setName("kabir"); booking.setNights(3); RoomBooking saved = bookings.saveAndFlush(booking); System.out.println("createdBy: " + saved.getCreatedBy()); System.out.println("updatedBy: " + saved.getUpdatedBy()); }; } }
Running the app prints:
textcreatedBy: priya updatedBy: kabir