Spring Boot JPA + CRUD
 
  1.  What is Spring Data JPA? How does it simplify database access?
  2.  What is the role of JpaRepository interface?
  3.  What is @Entity annotation?
  4.  @Id and @GeneratedValue – explain strategies
  5.  How to create custom query methods in repository?
  6.  @Query annotation – JPQL vs Native SQL
  7.  What is pagination in Spring Data?
  8.  How to use Pageable in controller?
  9.  What is @Transactional? When to use?
  10.  Difference between persist() and save() (JPA vs Spring Data)
  11.  Entity relationships basics (@OneToMany, @ManyToOne)
  12.  How to handle validation in entities/DTOs?
  13.  Global exception handling with @ControllerAdvice
  14.  Common pitfalls of Spring Boot JPA + CRUD
  15.  What are the best practices in Spring Boot JPA + CRUD ? 

What is Spring Data JPA? How does it simplify database access?

=> Spring Data JPA is a Spring Module that simplifies the implementation of JPA repository
=> We can define an Interface extending JpaRepository. From that Spring generates the implementation at runtime using proxies. 
=> It provides CRUD methods automatically, supports query derivation from method names, pagination and custom @Query
=> Compared to plain JPA, it eliminates boilerplate DAO classes, manual EntityManager handling, and repetitive code i.e we can focus on business logic instead of persistence details 
 
=> Example : Traditional (Without Spring Data JPA) vs Spring Data JPA 
 
Traditional (Without Spring Data JPA)
 
@Repository
public class EmployeeDaoImpl implements EmployeeDao {
    @PersistenceContext
    private EntityManager em;

    public List<Employee> findByDepartment(String dept) {
        return em.createQuery("SELECT e FROM Employee e WHERE e.department = :dept", Employee.class)
                 .setParameter("dept", dept)
                 .getResultList();
    }
    // ... save, delete, etc. — many methods
}
 
With Spring Data JPA 
 
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
    List<Employee> findByDepartment(String department);  // Auto-implemented!
}
 
JpaRepository<Employee, Long> means

Employee - The Entity class (first parameter T)
Long - The type of the primary key (second parameter ID)
 
Common Repository Interfaces 
 
=> CrudRepository<T, ID> — basic CRUD
=> PagingAndSortingRepository<T, ID> — adds pagination/sorting
=> JpaRepository<T, ID> — most used (extends above + flush, deleteInBatch, etc.)

Best Practices 
 
=> Use Interfaces only -- No implementation needed 
=> Prefer method name derivation for simple queries 
=> Use @Query for complex queries 
=> Always use DTOs in service/controller, never expose entities directly 
  
What is the role of JpaRepository interface?
 
=> JpaRepository is a Spring Data JPA interface extends CRUDRepository and PagingAndSortingRepository. 
=> It provides ready use to CRUD methods, pagination, sorting and jpa specific methods like flush and deleteInBatch
=> We just define an interface extending JpaRepository. From that Spring generates the implementation at runtime using proxies.
=> Compared to plain JPA, it eliminates boilerplate DAO classes, manual EntityManager handling, and repetitive code i.e we can focus on business logic instead of persistence details 
=> JpaRepository is the most commonly used repository interface in Spring Boot applications  
=> JPA - Java Persistence API, CRUD - Create Read Update Delete 

JpaRepository Hierarchy (The Full Inheritance Chain of JpaRepository)
 
JpaRepository<T, ID>
    extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T>

PagingAndSortingRepository<T, ID>
    extends CrudRepository<T, ID>

CrudRepository<T, ID>
    extends Repository<T, ID>
 
=> So the complete chain is JpaRepository → PagingAndSortingRepository → CrudRepository → Repository 


 Key Methods Provided by JpaRepository (Most Frequently Used)
 
MethodDescriptionExample Use Case
save(S entity)Save or update an entityInsert new employee or update existing
saveAll(Iterable<S> entities)Save multiple entitiesBulk insert
findById(ID id)Return Optional<T> for entity by IDGet employee by ID
existsById(ID id)Check if entity existsValidation before delete
findAll()Return all entitiesList all employees
findAll(Sort sort)Return all with sortingSort by salary descending
findAll(Pageable pageable)Return paginated result (Page<T>)Pagination in UI (page 1, size 10)
count()Return total number of entitiesShow total records
delete(T entity) / deleteById(ID id)Delete entityRemove employee
deleteAll()Delete all entitiesCleanup (rare in production)
flush()Synchronize persistence context with DBForce immediate write
saveAndFlush(S entity)Save and immediately flushEnsure ID available right after save

What Each Interface Adds
InterfaceKey Methods AddedPurpose
Repository<T, ID>Marker interface (no methods)Base marker
CrudRepository<T, ID>save(), findById(), findAll(), delete(), count(), etc.Basic CRUD
PagingAndSortingRepositoryfindAll(Pageable), findAll(Sort)Pagination + Sorting
QueryByExampleExecutorfindOne(), findAll(), count(), exists() by exampleQuery by example
JpaRepositoryflush(), saveAndFlush(), deleteInBatch(), deleteAllInBatch()JPA-specific (flush, batch operations)

