Across JPA, Spring Data JDBC, and MongoDB, how does entity-state detection differ, and how does it interact with @CreatedDate/@LastModifiedDate auditing?
answer
- same hierarchy, different blast radius
- JPA merge forgives; JDBC/Mongo silently lose data
- no persistence context in JDBC/R2DBC -> isNew is load-bearing
- IsNewAwareAuditingHandler: new=created+modified, existing=modified
- wrong isNew corrupts audit trail too
basics
~20 sAll stores share the same detection order (Persistable > nullable @Version > @Id null/0), but JPA can hide mistakes via merge while JDBC/Mongo have no persistence context and directly pick INSERT vs UPDATE. Auditing's IsNewAwareAuditingHandler reuses isNew() to set @CreatedDate only on new entities and @LastModifiedDate on every save.
solid answer
~50 sThe is-new contract is defined in Spring Data Commons and shared, but the consequences differ by store. **JPA** has a persistence context: `isNew()` chooses `persist` vs `merge`, and merge's load-then-write behavior can mask a wrong verdict (a bad 'existing' guess still inserts, just with a wasted SELECT). **Spring Data JDBC and R2DBC** have *no* persistence context — `isNew()` directly selects an INSERT or UPDATE statement, so a wrong verdict means a zero-row UPDATE and silent data loss. **MongoDB** is similar (insert vs replace by _id). Auditing is layered on top: `IsNewAwareAuditingHandler` calls the same `isNew()` to decide whether to stamp `@CreatedDate`/`@CreatedBy` (new only) or just `@LastModifiedDate`/`@LastModifiedBy` (every save). This is why correct is-new detection is a cross-cutting design concern: it drives both the SQL and the audit trail, and it's least forgiving precisely where there's no persistence context to fall back on.
code
java · 21 lines// Same detection contract, wired per store; auditing keys off isNew()
// JPA
@Configuration @EnableJpaAuditing class JpaAuditCfg {}
@Entity @EntityListeners(AuditingEntityListener.class)
class JpaCustomer implements Persistable<UUID> {
@Id private UUID id;
@CreatedDate private Instant created;
@LastModifiedDate private Instant modified;
@Override public UUID getId() { return id; }
@Override public boolean isNew() { return created == null; } // new -> INSERT + stamp created
}
// Spring Data JDBC — no persistence context; isNew() picks INSERT vs UPDATE directly
@Configuration @EnableJdbcAuditing class JdbcAuditCfg {}
record JdbcCustomer(@Id UUID id, @CreatedDate Instant created,
@LastModifiedDate Instant modified) implements Persistable<UUID> {
@Override public UUID getId() { return id; }
@Override public boolean isNew() { return created == null; }
}
// IsNewAwareAuditingHandler calls isNew(): true -> set created+modified; false -> modified only.go deeper
Know auditing sets @CreatedDate only for new entities and @LastModifiedDate always.
State that the detection hierarchy is shared but JDBC has no merge fallback.
Explain the merge-vs-direct-SQL difference and that IsNewAwareAuditingHandler reuses isNew().
Set team-wide id/version/Persistable policy, treat is-new as load-bearing in JDBC/R2DBC, and guard the audit-trail integrity and import paths.
## One contract, different blast radius Spring Data Commons defines the `EntityInformation.isNew` contract and the detection hierarchy — **Persistable > nullable @Version > @Id (null, or 0 for primitives)**. Every module reuses this ordering. What differs is *what a wrong answer costs*, because the modules have very different execution models. ### Spring Data JPA — persistence context softens errors `SimpleJpaRepository.save`: `isNew()` true -> `EntityManager.persist` (pure INSERT); false -> `EntityManager.merge`. `merge` is *tolerant*: even if you wrongly report an existing entity, Hibernate loads (SELECT), finds nothing, and inserts anyway. So a mis-detection usually degrades to a **performance bug** (extra SELECTs, detached-copy confusion) rather than data loss. The persistence context (first-level cache, dirty checking, flush) also means many saves are implicit — a loaded, modified entity is written at flush without an explicit `save()` at all. ### Spring Data JDBC / R2DBC — no safety net `JdbcAggregateTemplate.save` has no persistence context and no dirty checking: you *must* call `save()`, and `isNew()` **directly** decides INSERT vs UPDATE SQL. A wrong 'existing' verdict emits `UPDATE ... WHERE id = ?` that matches **zero rows and throws nothing** — silent data loss. This is why assigned-id entities in JDBC almost always need explicit `Persistable` or a nullable `@Version`. R2DBC (reactive relational) behaves the same way. ### MongoDB `MongoTemplate`/repository `save` inserts a new document or replaces an existing one keyed by `_id`. State detection uses the same hierarchy (Mongo commonly assigns `ObjectId`/String ids). A wrong verdict similarly leads to a replace that matches no document. ## How auditing rides on is-new Spring Data auditing (`@CreatedDate`, `@CreatedBy`, `@LastModifiedDate`, `@LastModifiedBy`) is driven by `IsNewAwareAuditingHandler`. On each save it calls the **same `isNew()`**: - If **new**: set created fields *and* modified fields. - If **existing**: set only the modified fields. Wiring differs by store: - **JPA**: `AuditingEntityListener` on the entity + `@EnableJpaAuditing`; created/modified stamps applied via `@PrePersist`/`@PreUpdate` lifecycle. - **JDBC**: `@EnableJdbcAuditing`; a `RelationalAuditingCallback` (a `BeforeConvertCallback`) invokes the handler before the row is written. - **MongoDB**: `@EnableMongoAuditing` with a `AuditingEntityCallback`. Because the handler reuses `isNew()`, **an incorrect is-new verdict corrupts the audit trail too**: a genuinely new row misclassified as existing won't get a `@CreatedDate`; a genuinely existing row misclassified as new gets its `@CreatedDate` overwritten. So is-new correctness is not just about INSERT vs UPDATE — it's the linchpin of the audit history. ## The elegant coupling: @CreatedDate as the is-new signal This interplay is what makes `isNew() { return createdDate == null; }` work: the handler reads `isNew()` **before** stamping, so a fresh row (null created-date) is seen as new, gets inserted, and gets stamped; a reloaded row (non-null) is seen as existing. The audit column becomes the persisted newness marker — one field serving both concerns, and robust across sessions and stores. ## Design guidance for a codebase - **Pick an id strategy per aggregate.** Generated ids: defaults just work. Assigned ids: mandate `Persistable` or nullable `@Version` as a team rule, enforced in review. - **In JDBC/R2DBC, treat is-new as load-bearing** — there's no merge to hide a mistake; a missing strategy is a data-loss defect, not a perf nit. - **Test across transactions.** A create-then-read in the same transaction can pass while masking a merge SELECT (JPA) or a not-yet-visible issue; assert row counts / re-read in a fresh transaction. - **Keep auditing and is-new consistent.** If you rely on `@CreatedDate` for both auditing and is-new, ensure auditing is enabled everywhere those entities are saved (e.g. batch/import paths), or newly imported rows will look 'new' forever. ## Key terms - **Persistence context / first-level cache** — JPA's per-transaction managed-entity registry enabling dirty checking and implicit flush. - **BeforeConvertCallback / RelationalAuditingCallback** — Spring Data JDBC extension points; auditing runs here before the entity becomes a row. - **IsNewAwareAuditingHandler** — routes auditing based on isNew().
- Why is a wrong is-new verdict merely a performance issue in JPA but a data-loss bug in Spring Data JDBC?JPA's merge loads then writes: a wrongly-'existing' new entity still gets inserted after a wasted SELECT. Spring Data JDBC has no merge or persistence context — isNew() directly emits an UPDATE, which matches zero rows for a missing record and throws nothing, so the data is silently never written.
- How can an incorrect isNew() implementation corrupt your audit history, not just the INSERT/UPDATE choice?IsNewAwareAuditingHandler calls the same isNew() to decide auditing. If a new row is seen as existing, @CreatedDate/@CreatedBy are never set; if an existing row is seen as new, its @CreatedDate gets overwritten on update. So is-new correctness directly governs the integrity of the created/modified stamps.
- What breaks if entities that use 'isNew = createdDate == null' are inserted through a path where auditing isn't enabled?createdDate stays null, so those rows report isNew() == true forever. Later saves try to re-INSERT (duplicate key in JPA, or wrong branch in JDBC), and the audit trail lacks created stamps. Auditing must be enabled on every save path for these entities.
saying these in an interview costs you the question
- Treating is-new detection as identical in effect across all stores.
- Assuming JDBC behaves like JPA and 'falls back to insert' when the row is missing.
- Not realizing auditing reuses isNew(), so a detection bug also breaks @CreatedDate/@LastModifiedDate.
- Relying on @CreatedDate-based is-new while leaving batch/import save paths without auditing enabled.