skip to content

Why does Spring Data JDBC have no lazy loading and no dirty tracking, and how does that change how you write code compared to JPA?

level: seniorimportance: must knowfreq 55%

answer

  1. no persistence context / no proxies
  2. eager whole-aggregate load
  3. no auto-flush — must call save()
  4. objects always detached / immutable-friendly
  5. small aggregates + ref others by id

basics

~20 s

Spring Data JDBC loads the whole aggregate immediately (no proxies) and returns plain objects with no persistence context tracking them. Changing a field does nothing until you explicitly call save(); there is no automatic flush.

solid answer

~40 s

Unlike JPA/Hibernate, Spring Data JDBC has no persistence context, no proxies, and no session. Consequences: (1) No lazy loading — loading an aggregate root eagerly fetches the entire aggregate, so what you hold is a complete, detached object graph; there's no LazyInitializationException and no N+1 from uninitialized proxies, but you can't defer loading part of an aggregate (so keep aggregates small and reference other aggregates by id). (2) No dirty tracking — mutating a loaded object has zero effect on the database; nothing is auto-flushed at transaction commit. You must explicitly call repository.save(aggregate) to persist changes. This makes behavior explicit and predictable at the cost of the automatic change detection JPA gives you. Objects are effectively always 'detached', which also makes them simple, immutable-friendly value objects.

code

java · 8 lines
java
@Transactional
void renameCustomer(Long id, String newName) {
    Customer c = repo.findById(id).orElseThrow(); // whole aggregate, eager, detached
    c.setName(newName);
    // JPA: commit would auto-flush this change.
    // Spring Data JDBC: nothing is tracked — without the next line the change is LOST.
    repo.save(c);                                  // the ONLY thing that persists it
}

go deeper

for a junior

Know that you must call save() and that the whole aggregate loads at once.

for a middle

Explain no proxies/no persistence context and the eager-load consequence.

for a senior

Contrast crisply with JPA (dirty tracking, lazy proxies, auto-flush) and derive the design rules (small aggregates, ref by id, immutability).

for a principal

Weigh the explicit/predictable model against JPA's ergonomics and pick per bounded context; connect no-dirty-tracking to the full child-rewrite-on-save behavior.

**The JPA model (for contrast).** JPA/Hibernate keeps an `EntityManager`/**persistence context** (a first-level cache + identity map). Loaded entities are *managed*: Hibernate wraps associations in **lazy proxies** (loaded on first access), performs **dirty tracking** (compares snapshots), and **auto-flushes** detected changes to SQL at query time or transaction commit — often without an explicit `save`. Powerful, but with famous surprises: `LazyInitializationException` when a proxy is touched after the session closes, N+1 selects, and 'why did this UPDATE happen?' from silent dirty checking. **Spring Data JDBC deliberately drops all of that.** It has **no persistence context, no identity map, no proxies, no session**. Entities you get back are ordinary objects, effectively always **detached**. **No lazy loading.** When you `findById` an aggregate root, Spring Data JDBC issues the SQL to load the root **and all its owned children eagerly** — the returned object graph is complete. Benefits: no `LazyInitializationException`, no accidental N+1 from uninitialized proxies, and you can pass the object anywhere (across threads, out of the transaction) safely. Cost: you cannot lazily defer part of an aggregate; loading a root always pays for its whole child graph. This is *why* the aggregate must stay small and why references to other aggregates are modeled as **ids/`AggregateReference`** rather than object links — otherwise one load could pull in a huge graph. **No dirty tracking.** There is no snapshot comparison and **no auto-flush**. Mutating fields of a loaded entity does *nothing* to the database. Persistence happens **only** when you call `repository.save(aggregate)` (or `delete`, etc.). This is the single biggest behavioral difference for developers: the classic JPA pattern of 'load, mutate, let commit flush it' does **not** work — you must explicitly save. **Knock-on effects.** - **Immutability-friendly.** Because there are no proxies or managed state, entities can be immutable (`record`/Kotlin `data class` with `val`), and 'wither' updates (`copy`) are natural. Spring uses constructors and can work without setters. - **Explicit and predictable.** Every INSERT/UPDATE/DELETE traces back to an explicit repository call — easier to reason about and to test. - **No first-level cache.** Loading the same row twice returns two distinct object instances; there's no identity guarantee within a transaction. - **On update, children are fully rewritten** (deleted and re-inserted) precisely *because* there is no dirty tracking to know which child changed. **Gotchas.** (1) Forgetting to `save` after mutation — the change silently vanishes. (2) Expecting `LazyInitializationException`-style deferral — there is none; design aggregates to be load-cheap. (3) Assuming object identity/caching from JPA. (4) Large aggregates are expensive to load and save. **When to use.** Choose Spring Data JDBC when you want explicit, predictable SQL, a clean DDD aggregate model, and simple/immutable domain objects. Prefer JPA when you genuinely need lazy graphs, automatic change detection, or rich ORM features (inheritance, complex fetch strategies).

  • A teammate loads an entity, sets a field, and expects the change to persist at commit like in JPA. What happens and why?
    Nothing persists. Spring Data JDBC has no dirty tracking or auto-flush, so the mutation is silently discarded unless repository.save() is called explicitly.
  • Given there's no lazy loading, how do you avoid loading a giant object graph?
    Keep aggregates small and model links to other aggregates as id values (or AggregateReference) instead of object references, so each root loads only its own owned children.
  • Does Spring Data JDBC guarantee object identity for the same row within a transaction?
    No. There is no first-level cache/identity map, so loading the same row twice yields two distinct instances.

saying these in an interview costs you the question

  • Expecting changes to auto-flush at commit without save()
  • Fearing LazyInitializationException (it can't happen here)
  • Assuming a first-level cache / identity map like JPA
  • Thinking associations are lazy proxies

context