Nowadays more and more applications are composed by several different data layers, beginning from the classical RDBMS and finishing in more adapted to some situations (search or graph relationships for example) NoSQL solutions. An interesting feature of JPA allows to notify no-JPA layer about some changes made on entities, exactly as in other solutions coming from event-driven development approach.
Data Engineering Design Patterns

Looking for a book that defines and solves most common data engineering problems? I'm currently writing
one on that topic and the first chapters are already available in 👉
Early Release on the O'Reilly platform
I also help solve your data engineering problems 👉 contact@waitingforcode.com 📩
In this article we'll focus on JPA possibilities to dispatch event information to observing objects. The first part will explain this concept while the second its implementation in Hibernate. At the end we'll write a simple test case illustrating the working of JPA events.
JPA listeners and callbacks
Methods invoked after JPA lifecycle events, as entity persisting, removing or updating, must be annotated with appropriate annotations. These methods can be called before executing given event (that means, before saving changes in the database) or after event execution (after saving changes in the database). This rule is applied for all writing events (persist, update, remove) but no for reading one (load). The loading allows only callback after event execution and it's logical because before loading, entity holding callbacks doesn't exist. To resume this part, there are all available annotations for callback methods:
- @PostLoad - called after entity loading from the database.
- @PrePersist - called after the call of entity manager's persist method but before making SQL query into database.
- @PostPersist - as @PrePersist, but called after execution of SQL query, for example after the execution of INSERT INTO clause.
- @PreUpdate - callback used for all updating actions (such as SQL query UPDATE table SET property = value). Once again, this callback prefixed with pre will be called before executing query in the database.
- @PostUpdate - as @PreUpdate, but invoked after SQL queries real execution on database side.
- @PreRemove - used for all remove operations (as DELETE FROM) and invoked before the execution of the query on database side.
- @PostRemove - used for the same actions as @PreRemove, but callback called after query execution.
To define a listener for JPA's events, we need to annotate entity with @EntityListeners and specify inside one or several listeners to apply to events defined in given class. Methods responsible to handle events must be annotated with the same annotations as methods responsible for launching event from entity (@PostLoad, @PrePersist, @PostPersist, @PreUpdate, @PostUpdate, @PreRemove and @PostRemove).
Exceptions caused by callback methods or listener handling methods occur before the invocation of EntityManager's flush() method, making changes persistent. It explains the fact that these exceptions won't cancel all pending changes.
Hibernate's implementation of JPA events
To see how events and listeners are implemented by Hibernate, let's begin by produce an exception inside callback method. It could give stack trace looking like:
java.lang.RuntimeException: Thrown by listener (...) at org.hibernate.jpa.event.internal.jpa.ListenerCallback.performCallback(ListenerCallback.java:49) at org.hibernate.jpa.event.internal.jpa.CallbackRegistryImpl.callback(CallbackRegistryImpl.java:112) at org.hibernate.jpa.event.internal.jpa.CallbackRegistryImpl.postCreate(CallbackRegistryImpl.java:71) at org.hibernate.jpa.event.internal.core.JpaPostInsertEventListener.onPostInsert(JpaPostInsertEventListener.java:55)
In returned stack trace, the last one JpaPostInsertEventListener is the most interesting. Because Hibernate is not exclusively JPA solution, it uses its owns events listeners, declared inside org.hibernate.event.spi package. You can find there listeners for data inserting, updating, deleting or loading, exactly as these ones defined in JPA specification.
All lifecycle callbacks and listeners are stored as the implementations of org.hibernate.jpa.event.internal.jpa.CallbackRegistry interface. One from implementations is CallbackRegistryImpl. It contains private field keeping the track of callbacks to invoke for each entity:
private HashMap<Class, Callback[]> preCreates = new HashMap<Class, Callback[]>(); private HashMap<Class, Callback[]> postCreates = new HashMap<Class, Callback[]>(); private HashMap<Class, Callback[]> preRemoves = new HashMap<Class, Callback[]>(); private HashMap<Class, Callback[]> postRemoves = new HashMap<Class, Callback[]>(); private HashMap<Class, Callback[]> preUpdates = new HashMap<Class, Callback[]>(); private HashMap<Class, Callback[]> postUpdates = new HashMap<Class, Callback[]>(); private HashMap<Class, Callback[]> postLoads = new HashMap<Class, Callback[]>();
Appropriated callbacks are after called inside listening methods, as for @PostPersist:
@Override public void postCreate(Object bean) { callback( postCreates.get( bean.getClass() ), bean ); }
Note that listeners defined in superclasses are invoked before listeners from children classes. This behaviour can be changed by excluding superclasses listeners with javax.persistence.ExcludeSuperclassListeners annotation.
Example of JPA listener
To show explained concepts in the code, let's write some test cases. But before, we need to define listeners and callback methods inside ours entities:
@Entity @Table(name = "letter") @EntityListeners({QueueStoreJpaListener.class, LetterListener.class}) public class Letter extends Document { private boolean loaded; private boolean prePersisted; private boolean postPersisted; private boolean preUpdated; private boolean postUpdated; private boolean preRemoved; private boolean postRemoved; private Queue<String> executedEvents = new LinkedBlockingQueue<String>(); @Transient public boolean isLoaded() { return this.loaded; } @Transient public boolean isPrePersisted() { return this.prePersisted; } @Transient public boolean isPostPersisted() { return this.postPersisted; } @Transient public boolean isPreUpdated() { return this.preUpdated; } @Transient public boolean isPostUpdated() { return this.postUpdated; } @Transient public boolean isPreRemoved() { return this.preRemoved; } @Transient public boolean isPostRemoved() { return this.postRemoved; } // JPA's callback methods @PostLoad public void loadEvent() { this.loaded = true; } @PrePersist public void prePersistEvent() { this.prePersisted = true; } @PostPersist public void postPersistEvent() { this.postPersisted = true; } @PreUpdate public void preUpdateEvent() { this.preUpdated = true; } @PostUpdate public void postUpdateEvent() { this.postUpdated = true; } @PreRemove public void preRemoveEvent() { this.preRemoved = true; } @PostRemove public void postRemoveEvent() { this.postRemoved = true; } public void addExecutedEvents(String event) { executedEvents.add(event); } @Transient public Queue<String> getExecutedEvents() { return executedEvents; } @Override public String toString() { return MoreObjects.toStringHelper(this).add("address", this.address).toString(); } } // Document.java @EntityListeners(DocumentListener.class) public abstract class Document { // ... }
Applied listeners look like:
// QueueStoreJpaListener.java public class QueueStoreJpaListener { public static final Queue<String> EVENTS = new LinkedBlockingQueue<String>(); @PostPersist public void sendNewLetterMail(Letter letter) { EVENTS.add("Add-"+letter.getAddress()); } @PostUpdate public void sendLetterUpdatedMail(Letter letter) { EVENTS.add("Update-"+letter.getAddress()); } @PostRemove public void sendLetterRemoveMail(Letter letter) { EVENTS.add("Remove-"+letter.getAddress()); } } // DocumentListener.java public class DocumentListener { public static final String NAME = "DOCUMENT"; @PostLoad public void loadLow(Letter letter) { letter.addExecutedEvents(NAME); } } // LetterListener.java public class LetterListener { public static final String NAME = "LETTER"; @PostLoad public void loadLow(Letter letter) { letter.addExecutedEvents(NAME); } }
And the test cases used to manipulate JPA events and listeners :
public class CallbackListenersTest extends AbstractJpaTester { @Test public void testPostLoadEvent() { Query query = entityManager.createQuery("SELECT l FROM Letter l WHERE l.id = 1"); Letter letter = (Letter) query.getSingleResult(); assertTrue("Category should be loaded with @PostLoad annotation", letter.isLoaded()); } @Test public void testPostLoadOrderWithSuperclassEvent() { Query query = entityManager.createQuery("SELECT l FROM Letter l WHERE l.id = 1"); Letter letter = (Letter) query.getSingleResult(); assertEquals("Listener from superclass should be invoked before the others", DocumentListener.NAME, letter.getExecutedEvents().poll()); assertEquals("Listener from class should be invoked after the listener of the listener of superclass", LetterListener.NAME, letter.getExecutedEvents().poll()); } @Test public void testPersistEvents() { EntityTransaction transaction = entityManager.getTransaction(); try { transaction.begin(); Letter letter = new Letter(); letter.setAddress("Address 1"); // *Persisted callbacks will be invoked now entityManager.persist(letter); assertTrue("Callback for @PrePersist should be invoked after persist() call", letter.isPrePersisted()); assertTrue("Callback for @PostPersist should be invoked after persist() call " + " (directly after execution of INSERT INTO statement)", letter.isPostPersisted()); entityManager.flush(); checkEvent("Add-Address 1"); } finally { transaction.rollback(); } } @Test public void testUpdateEvents() { EntityTransaction transaction = entityManager.getTransaction(); try { transaction.begin(); Query query = entityManager.createQuery("SELECT l FROM Letter l WHERE l.id = 1"); Letter letter = (Letter) query.getSingleResult(); checkIfAllAreFalse(letter); // *Persisted callbacks will be invoked now letter.setAddress("Address changed"); entityManager.persist(letter); entityManager.flush(); assertTrue("Pre updated callback should be invoked at this stage", letter.isPreUpdated()); assertTrue("Post updated callback should be invoked at this stage", letter.isPostUpdated()); checkEvent("Update-Address changed"); } finally { transaction.rollback(); } } @Test public void testRemoveEvents() { EntityTransaction transaction = entityManager.getTransaction(); try { transaction.begin(); Query query = entityManager.createQuery("SELECT l FROM Letter l WHERE l.id = 1"); Letter letter = (Letter) query.getSingleResult(); checkIfAllAreFalse(letter); // *Removed callbacks will be invoked now entityManager.remove(letter); entityManager.flush(); assertTrue("Pre removed callback should be invoked at this stage", letter.isPreRemoved()); assertTrue("Post removed callback should be invoked at this stage", letter.isPostRemoved()); checkEvent("Remove-39 street, 99999 City"); } finally { transaction.rollback(); } } @Test public void testPrePersistCallbackWithException() { EntityTransaction transaction = entityManager.getTransaction(); try { transaction.begin(); Category category = new Category(); category.setName(Category.TEST_NAME); category.setSeo(new Seo()); entityManager.persist(category); entityManager.flush(); fail("Saving entity with @PrePersist causing RuntimeException shouldn't allow to arrive here"); } catch (RuntimeException re) { // left empty } finally { transaction.rollback(); } } @Test public void testPostPersistCallbackWithException() { EntityTransaction transaction = entityManager.getTransaction(); try { transaction.begin(); Category category = new Category(); category.setName(Category.TEST_NAME); category.setSeo(new Seo()); entityManager.persist(category); entityManager.flush(); fail("Saving entity with @PostPersist causing RuntimeException shouldn't allow to arrive here"); } catch (RuntimeException re) { // left empty } finally { transaction.rollback(); } } @Test public void testExceptionThrownByListener() { String testName = "Other test class"; EntityTransaction transaction = entityManager.getTransaction(); transaction.begin(); try { Category category = new Category(); category.setName(testName); category.setSeo(new Seo()); category.getSeo().setDescription("desc Test"); category.getSeo().setKeywords("keywords Test"); category.getSeo().setTitle("title Test"); // Listener exception occurs inside persist() method, so flush() is not called. It means that changes won't be // pushed to database storage, ie. try to retrieve inserted category will fail - proof in the next try-catch-finally // block entityManager.persist(category); entityManager.flush(); } catch (RuntimeException e) { // left empty for test purposes } String expectedFragment = "null id in"; String errorMsg = ""; try { Query query = entityManager.createQuery("SELECT c FROM Category c WHERE c.name = :name"); query.setParameter("name", testName); Category category = (Category) query.getSingleResult(); } catch (AssertionFailure af) { errorMsg = af.getMessage(); } finally { transaction.rollback(); } assertTrue("Error on missing ID should be thrown when reading category inserted with listener failure", errorMsg.contains(expectedFragment)); } private void checkEvent(String expected) { String event = QueueStoreJpaListener.EVENTS.poll(); assertEquals("Bad event was caught", expected, event); } private void checkIfAllAreFalse(Letter letter) { assertFalse("Post persist callback shouldn't be invoked at this stage", letter.isPostPersisted()); assertFalse("Post removed callback shouldn't be invoked at this stage", letter.isPostRemoved()); assertFalse("Pre removed callback shouldn't be invoked at this stage", letter.isPreRemoved()); assertFalse("Pre updated callback shouldn't be invoked at this stage", letter.isPreUpdated()); assertFalse("Post updated callback shouldn't be invoked at this stage", letter.isPostUpdated()); } }
This articles shows which mechanisms are provided with JPA to deal implement a kind of event-driven system, able to link JPA with, for example, other storage solutions (for example: NoSQL). At the begin, we discovered the annotations used to handle entities lifecycle. After that, we explained the implementation of JPA events in Hibernate. The last part was about some real use-cases showing the use of presented concepts.
Consulting

With nearly 16 years of experience, including 8 as data engineer, I offer expert consulting to design and optimize scalable data solutions.
As an O’Reilly author, Data+AI Summit speaker, and blogger, I bring cutting-edge insights to modernize infrastructure, build robust pipelines, and
drive data-driven decision-making. Let's transform your data challenges into opportunities—reach out to elevate your data engineering game today!
👉 contact@waitingforcode.com
đź”— past projects