=> CrudRepository<T, ID> → basic CRUD (save, findById, findAll, delete, etc.)
=> PagingAndSortingRepository<T, ID> → adds findAll(Pageable) and findAll(Sort) 
=> QueryByExampleExecutor<T> → adds query-by-example methods
=> JpaRepository adds JPA-specific methods like flush(), saveAndFlush(), deleteInBatch(), etc 

Example in Practice
 
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
    // No need to implement anything — all methods above are available!
}

// Usage in Service
@Service
public class EmployeeService {
    @Autowired
    private EmployeeRepository repo;

    public Page<Employee> getEmployees(Pageable pageable) {
        return repo.findAll(pageable);  // Pagination + sorting
    }

    public Employee createEmployee(Employee emp) {
        return repo.save(emp);  // Insert
    }
}
 
Best Practices
 
=> Use JpaRepository for most cases (covers 90% of needs) 
=> Extend CrudRepository only if you want minimal methods (no pagination)
=> Use PagingAndSortingRepository if you need pagination but not JPA-specific methods 
 
What is @Entity annotation?
=> @Entity marks a class as a JPA entity
=> It tells JPA provider (Usually Hibernate in Spring Boot) that this class should be mapped to a database table 
=> The annotation @Entity applied at class level 
=> The entity class must have no-arg constructor (can be private) and usually a primary key marked with @Id 
=> By default, table name = class name, column names = field names 
=> We can use @Table, @Column for customization 
=> It is a part of the jakarta.persistence (or javax.persistence for older versions) package
=> It used to enable ORM (Object Relational Mapping) 
 
Example
 
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;

@Entity  // This makes the class an entity → mapped to table "employee" if no custom table name provided
@Table(name = "employees")  // Custom table name
public class Employee {

    @Id  // Primary key
    @GeneratedValue(strategy = GenerationType.IDENTITY)  // Auto-increment
    @Column(name = "emp_id")  // Custom column name
    private Long id;

    private String name;
    private int salary;

    // No-arg constructor (required by JPA)
    public Employee() {}

    public Employee(String name, int salary) {
        this.name = name;
        this.salary = salary;
    }

    // Getters and setters...
}

Rules and Requirements 
=> Must have no-arg constructor (JPA uses reflection)
=> Fields are usually private with getters/setters 
=> Should be non final (for proxying in lazy loading)
=> Can have relationships (@OneToMany, @ManyToOne, etc)
 
Best Practices
=> Never expose entities directly in REST APIs — use DTOs (security, avoid lazy loading issues).
=> Use @NotNull, @Size, etc. from Bean Validation for constraints
=> Keep entities simple — only data + minimal logic
 
@Id and @GeneratedValue – explain strategies
 
=> Annotation @Id marks a field or property as the primary key of the entity 
=> Every JPA entity must have exactly one @Id 
=> It can be applied at field or getter method level 
=> It's type is usually Long, Integer, String or UUID 
=> Annotation @GeneratedValue tells JPA how to automatically generate the primary key value when saving the new entity 
=> @GeneratedValue always paired with @Id 
=> If we omit the @GeneratedValue, then we need to set the Id manually 
 
Generation strategies 
  1. GenerationType.IDENTITY 
  2. GenerationType.SEQUENCE 
  3. GenerationType.TABLE 
  4. GenerationType.AUTO 
StrategyDescriptionWhen to UseExample / Notes
GenerationType.IDENTITYUses database auto-increment column (e.g., IDENTITY in MySQL, SERIAL in PostgreSQL). JPA retrieves generated ID after insert.Most common for MySQL, PostgreSQL, SQL ServerSimple, performant. No sequence needed.
GenerationType.SEQUENCEUses database sequence object (e.g., Oracle, PostgreSQL). JPA calls sequence before insert.Oracle (default), PostgreSQL with custom sequenceMore control (allocationSize, pre-allocation).
GenerationType.TABLEUses a database table to simulate sequence (portable but slow).When no native support (rare now)Poor performance — avoid in production.
GenerationType.AUTOJPA chooses the best strategy based on database dialect (default).Quick prototyping, let JPA decideOften maps to IDENTITY or SEQUENCE.
 
Detailed example with these 4 strategies  
 
 @Entity
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)  // MySQL/PostgreSQL auto-increment
    private Long id;

    // or
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "emp_seq")
    @SequenceGenerator(name = "emp_seq", sequenceName = "employee_seq", allocationSize = 50)
    private Long id;

    // or
    @Id
    @GeneratedValue(strategy = GenerationType.TABLE, generator = "emp_table")
    @TableGenerator(name = "emp_table", table = "id_generator", pkColumnValue = "employee")
    private Long id;

    // or
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)  // Let JPA decide
    private Long id;

    private String name;
}

