Lessons available in both languages
Java Backend · Interview Prep

Spring Data JPA & Hibernate interview questions & answers

203+ real Spring Data JPA & Hibernate interview questions with model answers, plus free lessons to learn the concepts. Prepare in English & Hinglish, then practise with an AI mock interview.

18 topics · 203+ questions

How Hirenix teaches

One chapter. 90 minutes.
Interview-ready.

Every concept starts with a real-world problem — the kind that actually shows up in production code. Nothing to cram; it just clicks. Every question comes with a model answer: exactly what to say in the room, and why. Then an AI mock interview on the same chapter.

  • 📖Concept in 5 minutesNo jargon — straight to the point
  • 🛠️Real-world problemThe kind production code throws at you
  • 💬Model answerExactly what to say in the room
  • 🧠FlashcardsRevise in 10 minutes
  • 🤖AI mock interviewIt asks follow-ups too
  • 📊Weak topicsSee exactly where you're stuck
Start this chapter — free🌐 English🇮🇳 Hinglish
A student learning an interview concept on Hirenix at home
Video playlistbuilt around a syllabus18h+
Hirenix chapterbuilt around interviews90 min

The difference isn’t the content — it’s the filter. Only what’s actually used in production and actually asked in interviews. Textbook topics the industry never touches don’t make the cut.

Lessons available in both languages

What you’ll learn

  • Why JPA: what ORM solves, and JPA vs Hibernate vs Spring Data
  • Entities and mapping: Entity, Id, GeneratedValue, Column
  • The persistence context and EntityManagerFree account
  • Repositories and derived query methodsFree account
  • JPQL, the Query annotation, and native queriesFree account
  • Relationships and the owning sideFree account
  • Fetch types: lazy vs eager, and LazyInitializationExceptionFree account
  • The N+1 problem, and how to measure itFree account
  • Transactions, dirty checking, and the self-invocation trapFree account
  • Entity states: save, persist and mergeFree account
  • Cascading and orphan removalFree account
  • Pagination and sorting: Pageable, Page vs SliceFree account
  • Schema management: ddl-auto and migrationsFree account
  • Entity equality and the common pitfallsFree account
  • RecapFree account
  • Project: Job Board API
  • Project: Find and Fix an N+1Free account
  • Project: Audited RepositoryFree account

Why JPA: what ORM solves, and JPA vs Hibernate vs Spring Data

Three names get used as if they were one thing, and an interviewer will find out within a minute whether you can separate them.

JPA is a specification. It defines annotations like @Entity and interfaces like EntityManager, and it implements nothing.

Hibernate is an implementation of that specification - the code that actually turns your objects into SQL. It is the default in Spring Boot, and it has features beyond the spec.

Spring Data JPA is a layer on top of both. It is the reason you write an interface with no implementation and get a working repository. It does not replace JPA or Hibernate; it removes the boilerplate of calling them.

The demo shows that stack in one line of output: repo bean class = $Proxy96. You declared JobRepo as an interface and wrote no class - Spring Data generated a proxy at runtime, and underneath it Hibernate ran the SQL against H2.

What ORM actually solves

Java has objects with references; a relational database has rows with foreign keys. Moving between the two by hand means writing the same shape of code over and over - read a ResultSet, pull columns by name, build an object, and reverse all of it to save. It is mechanical, and every project writes it slightly differently.

An ORM does that mapping from metadata instead. @Entity, @Id and the field names describe the shape once, and the framework generates the rest. The demo saves two rows and reads one back with a method called findByTitle - no SQL is written anywhere in the application.

When to use it: reach for JPA when your application works with a domain model - entities with relationships, business rules, and a lifecycle - and the database is where that model is stored. That is most business applications, which is why it is the default in the Spring world and why interviews assume it.

When NOT to use it: the honest answer is that ORM hides the SQL, and hidden SQL is where the problems live. A reporting query that aggregates across five tables is clearer, and usually faster, written as SQL - JdbcTemplate or a native query is the right tool there, not a fight with JPQL. Bulk operations are the other case: loading a million rows into a persistence context to update a column is the wrong shape, and a single UPDATE statement is the right one. The rule of thumb is that JPA is for a domain model you manipulate, not for set-based work over large amounts of data.

