Skip to content

JPA Entities Interview Questions & Answers

15 questions Updated 2026-06-26 Share:

Mapping JPA entities in Spring Boot — @Entity, @Id and generation strategies, @Column, @Table, embeddables, enums, the entity lifecycle, equals/hashCode, and how Hibernate tracks managed objects.

Read the in-depth guideMapping JPA Entities in Spring Boot, Explained(opens in new tab)
15 of 15

A JPA entity is a plain Java class mapped to a database table, where each instance corresponds to a row. You mark it with @Entity, give it an identity field with @Id, and the persistence provider (Hibernate in Spring Boot) handles the SQL.

@Entity                       // managed by the persistence context
class Customer {
    @Id @GeneratedValue
    Long id;                  // maps to the primary key column
    String name;              // maps to a "name" column by convention

    protected Customer() {}   // JPA requires a no-arg constructor
}

An entity must have a no-arg constructor (any visibility), must not be final, and needs an identity field. Hibernate proxies and instantiates entities reflectively, which is why those rules exist.

Rule of thumb: @Entity + @Id + a no-arg constructor turns a POJO into a table-backed object Hibernate can manage.

@GeneratedValue tells JPA how the primary key is produced. There are four strategies:

@Id @GeneratedValue(strategy = GenerationType.IDENTITY)  // DB auto-increment column
Long id;
  • IDENTITY — relies on an auto-increment column. Simple, but disables JDBC batch inserts because Hibernate needs the key back immediately after each insert.
  • SEQUENCE — uses a database sequence; Hibernate can pre-fetch a block of ids, so it batches well. Preferred on PostgreSQL/Oracle.
  • TABLE — emulates a sequence with a table. Portable but slow; avoid.
  • AUTO (default) — lets the provider pick based on the dialect.

Rule of thumb: Prefer SEQUENCE (with an allocation size) on databases that support it; use IDENTITY only when you must, knowing it costs batch inserts.

@Column customizes the column mapping for a field. Without it, Hibernate infers the column name and sensible defaults, so you only add it to override something.

@Column(name = "email_address",   // column name differs from the field
        nullable = false,          // adds NOT NULL to the generated DDL
        unique = true,             // adds a unique constraint
        length = 320)              // VARCHAR(320) instead of the default 255
String email;

Note nullable/unique/length only affect schema generation (ddl-auto). They are not runtime validation — for that use Bean Validation (@NotNull, @Size). If you manage schema with Flyway/Liquibase, these attributes are documentation, not enforcement.

Rule of thumb: Add @Column only to override the default name, nullability, length, or precision — and remember it shapes DDL, not runtime checks.

Use @Table to set the table name, schema, or table-level constraints when the default (the entity's simple class name) isn't what you want.

@Entity
@Table(name = "app_users",                 // override the default "Customer" → "app_users"
       schema = "sales",
       uniqueConstraints = @UniqueConstraint(columnNames = {"tenant_id", "email"}),
       indexes = @Index(name = "idx_email", columnList = "email"))
class User { ... }

@Table is optional. The composite @UniqueConstraint is the way to express a multi-column unique rule (you can't do that with @Column(unique=true)). Naming strategies can also map CamelCase fields to snake_case columns automatically.

Rule of thumb: Use @Table for a non-default table/schema name or for composite unique constraints and indexes.

JPA reads and writes entity state either through fields (direct reflection) or properties (getters/setters). The placement of @Id decides which Hibernate uses for the whole entity.

@Entity
class Order {
    @Id Long id;        // annotation on the FIELD → field access for all mappings
    String status;
}

@Entity
class Invoice {
    private Long id;
    @Id Long getId() { return id; }   // annotation on the GETTER → property access
}

Field access is the common default: Hibernate sets values directly, bypassing your getters, so accessor logic never fires unexpectedly during loading. Property access runs your getters/setters, useful for derived or transformed values. Mixing both needs @Access.

Rule of thumb: Put annotations on fields (field access) unless you specifically need getter/setter logic to run during persistence.

Mark it @Transient. JPA then ignores the field entirely — no column, no read, no write.

@Entity
class Employee {
    LocalDate birthDate;

    @Transient                          // computed, never stored
    int getAge() {
        return Period.between(birthDate, LocalDate.now()).getYears();
    }
}

Use it for derived values, caches, or helper state that lives only in memory. Don't confuse JPA's javax.persistence.Transient/jakarta.persistence.Transient with Java's transient keyword — the keyword affects Java serialization, the annotation affects JPA mapping (though Hibernate also honors the keyword).

Rule of thumb: Use @Transient for any field you compute at runtime and never want persisted.

They model a value object whose fields live in the same table as the owning entity — grouping related columns into a reusable class without a separate table or id.

@Embeddable
class Address {
    String street;
    String city;
    String zip;
}

@Entity
class Customer {
    @Id Long id;
    @Embedded
    Address address;       // street/city/zip become columns on the customer table
}

The embeddable has no identity of its own — it's part of the entity. Use @AttributeOverride to remap column names when you embed the same type twice (e.g. billingAddress and shippingAddress). It's the JPA way to keep cohesive columns together cleanly.

Rule of thumb: Use @Embeddable/@Embedded for cohesive groups of columns that have no independent identity, like an address or a money amount.

Annotate the field with @Enumerated and choose between ORDINAL (stores the position) and STRING (stores the name). The default is ORDINAL, which is dangerous.

enum Status { NEW, PAID, SHIPPED }

@Enumerated(EnumType.STRING)   // stores "PAID" — stable and readable
Status status;

// @Enumerated(EnumType.ORDINAL) would store 0/1/2 — reordering the enum corrupts data!

With ORDINAL, inserting a new constant in the middle (or reordering) silently shifts every stored value to a different meaning. STRING is self-describing and reorder-safe; the only cost is a few bytes per row.

Rule of thumb: Almost always use @Enumerated(EnumType.STRING) — never let enum ordering become a hidden data dependency.

An entity instance is always in one of four states relative to the persistence context:

Customer c = new Customer();   // TRANSIENT — new object, no id, not tracked
em.persist(c);                 // MANAGED — tracked; changes auto-flush to DB
em.detach(c);                  // DETACHED — has an id but no longer tracked
em.remove(managed);            // REMOVED — scheduled for DELETE on flush
  • Transient — a plain new object Hibernate knows nothing about.
  • Managed (persistent) — attached to the persistence context; Hibernate dirty-checks it and writes changes automatically at flush time.
  • Detached — was managed, but the context closed or it was evicted; edits are not tracked until you merge it back.
  • Removed — marked for deletion.

The key consequence: on a managed entity you don't call save() after changing a field — the change is flushed automatically within the transaction.

Rule of thumb: Know that managed entities are auto-flushed via dirty checking; detached ones need merge to reattach.

Dirty checking is how Hibernate detects modifications to managed entities and issues UPDATEs automatically — you change a field, you don't call any save method.

@Transactional
void raise(Long id) {
    Order o = repo.findById(id).orElseThrow();  // o is now MANAGED
    o.setStatus(SHIPPED);                        // just mutate it
    // no repo.save(o) needed — Hibernate flushes the UPDATE at commit
}

At flush, Hibernate compares each managed entity against the snapshot it took at load time; changed properties generate an UPDATE. This is why a missing @Transactional (no open context) means your changes silently never persist, and why huge persistence contexts are slow — every managed entity is compared on flush.

Rule of thumb: Inside a transaction, mutating a loaded entity is enough; Hibernate's dirty checking writes the UPDATE for you.

Carefully — the generated id is null before persist, so naive id-based or all-field implementations break when entities sit in a HashSet across the persist boundary.

@Entity
class Product {
    @Id @GeneratedValue Long id;

    @Override public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Product p)) return false;
        return id != null && id.equals(p.id);   // equal only when both have the same non-null id
    }
    @Override public int hashCode() {
        return getClass().hashCode();            // constant — stable across id assignment
    }
}