Best Practice
=> Use GenerationType.IDENTITY for most cases (MySQL, PostgreSQL, H2)
=> Use GenerationType.SEQUENCE for Oracle or when you want allocationSize > 1 (batch inserts) 
=> Avoid TABLE — slow and outdated
=> Avoid AUTO in production — unpredictable across databases 

How can we use @Id with getter method ? 

Example
 
@Entity
public class Employee {
    private Long id;
    private String name;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    public Long getId() { 
        return id; 
    }

    public void setId(Long id) { 
        this.id = id; 
    }

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
 
=> Use @Id on field — simpler, clearer, most common.
=> Use getter only if you have a specific reason (When you want to add logic in getter (rare for ID))
=> If you place @Id on getter but have other annotations on fields, JPA might get confused. To be safe, explicitly tell JPA to use property access
=> Example :  
@Entity
@Access(AccessType.PROPERTY)  // Important when @Id is on getter
public class Employee {
    private Long id;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    public Long getId() { return id; }

    public void setId(Long id) { this.id = id; }

How to create custom query methods in repository?
=> We can create custom query methods in Spring Data JPA in the following two ways :
  1. Query Method Derivation (Naming Convention – No @Query Needed) 
  2. Custom Queries Using @Query Annotation (When Naming Convention Is Not Enough)
 
1. Query Method Derivation (Naming Convention – No @Query Needed)  
 
=> Spring Data JPA parses the method name and creates the query automatically
=> Rules for Method Names:
        Start with: find…By, read…By, query…By, count…By, exists…By, delete…By.
        Keywords: And, Or, Is, Equals, Between, LessThan, GreaterThan, Like, NotLike, In, NotIn, OrderBy, First, Top.
=> Examples :  
Method SignatureGenerated JPQL (Approximate)Use Case
findByName(String name)SELECT e FROM Employee e WHERE e.name = ?1Find by exact name
findBySalaryGreaterThan(int salary)SELECT e FROM Employee e WHERE e.salary > ?1Salary > given
findByDepartmentAndSalaryGreaterThan(String dept, int salary)SELECT e FROM Employee e WHERE e.department = ?1 AND e.salary > ?2Combined conditions
findByNameContaining(String keyword)SELECT e FROM Employee e WHERE e.name LIKE %?1%Search containing
findBySalaryBetween(int min, int max)SELECT e FROM Employee e WHERE e.salary BETWEEN ?1 AND ?2Range query
findByNameIsNull()SELECT e FROM Employee e WHERE e.name IS NULLNull check
findByDepartmentOrderBySalaryDesc(String dept)SELECT e FROM Employee e WHERE e.department = ?1 ORDER BY e.salary DESCSorted results
findFirstByDepartmentOrderBySalaryDesc(String dept)SELECT e FROM Employee e WHERE e.department = ?1 ORDER BY e.salary DESC LIMIT 1First/top result
findTop3ByDepartmentOrderBySalaryDesc(String dept)SELECT e FROM Employee e WHERE e.department = ?1 ORDER BY e.salary DESC LIMIT 3Top N results
 
2. Custom Queries Using @Query Annotation (When Naming Convention Is Not Enough)
 
=> When method name derivation is not sufficient (complex joins, subqueries, native SQL), use @Query 
 
JPQL Example (JPQL - Java Persistence Query Language)
@Query("SELECT e FROM Employee e WHERE e.department = :dept AND e.salary > :salary")
List<Employee> findHighSalaryEmployees(@Param("dept") String dept, @Param("salary") int salary); 
 
Native SQL Example
@Query("SELECT e FROM Employee e WHERE e.department = :dept AND e.salary > :salary")
List<Employee> findHighSalaryEmployees(@Param("dept") String dept, @Param("salary") int salary); 
  
Best Practices
=> Prefer method name derivation for simple queries — readable and maintainable
=> Use @Query only for complex queries or native SQL
=> Always use @Param for named parameters in @Query
=> Avoid native queries unless necessary (JPQL is portable across databases)
 
@Query annotation – JPQL vs Native SQL 
  
Difference Between JPQL and Native SQL in Spring Data JPA
AspectJPQL (Java Persistence Query Language)Native SQL
Language TypeObject-oriented — queries are written against entity classes and their properties (not database tables/columns).Database-specific — direct SQL against actual tables and columns.
PortabilityPortable across databases — same JPQL works on MySQL, PostgreSQL, Oracle, etc. (JPA provider translates to dialect).Not portable — tied to specific database (syntax, functions differ).
Syntax ExampleSELECT e FROM Employee e WHERE e.salary > :salarySELECT * FROM employee WHERE salary > ?
Entity MappingUses entity names (Employee) and property names (salary) — matches Java class.Uses table names (employee) and column names (salary).
Result TypeReturns managed entities or projections (can be DTOs with SELECT new).Returns raw data (Object[], Map, or custom result with @SqlResultSetMapping).
How to Use in RepositoryDefault for method name derivation and @Query without nativeQuery=true.Use @Query(nativeQuery = true)
PerformanceSlight overhead (translation to SQL), but usually negligible.Direct — potentially faster for complex queries.
Database-Specific FeaturesLimited — cannot use vendor-specific functions (e.g., MySQL JSON_EXTRACT).Full access — use any database function, hint, etc.
SecurityParameters are bound safely — better protection against SQL injection.Same if using parameters (? or :param), but risk if concatenating strings.
When to UseMost cases — business logic, CRUD, when portability matters.Complex reports, database-specific features, performance-critical queries.
JPQL Example (JPQL - Java Persistence Query Language)
@Query("SELECT e FROM Employee e WHERE e.department = :dept AND e.salary > :salary")
List<Employee> findHighSalaryEmployees(@Param("dept") String dept, @Param("salary") int salary); 
 
Native SQL Example
@Query("SELECT e FROM Employee e WHERE e.department = :dept AND e.salary > :salary")
List<Employee> findHighSalaryEmployees(@Param("dept") String dept, @Param("salary") int salary); 

What is pagination in Spring Data?
 
=> Pagination is a mechanism to retrieve large data in small manageable chunks (pages) instead of loading everything at once 
=> It is essential for performance, better user experience in applications displaying lists 

Core Intefaces and Classes
 
1. Pageable - Represents pagination information (page number, page size, sort)
2. Page<T> - Return Type containing : 
        List of entities for current page (getContent())
        Total  pages (getTotalPages())
        Total elements (getTotalElements())
        Current page number, size, etc
3. Slice<T> - No count query. Only knows if next slice exists (faster for larger/infinite dataset)
  
Best Practices
=> Always use pagination for list endpoints 
=> Default reasonable size (10-20) 
=> Use Slice when total count not needed
=> Combine with @PageableDefault annotation for defaults

Example
 
//Repository method
public interface EmployeeRepository extends JpaRepository<Employee, Long> {

