Jakarta Persistence JPA Interview Questions and Answers

Master Jakarta Persistence and JPA with production-ready interview questions covering entities, EntityManager, persistence context, lifecycle states, relationship mappings, lazy loading, JPQL, Criteria API, transactions, dirty checking, locking, and senior-level best practices.


Introduction

Jakarta Persistence is the standard Jakarta EE specification for mapping Java objects to relational database tables.

It was previously known as the Java Persistence API, or JPA.

Jakarta Persistence defines standard APIs and annotations for:

  • Entity mapping
  • Primary keys
  • Relationship mapping
  • Persistence contexts
  • Entity lifecycle management
  • Dirty checking
  • Transactions
  • JPQL
  • Criteria API
  • Optimistic and pessimistic locking
  • Caching
  • Inheritance mapping
  • Lifecycle callbacks

Jakarta Persistence is a specification, not a database or standalone implementation.

Popular implementations include:

  • Hibernate ORM
  • EclipseLink
  • OpenJPA

In modern enterprise applications, JPA commonly works with:

  • Jakarta CDI
  • Jakarta Transactions
  • Jakarta REST
  • Jakarta Bean Validation
  • Spring Data JPA
  • Relational databases

A production-ready JPA design requires more than entity annotations. Developers must also understand:

  • Generated SQL
  • Transaction boundaries
  • Fetch strategies
  • Persistence-context memory
  • N+1 queries
  • Database indexes
  • Concurrency
  • Schema migrations
  • Batch operations

This guide covers the 15 most frequently asked Jakarta Persistence and JPA interview questions with code examples, Mermaid diagrams, production scenarios, common mistakes, and senior-level recommendations.


Q1. What is Jakarta Persistence?

Answer

Jakarta Persistence is the Jakarta EE specification for Object-Relational Mapping and relational database persistence.

It allows developers to represent database data using Java objects called entities.

Example Entity

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "customers")
public class Customer {

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

    @Column(
        nullable = false,
        length = 100
    )
    private String name;

    @Column(
        nullable = false,
        unique = true
    )
    private String email;

    protected Customer() {
    }

    public Customer(
        String name,
        String email
    ) {
        this.name = name;
        this.email = email;
    }
}

Mapping Flow

flowchart TD
    A[Java Entity] --> B[Jakarta Persistence Mapping]
    B --> C[Persistence Provider]
    C --> D[Generated SQL]
    D --> E[Relational Database]

Why Interviewers Ask This

Interviewers want to confirm that you understand Jakarta Persistence as a standard specification rather than one implementation.

Production Example

An insurance platform maps:

  • Customer
  • Policy
  • Claim
  • ClaimLine
  • Payment

to relational database tables.

Common Mistake

Calling JPA a database framework implementation.

Senior Follow-up

Jakarta Persistence defines contracts and behavior, while providers such as Hibernate perform the actual persistence work.


Q2. What is JPA?

Answer

JPA stands for Java Persistence API.

The modern specification is named Jakarta Persistence, but developers still commonly use the term JPA.

JPA defines:

  • Standard annotations
  • EntityManager
  • Persistence context behavior
  • Entity lifecycle
  • Query APIs
  • Transaction integration
  • Locking semantics
  • Mapping rules

Architecture

flowchart TD
    A[Application Code] --> B[JPA APIs]
    B --> C[Persistence Provider]
    C --> D[Hibernate or EclipseLink]
    D --> E[JDBC]
    E --> F[Database]

Standard API Example

Customer customer =
    entityManager.find(
        Customer.class,
        customerId
    );

The application calls the standard EntityManager API.

The configured provider executes the required SQL.

Common Mistake

Saying JPA and Hibernate are exactly the same.

Senior Follow-up

Application code should prefer standard JPA APIs unless provider-specific behavior delivers a clear benefit.


Q3. What is the difference between JPA and Hibernate?

Answer

JPA is a specification.

Hibernate is one implementation of that specification.

Comparison

JPA Hibernate
Persistence specification Persistence implementation
Defines standard APIs Implements the APIs
Uses EntityManager Native API uses Session
Provider-portable Hibernate-specific capabilities
Standard JPQL HQL extensions available
Standard annotations Additional Hibernate annotations

Relationship

flowchart LR
    A[Application] --> B[JPA Specification]
    B --> C[Hibernate Implementation]
    C --> D[JDBC]
    D --> E[Database]

JPA Example

entityManager.persist(customer);

Native Hibernate Example

session.persist(customer);

Provider-Specific Feature Example

@org.hibernate.annotations.BatchSize(
    size = 50
)
private List<Order> orders;

Common Mistake

Using Hibernate-specific APIs everywhere without considering portability.

Senior Follow-up

Provider-specific features are acceptable when documented, tested, and justified by production requirements.


