skip to content

How do you implement Persistable<ID> to take full control of is-new detection, and how does @CreatedDate fit in?

level: seniorimportance: must knowfreq 44%

answer

  1. Persistable = getId() + isNew(), overrides everything
  2. transient flag flipped in @PostLoad/@PostPersist
  3. or isNew() = createdDate == null
  4. auditing asks isNew BEFORE stamping createdDate
  5. flag resets on manual reconstruction; created-date is persisted

basics

~20 s

Implement Persistable<ID> and override getId() plus isNew(). Back isNew() with a transient boolean set false on load/persist, or return createdDate == null using an @CreatedDate field. When present, Persistable.isNew() overrides all the default id/version checks.

solid answer

~40 s

`Persistable<ID>` is the explicit escape hatch: implement `getId()` and `isNew()`, and Spring Data uses your `isNew()` verdict directly (a dedicated `JpaPersistableEntityInformation` short-circuits the default id/version logic). Two common backings: (1) a `@Transient boolean isNew = true` flag flipped to `false` in a `@PostLoad` and `@PostPersist` callback — so freshly loaded or freshly inserted rows report existing; (2) `return getCreatedDate() == null`, leveraging an `@CreatedDate` auditing field. The created-date trick works because the auditing handler (`IsNewAwareAuditingHandler`) calls `isNew()` *before* populating the timestamp: at save time createdDate is still null (new -> INSERT), and after insert / on reload it's non-null (existing -> UPDATE). This is the idiomatic way to make assigned ids behave correctly, especially in Spring Data JDBC where there's no merge to paper over mistakes.

code

java · 23 lines
java
@Entity
@EntityListeners(AuditingEntityListener.class)   // requires @EnableJpaAuditing
class Account implements Persistable<UUID> {

    @Id
    private UUID id;                 // assigned by us, never null

    @CreatedDate
    private Instant createdDate;     // stamped by auditing on the first (new) save

    @Override
    public UUID getId() { return id; }

    @Override
    public boolean isNew() {         // Spring Data uses THIS, ignoring the id/version defaults
        return createdDate == null;  // null before first save -> INSERT; set afterward -> UPDATE
    }
}

// Alternative backing without auditing: a transient flag
// @Transient private boolean isNew = true;
// @Override public boolean isNew() { return isNew; }
// @PostPersist @PostLoad void markNotNew() { this.isNew = false; }

go deeper

for a junior

Know Persistable has getId() and isNew() and lets you decide newness.

for a middle

Implement it with a transient flag flipped in a lifecycle callback.

for a senior

Explain the @CreatedDate approach and why it isn't circular, plus when to pick flag vs version vs created-date.

for a principal

Reason about signal persistence (memory vs column), reconstruction safety, and auditing/isNew ordering across JPA and JDBC.