That trade-off is the whole reason this chapter measures things. Almost every JPA performance problem you will meet - the N+1 later in this chapter is the classic - comes from not knowing what SQL your code caused.

Two costs worth naming before you meet them

The generated SQL is invisible. You never wrote a query, so you cannot read one - and the statement that actually reached the database may be nothing like what the Java implies. That is why performance problems here hide inside correct-looking code, and why the first skill this chapter teaches is switching the generated SQL back on and counting it.

Portability is real but partial. Writing against JPA rather than Hibernate directly means the mapping and JPQL carry across implementations, and switching databases is mostly a dialect change. But Hibernate has useful features the specification does not define, and the moment you use one - a Hibernate-specific annotation, a vendor function in a query - you have traded that portability away. The honest position is to prefer the JPA API by default and reach for a vendor feature deliberately, knowing what it costs, rather than drifting into it.

// ---- smoke/Job.java ----
package smoke;

import jakarta.persistence.*;

@Entity
@Table(name = "job")
public class Job {
  @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  private String title;

  protected Job() { }                    // JPA ke liye zaroori
  public Job(String title) { this.title = title; }
  public Long getId() { return id; }
  public String getTitle() { return title; }
}

// ---- smoke/JobRepo.java ----
package smoke;

import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;

// DHYAN: repository interface TOP-LEVEL honi chahiye. Nested interface ko
// Spring Data ka scanner nahi uthata - ye is env me chala kar pakda gaya.
public interface JobRepo extends JpaRepository<Job, Long> {
  List<Job> findByTitle(String title);
}

// ---- smoke/Smoke.java ----
package smoke;

@SpringBootApplication
public class Smoke {
  public static void main(String[] args) {
    System.setProperty("spring.datasource.url", "jdbc:h2:mem:smoke;DB_CLOSE_DELAY=-1");
    System.setProperty("spring.jpa.hibernate.ddl-auto", "create-drop");
    try (ConfigurableApplicationContext ctx = SpringApplication.run(Smoke.class, args)) {
      JobRepo repo = ctx.getBean(JobRepo.class);
      repo.save(new Job("Java Developer"));
      repo.save(new Job("Backend Engineer"));
      List<Job> found = repo.findByTitle("Java Developer");
      System.out.println("  repo bean class = " + repo.getClass().getSimpleName());
      System.out.println("  saved rows      = " + repo.count());
      System.out.println("  derived query   = " + found.size() + " -> " + found.get(0).getTitle()
          + " (id=" + found.get(0).getId() + ")");
    }
  }
}

Entities and mapping: Entity, Id, GeneratedValue, Column

An entity is an ordinary Java class plus metadata that says how it maps to a row. The interviewer's version of this topic is not "name the annotations" - it is "which of these rules does the framework actually enforce, and when".

The rules that are genuinely mandatory

Three things: the class carries @Entity, it has an @Id, and it has a no-arg constructor. The class must also not be final, and the persistent fields must not be final either, because Hibernate needs to subclass it and populate instances reflectively.

The no-arg rule is the one worth running, because the timing surprises people. In the demo, NoArgJob has only a (String) constructor. The application started fine. save() worked. The failure came on the first read: org.hibernate.InstantiationException: No default constructor for entity. That is the whole lesson - Hibernate needs the constructor when it has to create an object from a row, so writing succeeds and reading blows up. A test that only writes will pass.

The constructor does not have to be public. protected Job() {} is the usual choice: JPA can reach it, and application code cannot accidentally build a half-empty entity.

What the defaults do when you say nothing

The demo asked H2 for its own information_schema rather than trusting the Java, and the answer is the real schema:

  • @Table(name = "job_posting") renamed the table; without it the table would be job.
  • @Column(name = "job_title", nullable = false, length = 120) produced JOB_TITLE ... nullable=NO.
  • postedCity with no @Column became POSTED_CITY - the default naming strategy converts camelCase to snake_case.
  • @Embedded Salary did not create a second table. Its fields landed in the same row as MIN_LPA and MAX_LPA.
  • @Transient displayLabel has no column at all. Setting it, saving, clearing the context and reloading gave back null.