    // Returns a Page of employees with pagination + optional sorting
    Page<Employee> findAll(Pageable pageable);

    // Custom query with pagination
    Page<Employee> findByDepartment(String department, Pageable pageable);
}
 
//Controller usage
@RestController
@RequestMapping("/employees")
public class EmployeeController {

    @Autowired
    private EmployeeRepository repo;

    @GetMapping
    public Page<Employee> getEmployees(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size,
            @RequestParam(defaultValue = "id") String sortBy,
            @RequestParam(defaultValue = "asc") String sortDir) {

        Sort sort = sortDir.equalsIgnoreCase("desc") 
            ? Sort.by(sortBy).descending() 
            : Sort.by(sortBy).ascending();

        Pageable pageable = PageRequest.of(page, size, sort);

        return repo.findAll(pageable);
    }
}
 
URL examples
http://localhost:8080/employees?page=0&size=10&sortBy=salary&sortDir=desc
(Page 0 (first), 10 items, sorted by salary descending)
 
Response JSON snippet
{
  "content": [/* 10 employees */],
  "pageable": { "pageNumber": 0, "pageSize": 10, ... },
  "totalPages": 25,
  "totalElements": 250,
  "last": false,
  "numberOfElements": 10
}
 
How to use Pageable in controller?

=> Pageable is a Spring Data Interface represents Pagination information (page number, page size, sort)
 
=> Example :
@GetMapping("/employees")
public Page<Employee> getEmployees(Pageable pageable) {
    return employeeRepository.findAll(pageable);
}
 
=> When you add a Pageable parameter to a controller method, Spring Boot automatically binds request parameters like page, size, sort to it
 
ParameterDefault Value
page0
size20
sortnone
 
=> URL Examples : 
http://localhost:8080/employees
    => page 0, size 20, no sort (default)
http://localhost:8080/employees?page=2&size=10
    
=> page 2, 10 items per page
http://localhost:8080/employees?sort=salary,desc
    
=> sorted by salary descending
http://localhost:8080/employees?page=1&size=5&sort=name,asc&sort=salary,desc
    
=> page 1, size 5, sort by name ascending then salary descending 
 
Complete Example
 
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
    Page<Employee> findAll(Pageable pageable);
    Page<Employee> findByDepartment(String department, Pageable pageable);
 
@RestController
@RequestMapping("/employees")
public class EmployeeController {

    @Autowired
    private EmployeeRepository employeeRepository;

    @GetMapping
    public Page<Employee> getAllEmployees(Pageable pageable) {
        return employeeRepository.findAll(pageable);
    }