Q4. What is a JPA entity?

Answer

An entity is a persistent Java class mapped to a database table.

Each entity instance usually represents one database row.

Entity Requirements

A typical entity should have:

  • @Entity
  • A primary key
  • A no-argument constructor
  • Persistent fields or properties
  • A stable identity strategy
  • Controlled relationship mappings

Example

@Entity
@Table(name = "products")
public class Product {

    @Id
    @GeneratedValue(
        strategy = GenerationType.SEQUENCE,
        generator = "product_seq"
    )
    @SequenceGenerator(
        name = "product_seq",
        sequenceName = "product_sequence",
        allocationSize = 50
    )
    private Long id;

    @Column(
        nullable = false,
        length = 100
    )
    private String name;

    @Column(
        nullable = false,
        precision = 19,
        scale = 2
    )
    private BigDecimal price;

    protected Product() {
    }
}

Entity Mapping

classDiagram
    class Product {
        -Long id
        -String name
        -BigDecimal price
    }

    class products {
        BIGINT id
        VARCHAR name
        DECIMAL price
    }

    Product --> products : mapped to

Common Mapping Annotations

Annotation Purpose
@Entity Marks persistent class
@Table Defines table mapping
@Id Defines primary key
@GeneratedValue Defines ID generation
@Column Defines column mapping
@Transient Excludes field from persistence
@Enumerated Maps enum
@Version Enables optimistic locking
@Embedded Embeds value object

Common Mistake

Using public setters for every field and allowing invalid entity state.

Senior Follow-up

Entities should protect important invariants while remaining persistence-compatible.


Q5. What is EntityManager?

Answer

EntityManager is the primary JPA interface used to interact with the persistence context.

It provides operations for:

  • Persisting entities
  • Finding entities
  • Removing entities
  • Merging detached state
  • Flushing changes
  • Creating queries
  • Locking entities
  • Refreshing entities
  • Detaching entities

Example

@ApplicationScoped
public class CustomerRepository {

    @PersistenceContext
    private EntityManager entityManager;

    public Customer findById(
        Long customerId
    ) {
        return entityManager.find(
            Customer.class,
            customerId
        );
    }

    public void persist(
        Customer customer
    ) {
        entityManager.persist(customer);
    }
}

EntityManager Responsibilities

flowchart TD
    A[EntityManager] --> B[Persistence Context]
    A --> C[Entity Lifecycle]
    A --> D[Query Creation]
    A --> E[Flush]
    A --> F[Locking]
    B --> G[Managed Entities]
    G --> H[Database Synchronization]

Thread Safety

EntityManager should not be shared manually across concurrent threads.

In Jakarta EE, the container usually injects a context-aware proxy.

Common Mistake

Creating one global EntityManager and sharing it across threads.

Senior Follow-up

The injected proxy may be shared safely, but the underlying persistence context remains scoped according to container rules.


Q6. What is a persistence context?

Answer

A persistence context is the environment in which JPA manages entity instances.

It acts as:

  • An identity map
  • A first-level cache
  • A unit of work
  • A change-tracking context
  • A write-behind queue

Responsibilities

  • Maintain one managed entity instance per type and ID
  • Detect changes
  • Track inserts, updates, and deletes
  • Manage entity lifecycle
  • Coordinate flush behavior
  • Support relationship consistency

Architecture

flowchart TD
    A[Persistence Context] --> B[Managed Entities]
    A --> C[Original State]
    A --> D[Pending Inserts]
    A --> E[Pending Updates]
    A --> F[Pending Deletes]

    B --> G[Dirty Checking]
    C --> G
    G --> H[Flush]
    D --> H
    E --> H
    F --> H
    H --> I[Database]

Identity Guarantee

Customer first =
    entityManager.find(
        Customer.class,
        101L
    );

Customer second =
    entityManager.find(
        Customer.class,
        101L
    );

System.out.println(
    first == second
);

Within one persistence context, this is normally:

true

Common Mistake

Treating the persistence context as only a query cache.

Senior Follow-up

The persistence context manages identity, lifecycle, changes, cascading, and synchronization—not just repeated reads.


Q7. What are the JPA entity lifecycle states?

Answer

The main entity lifecycle states are:

  • Transient
  • Managed
  • Detached
  • Removed

Lifecycle Diagram

stateDiagram-v2
    [*] --> Transient

    Transient --> Managed: persist()
    Managed --> Detached: detach(), clear(), close()
    Detached --> Managed: merge() returns managed copy
    Managed --> Removed: remove()
    Removed --> [*]: flush and commit

Transient

A normal Java object not tracked by JPA.

Customer customer =
    new Customer(
        "Venu",
        "[email protected]"
    );

Managed

Associated with a persistence context.

entityManager.persist(customer);

Detached

Previously managed but no longer tracked.

entityManager.detach(customer);

Removed

Managed and scheduled for deletion.

entityManager.remove(customer);

State Comparison

State Tracked Dirty Checking Automatic SQL
Transient No No No
Managed Yes Yes Yes
Detached No No No
Removed Yes Deletion tracked DELETE

Common Mistake

Assuming changes to detached entities are automatically persisted.

Senior Follow-up

For update APIs, load a managed entity and apply controlled changes instead of blindly merging detached payloads.


Q8. How are relationships mapped in JPA?

Answer

JPA supports:

  • One-to-one
  • One-to-many
  • Many-to-one
  • Many-to-many

Example Domain

classDiagram
    class Customer {
        -Long id
        -List~Order~ orders
    }

    class Order {
        -Long id
        -Customer customer
        -List~OrderItem~ items
    }

    class OrderItem {
        -Long id
        -Order order
        -Product product
    }

    class Product {
        -Long id
    }

    Customer "1" --> "*" Order
    Order "1" *-- "*" OrderItem
    OrderItem "*" --> "1" Product

One-to-Many

@OneToMany(
    mappedBy = "customer",
    cascade = {
        CascadeType.PERSIST,
        CascadeType.MERGE
    },
    orphanRemoval = true
)
private List<Order> orders =
    new ArrayList<>();

Many-to-One

@ManyToOne(
    fetch = FetchType.LAZY,
    optional = false
)
@JoinColumn(
    name = "customer_id",
    nullable = false
)
private Customer customer;

Relationship Ownership

The side containing @JoinColumn usually owns the foreign-key mapping.

The inverse side uses mappedBy.

Helper Method

public void addOrder(
    Order order
) {
    orders.add(order);
    order.assignCustomer(this);
}

Ownership Flow

flowchart LR
    A[Customer.orders] -->|mappedBy customer| B[Inverse Side]
    C[Order.customer] -->|JoinColumn customer_id| D[Owning Side]
    D --> E[Controls Foreign Key]

Common Mistake

Updating only one side of a bidirectional relationship.

Senior Follow-up

Use helper methods to keep both in-memory sides consistent.


Q9. What are LAZY and EAGER loading?

Answer

Fetch type defines when an association should be initialized.

LAZY

Loaded when accessed.

@OneToMany(
    mappedBy = "customer",
    fetch = FetchType.LAZY
)
private List<Order> orders;

EAGER

Must be available immediately.

@ManyToOne(
    fetch = FetchType.EAGER
)
private Department department;

JPA Defaults

Relationship Default
@OneToOne EAGER
@ManyToOne EAGER
@OneToMany LAZY
@ManyToMany LAZY
@ElementCollection LAZY

Loading Flow

flowchart TD
    A[Load Entity] --> B{Fetch Type}
    B -->|LAZY| C[Load association when accessed]
    B -->|EAGER| D[Load association immediately]

Risks

LAZY Risks

  • N+1 queries
  • LazyInitializationException
  • Hidden SQL

EAGER Risks

  • Over-fetching
  • Large joins
  • High memory usage
  • Unpredictable secondary selects

Common Mistake

Making every association EAGER to avoid lazy-loading errors.

Senior Follow-up

Use conservative mappings and query-specific fetch plans through fetch joins, Entity Graphs, batch fetching, or DTO projections.


Q10. What is JPQL?

Answer

JPQL stands for Jakarta Persistence Query Language.

It queries entities and entity attributes rather than database tables and columns.

Entity Query

List<Customer> customers =
    entityManager.createQuery(
        """
        select c
        from Customer c
        where c.active = true
        order by c.name
        """,
        Customer.class
    )
    .getResultList();

JPQL vs SQL

JPQL SQL
Uses entity names Uses table names
Uses Java properties Uses column names
Navigates relationships Uses join conditions
Provider generates SQL Database executes directly

Query Translation

flowchart TD
    A[JPQL Query] --> B[Persistence Provider Parser]
    B --> C[Entity Metadata]
    C --> D[Generated SQL]
    D --> E[Database]
    E --> F[Rows]
    F --> G[Entities or DTOs]

Named Parameter

List<Order> orders =
    entityManager.createQuery(
        """
        select o
        from Order o
        where o.status = :status
        """,
        Order.class
    )
    .setParameter(
        "status",
        OrderStatus.OPEN
    )
    .getResultList();

Common Mistake

Using table and column names in JPQL.

Senior Follow-up

Always inspect generated SQL and database execution plans for important queries.


Q11. What is the Criteria API?

Answer

The Criteria API provides a programmatic way to construct JPA queries.

It is useful when search conditions are dynamic.

Example

CriteriaBuilder builder =
    entityManager.getCriteriaBuilder();

CriteriaQuery<Customer> query =
    builder.createQuery(
        Customer.class
    );

Root<Customer> customer =
    query.from(Customer.class);

List<Predicate> predicates =
    new ArrayList<>();

if (name != null) {
    predicates.add(
        builder.like(
            builder.lower(
                customer.get("name")
            ),
            "%"
                + name.toLowerCase(
                    Locale.ROOT
                )
                + "%"
        )
    );
}

if (active != null) {
    predicates.add(
        builder.equal(
            customer.get("active"),
            active
        )
    );
}

query.select(customer)
    .where(
        predicates.toArray(
            Predicate[]::new
        )
    )
    .orderBy(
        builder.asc(
            customer.get("name")
        )
    );

List<Customer> result =
    entityManager
        .createQuery(query)
        .getResultList();

Criteria Architecture

flowchart TD
    A[CriteriaBuilder] --> B[CriteriaQuery]
    B --> C[Root Entity]
    B --> D[Predicates]
    B --> E[Joins]
    B --> F[Ordering]
    C --> G[Final Query Model]
    D --> G
    E --> G
    F --> G
    G --> H[Generated SQL]

JPQL vs Criteria

JPQL Criteria
Concise Verbose
Good for static queries Good for dynamic queries
String-based Object-based
Easy to read Easy to compose
Limited refactoring safety Better with metamodel

Common Mistake

Using Criteria for every simple fixed query.

Senior Follow-up

Use the simplest query approach that remains readable, safe, testable, and performant.


Q12. What is dirty checking?

Answer

Dirty checking is the JPA provider's ability to detect changes to managed entities automatically.

Example

@Transactional
public void updateCustomerEmail(
    Long customerId,
    String email
) {

    Customer customer =
        entityManager.find(
            Customer.class,
            customerId
        );

    customer.changeEmail(email);
}

No explicit update call is required.

Dirty-Checking Flow

sequenceDiagram
    participant Service
    participant PersistenceContext
    participant Database
    Service->>PersistenceContext: Load Customer
    PersistenceContext->>Database: SELECT Customer
    Database-->>PersistenceContext: Row
    PersistenceContext-->>Service: Managed Customer
    Service->>Service: Change email
    Service->>PersistenceContext: Transaction flush
    PersistenceContext->>PersistenceContext: Compare state
    PersistenceContext->>Database: UPDATE Customer

Benefits

  • Less persistence boilerplate
  • Cleaner business logic
  • Automatic transaction synchronization

Costs

  • Memory for managed entities
  • Snapshot tracking
  • Flush-time comparison
  • Performance impact for huge contexts

Common Mistake

Calling merge() or repository save() unnecessarily for an already managed entity.

Senior Follow-up

Keep persistence contexts small and use DTO projections for read-only operations.


Q13. How are transactions managed in JPA?

Answer

JPA operations normally execute inside a transaction.

In Jakarta EE, transactions are commonly managed using Jakarta Transactions.

Example

@ApplicationScoped
public class TransferService {

    @PersistenceContext
    private EntityManager entityManager;

    @Transactional
    public void transfer(
        Long sourceId,
        Long destinationId,
        BigDecimal amount
    ) {

        Account source =
            entityManager.find(
                Account.class,
                sourceId
            );

        Account destination =
            entityManager.find(
                Account.class,
                destinationId
            );

        source.debit(amount);
        destination.credit(amount);
    }
}

Transaction Flow

flowchart TD
    A[Begin Transaction] --> B[Load Managed Entities]
    B --> C[Apply Business Changes]
    C --> D[Dirty Checking]
    D --> E[Flush SQL]
    E --> F{Success?}
    F -->|Yes| G[Commit]
    F -->|No| H[Rollback]

Best Practices

  • Place transactions in the service layer.
  • Cover complete business operations.
  • Keep transactions short.
  • Avoid remote calls inside database transactions.
  • Use read-only query paths where supported.
  • Understand rollback behavior.
  • Avoid one massive transaction for very large imports.

Common Mistake

Treating flush() as equivalent to commit().

Senior Follow-up

Flush sends SQL within the current transaction; rollback is still possible afterward.


Q14. What are common JPA mistakes?

Answer

Common mistakes include:

  • Exposing entities directly through APIs.
  • Using EAGER fetching everywhere.
  • Ignoring N+1 queries.
  • Keeping large persistence contexts.
  • Using broad cascade operations.
  • Applying REMOVE cascade to shared entities.
  • Blindly merging request payloads.
  • Incorrect equals() and hashCode().
  • Including lazy associations in toString().
  • Ignoring transaction boundaries.
  • Accessing lazy relationships after the context closes.
  • Running unbounded queries.
  • Combining collection fetch joins with pagination.
  • Ignoring generated SQL.
  • Using schema auto-update in production.
  • Missing optimistic locking.
  • Updating only one side of bidirectional relationships.
  • Assuming JPA eliminates SQL knowledge.
  • Using bulk DML without clearing stale entities.
  • Storing entities in HTTP sessions.

Dangerous API

@PUT
@Path("/{id}")
public Customer update(
    @PathParam("id")
    Long id,
    Customer customer
) {
    customer.setId(id);

    return entityManager.merge(customer);
}

Risks

  • Mass assignment
  • Stale-field overwrite
  • Relationship replacement
  • Cascade side effects
  • Persistence model exposure

Safer Flow

flowchart TD
    A[Request DTO] --> B[Validate Request]
    B --> C[Load Managed Entity]
    C --> D[Apply Allowed Fields]
    D --> E[Dirty Checking]
    E --> F[Return Response DTO]

Interview Tip

Strong candidates explain both the feature and its production failure modes.


Q15. What are senior-level JPA best practices?

Answer

Senior developers should design JPA usage around explicit transaction, lifecycle, query, and database boundaries.

Best Practices

  • Use DTOs at API boundaries.
  • Keep transactions in services.
  • Load managed entities for updates.
  • Avoid blind detached graph merges.
  • Keep collections LAZY.
  • Use explicit fetch plans.
  • Detect N+1 queries in tests.
  • Use projections for read models.
  • Paginate large queries.
  • Keep persistence contexts small.
  • Use JDBC batching for large writes.
  • Use bulk DML for uniform mass changes.
  • Clear the context after bulk DML.
  • Use @Version for concurrent updates.
  • Model cascades according to ownership.
  • Keep both sides of relationships synchronized.
  • Use database constraints.
  • Manage schema changes with Flyway or Liquibase.
  • Review generated SQL and execution plans.
  • Test with production-scale data.

Design Decision Flow

flowchart TD
    A[Persistence Use Case] --> B{Read or Write?}

    B -->|Read| C{Need Full Entity?}
    C -->|No| D[DTO Projection]
    C -->|Yes| E[Explicit Entity Fetch Plan]

    B -->|Write| F[Transactional Service]
    F --> G[Load Managed Aggregate]
    G --> H[Validate and Modify]
    H --> I[Dirty Checking]

    D --> J[Review SQL and Indexes]
    E --> J
    I --> J

    J --> K[Integration Test]
    K --> L[Production Monitoring]

Senior Follow-up

A good JPA design remains predictable under real data volume, concurrent updates, failures, and deployment changes.


JPA Application Architecture

flowchart TD
    A[REST Resource] --> B[Request DTO]
    B --> C[Transactional Service]
    C --> D[Repository]
    D --> E[EntityManager]
    E --> F[Persistence Context]
    F --> G[JPA Provider]
    G --> H[JDBC]
    H --> I[Database]

    C --> J[Response Mapper]
    J --> K[Response DTO]
    K --> A

Entity Lifecycle

stateDiagram-v2
    [*] --> Transient
    Transient --> Managed: persist
    Managed --> Detached: detach / clear / close
    Detached --> ManagedCopy: merge
    ManagedCopy --> Managed
    Managed --> Removed: remove
    Removed --> [*]: flush and commit

Persistence Context Lookup

flowchart TD
    A[find Entity by ID] --> B[Check Persistence Context]
    B -->|Found| C[Return Managed Instance]
    B -->|Not Found| D[Check Cache if Configured]
    D -->|Found| E[Create Managed Instance]
    D -->|Not Found| F[Query Database]
    F --> G[Store Managed Entity]
    E --> H[Return Entity]
    G --> H

JPA Relationship Architecture

classDiagram
    class Customer {
        -Long id
        -List~Order~ orders
    }

    class Order {
        -Long id
        -Customer customer
        -List~OrderItem~ items
    }

    class OrderItem {
        -Long id
        -Order order
        -Product product
        -Integer quantity
    }

    class Product {
        -Long id
        -String name
    }

    Customer "1" --> "*" Order
    Order "1" *-- "*" OrderItem
    Product "1" <-- "*" OrderItem

Safe Aggregate Mapping

@Entity
@Table(name = "orders")
public class Order {

    @Id
    @GeneratedValue(
        strategy = GenerationType.SEQUENCE,
        generator = "order_seq"
    )
    @SequenceGenerator(
        name = "order_seq",
        sequenceName = "order_sequence",
        allocationSize = 50
    )
    private Long id;

    @Version
    private Long version;

    @ManyToOne(
        fetch = FetchType.LAZY,
        optional = false
    )
    @JoinColumn(
        name = "customer_id",
        nullable = false
    )
    private Customer customer;

    @OneToMany(
        mappedBy = "order",
        cascade = {
            CascadeType.PERSIST,
            CascadeType.MERGE
        },
        orphanRemoval = true
    )
    private List<OrderItem> items =
        new ArrayList<>();

    protected Order() {
    }

    public void addItem(
        OrderItem item
    ) {
        items.add(item);
        item.assignTo(this);
    }

    public void removeItem(
        OrderItem item
    ) {
        if (items.remove(item)) {
            item.removeFromOrder();
        }
    }
}

DTO Projection

public record OrderSummary(
    Long orderId,
    String customerName,
    OrderStatus status,
    BigDecimal total
) {
}
List<OrderSummary> summaries =
    entityManager.createQuery(
        """
        select new com.codewithvenu.order.OrderSummary(
            o.id,
            o.customer.name,
            o.status,
            o.total
        )
        from Order o
        where o.status = :status
        order by o.createdAt desc, o.id desc
        """,
        OrderSummary.class
    )
    .setParameter(
        "status",
        OrderStatus.COMPLETED
    )
    .setMaxResults(100)
    .getResultList();

Projection Flow

flowchart LR
    A[API Needs Summary] --> B[JPQL Projection]
    B --> C[Select Required Columns]
    C --> D[Create DTO]
    D --> E[No Managed Entity Overhead]

Fetch Join

List<Order> orders =
    entityManager.createQuery(
        """
        select distinct o
        from Order o
        join fetch o.customer
        left join fetch o.items
        where o.id = :orderId
        """,
        Order.class
    )
    .setParameter(
        "orderId",
        orderId
    )
    .getResultList();

Use fetch joins only for required, bounded relationships.


Entity Graph

@Entity
@NamedEntityGraph(
    name = "Order.withCustomerAndItems",
    attributeNodes = {
        @NamedAttributeNode("customer"),
        @NamedAttributeNode("items")
    }
)
public class Order {
}
EntityGraph<?> graph =
    entityManager.getEntityGraph(
        "Order.withCustomerAndItems"
    );

Map<String, Object> hints =
    Map.of(
        "jakarta.persistence.fetchgraph",
        graph
    );

Order order =
    entityManager.find(
        Order.class,
        orderId,
        hints
    );

N+1 Query Problem

sequenceDiagram
    participant Service
    participant JPA
    participant Database
    Service->>JPA: Load 100 Customers
    JPA->>Database: SELECT customers
    Database-->>JPA: 100 rows
    loop Each Customer
        Service->>JPA: Access orders
        JPA->>Database: SELECT orders by customer
        Database-->>JPA: Order rows
    end

Total:

1 parent query + 100 child queries = 101 queries

Common Solutions

  • Fetch join
  • Entity Graph
  • Batch fetching
  • DTO projection
  • Dedicated query
  • Query-count testing

Optimistic Locking

@Version
private Long version;

Concurrent Update Flow

sequenceDiagram
    participant TransactionA
    participant TransactionB
    participant Database
    TransactionA->>Database: Read version 4
    TransactionB->>Database: Read version 4
    TransactionA->>Database: Update where version = 4
    Database-->>TransactionA: Success, version 5
    TransactionB->>Database: Update where version = 4
    Database-->>TransactionB: Zero rows updated
    TransactionB-->>TransactionB: OptimisticLockException

Pessimistic Locking

Account account =
    entityManager.find(
        Account.class,
        accountId,
        LockModeType.PESSIMISTIC_WRITE
    );

Comparison

Optimistic Lock Pessimistic Lock
Uses version field Uses database lock
Better for low contention Useful for high contention
Detects conflicts Prevents competing updates
More scalable Can block
Requires retry or conflict handling Can cause deadlocks

Bulk Update

@Transactional
public int deactivateCustomers(
    Instant cutoff
) {

    entityManager.flush();

    int updated =
        entityManager.createQuery(
            """
            update Customer c
            set c.active = false
            where c.lastLoginAt < :cutoff
            """
        )
        .setParameter(
            "cutoff",
            cutoff
        )
        .executeUpdate();

    entityManager.clear();

    return updated;
}

Bulk DML Risk

flowchart TD
    A[Managed Entity in Context] --> B[Bulk Update Executes Directly]
    B --> C[Database Row Changes]
    A --> D[Managed Entity Still Has Old State]
    C --> E[Persistence Context Stale]
    D --> E
    E --> F[Clear or Refresh Required]

Batch Insert

@Transactional
public void importCustomers(
    List<CustomerImportRow> rows
) {

    int flushSize = 500;

    for (
        int index = 0;
        index < rows.size();
        index++
    ) {

        Customer customer =
            new Customer(
                rows.get(index).name(),
                rows.get(index).email()
            );

        entityManager.persist(customer);

        if (
            (index + 1) % flushSize == 0
        ) {
            entityManager.flush();
            entityManager.clear();
        }
    }

    entityManager.flush();
    entityManager.clear();
}

Configuration

jakarta.persistence.jdbc.url=jdbc:postgresql://localhost:5432/appdb

hibernate.jdbc.batch_size=50
hibernate.order_inserts=true
hibernate.order_updates=true
hibernate.batch_versioned_data=true

Entity Callbacks

@PrePersist
public void beforePersist() {
    this.createdAt =
        Instant.now();
}

@PreUpdate
public void beforeUpdate() {
    this.updatedAt =
        Instant.now();
}

Callback Lifecycle

flowchart LR
    A[New Entity] --> B[PrePersist]
    B --> C[INSERT]
    C --> D[PostPersist]

    E[Managed Entity Modified] --> F[PreUpdate]
    F --> G[UPDATE]
    G --> H[PostUpdate]

    I[Entity Removed] --> J[PreRemove]
    J --> K[DELETE]
    K --> L[PostRemove]

Common Callbacks

  • @PrePersist
  • @PostPersist
  • @PreUpdate
  • @PostUpdate
  • @PreRemove
  • @PostRemove
  • @PostLoad

Avoid complex remote calls or heavy business logic inside entity callbacks.


Inheritance Mapping

JPA supports:

  • Single table
  • Joined
  • Table per class

Single Table

@Entity
@Inheritance(
    strategy = InheritanceType.SINGLE_TABLE
)
@DiscriminatorColumn(
    name = "payment_type"
)
public abstract class Payment {
}

Joined Strategy

@Inheritance(
    strategy = InheritanceType.JOINED
)

Comparison

Strategy Benefit Risk
Single table Fast polymorphic queries Nullable columns
Joined Normalized schema More joins
Table per class Separate tables Complex union queries

Embedded Value Object

@Embeddable
public class Address {

    private String street;
    private String city;
    private String state;
    private String postalCode;
}
@Embedded
private Address billingAddress;

Mapping

flowchart LR
    A[Customer Entity] --> B[Embedded Address]
    B --> C[Customer Table Columns]
    C --> D[street city state postal_code]

Embedded types do not normally have independent entity identity.


Entity vs Embeddable

Entity Embeddable
Has identity No independent identity
Has lifecycle Owned by containing entity
Can be queried independently Usually queried through owner
Maps to table Maps fields into owner table
Can have relationships Intended as value object

Persistence Unit

A persistence unit defines persistence configuration and managed entities.

Example persistence.xml

<persistence
    xmlns="https://jakarta.ee/xml/ns/persistence"
    version="3.0">

    <persistence-unit
        name="applicationPU"
        transaction-type="JTA">

        <jta-data-source>
            java:app/jdbc/ApplicationDS
        </jta-data-source>

        <properties>
            <property
                name="jakarta.persistence.schema-generation.database.action"
                value="none"/>
        </properties>
    </persistence-unit>
</persistence>

Architecture

flowchart TD
    A[Persistence Unit] --> B[DataSource]
    A --> C[Managed Entities]
    A --> D[Provider Configuration]
    A --> E[Transaction Type]
    B --> F[Database]

JTA vs Resource-Local Transactions

JTA Resource Local
Container-managed Application-managed
Can coordinate multiple resources Usually one persistence resource
Common in Jakarta EE Common in standalone apps
Declarative transactions Manual transaction APIs
Integrates with messaging Limited coordination

Resource-Local Example

EntityTransaction transaction =
    entityManager.getTransaction();

try {
    transaction.begin();

    entityManager.persist(customer);

    transaction.commit();
} catch (RuntimeException exception) {

    if (transaction.isActive()) {
        transaction.rollback();
    }

    throw exception;
}

JPA Cache Layers

flowchart TD
    A[Entity Lookup] --> B[First-Level Cache]
    B -->|Miss| C[Second-Level Cache]
    C -->|Miss| D[Database]
    D --> E[Store Entity State]
    E --> F[Return Managed Entity]

First-Level Cache

  • Persistence-context scoped
  • Mandatory
  • Maintains identity

Second-Level Cache

  • Provider scoped
  • Optional
  • Shared across contexts
  • Requires careful consistency strategy

Pagination

List<OrderSummary> content =
    entityManager.createQuery(
        """
        select new com.codewithvenu.order.OrderSummary(
            o.id,
            o.status,
            o.total
        )
        from Order o
        order by o.createdAt desc, o.id desc
        """,
        OrderSummary.class
    )
    .setFirstResult(
        page * size
    )
    .setMaxResults(size)
    .getResultList();

Best Practices

  • Use stable ordering.
  • Set maximum page size.
  • Avoid collection fetch joins.
  • Consider keyset pagination for large tables.
  • Optimize count queries separately.

JPA Testing Strategy

Test:

  • Entity mappings
  • Relationship ownership
  • Cascades
  • Orphan removal
  • Transaction rollback
  • Optimistic locking
  • JPQL queries
  • Criteria queries
  • Pagination
  • Bulk DML
  • N+1 prevention
  • Database constraints
  • Schema migrations

Repository Test Flow

flowchart TD
    A[Arrange Test Data] --> B[Persist and Flush]
    B --> C[Clear Persistence Context]
    C --> D[Execute Query]
    D --> E[Assert Result]
    E --> F[Assert SQL or Query Count]

Clearing the context helps ensure the query actually reads from the database.


Production JPA Architecture

flowchart TD
    A[Client] --> B[Jakarta REST Resource]
    B --> C[Request DTO Validation]
    C --> D[Transactional Service]
    D --> E[Repository]
    E --> F[EntityManager]
    F --> G[Persistence Context]
    G --> H[Persistence Provider]
    H --> I[JDBC]
    I --> J[Connection Pool]
    J --> K[Database]

    E --> L[Projection Query]
    D --> M[Response Mapper]
    L --> M
    M --> N[Response DTO]
    N --> B

JPA Decision Matrix

Use Case Recommended Approach
Create aggregate persist() root with owned cascades
Update aggregate Load managed entity and modify
Read summary DTO projection
Read detail Entity Graph or fetch join
Large list Projection with pagination
Uniform mass update Bulk JPQL
Large import JDBC batching and flush/clear
Concurrent editing Optimistic locking
Critical contention Evaluate pessimistic locking
Schema changes Flyway or Liquibase
API contract Request and response DTOs

Common JPA Anti-Patterns

Entity as Request Model

@POST
public Customer create(
    Customer customer
) {
    entityManager.persist(customer);
    return customer;
}

Problems:

  • Mass assignment
  • Persistence coupling
  • Relationship manipulation
  • Sensitive-field exposure
  • Weak validation

Eager Collection

@OneToMany(
    fetch = FetchType.EAGER
)
private List<Order> orders;

Problems:

  • Over-fetching
  • Large joins
  • Slow pagination
  • High memory use

Blind Cascade

@ManyToOne(
    cascade = CascadeType.ALL
)
private Department department;

Deleting an employee could propagate deletion to a shared department.


Senior JPA Checklist

Stable Entity Identity
        │
        ▼
Clear Relationship Ownership
        │
        ▼
Service Transaction Boundary
        │
        ▼
Use-Case Fetch Plan
        │
        ▼
DTO API Boundary
        │
        ▼
Optimistic Concurrency
        │
        ▼
Efficient SQL and Indexes
        │
        ▼
Migration and Monitoring

Interview Quick Revision

Concept Purpose
Jakarta Persistence Standard ORM specification
JPA Common name for persistence API
Entity Persistent Java object
EntityManager Persistence operations
Persistence Context Entity identity and tracking
Transient New untracked object
Managed Tracked entity
Detached Previously managed object
Removed Scheduled for deletion
JPQL Entity query language
Criteria API Programmatic query construction
Dirty Checking Automatic managed updates
Fetch Type Association loading timing
@Version Optimistic locking
Transaction Atomic business operation

Key Takeaways

  • Jakarta Persistence is the standard Jakarta EE specification for Object-Relational Mapping and relational persistence.
  • JPA remains the commonly used name for Jakarta Persistence.
  • JPA defines APIs and behavior, while Hibernate, EclipseLink, and other providers implement them.
  • Entities map Java objects to relational database rows.
  • EntityManager manages persistence operations, lifecycle, queries, flushing, and locking.
  • A persistence context provides identity management, first-level caching, dirty checking, and transactional write-behind.
  • Entity lifecycle states are transient, managed, detached, and removed.
  • Managed entities are automatically synchronized through dirty checking.
  • Bidirectional relationships require explicit ownership and helper methods to keep both sides consistent.
  • Collections should generally remain LAZY, with query-specific fetch plans used where needed.
  • JPQL queries entities and attributes rather than database tables and columns.
  • The Criteria API is useful for dynamically composed queries.
  • Transactions should wrap complete business operations in the service layer.
  • DTOs should be used at API boundaries instead of exposing entities.
  • Fetch joins, Entity Graphs, batch fetching, and DTO projections help prevent N+1 queries.
  • Bulk DML can improve performance but bypasses normal persistence-context synchronization.
  • Large write workloads require batching and periodic flush() and clear().
  • Optimistic locking using @Version protects against silent lost updates.
  • Database constraints, indexes, migrations, execution plans, and monitoring remain essential.
  • Senior developers design JPA systems for predictable behavior under production data volume, concurrency, failure, and deployment conditions.