skip to content

How do you move @PrePersist and @PreUpdate logic out of the entity class into a reusable component, and in what order does JPA invoke several such components together with the entity's own callback methods?

level: middleimportance: should knowfreq 38%

answer

  1. @EntityListeners(X.class) on entity or @MappedSuperclass
  2. listener method: void, ONE parameter (the entity)
  3. order: default -> superclass -> entity listeners -> entity's own method
  4. @ExcludeDefaultListeners / @ExcludeSuperclassListeners
  5. one instance per unit: stateless, thread-safe, context via thread-local

basics

~20 s

Put the callback methods on a separate class and attach it with @EntityListeners on the entity or mapped superclass; listener methods take the entity as their single parameter. Order: default listeners, then superclass listeners, then this entity's listeners in declaration order, then the entity's own methods.

solid answer

~50 s

Extract the logic into a plain class whose methods are annotated the same way but take one parameter — the entity — and attach it with `@EntityListeners(AuditListener.class)` on the entity or, better, on a `@MappedSuperclass` so every entity that extends it inherits the behaviour. Invocation order for a given event is defined by the specification: 1. **default listeners** (declared in `orm.xml` and applied to every entity), in declaration order; 2. **listeners attached to superclasses**, most general superclass first; 3. **listeners attached to this entity**, in the order listed in `@EntityListeners`; 4. **the entity's own callback method**, superclass method before subclass method. An entity can opt out with `@ExcludeDefaultListeners` and `@ExcludeSuperclassListeners`. The listener class needs a public no-arg constructor and is instantiated once per persistence unit, so it must be **stateless and thread-safe** — no per-entity fields. Any contextual data it needs, such as the current user, has to come from something ambient like a thread-local, which is the main design wrinkle of this approach.

code

java · 26 lines
java
public class AuditListener {

    @PrePersist
    void onCreate(Auditable entity) {
        Instant now = Instant.now();
        entity.setCreatedAt(now);
        entity.setUpdatedAt(now);
        entity.setCreatedBy(CurrentUser.get());   // thread-local
    }

    @PreUpdate
    void onUpdate(Auditable entity) {
        entity.setUpdatedAt(Instant.now());
        entity.setUpdatedBy(CurrentUser.get());
    }
}

@MappedSuperclass
@EntityListeners(AuditListener.class)
public abstract class Auditable {
    private Instant createdAt;
    private Instant updatedAt;
    private String createdBy;
    private String updatedBy;
    // getters/setters
}

go deeper

for a junior

Know that @EntityListeners points at a class whose callback methods take the entity as a parameter.

for a middle

Recite the invocation order, know the exclusion annotations, and know listeners are stateless singletons per persistence unit.

for a senior

Discuss placing the listener on a mapped superclass, obtaining ambient context safely, and why calling the EntityManager from a listener is unsupported.

for a principal

Weigh implicit write-path interception against explicit domain methods: callbacks are invisible at the call site, and that invisibility is what makes them both convenient and hard to reason about later.

## Why extract at all Callback methods on the entity are fine for one entity and one concern. The moment forty entities all need `createdAt`/`updatedAt`/`createdBy`, duplicating the methods is a maintenance problem and mixes an infrastructural concern into the domain class. JPA's answer is the entity listener: the same callback annotations, on a separate class. ## Declaring a listener ``` public class AuditListener { @PrePersist void onCreate(Auditable e) { ... } @PreUpdate void onUpdate(Auditable e) { ... } } @EntityListeners(AuditListener.class) @MappedSuperclass public abstract class Auditable { ... } ``` Rules for listener methods: `void`, exactly one parameter, whose type is the entity type or a supertype (commonly `Object` or a shared interface/mapped superclass). No checked exceptions. The class needs a public no-arg constructor. `@EntityListeners` accepts an array, and the array order is significant. Putting `@EntityListeners` on a `@MappedSuperclass` is the usual production shape: entities inherit both the audit columns and the listener, so adding a new entity to the scheme is a single `extends`. ## The ordering rules For one event on one entity, the provider invokes, in this order: 1. **Default listeners.** Declared once in `orm.xml` under `<persistence-unit-defaults><entity-listeners>` and applied to every entity in the unit. They run first, in declaration order. An entity annotated `@ExcludeDefaultListeners` skips them. 2. **Superclass listeners.** Listeners attached to superclasses of the entity, starting from the *most general* superclass and working down. `@ExcludeSuperclassListeners` on an entity or mapped superclass stops this inheritance at that point. 3. **This entity's listeners**, in the order given in its `@EntityListeners` array. 4. **Callback methods declared on the entity hierarchy itself**, superclass method before subclass method. The practical reading: infrastructure first, domain last. If the entity's own `@PrePersist` and a listener both write `createdAt`, the entity wins because it runs last. Knowing which layer wins is exactly what this ordering question is testing. ## Statelessness The provider creates one listener instance per persistence unit and calls it from every thread that flushes. Fields on the listener are shared mutable state. The correct shape is a stateless class that derives everything from its parameter, plus whatever ambient context it needs. That ambient context is the real design constraint. A listener that stamps `createdBy` needs the current principal, and a plain JPA listener has no injection point in the specification — the provider instantiates it reflectively. The portable answer is a thread-local or an explicitly passed context holder that the surrounding layer populates; the listener reads from it. Anything asynchronous or pooled must propagate that context deliberately or the listener silently stamps the wrong value, or null. ## What listeners must not do The restrictions are the same as for entity-level callbacks: a listener method must not invoke `EntityManager` or `Query` operations, and must not access other entity instances, because it runs in the middle of the provider's flush processing. Practically, calling back into the persistence context while it is iterating its action queue risks concurrent-modification failures, re-entrant flushes and non-deterministic ordering. If your listener genuinely needs to write another row, you are past what callbacks support — collect the intent and act after the transaction, or move to Hibernate's event/interceptor SPI, which is designed for it. ## Choosing between entity method and listener - **Entity method** — the logic is domain behaviour of that one entity, uses its private fields, and would look strange elsewhere. - **Listener** — the logic is cross-cutting, applies to many entities, and has nothing to do with the domain meaning of any one of them. A useful test: if the method name reads like infrastructure ('stampAudit', 'encryptSensitiveFields'), it belongs in a listener; if it reads like the domain ('recalculateTotal'), keep it on the entity — and consider whether it should be an explicit method the caller invokes rather than a callback at all, because callbacks are invisible at the call site and that invisibility is their main long-term cost.

  • Your listener needs the identity of the user performing the change. What are the options and their drawbacks in plain JPA?
    The provider instantiates the listener reflectively with a no-arg constructor, so there is no portable injection. The usual answer is an ambient context holder, typically a thread-local, populated at the edge of the request and cleared in a finally block. Its drawbacks are real: it must be propagated explicitly across thread hand-offs and async work, background jobs and migrations have no user at all, and a leaked value from a pooled thread stamps the wrong identity.

saying these in an interview costs you the question

  • Giving a listener method zero parameters, copying the entity-level signature
  • Keeping mutable per-entity state in listener fields, forgetting that one instance serves all threads
  • Claiming the entity's own callback runs before its listeners — it runs last
  • Forgetting that @EntityListeners on a mapped superclass is inherited, then wondering why the listener fires for subclasses
  • Calling EntityManager operations from inside a listener because 'it seemed to work locally'

context