nullable = false is not decoration either. Saving a Job with a null title threw DataIntegrityViolationException, and the root cause named the field: PropertyValueException: not-null property references a null or transient value: mapdemo.Job.title. Note that Hibernate caught this itself, before the database did.

@GeneratedValue - the strategy question

IDENTITY leans on the database's auto-increment column: the id is only known after the INSERT runs, which is why Hibernate cannot batch inserts under it. SEQUENCE uses a database sequence object, so ids can be fetched in advance and inserts can be batched - it is the better default on PostgreSQL and Oracle. AUTO lets the provider pick, and TABLE emulates a sequence with an extra table and is effectively obsolete.

The demo used both. The output shows IDENTITY ids = 1, 2 and SEQUENCE ids = 1, 2, and the schema listing shows sequences in schema = [COMPANY_SEQ] - a real sequence object exists for the SEQUENCE entity and none for the IDENTITY one. That sequence object is the thing the strategy is named after.

When to use it: map with annotations whenever the class is genuinely part of your domain and the database is where it lives. Be explicit about the things you care about - table name, nullability, lengths, enum storage - and let the naming strategy handle the rest rather than annotating every field by hand.

When NOT to use it: do not make an entity out of a read-only shape you only need for a screen or a report. A projection or a DTO is cheaper and does not drag a persistence context along with it. And @Enumerated(EnumType.ORDINAL) - the default when you forget STRING - is a trap worth refusing outright: it stores the position of the constant, so reordering the enum silently rewrites the meaning of existing rows.

One H2 detail to be honest about: the output shows the status column typed as ENUM, because H2 has a native enum type that Hibernate 6 used here. On PostgreSQL or MySQL you would normally see a varchar. The value read back was the String OPEN either way - that part is what the annotation guarantees.

Should entities carry business logic?

This is the "anemic domain model" argument, and it is a fair interview question rather than a settled one. The anemic position is that entities are data holders and behaviour belongs in services; the domain-model position is that a class with only getters and setters is not modelling anything.

The practical middle is to keep logic that is about the entity's own state on the entity - a method that closes a job posting and stamps the time - and keep logic that coordinates several entities, calls other services or opens transactions in the service layer. The reason to care is not purity: an entity is a managed object inside a persistence context, so anything you do to it can become an UPDATE without a save call, which the next topic measures.

// ---- mapdemo/Job.java ----
@Entity
@Table(name = "job_posting")
public class Job {

  public enum Status { OPEN, CLOSED }

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @Column(name = "job_title", nullable = false, length = 120)
  private String title;

  private String postedCity;          // koi @Column nahi

  @Enumerated(EnumType.STRING)
  private Status status;

  @Embedded
  private Salary salary;              // minLpa, maxLpa

  @Transient
  private String displayLabel;        // kabhi store nahi hoga

  protected Job() {}                  // JPA ko yahi chahiye
  public Job(String title, String postedCity, Status status, Salary salary) { ... }
}

// ---- mapdemo/Company.java : doosri strategy ----
@Entity
public class Company {
  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "company_seq")
  @SequenceGenerator(name = "company_seq", sequenceName = "company_seq", allocationSize = 50)
  private Long id;
  private String name;
}

// ---- noargdemo/NoArgJob.java : jaan-boojh kar galat ----
@Entity
public class NoArgJob {
  @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
  private String title;
  public NoArgJob(String title) { this.title = title; }   // sirf yahi constructor
}

Project: Job Board API

This is the chapter's entry project. It is deliberately small - two entities, one repository, one service - because the point is not the job board. The point is that almost every decision from this chapter shows up in a persistence layer this simple, and an interviewer can ask about any of them from one screen of code.

What it does

A company posts jobs. Jobs can be searched by city with paging and sorting, listed as a feed, and closed. That is all.

What it exercises, and where each piece came from

Mapping decisions, not defaults. @Table(name = "job_posting") and @Column(name = "job_title", nullable = false, length = 140) are written out rather than left to the naming strategy, because the schema is a contract. The status is @Enumerated(EnumType.STRING) - never ORDINAL. That is topic 2.

A deliberate fetch type. @ManyToOne(fetch = FetchType.LAZY) on Job.company overrides the EAGER default. Without it, every job list quietly loads a company per row. That is topic 7, applied rather than recited.