    // With custom query
    @GetMapping("/search")
    public Page<Employee> searchByDepartment(
            @RequestParam String department,
            Pageable pageable) {
        return employeeRepository.findByDepartment(department, pageable);
    }

//Customizing Defaults with @PageableDefault
@GetMapping
public Page<Employee> getEmployees(
@PageableDefault(page = 0, size = 50, sort = "salary", direction = Sort.Direction.DESC)
Pageable pageable) {
return employeeRepository.findAll(pageable);
}


}
 
Response (JSON from Page<T>)
 
{
  "content": [/* list of employees */],
  "pageable": {
    "pageNumber": 0,
    "pageSize": 10,
    "sort": {"sorted": true, "unsorted": false, "empty": false},
    "offset": 0,
    "paged": true,
    "unpaged": false
  },
  "totalPages": 15,
  "totalElements": 150,
  "last": false,
  "numberOfElements": 10,
  "size": 10,
  "number": 0,
  "sort": {"sorted": true, "unsorted": false, "empty": false},
  "first": true,
  "empty": false
}

What is @Transactional? When to use?

=> It tells Spring to automatically manage database transactions 
=> When a method annotated with @Transactional is called, Spring : 
        Starts a transaction before the method begins 
        Commits the transaction if the method completes successfully 
        Rolls back the transaction if an unchecked exception (runTimeException or error) is thrown 
 
How It Works (Behind the Scenes) 
          
=> Spring uses AOP (Aspect-Oriented Programming) — creates a proxy around the bean
=> The proxy intercepts method calls and manages the transaction using a PlatformTransactionManager (e.g., DataSourceTransactionManager for JDBC/JPA).
 
Basic syntax 
 
@Service
public class EmployeeService {

    @Transactional  // Applies to all methods
    public class EmployeeService { ... }

    // Or on individual method
    @Transactional
    public void transferSalary(Long fromId, Long toId, BigDecimal amount) {
        // Multiple DB operations — all in one transaction
        accountRepo.debit(fromId, amount);
        accountRepo.credit(toId, amount);
    }

Key Attributes of @Transactional
AttributeDefaultDescription
propagationREQUIREDHow transaction propagates (REQUIRED, REQUIRES_NEW, NESTED, etc.)
isolationDEFAULTIsolation level (READ_UNCOMMITTED, READ_COMMITTED, etc.)
timeout-1 (no timeout)Transaction timeout in seconds
readOnlyfalseHint for read-only (optimizes for reads)
rollbackForunchecked exceptionsSpecify which exceptions trigger rollback (default: RuntimeException/Error)
noRollbackFornoneExceptions that do not trigger rollback
 
When to Use @Transactional

=> Use it whenever you have multiple database operations that must succeed or fail together.
=> Example : Multiple writes (transfer money, order processing) 
@Transactional
public void placeOrder(Order order) {
    orderRepo.save(order);
    inventoryRepo.reduceStock(order.getItems());
    paymentRepo.charge(order.getAmount());
 
=> All or nothing (eg. if payment fails, stock and order are rolled back)
=> Service layer is the best place to have @Transactional (not in controller or repository)
=> Read-only optimization:
@Transactional(readOnly = true)
public List<Employee> getAllEmployees() { ... }
 
When NOT to Use 
 
=> Single read operation (no need for transaction overhead)
=> Non-database operations
=> Common Pitfall: Proxy Limitation
Example : 
@Service
public class EmployeeService {

    @Transactional
    public void methodA() { ... }

    public void methodB() {
        methodA();  // No transaction! Called directly — not through proxy
    }
}
 
=> In the above example, the calling methodA inside methodB breaks the proxy. So avoid self calls
 
What does proxy mean here?
 
=> In the context of Spring Data JPA (and Spring in general), a proxy is a dynamically generated subclass created at runtime that "wraps" your original bean (e.g., repository, service, or entity) to add extra behavior without changing the original code
 
=> Remember the behavior of JpaRepository (Spring Data JPA)

=> Example :

public interface EmployeeRepository extends JpaRepository<Employee, Long> { }
 
=> You write only the interface — no implementation.
=> Spring creates a proxy at runtime that implements this interface.
=> When you call employeeRepository.findById(1L), the proxy generates and executes the SQL behind the scenes.
=> You never see the real implementation class — it's all proxy magic.
 
Difference between persist() and save() (JPA vs Spring Data)

=> persist() is a pure JPA(EntityManager) method
=> save() is a Spring Data JPA(CRUD repository/JPA repository) method 
=> persist() schedules insert, returns void, does nothing on existing entity
=> save() returns saved entity with generated ID, handles both insert (new) and merge (existing)
 
Code Example (Shows the Difference) 
 
Employee emp = new Employee();
emp.setName("Sachin");

// JPA way (EntityManager)
entityManager.persist(emp);  // OK — new entity
entityManager.persist(emp);  // Second call — does nothing (no error, but no update)

// Spring Data JPA way
Employee saved1 = repo.save(emp);     // Insert — returns entity with generated ID
saved1.setName("Sachin Tendulkar");
Employee saved2 = repo.save(saved1);  // Update — returns updated entity
 
FeatureEntityManager.persist() (JPA)repository.save() (Spring Data JPA)
Return valuevoidReturns saved entity (with generated ID)
New entity (no ID)InsertsInserts
Existing entity (has ID)Does nothing (no update)Updates (merge)
Immediate DB insertNo (only on flush/commit)No (on flush/commit)
Typical usageLow-level JPA codeStandard in Spring Boot
Error on existing entityMay throw EntityExistsExceptionNo error — updates
ConvenienceLowHigh — handles both insert and update
 
Entity relationships basics (@OneToMany, @ManyToOne)
Aspect@OneToMany@ManyToOne
MeaningOne entity instance is associated with multiple instances of another entity.Many entity instances are associated with one instance of another entity.
OwnershipNon-owning side (foreign key is in the target entity table).Owning side (foreign key column is in this entity's table).
Typical ExampleOne Department has many Employees.Many Employees belong to one Department.
Annotation PlacementOn the collection field in the parent entity (one side).On the reference field in the child entity (many side).
Field TypeCollection (List, Set) e.g., List<Employee> employeesSingle reference e.g., Department department
Default Fetch TypeLAZYEAGER
mappedByRequired — specifies the owning side field name.Not used
CascadeOften used (e.g., CascadeType.ALL) to propagate operations to children.Less common — usually no cascade from child to parent.
Orphan RemovalOften used with @OneToMany to delete orphaned children.Not applicable.
Join ColumnUsually not needed (placed on owning side).Use @JoinColumn to customize foreign key column name.
Bidirectional?Common — pair with @ManyToOne on child side.Common — reference back to parent.
Database ImpactNo foreign key in parent table.Foreign key column in child table.
 
Key points 
=> @OneToMany is non-owning ie foreign key is in the target entity table
=> mappedBy is mandatory on @OneToMany to avoid duplicate FK columns. 
=> @ManyToOne is Owning side (foreign key column is in this entity's table).

Code Example (Bidirectional relationship)
 
@Entity
public class Department {
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    // One Department → Many Employees
    @OneToMany(mappedBy = "department", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Employee> employees = new ArrayList<>();

    // getters/setters
}

@Entity
public class Employee {
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    // Many Employees → One Department (owning side)
    @ManyToOne
    @JoinColumn(name = "dept_id")  // Optional — custom FK column name
    private Department department;

    // getters/setters
}
 
Database Tables
department table: id, name
employee table: id, name, dept_id (foreign key → department.id) 

How to handle validation in entities/DTOs?

=> In Spring Boot applications, validation is primarily handled using Bean Validation API (Jakarta validation)
=> Best practice is keeping the validation on DTOs and keeping the entities clean 
=> Use Validation annotations such as @Valid, @NotBlank, etc 
=> Use @Valid on controller paratmeters to trigger validations 
=> Handle errors globally with @ControllerAdvice and MethodArgumentNotValidException for custom JSON responses
=> Avoid validation on entities to maintain separation of concerns 

Best Practice: Validate on DTOs (Not Entities), Why?
 
=> DTOs (Data Transfer Objects): Plain classes used only for API input/output
=> Separation of concerns: Entities are for persistence (JPA), DTOs for API contract
=> Different API versions can have different validation
=> Prevent accidental exposure of entity constraints in JSON
 
Validation Annotations (Most Common)
AnnotationPackageExample UseMeaning
@NotNullconstraints@NotNull private String name;Cannot be null
@NotEmptyconstraints@NotEmpty private String name;Not null and not empty (for collections/strings)
@NotBlankconstraints@NotBlank private String name;Not null, not empty, not whitespace-only (strings)
@Size(min=, max=)constraints@Size(min=2, max=50) private String name;Length between min–max
@Min/@Maxconstraints@Min(18) private int age;Minimum/maximum value
@Positive / @PositiveOrZeroconstraints@Positive private int salary;>0 or ≥0
@Emailconstraints@Email private String email;Valid email format
@Pattern(regexp=)constraints@Pattern(regexp="\\d{10}") private String phone;Matches regex
@Validconstraints@Valid @RequestBody EmployeeDto dtoTrigger nested object validation
 
Example : Step-by-Step: Validation on DTOs (Recommended Way)

1. Create DTO 
 
public class EmployeeRequestDto {
    @NotBlank(message = "Name is required")
    @Size(min = 2, max = 100, message = "Name must be 2-100 characters")
    private String name;

    @NotNull(message = "Salary cannot be null")
    @Positive(message = "Salary must be positive")
    private Integer salary;

    @Email(message = "Invalid email format")
    private String email;

    // getters and setters
}
 
2. Use @Valid in Controller
 
@PostMapping("/employees")
public ResponseEntity<Employee> create(@Valid @RequestBody EmployeeRequestDto dto) {
    Employee employee = employeeService.create(dto);
    return ResponseEntity.status(201).body(employee);
}
 
3. Automatic Validation
 
=> Spring sees @Valid → validates the DTO using Bean Validation.
=> If invalid → throws MethodArgumentNotValidException.
 
4. Handle Validation Errors Globally (@ControllerAdvice)
 
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationErrors(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getFieldErrors().forEach(error -> 
            errors.put(error.getField(), error.getDefaultMessage()));
        return ResponseEntity.badRequest().body(errors);
    }
}
 
Error Response Example:
{
  "name": "Name is required",
  "salary": "Salary must be positive"
}

Global exception handling with @ControllerAdvice

=> @ControllerAdvice is a specialized @Component annotation that allows you to handle exceptions globally across all @Controller or @RestController classes in your Spring Boot application
=> One centralized place for all exception handling logic
=> Uniform error format (e.g., JSON with error code, message, timestamp)
=> Separation of Concerns: Controllers stay clean — focus on business logic, not error handling
=> Reusable ie Works across all controllers
 
How It Works 
 
=> Spring scans for classes annotated with @ControllerAdvice
=> Methods annotated with @ExceptionHandler inside it are invoked when the specified exception is thrown from any controller.  
  
Basic Example – Global Exception Handler 

import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.*;

@RestControllerAdvice  // = @ControllerAdvice + @ResponseBody (for REST)
public class GlobalExceptionHandler {

    // Handle specific exception
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
        ErrorResponse error = new ErrorResponse(
            HttpStatus.NOT_FOUND.value(),
            ex.getMessage(),
            LocalDateTime.now()
        );
        return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
    }

    // Handle validation errors (from @Valid)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationErrors(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getFieldErrors().forEach(error ->
            errors.put(error.getField(), error.getDefaultMessage()));
        return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
    }

    // Fallback for any other exception
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
        ErrorResponse error = new ErrorResponse(
            HttpStatus.INTERNAL_SERVER_ERROR.value(),
            "Unexpected error: " + ex.getMessage(),
            LocalDateTime.now()
        );
        return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

// Simple error response DTO
record ErrorResponse(int status, String message, LocalDateTime timestamp) { }
 
Common Exceptions Handled 
Exception TypeTypical StatusUse Case
ResourceNotFoundException404Entity not found
MethodArgumentNotValidException400@Valid validation failures
ConstraintViolationException400@Valid on method parameters
DataIntegrityViolationException400/409DB constraint violation
AccessDeniedException (Security)403Authorization failure
Exception (fallback)500Unexpected errors
 
Advanced Features
=> @Order — control precedence if multiple @ControllerAdvice. 
=> @ExceptionHandler with multiple exceptions:
@ExceptionHandler({ResourceNotFoundException.class, IllegalArgumentException.class})
 
Best Practice: Place GlobalExceptionHandler in a separate package, not inside Model, Service, or Controller layers.
 
com.example.demo
├── controller      → Your @RestController classes
├── service         → Business logic
├── repository      → JPA repositories
├── model/entity    → @Entity classes and DTOs
├── exception       → Custom exceptions (e.g., ResourceNotFoundException)
└── config          → Configuration classes
    └── GlobalExceptionHandler.java  
 
Common pitfalls of Spring Boot JPA + CRUD
#PitfallDescription & Why It HappensSymptoms / ErrorsHow to Avoid / Fix
1N+1 Select ProblemLazy-loaded associations (@OneToMany, @ManyToOne default LAZY) cause separate query for each entity when accessed in a loop.Slow performance, many DB queries (e.g., 1 + N queries).Use fetch join in custom @Query, EntityGraph, or eager fetch for specific use case.
2LazyInitializationExceptionAccessing lazy association outside transaction (e.g., in view layer after service returns).LazyInitializationException: could not initialize proxyKeep access inside @Transactional, use DTO projection, or Open Session in View (OSIV — not recommended in production).
3Exposing Entities in REST APIsReturning JPA @Entity directly from controller.Lazy loading errors, infinite recursion in JSON, over-posting security risk.Use DTOs — map entity to DTO (MapStruct, ModelMapper, or manual). Never expose entities.
4Missing @TransactionalForgetting @Transactional on service methods with multiple repository calls.Only last operation saved, or no rollback on error.Always annotate service methods with @Transactional (or class level).
5Incorrect Cascade / OrphanRemovalWrong cascade or orphanRemoval settings leading to unexpected deletes or missing saves.Child entities not saved/deleted when parent is.Understand cascade types (ALL, PERSIST, MERGE, REMOVE). Use orphanRemoval = true carefully.
6Using Entity in Multiple LayersPassing entity from repository → service → controller → view.Accidental modification, serialization issues.Convert to DTO as early as possible (in service layer).
7No Validation on Input (DTO)Accepting @RequestBody without @Valid and validation annotations.Invalid data saved to DB.Use @Valid @RequestBody EmployeeDto dto + Bean Validation annotations (@NotBlank, @Positive).
8Open Session in View (OSIV) EnabledDefault in Spring Boot Web — keeps session open till view rendering.Performance hit, unexpected lazy loading in view.Disable in application.properties: spring.jpa.open-in-view=false (recommended for APIs).
9Wrong Fetch TypeLeaving @ManyToOne as EAGER (default) when not needed.Unnecessary data loading, performance degradation.Explicitly set fetch = FetchType.LAZY when possible.
10Modifying Entities in Read OperationsAccidentally modifying entity in read-only method (no @Transactional(readOnly = true)).Unnecessary dirty checking, performance cost.Use readOnly = true for query methods.
11Bi-directional Relationship IssuesForgetting mappedBy in @OneToMany or infinite recursion in JSON.StackOverflowError in toString/JSON, duplicate FK columns.Always use mappedBy, add @JsonIgnore or DTOs.
12Using save() for Bulk OperationsCalling save() in loop for thousands of entities.Very slow (one query per entity).Use saveAll() or batch inserts (spring.jpa.properties.hibernate.jdbc.batch_size).

 
Bottom line: Pitfalls are the possible mistakes — that's what the question asks.

What are the best practices in Spring Boot JPA + CRUD ?

1. Never expose JPA Entities in REST APIs  
   - Use DTOs (Data Transfer Objects) for request/response.  
   - Reason: Avoid lazy loading exceptions, over-posting, infinite recursion in JSON.

2. Keep Controllers Thin  
   - Controllers: Only handle HTTP (mapping, @RequestBody, ResponseEntity).  
   - Move business logic to Service layer.

3. Place @Transactional in Service Layer  
   - Not in controller or repository.  
   - Use `@Transactional(readOnly = true)` for query methods.

4. Validate Input on DTOs  
   - Use `@Valid @RequestBody EmployeeDto dto` + Bean Validation (`@NotBlank`, `@Positive`, `@Email`).  
   - Handle errors globally with `@ControllerAdvice`.

5. Prefer LAZY Fetching  
   - Default for `@OneToMany` — good.  
   - Change `@ManyToOne` to `fetch = FetchType.LAZY` when possible to avoid unnecessary loads.

6. Use DTO Projections or EntityGraph for N+1 Problem  
   - Avoid accessing lazy collections in loops.  
   - Use `@EntityGraph`, fetch join in `@Query`, or DTO projections.

7. Disable Open Session in View (OSIV) for APIs  
   - `spring.jpa.open-in-view=false` in `application.properties`.  
   - Prevents lazy loading in view layer (better performance).

8. Use Proper Cascade and orphanRemoval  
   - `cascade = CascadeType.ALL` + `orphanRemoval = true` on `@OneToMany` when children belong only to parent.  
   - Be careful — wrong settings cause unexpected deletes.

9. Use PagingAndSortingRepository or Pageable for Lists  
   - Never return full `List<T>` for large tables — use `Page<T>` with `Pageable`.

10. Logging and Exception Handling  
    - Log at appropriate levels (info for entry/exit, error for exceptions).  
    - Global `@ControllerAdvice` for consistent error responses.

11. Use Constructor Expressions in Queries (for DTOs)  
    @Query("SELECT new com.example.dto.EmployeeDto(e.name, e.salary) FROM Employee e")
    List<EmployeeDto> findEmployeeDtos();

12. Batch Inserts/Updates  
    - For bulk operations: `spring.jpa.properties.hibernate.jdbc.batch_size=50`.
_____________________________________________________________________
 
Coding Practice : 
 
=> Do the Setup for Spring Boot JPA + CRUD 
=> Exercises (create Employee entity + repository + controller):
  1. Create Employee entity (id, name, salary, department).
  2. Create EmployeeRepository extends JpaRepository.
  3. Test basic CRUD in main (save, findAll, findById).
  4. Custom method: findByName(String name).
  5. Custom method: findBySalaryGreaterThan(int salary).
  6. @Query JPQL: select by department.
  7. @Query native SQL.
  8. Pagination: Page<Employee> with Pageable.
  9. Sort by salary descending.
  10. EmployeeDto with validation (@NotBlank, @Positive).
  11. @PostMapping with @Valid + BindingResult.
  12. Custom error response for validation.
  13. @ControllerAdvice for MethodArgumentNotValidException.
  14. Delete endpoint with proper status.
  15. Update (PUT) endpoint.
  16. Mix: Department entity, @OneToMany relationship, custom queries, etc.
  17. Mix: Department entity, @OneToMany relationship, custom queries, etc.
  18. Mix: Department entity, @OneToMany relationship, custom queries, etc.
  19. Mix: Department entity, @OneToMany relationship, custom queries, etc.
  20. Mix: Department entity, @OneToMany relationship, custom queries, etc.