Hibernate Interview Questions

Commonly asked Hibernate interview questions with clear, practical answers.

A curated set of Hibernate interview questions, covering caching, fetching strategies, and entity lifecycle — the topics that come up most in real interviews.

Q: What's the difference between the first-level and second-level cache?

The first-level cache is the persistence context itself (the Session/EntityManager) — it's mandatory, always enabled, and scoped to a single unit of work, so requesting the same entity by ID twice in one session returns the same object without a second query. The second-level cache is optional, requires a separate cache provider (Ehcache, Caffeine), and is shared across sessions — letting one session reuse data another session already loaded. First-level caching is free and automatic for every entity; second-level caching is an opt-in decision best reserved for read-heavy, rarely-changing data, since keeping it correctly invalidated has real cost for frequently updated entities.

Q: What's the difference between lazy and eager fetching?

Lazy fetching (FetchType.LAZY) defers loading an association until it's actually accessed, issuing a separate query at that point. Eager fetching (FetchType.EAGER) loads the association immediately, as part of the original query. The JPA spec defaults @ManyToOne/@OneToOne to EAGER and @OneToMany/@ManyToMany to LAZY, but in practice almost every relationship is set to LAZY explicitly — eager loading by default tends to pull back far more data than a given use case needs, and can chain unpredictably across a deep entity graph.

Q: What's the difference between get() and load() (or find() returning null vs. a proxy)?

session.get(Entity.class, id) hits the database immediately and returns null if no row exists. session.load(Entity.class, id) returns a lazy proxy immediately without querying the database at all — the actual query only fires the first time a real field on that proxy is accessed, and if no row exists, it throws ObjectNotFoundException at that access point rather than returning null up front. load() is useful when you only need the ID to set up a relationship (no need to actually fetch the row's data), while get() is the right choice whenever you genuinely need the entity's data and want a straightforward null check for "does this exist."

Q: What are the entity states in Hibernate, and what triggers the transition between them?

Transient — a plain Java object, never associated with a persistence context, no corresponding database row. Persistent — tracked by an active persistence context (after persist() or find()); Hibernate automatically detects field changes on a persistent entity and flushes them as SQL without an explicit save call. Detached — was persistent, but its originating session has since closed; the object still holds data, but changes to it are no longer tracked, and it must be reattached with merge() before further changes will be saved.

Q: What is "dirty checking," and why does Hibernate not need an explicit update() call?

Dirty checking is Hibernate comparing a persistent entity's current field values against the values it originally loaded, at flush/commit time, and automatically generating an UPDATE for whatever changed — because the entity is being tracked by the persistence context, simply calling a setter is enough; there's no need to call save() or update() again for a change to an already-persistent entity.

Q: Why is @Enumerated(EnumType.ORDINAL) considered risky, and what should you use instead?

ORDINAL stores an enum's numeric position in its declaration (0, 1, 2, ...) rather than its name. If a new constant is later inserted anywhere but the very end of the enum, or the order is otherwise changed, every already-stored row's meaning silently shifts without any error — a data corruption bug that's easy to introduce and hard to notice. @Enumerated(EnumType.STRING) stores the constant's actual name instead, which is stable regardless of how the enum's declaration order changes later.

Q: What is the N+1 query problem, and how do you actually go about fixing it?

It happens when loading N parent rows and then accessing a lazily-loaded association on each one individually, which issues N additional queries — one per parent — instead of one combined query, for N+1 total round trips. It's invisible by reading application code and only shows up in the actual SQL log, which is why enabling spring.jpa.show-sql/SQL logging during development is the first step to noticing it at all. The fix is fetching the association up front in one query, using JPQL's JOIN FETCH or Spring Data's @EntityGraph, rather than defaulting associations to FetchType.EAGER, which just moves the same over-fetching to every query instead of the one that actually needs it.

Q: What's the difference between a plain JOIN and a JOIN FETCH in HQL/JPQL?

A plain JOIN only affects the query's filtering/join logic — it does not populate the joined association on the returned entities, so accessing it afterward still triggers a separate lazy-loading query. JOIN FETCH additionally tells Hibernate to actually load and attach the associated data as part of the same single query, so no further query is issued when the association is accessed afterward. Using a plain JOIN when JOIN FETCH was actually needed is a common, easy-to-miss cause of N+1 that looks like it should have already been fixed.

Q: What's the practical difference between the READ_ONLY, READ_WRITE, and NONSTRICT_READ_WRITE second-level cache concurrency strategies?

READ_ONLY caches an entity and never expects it to change — an update attempt against a read-only-cached entity throws, so it's only appropriate for genuinely immutable reference data. READ_WRITE is the common general-purpose choice: it supports updates and uses a locking mechanism to keep the cache consistent with concurrent writes, at some added overhead. NONSTRICT_READ_WRITE updates the cache without strict locking, accepting a brief window where a read could return slightly stale data, in exchange for lower overhead — appropriate only when that brief staleness is genuinely an acceptable trade-off for the entity in question.