Lifecycle Callbacks and EntityListeners

JPA Callback Annotations

Auditing is built on standard JPA lifecycle callbacks. You can use them yourself, on the entity or in a separate listener class registered with @EntityListeners.

AnnotationRuns
@PrePersist / @PostPersistbefore / after the INSERT is scheduled or executed
@PreUpdate / @PostUpdatebefore / after an UPDATE (only if the entity is dirty)
@PreRemove / @PostRemovebefore / after a delete
@PostLoadafter the entity is loaded from the database
public class ProductSlugListener {

    @PrePersist
    @PreUpdate
    void makeSlug(Product p) {
        p.setSlug(p.getName().toLowerCase().trim().replaceAll("[^a-z0-9]+", "-"));
    }
}

@Entity
@EntityListeners({AuditingEntityListener.class, ProductSlugListener.class})
public class Product extends Auditable {
    private String name;
    private String slug;            // "boat-airdopes-141" for SEO-friendly URLs

    @PostLoad
    void afterLoad() { /* e.g. compute a transient field */ }
}

Simulating an Auditing Listener

Example

This is roughly what AuditingEntityListener does: on persist set all four fields, on update only the "last modified" pair. A fixed Clock keeps the output stable.

import java.time.*;
import java.util.function.Supplier;

public class Main {
    public static void main(String[] args) {
        Clock t0 = Clock.fixed(Instant.parse("2026-03-14T04:45:10Z"), ZoneOffset.UTC);
        Product kurta = new Product("Cotton Kurta", 799);

        new AuditingListener(t0, () -> "[email protected]").prePersist(kurta);
        System.out.println("after persist: " + kurta);

        kurta.price = 699;
        Clock t1 = Clock.offset(t0, Duration.ofHours(26));
        new AuditingListener(t1, () -> "[email protected]").preUpdate(kurta);
        System.out.println("after update : " + kurta);
    }
}

abstract class Auditable {
    Instant createdAt, updatedAt;
    String createdBy, updatedBy;
}

class Product extends Auditable {
    final String name;
    int price;
    Product(String name, int price) { this.name = name; this.price = price; }
    public String toString() {
        return name + " Rs." + price + " | created " + createdAt + " by " + createdBy
             + " | updated " + updatedAt + " by " + updatedBy;
    }
}

class AuditingListener {
    private final Clock clock;
    private final Supplier<String> auditor;
    AuditingListener(Clock clock, Supplier<String> auditor) { this.clock = clock; this.auditor = auditor; }

    void prePersist(Auditable e) {
        Instant now = clock.instant();
        e.createdAt = now; e.createdBy = auditor.get();
        e.updatedAt = now; e.updatedBy = auditor.get();
    }

    void preUpdate(Auditable e) {
        e.updatedAt = clock.instant();
        e.updatedBy = auditor.get();
    }
}

Output

after persist: Cotton Kurta Rs.799 | created 2026-03-14T04:45:10Z by [email protected] | updated 2026-03-14T04:45:10Z by [email protected]
after update : Cotton Kurta Rs.699 | created 2026-03-14T04:45:10Z by [email protected] | updated 2026-03-15T06:45:10Z by [email protected]

Rules for Callbacks

Note: Callbacks must not call the EntityManager or repositories (the JPA spec forbids it and Hibernate may be in the middle of a flush). Bulk JPQL updates (@Modifying) do not trigger callbacks, so set updatedAt in the query yourself: set p.stock = p.stock - :q, p.updatedAt = CURRENT_TIMESTAMP.