## What Persistable is `org.springframework.data.domain.Persistable<ID>` is a tiny interface: ```java public interface Persistable<ID> { ID getId(); boolean isNew(); } ``` By implementing it, your entity **declares its own newness**. Spring Data detects the interface and selects a `Persistable`-aware `EntityInformation` (e.g. `JpaPersistableEntityInformation`) whose `isNew()` simply delegates to your method — overriding the default id-null and @Version strategies entirely. This is the highest-priority strategy in the detection hierarchy. ## Why you'd reach for it You reach for `Persistable` when the framework can't infer newness on its own — almost always because you **assign ids yourself** (client UUIDs, natural keys). Rather than fight `merge`/zero-row-update surprises, you state the truth explicitly. ## Implementation A: transient flag ```java @Entity class Customer implements Persistable<UUID> { @Id private UUID id; @Transient private boolean isNew = true; // not persisted; true only in memory pre-save @Override public UUID getId() { return id; } @Override public boolean isNew() { return isNew; } @PostPersist @PostLoad void markNotNew() { this.isNew = false; } } ``` - `@Transient` keeps the flag out of the table. - A brand-new object has `isNew == true` -> INSERT. - `@PostLoad` (after a read) and `@PostPersist` (after the insert) flip it to `false`, so subsequent saves UPDATE. - These are JPA lifecycle callbacks; for Spring Data JDBC you'd use the equivalent (e.g. set the flag in an `AfterLoad`/callback), since JDBC doesn't fire JPA annotations. ## Implementation B: the @CreatedDate trick ```java @Entity @EntityListeners(AuditingEntityListener.class) class Customer implements Persistable<UUID> { @Id private UUID id; @CreatedDate private Instant createdDate; @Override public UUID getId() { return id; } @Override public boolean isNew() { return createdDate == null; } } ``` Why this is correct and not circular: - Spring Data auditing runs through `IsNewAwareAuditingHandler`, which asks `isNew()` to decide whether to populate `@CreatedDate` (only on new) versus just `@LastModifiedDate` (always). - At **save time on a fresh object**, `createdDate` is still `null`, so `isNew()` returns `true` -> the save chooses INSERT **and** the auditing handler stamps `createdDate`. - On any **reloaded** object, `createdDate` is non-null -> `isNew()` returns `false` -> UPDATE. So the timestamp doubles as the persisted 'have I ever been saved?' marker — no extra transient field, and it survives across sessions (unlike a transient flag, which resets to `true` if you reconstruct a detached object by hand). ## Ordering nuance to be ready for The trick depends on the save's is-new check reading `createdDate` **before** it's stamped. In both Spring Data JPA and JDBC the insert-vs-update decision is taken up front while `createdDate` is null, and the stamping happens as part of the same save — so the verdict and the stamp are consistent. Enabling auditing is required: `@EnableJpaAuditing` (JPA) or `@EnableJdbcAuditing` (JDBC), plus `AuditingEntityListener` wiring for JPA. ## Pitfalls - **Transient flag + manual reconstruction**: if you `new` up an object and set its id to represent an existing row (e.g. building a reference), the flag is `true`, so a save would try to INSERT. The created-date approach is more robust here because state lives in a persisted column. - **Forgetting to enable auditing** makes `@CreatedDate` always null -> every save looks new -> duplicate-insert attempts. - **getId() must be honest** — returning the assigned id is fine; Spring Data still relies on `isNew()` for the branch. ## When to prefer which - Prefer **nullable @Version** if you also want optimistic locking and minimal code. - Prefer **@CreatedDate isNew** when you already audit and want a persisted, reconstruction-safe signal. - Use the **transient flag** for entities without auditing or versioning, accepting the reconstruction caveat. ## Key terms - **@Transient** — marks a field as non-persistent. - **@PostLoad / @PostPersist** — JPA lifecycle callbacks firing after load and after insert. - **IsNewAwareAuditingHandler** — Spring Data component that uses isNew() to decide created vs modified auditing fields.

  • Isn't 'isNew() returns createdDate == null' circular, since auditing sets createdDate based on isNew()?
    No. The auditing handler calls isNew() first, while createdDate is still null, so it (a) picks the INSERT path and (b) then stamps createdDate. After that the field is non-null, so future isNew() calls return false. The read happens before the write, so it's consistent, not circular.
  • What breaks if you use a transient boolean flag and later build a detached entity by hand to represent an existing row?
    The transient flag resets to its default (true), so isNew() wrongly reports new and save() attempts an INSERT (duplicate key) or, in JDBC, the wrong branch. A persisted signal like @CreatedDate or @Version avoids this because the state lives in a column, not a memory-only field.

saying these in an interview costs you the question

  • Claiming the createdDate==null trick is circular and can't work.
  • Forgetting that Persistable.isNew() overrides both the id and @Version defaults.
  • Using @CreatedDate for is-new without enabling auditing (field stays null -> every save looks new).
  • Assuming a transient boolean flag is safe when entities are reconstructed manually across transactions.

context