A constant hashCode (or one based on a business/natural key) keeps the object findable in a set even after Hibernate assigns its id. Avoid Lombok's @Data on entities — it generates all-field equals/hashCode that trigger lazy loads and break this contract.

Rule of thumb: Base equals on a stable business key or a null-safe id check, and use a constant hashCode — never auto-generate them from all fields.

It controls whether Hibernate generates or alters the schema from your entities at startup.

# application.properties
spring.jpa.hibernate.ddl-auto=update   # values: none | validate | update | create | create-drop
  • none — do nothing (use a real migration tool).
  • validate — check that the schema matches the entities, fail fast otherwise.
  • update — add missing tables/columns (never drops). Handy in dev, risky in prod.
  • create / create-drop — drop and recreate on startup/shutdown; for tests only.

With an embedded DB Spring Boot defaults to create-drop; with a real datasource it defaults to none. In production you should manage schema with Flyway or Liquibase and set validate.

Rule of thumb: update/create-drop for local dev and tests; validate plus a migration tool in production — never update against a real database.

Modern JPA maps java.time types automatically — no annotation needed. The legacy @Temporal is only for old java.util.Date/Calendar. Large content uses @Lob.

LocalDate orderedOn;          // → DATE, mapped automatically
LocalDateTime createdAt;      // → TIMESTAMP
Instant updatedAt;            // → TIMESTAMP (UTC)

@Lob String description;      // → CLOB / TEXT for large text
@Lob byte[] document;         // → BLOB for binary

@Temporal(TemporalType.DATE)  // only needed for the legacy java.util.Date
Date legacyDate;

Prefer the java.time types over Date/Calendar; they're immutable, unambiguous, and need no @Temporal. Reach for @Lob only when the value is genuinely large.

Rule of thumb: Use java.time types directly (no @Temporal) and @Lob for large text/binary; @Temporal is legacy-only.

@MappedSuperclass defines shared mappings that subclasses inherit, without the superclass being an entity or a table itself. It's the standard home for audit columns.

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
abstract class Auditable {
    @CreatedDate   LocalDateTime createdAt;
    @LastModifiedDate LocalDateTime updatedAt;
}

@Entity
class Article extends Auditable {   // inherits createdAt/updatedAt columns
    @Id @GeneratedValue Long id;
}

Combined with @EnableJpaAuditing on a config class, Spring fills @CreatedDate/ @LastModifiedDate automatically. Unlike @Inheritance, there's no shared table or polymorphic query — each subclass just gets the columns copied into its own table.

Rule of thumb: Use @MappedSuperclass to share columns/mappings (like audit fields) across entities without modeling the parent as its own table.

Open Session in View (OSIV) keeps the Hibernate session/persistence context open for the whole HTTP request — including view rendering — so lazy associations can still be loaded after the service returns. Spring Boot enables it by default.

spring.jpa.open-in-view=true    # default; set false to disable

It's convenient — it prevents LazyInitializationException when serializing lazy fields — but controversial because it holds a database connection for the entire request, hides N+1 queries behind the controller, and blurs the transaction boundary. Lazy loads then happen outside any explicit transaction during serialization.

The disciplined approach: set open-in-view=false and fetch exactly what you need in the service layer (join fetch, entity graphs, or DTO projections).

Rule of thumb: Understand OSIV is on by default; for a clean architecture disable it and load associations explicitly in the service layer.

More ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel