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()andhashCode(). - 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
@Versionfor 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.
EntityManagermanages 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()andclear(). - Optimistic locking using
@Versionprotects 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.