A proxy where a proxy is enough. postJob needs a company only to set the foreign key, so it uses getReferenceById - no SELECT for a row nobody reads. That is topic 10.

Dirty checking instead of save(). closeJob loads the job, calls j.close(), and returns. No repository call. The output confirms the row came back CLOSED, and the search total dropped from 9 to 8 because the closed job left the results. That is topic 9.

Behaviour on the entity. close() lives on Job, not in the service, because changing a job's own status is the job's business. Coordination stays in the service. That is the anemic-model line from topic 2, drawn in a concrete place.

Page where totals matter, Slice where they do not. The search returns Page and cost 2 queries while reporting totalElements; the feed returns Slice and cost 1. Same data, different screen, different price. That is topic 12.

The transaction boundary on the service. Every public method carries @Transactional, read-only where it only reads. The repository carries nothing. That is topic 9 again, and it is the thing most candidates get wrong out loud.

When to build it this way: when the persistence layer is the interesting part - which is most CRUD-shaped backend work. This shape scales: entities own their state, the service names the operation and owns the transaction, and the repository stays a set of queries.

When NOT to: do not reach for this when the work is genuinely set-based. A monthly report that aggregates a million rows should not become entities at all; a projection or plain SQL is the right tool, and this project deliberately does not pretend otherwise.

What is honestly missing

There is no HTTP layer here. This chapter's environment runs with spring.main.web-application-type=none, and the REST half - @RestController, ResponseEntity, validation, ProblemDetail - was already built and measured in Ch8. Putting a controller on top of this service is mechanical once both halves exist, and the persistence half is the part this chapter is responsible for. The service methods are the seam where that controller would attach.

// ---- proj16/Job.java ----
@Entity
@Table(name = "job_posting")
public class Job {
  @Column(name = "job_title", nullable = false, length = 140) private String title;
  @Enumerated(EnumType.STRING) private Status status = Status.OPEN;

  @ManyToOne(fetch = FetchType.LAZY)          // topic 7 ka faisla
  @JoinColumn(name = "company_id") private Company company;

  public void close() { this.status = Status.CLOSED; }   // entity apni state ki maalik
}

// ---- proj16/JobRepo.java ----
Page<Job>  findByStatusAndCityIgnoreCase(Job.Status status, String city, Pageable pageable);
Slice<Job> findByStatus(Job.Status status, Pageable pageable);

// ---- proj16/JobService.java : transaction ki seema YAHAN ----
@Transactional
public Long postJob(Long companyId, String title, String city, int salary) {
  Company c = companies.getReferenceById(companyId);     // FK ke liye proxy kaafi
  Job j = new Job(title, city, salary);
  j.setCompany(c);
  return jobs.save(j).getId();
}

@Transactional
public void closeJob(Long jobId) {
  Job j = jobs.findById(jobId).orElseThrow();
  j.close();                                              // koi save() nahi
}

Spring Data JPA & Hibernateinterview questions & answers

10 sample questions below — 203+ in the full bank inside.

What are the default fetch types in JPA?

To-one associations - @ManyToOne and @OneToOne - default to EAGER, and to-many associations - @OneToMany and @ManyToMany - default to LAZY. The demo proved it without touching the fields, by asking PersistenceUnitUtil.isLoaded: true for @ManyToOne, false for @OneToMany, and the object returned for the to-one was a real entity rather than a proxy. That asymmetry is why loading a list of jobs quietly loads a company per row.

In simple terms: Many candidates get this backwards, which is a strong negative signal. Stating the consequence for a list query shows you have actually felt the default.

Named or positional parameters in @Query, and why?

Named. :title with @Param("title") survives someone reordering the method arguments; ?1 positional binding silently binds the wrong value when that happens, and nothing fails - you just get wrong results. Named parameters also read better in a long query. Either way the binding itself is what keeps SQL injection out, which is why you never build a query by concatenating strings.

In simple terms: The injection point is a bonus but the reordering scenario is the real argument, because it describes a silent failure rather than a crash.

Why is the no-arg constructor usually protected rather than public?

Because JPA only needs to reach it, not everyone else. A protected constructor is visible to Hibernate through reflection and to subclasses, while application code is pushed towards the real constructor that sets the required fields. That keeps half-initialised entities out of the codebase, which matters because an entity with null required fields will fail at flush time rather than at the line that created it.

In simple terms: Small question, but the answer reveals whether the candidate thinks about constructors as an invariant boundary or as ceremony JPA demands.

Name the four entity states and how you would prove which one an object is in.

Transient, managed, detached and removed. The demo proved each with em.contains() rather than describing them: a new object gave contains = false with id = null; after persist, contains = true and id = 1; after detach, contains = false again although the object still held its id and data; and after remove plus flush the row count went to 0. The distinction matters because only a managed entity is dirty-checked.

In simple terms: The proof-by-contains framing is better than the textbook list, and the last sentence is what makes the states worth knowing at all.

What does JPA require of an entity class?

The class must carry @Entity, have an @Id, and have a no-arg constructor, and it must not be final - nor may its persistent fields be - because Hibernate subclasses it and populates instances reflectively. The constructor may be protected, which is the usual choice: JPA can reach it and application code cannot accidentally build a half-empty entity. Those are the hard requirements; everything else, including @Table and @Column, is optional refinement.

In simple terms: This is a warm-up, but the follow-up about when the rules are enforced is where most candidates come apart, so answer it precisely and expect the second question.

How does JPQL differ from SQL?

JPQL is written against the entity model - entity names and field names - while SQL is written against tables and columns. The chapter's demo makes the difference concrete: the entity is Job with a field title, but the table is job_posting and the column is job_title. The JPQL query says select j from Job j where j.title = :title and works; the native query for the same rows has to say select * from job_posting where job_title = :title. Hibernate translates JPQL into SQL for whichever dialect is configured.

In simple terms: Anyone can say "JPQL uses entities". Naming a case where the entity and table names genuinely differ is what proves you have seen the translation happen.

What does CascadeType control, and what are its values?

It names the operations that travel from parent to child: PERSIST, MERGE, REMOVE, REFRESH, DETACH, and ALL as shorthand for all of them. Nothing propagates unless you ask - the demo measured both sides: with cascade = CascadeType.ALL, saving only the parent wrote 2 job rows, and with no cascade at all the same flow wrote 0 emp rows. The parent was saved and the children were silently not.

In simple terms: The zero-rows measurement is the useful half, because 'nothing propagates by default' is easy to say and easy to forget when debugging.

If you write no @Column, what column name do you get?

The default naming strategy converts camelCase to snake_case, so a field postedCity becomes posted_city - the demo confirmed it by reading H2's own information_schema rather than trusting the Java. @Column lets you override the name and also set nullable, length, unique and updatable. It is worth writing the ones you care about explicitly, because the column name is part of your schema and should not change silently when someone renames a Java field.

In simple terms: The technical answer is easy; the reason to write it explicitly is the part that shows judgment. Mentioning that you verified it against information_schema is a small credibility win.

Is @Table required? When would you write it?

Not required - without it the table name defaults to the entity name, adjusted by the naming strategy. You write it when the table name must differ from the class name, which happens more often than people expect: a legacy schema you do not control, a name that collides with a reserved word, or a plural convention the DBA insists on. Writing it also pins the name so a class rename cannot become a schema change.

In simple terms: The last sentence is the real answer. Interviewers are checking whether you treat the schema as a contract or as output from your Java code.

What is the N+1 problem?

One query to fetch a list, then one more query per item when a lazy association is touched. The measured demo had 3 companies with 2 jobs each: findAll() followed by touching each company's getJobs() cost 4 SQL queries - 1 plus 3. In the chapter's project the same shape with 10 companies cost 11. The reason it survives code review is that the loop causing it contains no query at all; it is an ordinary for over a collection.

In simple terms: Everyone can define N+1. The detail that makes an answer credible is naming the measured numbers and pointing out that the offending line does not look like database access.

193+ more Spring Data JPA & Hibernate questions inside

Create a free account to read the full question bank, learn every topic, and practise with an AI mock interview.

Unlock all questions — free

Ready to practise Spring Data JPA & Hibernate?

Unlock every topic free, then face an AI interviewer that asks follow-ups and grades your answers.