Spring Boot JPA & Hibernate: Entity Mapping & Loading
Learn JPA and Hibernate entity mapping, relationship annotations, fetch strategies, and how to avoid N+1 queries and LazyInitializationException in Spring Boot.
Learn JPA and Hibernate entity mapping, relationship annotations, fetch strategies, and how to avoid N+1 queries and LazyInitializationException in Spring Boot. The guide uses practical examples to explain when to use jpa and hibernate, jpa entity lifecycle and relationship mapping and shows how to apply the ideas in a Spring Boot project.
Spring Boot JPA and Hibernate: Entity Mapping & Loading
JPA and Hibernate sit at the heart of data access in most Spring Boot applications. JPA (Java Persistence API) is the specification; Hibernate is the implementation most teams actually use. Together they let you describe your data model in plain Java, push most SQL generation to the framework, and wire everything into Spring’s transaction and dependency injection system.
Sounds great. It is great — until it is not. Relationship loading strategies, dirty checking, automatic schema generation: the same features that make JPA convenient are the ones that cause production incidents. N+1 queries, LazyInitializationException, and entity graphs that pull half the database into memory are all problems I have seen repeatedly in codebases that started clean.
This post skips the overview and focuses on what you need to know to use this stuff correctly: mapping entities without fighting the framework, picking the right fetch strategy for each access pattern, and debugging when Hibernate does something unexpected.
When to Use JPA and Hibernate
Introduction
Spring Boot’s JPA integration uses Hibernate to map Java objects to relational data and manage persistence through an entity manager. This guide explains entity mappings, persistence contexts, fetching, cascading, transactions, and the query and performance pitfalls that appear when ORM behavior is misunderstood.
JPA Entity Lifecycle and Relationship Mapping
Entities in JPA move through a defined lifecycle. A new object is transient — it exists only in memory and has no database identity. Call persist() on it and it becomes managed, tracked by the persistence context. Flush the context and the changes hit the database. Close the session or clear the context and the entity becomes detached — Hibernate stops tracking it, but its data remains. Call remove() on a managed entity and it is scheduled for deletion on the next flush. For a deeper look at how transactions interact with this lifecycle, see Spring Boot DAO Pattern and Transactions.
The diagram below shows the states and transitions:
graph LR
A[Transient] -->|persist| B[Managed]
B -->|flush| C[Database]
B -->|clear session| D[Detached]
D -->|merge| B
D -->|remove| E[Removed]
C -->|find| B
E -->|flush| F[Removed from DB]
B -->|delete| E
Basic Entity Mapping
A JPA entity is a POJO annotated with @Entity. The @Table annotation is optional but recommended when your table name does not match the class name.
@Entity
@Table(name = "authors")
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name", nullable = false, length = 255)
private String name;
@Column(name = "bio", columnDefinition = "TEXT")
private String bio;
// Getters and setters
}
The @GeneratedValue annotation controls how primary keys are generated. IDENTITY delegates to the database’s auto-increment column. SEQUENCE uses a named sequence (more performant for batch inserts). TABLE uses a hibernate_sequence table and works across databases but adds synchronization overhead. UUID generates a UUID before persisting.
Relationship Annotations
Relationships in JPA are directional. You declare them using @OneToMany, @ManyToOne, @OneToOne, and @ManyToMany on the owning side of the relationship.
@Entity
@Table(name = "posts")
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@Column(columnDefinition = "TEXT")
private String content;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id")
private Author author;
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Comment> comments = new ArrayList<>();
// Getters and setters
}
The mappedBy attribute tells JPA which field owns the relationship. The owning side — in this case Comment.post — is where the foreign key lives. The non-owning side (Post.comments) does not generate a column.
@Entity
@Table(name = "comments")
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, columnDefinition = "TEXT")
private String body;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id", nullable = false)
private Post post;
// Getters and setters
}
Failure Scenarios
The N+1 Query Problem
The N+1 problem is the most common performance issue with JPA. It happens when you load a list of entities and each one triggers a separate query to fetch a lazily-loaded relationship.
// Executes 1 query to fetch all posts
List<Post> posts = postRepository.findAll();
// Executes N additional queries — one per post to fetch its author
for (Post post : posts) {
System.out.println(post.getAuthor().getName());
}
If you have 100 posts, this executes 101 queries instead of 2. The fix is to use JOIN FETCH in your JPQL or to define an entity graph.
@Query("SELECT p FROM Post p JOIN FETCH p.author")
List<Post> findAllWithAuthors();
LazyInitializationException
This exception occurs when you try to access a lazily-loaded relationship outside of an active Hibernate session.
@Transactional(readOnly = true)
public List<Post> getAllPosts() {
return postRepository.findAll();
}
public void callingMethod() {
List<Post> posts = getAllPosts();
// The session is closed here
posts.get(0).getAuthor().getName(); // LazyInitializationException
}
The session is bound to the thread for the duration of the transaction. Once the method returns and the transaction ends, the session closes. Any subsequent access to lazy associations fails.
Fixes include keeping access within the transaction, using FetchType.EAGER (with caution), initializing relationships with JOIN FETCH, or calling Hibernate.initialize() within the transaction boundary.
Dirty Checking Gone Wrong
Hibernate tracks changes to managed entities automatically. On flush, it compares the in-memory state with the database and writes only what changed. This is useful, but it can catch you off guard.
Author author = authorRepository.findById(1L).orElseThrow();
author.setName("New Name");
// No explicit save call — Hibernate writes this on flush
If you load an entity and modify it inside a transaction, you do not need to call save(). Hibernate figures it out. The danger is that entities loaded in one transaction and modified in another can lead to unexpected overwrites if the second transaction commits first.
Lazy vs Eager vs Batch Loading
Fetch strategy choice is one of the decisions that bites teams later. Each option has a distinct performance profile:
| Strategy | Description | Use When | Watch Out For |
|---|---|---|---|
FetchType.LAZY |
Relationship loaded on first access | Default for @OneToMany and @ManyToMany |
N+1 queries if not fetched explicitly |
FetchType.EAGER |
Relationship loaded immediately with owner | Rarely needed — use sparingly | Unbounded data loading, cartesian product with multiple collections |
@BatchSize |
Batches lazy loads into fixed-size queries | Collection loading patterns | Still executes multiple queries, just fewer |
JOIN FETCH (JPQL) |
Fetches relationship in single query | One-off queries that need related data | Can cause memory pressure if data is large |
Entity graphs (@NamedEntityGraph) |
Defines a subgraph to fetch declaratively | API responses, read-heavy operations | Adds complexity, must be maintained with schema changes |
Hibernate’s own documentation recommends LAZY as the default for most associations. Eager fetching is almost never the right answer at the mapping level because it hides the real cost of operations and makes performance hard to reason about.
// Using entity graphs to control what gets fetched
@Entity
@NamedEntityGraph(
name = "Post.withAuthorAndComments",
attributeNodes = {
@NamedAttributeNode("author"),
@NamedAttributeNode("comments")
}
)
public class Post { }
// In the repository
@EntityGraph(value = "Post.withAuthorAndComments", type = EntityGraph.EntityGraphType.LOAD)
Optional<Post> findById(Long id);
Trade-off Analysis
Understanding the trade-offs between different JPA approaches helps you make informed decisions for your specific use case.
Fetch Strategy Trade-offs
| Approach | Performance | Complexity | Best For |
|---|---|---|---|
FetchType.LAZY + JOIN FETCH |
Single query, minimal data | Requires explicit query design | Known access patterns, API responses |
FetchType.LAZY + Entity Graph |
Single query, defined subgraph | Declarative, maintained with schema | Reusable fetch plans, multiple operations |
FetchType.EAGER |
Simple usage | Hidden query cost, cartesian products | Truly optional data only |
@BatchSize |
Fewer queries (N/batch), still multiple | Low | Large collections, predictable batch patterns |
Entity State Management Trade-offs
| Operation | When Used | Trade-off |
|---|---|---|
persist() |
New entities, generates ID immediately | Cascade can cause unexpected inserts |
merge() |
Reattaching detached entities | Copies state, can trigger unnecessary updates |
remove() |
Deleting managed entities | Cascade delete can remove unrelated data |
flush() |
Controlling write timing | Manual flush can cause deadlocks if misused |
Projection vs Entity Trade-offs
| Approach | Memory | Flexibility | Use Case |
|---|---|---|---|
| Full Entity | High (persistence context) | Complete object graph | Write operations, complex business logic |
| DTO Projection | Low (new object) | Limited to projection interface | Read-only API responses, reports |
| Dynamic Projection | Medium | Flexible per-query | Ad-hoc reporting needs |
Schema Generation Strategy Trade-offs
| Strategy | Development | Production | Risk |
|---|---|---|---|
none |
Manual migrations required | Safe, predictable | Schema drift if migrations missed |
validate |
Slow feedback | Safe | Delays deployment if drift exists |
update |
Convenient | Risky | Can corrupt data in production |
create |
Very convenient | Dangerous | Destroys existing data |
create-drop |
Testing only | Dangerous | Never use in production |
Implementation Snippets
Repositories and Custom Queries
Spring Data JPA builds repository interfaces automatically from method names. For more complex operations, use @Query.
@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
List<Post> findByTitleContainingIgnoreCase(String keyword);
@Query("SELECT p FROM Post p WHERE p.author.id = :authorId ORDER BY p.id DESC")
List<Post> findByAuthorId(@Param("authorId") Long authorId);
@Query(value = "SELECT * FROM posts WHERE published = true LIMIT :limit OFFSET :offset",
nativeQuery = true)
List<Post> findPublishedPosts(@Param("limit") int limit, @Param("offset") int offset);
}
Controlling Transactions
Spring Data JPA methods are transactional by default for read operations. For write operations that need explicit control, use @Transactional.
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED)
public void savePost(Post post) {
postRepository.save(post);
// Transaction commits on method exit
}
Projection Interfaces
If you only need a subset of entity fields, use a projection to avoid loading unnecessary data.
public interface PostSummary {
String getTitle();
String getAuthorName();
}
public interface PostRepository extends JpaRepository<Post, Long> {
List<PostSummary> findProjectedBy();
}
Converter for Custom Types
For enums stored as strings in the database:
public enum PostStatus {
DRAFT, PUBLISHED, ARCHIVED
}
@Converter
public class PostStatusConverter implements AttributeConverter<PostStatus, String> {
@Override
public String convertToDatabaseColumn(PostStatus status) {
return status == null ? null : status.name();
}
@Override
public PostStatus convertToEntityAttribute(String dbData) {
return dbData == null ? null : PostStatus.valueOf(dbData);
}
}
Observability Checklist
Monitoring JPA and Hibernate in production requires looking at both the ORM layer and the database itself.
Enable SQL logging in development:
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true
Log slow queries:
spring.jpa.properties.hibernate.jdbc.time_zone=UTC
// In application properties or via JMX
// Set hibernate.generate_statistics=true
// and log org.hibernate.stat category
Key metrics to track:
- EntityManagerFactory metrics via Micrometer — connection pool usage, transaction count
- Query execution time via
hibernate.statistics - Second-level cache hit ratio if using L2 cache
- Flush mode and dirty entity count at flush time
- Open session in view anti-pattern occurrence
Structured logging for audits:
@Aspect
@Component
public class EntityAccessAudit {
@Around("execution(* javax.persistence.EntityManager.persist(..))")
public Object auditPersist(ProceedingJoinPoint pjp) throws Throwable {
Object entity = pjp.getArgs()[0];
log.info("Persisting entity: {} with id annotation", entity.getClass().getSimpleName());
return pjp.proceed();
}
}
Use Spring Boot Actuator with Micrometer to expose JPA metrics to Prometheus or your observability stack. For more detail on setting up observability with Spring Boot, see Spring Boot Micrometer Observability.
Security Notes
Entity-level security is often overlooked but matters more as applications grow.
Avoid exposing entities directly in APIs. Entities carry more data than the client needs and can leak internal structure. Use DTOs (Data Transfer Objects) to shape what the API returns.
// Instead of returning Post entity directly
@GetMapping("/posts/{id}")
public PostDto getPost(@PathVariable Long id) {
Post post = postRepository.findById(id).orElseThrow();
return new PostDto(post.getTitle(), post.getAuthor().getName(), post.getContent());
}
Be cautious with @Transactional and entity modification. When a controller calls a service method that modifies an entity, the transaction boundary determines what gets persisted. If you expose repository references from controllers, you risk bypassing business logic and validation.
Prevent SQL injection through JPQL parameters. Always use parameterized queries with @Query.
// Safe — parameter is bound, not concatenated
@Query("SELECT p FROM Post p WHERE p.title LIKE %:keyword%")
// Unsafe — never do this
// @Query("SELECT p FROM Post p WHERE p.title LIKE '%" + keyword + "%'")
Mass assignment vulnerabilities. When binding request parameters to entities, use explicit setters or a binding framework that lets you control which fields are assignable. Spring’s @ModelAttribute with entity classes can allow callers to set fields they should not touch.
Schema validation. Set spring.jpa.hibernate.ddl-auto=validate in production to verify your entity mappings match the actual database schema. Never use update or create in production. For testing strategies that cover entity mapping and persistence concerns, see Spring Boot Test Stack.
Security and Compliance Notes
JPA applications handle sensitive data and require controls at both the application and database layers.
Entity Exposure in API Responses
Returning JPA entities directly from controllers exposes internal database structure and may leak sensitive fields. Use DTOs (Data Transfer Objects) to control exactly what data travels across the API boundary. Map only the fields the client needs. This also prevents over-fetching and reduces payload size.
Mass Assignment Vulnerabilities
Spring’s data binding can set entity fields that should not be mutable from the API. If you bind request parameters directly to an entity, callers can set internal IDs, version fields, or other restricted properties. Use explicit setter calls or a binding framework that lets you control which fields are assignable.
SQL Injection Through JPQL String Concatenation
JPQL @Query annotations with string concatenation introduce SQL injection vulnerabilities the same way raw SQL does. Always use parameterized queries: @Query(“SELECT p FROM Post p WHERE p.title LIKE %:keyword%”) with @Param(“keyword”). Native SQL queries (nativeQuery = true) bypass JPQL validation entirely and require extra care with user input.
Schema Validation in Production
Set spring.jpa.hibernate.ddl-auto=validate in production to verify entity mappings against the actual database schema. This catches drift between your entity definitions and the database before it causes runtime errors. Never use update or create in production — they modify the schema and can destroy data.
Audit Trail Requirements
For applications with compliance requirements, implement entity change auditing at the service layer using JPA @PrePersist, @PreUpdate, and @PreRemove callbacks or Hibernate’s Envers for full history tracking. Route audit logs to a secure, immutable store separate from the application database.
Production Failure Scenarios
JPA and Hibernate production issues tend to cluster around a few predictable failure modes. Knowing these patterns helps you recognize them quickly when they surface.
N+1 Queries Causing Database Timeout
The N+1 problem triggers a separate database query for each entity in a collection. With a lazy @OneToMany mapped on a list of 1000 entities, a single request fires 1001 queries. Under load, this overwhelms the connection pool and causes timeouts. The fix is JOIN FETCH in JPQL or entity graphs to load associated data in a single query. Monitor slow query logs and alert when any single request triggers more than a threshold number of queries.
LazyInitializationException in REST Controllers
When a JPA lazy collection is accessed outside an active Hibernate session, you get LazyInitializationException. This commonly happens when entities are returned from a controller and the view tries to render lazy collections after the session closes. The fix is to either keep access within the transaction boundary or use FetchType.EAGER (carefully) or JOIN FETCH to eagerly load what the API needs. The Open Session in View anti-pattern is a common cause — disable it in production.
EntityVersion Conflicts Under Concurrent Load
Optimistic locking through @Version detects concurrent modifications, but under high write concurrency the retry rate becomes significant. If your application frequently sees OptimisticLockException, you have two choices: reduce concurrency through application-level queuing, or switch to pessimistic locking through SELECT FOR UPDATE. Profile your write patterns to understand whether optimistic locking retry overhead exceeds pessimistic locking’s lock hold time.
Heap Pressure from Large Persistence Contexts
The persistence context holds all managed entities in memory. Loading large result sets with findAll() keeps thousands of entities in the persistence context, increasing heap usage and garbage collection pressure. Use pagination (Pageable) and clear() the persistence context periodically when processing large batches. Consider DTO projections for read-only API responses to avoid loading entity state you do not need.
Common Pitfalls / Anti-Patterns
The open session in view anti-pattern keeps the Hibernate session open across the entire view rendering phase. It enables lazy loading outside the transaction but leaks connections under load. Use @Transactional(readOnly = true) on the controller or service method instead, or fetch everything you need before returning.
Ignoring the persistence context size. Every managed entity lives in the persistence context. Loading thousands of entities in a single query keeps them all in memory. Use pagination with Pageable, clear the context periodically with EntityManager.clear(), or use DTO projections for large result sets.
Bidirectional relationships without equals() and hashCode() cause problems in HashSet and HashMap operations. Hibernate uses these for deduplication inside the persistence context. Implement them using a business key or a stable natural key — never the generated ID before persistence.
Cascading delete without understanding the impact. cascade = CascadeType.ALL propagates every operation including removal. If you set orphanRemoval = true on a @OneToMany, removing a child from the collection deletes it from the database on flush. This can be surprising and dangerous without explicit safeguards.
Using EAGER by default. Hibernate initializes EAGER associations immediately using a cartesian product when multiple collections are involved. The SQL generated can be enormous and slow. Stick with LAZY and use JOIN FETCH or entity graphs when you actually need the data.
Quick Recap Checklist
Before shipping any JPA code, run through these points:
- All lazily-loaded relationships accessed outside a transaction cause
LazyInitializationException— verify every access path - Run
EXPLAINon queries generated by JPQL or method naming conventions — do not assume they are efficient -
@ManyToOnedefaults toEAGERin JPA — override explicitly toFetchType.LAZY - Pagination should be your default for list queries, not
findAll() - DTO projections for read-only API responses avoid loading unnecessary columns and relationships
- Bidirectional entities need consistent
equals()andhashCode()using a stable key -
ddl-auto=validatein production catches mapping drift before it causes runtime errors -
JOIN FETCHin JPQL is the most direct way to fix N+1 on specific queries - Entity graphs provide a declarative way to define fetch plans per operation
- Audit log entity changes at the service layer, not the repository layer, for traceability
Interview Questions
FetchType.LAZY and FetchType.EAGER in JPA?FetchType.LAZY postpones loading the related entity or collection until you first access it in code. This avoids fetching data you do not need but can trigger additional queries at unexpected moments, leading to N+1 problems. FetchType.EAGER loads the associated data immediately when the owner entity is fetched, which simplifies usage but can cause large cartesian products when multiple eager collections are involved. The Hibernate documentation recommends lazy loading as the default for almost all associations, using explicit JOIN FETCH or entity graphs when related data is actually required.
The N+1 problem occurs when loading a collection of entities and each one triggers a separate query for a lazy association. Hibernate addresses this in several ways. The most direct is using JOIN FETCH in a JPQL query to load the relationship in the same SQL statement. Entity graphs let you define which associations to fetch declaratively per operation. The @BatchSize annotation batches lazy loads by issuing queries that fetch multiple entities at once, reducing N queries to N/batch-size queries. Statistics mode (hibernate.generate_statistics) helps identify where N+1 is occurring so you can target the right queries.
LazyInitializationException and how do you prevent it?LazyInitializationException is thrown when you try to access a lazily-loaded relationship after the Hibernate session has closed. This happens when entity access occurs outside the transaction boundary — for example, in a view layer after the service method has returned. The fix is to ensure all required data is fetched within the active transaction. Options include using JOIN FETCH in the original query, calling Hibernate.initialize() on the lazy association before the session closes, using a Contextual Session via sessionFactory.getCurrentSession() which is bound to the Spring transaction, or restructuring the code so lazy access never occurs outside a transaction boundary.
mappedBy and @JoinColumn in JPA relationships?@JoinColumn defines the physical database column that holds the foreign key and marks the owning side of the relationship. The owning side is responsible for the actual foreign key management in the database. mappedBy is used on the non-owning side of a bidirectional relationship and tells JPA which field owns the foreign key. The non-owning side does not generate a column in the database. Changes made to the non-owning side are not persisted unless you manipulate the owning side, because JPA only looks at the owning side when writing the foreign key to the database.
Unidirectional relationships are simpler — one side knows about the other but not vice versa. Use them when the access pattern is naturally one-way, like a Comment knowing its parent Post when you always query comments by post. Bidirectional relationships make sense when you need to navigate from both sides, for example loading a Post and then accessing its comments without a separate query. The cost of bidirectional is maintaining consistency on both sides in your domain logic and understanding that the owning side (@JoinColumn) is what Hibernate uses for persistence. If you only need to query in one direction, unidirectional is usually the better choice.
persist(), merge(), remove(), and flush() in JPA.persist() makes a transient entity managed and schedules an insert for the next flush. It generates the ID immediately for identity strategy. merge() takes a detached entity and copies its state into a managed entity, returning the managed instance. If the entity does not exist in the persistence context, it queries the database first. remove() marks a managed entity for deletion and schedules a delete operation at flush time. flush() forces the persistence context to synchronize with the database immediately, executing all pending SQL statements rather than waiting for the transaction commit or auto-flush.
Dirty checking is Hibernate's automatic mechanism for detecting changes to managed entities and propagating them to the database at flush time. When you load an entity into the persistence context, Hibernate snapshot-copies its state. On flush, Hibernate compares the current state against the snapshot and generates UPDATE statements only for the changed fields. This means you can modify an entity's fields without calling save — the changes are detected and written automatically. The downside is memory overhead from snapshots and unexpected writes if entities are modified in long-running transactions or when they should not be.
Optimistic locking relies on a version field (@Version) that Hibernate increments on each update. When you try to update a row, Hibernate checks if the version matches — if another transaction modified it first, it throws OptimisticLockException. The application then retries or handles the conflict. This works well for read-heavy workloads with rare collisions. Pessimistic locking uses SELECT FOR UPDATE or equivalent database locks to block other transactions from reading or modifying the same row until the lock is released. This prevents conflicts entirely but reduces concurrency and can cause lock contention under high write load. Use pessimistic locking when collisions are frequent and immediate consistency matters.
The first-level cache is the persistence context itself — it is scoped to a single Hibernate Session and is always enabled. It caches entities within a single transaction, so multiple calls to find() for the same entity within one session return the same instance and hit the database once. The second-level cache is optional and scoped to the EntityManagerFactory, meaning it is shared across all sessions and threads. It caches entities and queries to reduce database hits in read-heavy scenarios. L2 cache requires explicit configuration with a cache provider like EhCache and works best for entities that are read frequently but modified rarely. Without L2 cache, every session is independent and no entity state is shared between them.
@PrePersist, @PreUpdate, and @PreRemove?JPA lifecycle callbacks let you hook into entity state transitions to execute custom logic. @PrePersist runs before Hibernate issues the INSERT statement — useful for setting creation timestamps, generating UUIDs, or validating invariants. @PreUpdate runs before the UPDATE statement — useful for updating modification timestamps or deriving computed fields. @PreRemove runs before DELETE — useful for cleanup operations. These callbacks are on the entity itself or on a separate listener class. They are synchronous and execute within the same transaction as the operation. Use them for cross-cutting concerns that belong to the entity rather than the service layer, but avoid heavy operations or database calls from callbacks since they run during flush and can affect transaction timing.
Further Reading
- Spring Data JPA Documentation — Official docs for Spring Data JPA repository abstractions and query methods
- Hibernate ORM User Guide — Comprehensive guide to Hibernate internals, including persistence context behavior and bytecode enhancement
- Java Persistence API Specification — The formal specification that Hibernate implements
- High-Performance Java Persistence — Vlad Mihalcea’s in-depth book on squeezing performance out of JPA and Hibernate
- Baeldung JPA/Hibernate Guides — Practical tutorials covering common patterns and troubleshooting
Conclusion
JPA and Hibernate reward teams that invest time in understanding how they actually work. The persistence context, fetch strategies, and the lifecycle of a managed entity are not optional reading — they are the difference between code that scales and code that causes 3 AM incidents.
Four things prevent most of the problems: make LAZY your default fetch strategy, use JOIN FETCH or entity graphs to pull in what you need per query, keep entity access inside transaction boundaries, and never expose raw entities to your API layer.
Category
Related Posts
Spring Boot Build Tools: Maven & Gradle
Configure Maven and Gradle for Spring Boot projects—plugins, dependency management, packaging JARs and WARs, and build automation essentials.
Embedded Web Servers in Spring Boot: Tomcat, Jetty, Undertow
Configure embedded servers in Spring Boot: compare Tomcat, Jetty, and Undertow, tune thread pools, enable access logs, and switch implementations.
JUnit 5 & Jupiter: Lifecycle, Nested & Parameterized Tests
Explore JUnit 5 Jupiter features: master test lifecycle annotations, organize tests with @Nested, and parameterize tests with @CsvSource and @MethodSource.