Writing database access by hand — open a connection, write the SQL, map each column to a field, close everything, repeat for every table — is exactly the kind of repetitive work Spring exists to remove. Spring Data JPA takes it to an extreme: you declare an interface with no implementation, and Spring writes the data-access code for you at runtime.
This guide connects the three pieces — entity, repository, query — and shows how a few lines give you full CRUD plus custom finders, with almost no SQL.
Where JPA, Hibernate and Spring Data fit
These names get muddled, so pin them down. JPA is the Java specification for mapping objects to relational tables. Hibernate is the most common implementation of that spec. Spring Data JPA sits on top of both and eliminates boilerplate by generating repository implementations for you. JPA defines the mapping rules; Hibernate does the actual work; Spring Data JPA saves you from writing the repetitive plumbing.
To use it, add the starter and a database driver:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId> <!-- in-memory DB, great for learning -->
<scope>runtime</scope>
</dependency>
Auto-configuration notices the driver and wires a data source automatically — the same classpath-inspection behaviour that powers the rest of Spring Boot. If SQL itself is unfamiliar, what is SQL covers the relational basics this builds on.
Step 1: Map an entity
An entity is a Java class mapped to a table. You annotate it with @Entity and mark its
primary key with @Id.
import jakarta.persistence.*;
@Entity
@Table(name = "customers")
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // DB assigns the id
private Long id;
@Column(nullable = false)
private String name;
@Column(unique = true)
private String email;
protected Customer() { } // JPA needs a no-arg constructor
public Customer(String name, String email) {
this.name = name;
this.email = email;
}
// getters and setters ...
}
Each field maps to a column; @GeneratedValue lets the database assign the id on insert.
Hibernate can even create the customers table for you from this class during development.
The class is a plain object with annotations — no base class to extend, no interface to
implement.
Step 2: Declare a repository
Here is the part that surprises people. You define an interface, extend JpaRepository, and
write no implementation at all.
import org.springframework.data.jpa.repository.JpaRepository;
public interface CustomerRepository extends JpaRepository<Customer, Long> {
// That's it. No implementation. Really.
}
The two type parameters say "this repository manages Customer entities whose id is a
Long". At startup Spring generates a concrete class implementing this interface and
registers it as a bean — one you can inject anywhere, exactly like the beans in
Spring beans. That generated bean already has a full
set of methods.
@Service
public class CustomerService {
private final CustomerRepository repo;
public CustomerService(CustomerRepository repo) { // constructor injection
this.repo = repo;
}
public Customer register(String name, String email) {
return repo.save(new Customer(name, email)); // INSERT, no SQL written
}
public List<Customer> everyone() {
return repo.findAll(); // SELECT *, for free
}
}
save, findAll, findById, count, delete and paginated variants all come from
JpaRepository without a line of implementation. That is the boilerplate-removal Spring Data
is famous for.
Pro tip: In interviews, be precise about the layering: "JPA is the spec, Hibernate is the implementation, Spring Data JPA generates the repository so I don't write DAOs." Naming all three and their roles is a quick way to show you understand what is happening rather than treating it as magic.
Step 3: Derived query methods
Beyond the free CRUD, you often need custom lookups. Spring can infer the query straight from the method name — no SQL, no body.
public interface CustomerRepository extends JpaRepository<Customer, Long> {
// Spring reads the name and generates: WHERE email = ?
Optional<Customer> findByEmail(String email);
// WHERE name LIKE ?
List<Customer> findByNameContainingIgnoreCase(String fragment);
// WHERE email = ? -> returns true/false
boolean existsByEmail(String email);
}
Spring parses findByEmail, findByNameContainingIgnoreCase and existsByEmail into the
right queries by convention. Returning Optional<Customer> for a single-result finder is the
idiomatic way to represent "maybe not found" — the same Optional you meet in core Java.
Step 4: Custom @Query for the hard cases
When a query is too complex to express as a method name, drop to @Query and write JPQL (or
native SQL).
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface CustomerRepository extends JpaRepository<Customer, Long> {
@Query("SELECT c FROM Customer c WHERE c.email LIKE %:domain")
List<Customer> findByEmailDomain(@Param("domain") String domain);
@Query(value = "SELECT * FROM customers WHERE name = ?1", nativeQuery = true)
List<Customer> findByExactNameNative(String name);
}
The first uses JPQL, which queries the entity model (Customer, not customers). The
second sets nativeQuery = true to run raw SQL when you need database-specific features.
This is the escape hatch: derived methods for the common cases, @Query when the query gets
real. If your queries start joining tables, the SQL joins
guide is worth a look to reason about what the ORM generates.
Common mistake: Loading an entire table with
findAll()and filtering in Java. That pulls every row into memory and throws most of it away. Push the filtering into the query — a derived method or@Query— so the database does the work and returns only what you need. This matters enormously as data grows.
Wiring it into an API
Spring Data JPA is the persistence half of a backend; the web half is a REST controller. Inject the service into a controller and you have an API backed by a real database:
@RestController
@RequestMapping("/api/customers")
public class CustomerController {
private final CustomerService service;
public CustomerController(CustomerService service) {
this.service = service;
}
@PostMapping
public Customer register(@RequestBody Customer body) {
return service.register(body.getName(), body.getEmail());
}
}
That is a complete slice: HTTP in, JSON out, persisted to a database — and you wrote almost no data-access code. Combine it with the controller patterns from building a REST API with Spring Boot and you have the backbone of a real backend.
Transactions: keeping writes consistent
A single business operation often touches several rows or tables, and you need all of it to
succeed or none of it. Spring Data JPA wraps each repository method in a transaction
automatically, but for multi-step operations in your service you take control with
@Transactional.
@Service
public class TransferService {
private final AccountRepository accounts;
public TransferService(AccountRepository accounts) {
this.accounts = accounts;
}
@Transactional // both saves commit together, or both roll back
public void transfer(Long fromId, Long toId, double amount) {
Account from = accounts.findById(fromId).orElseThrow();
Account to = accounts.findById(toId).orElseThrow();
from.debit(amount);
to.credit(amount);
accounts.save(from);
accounts.save(to);
// If anything throws here, the whole transaction rolls back
}
}
If the second save fails, Spring rolls back the first automatically, so money is never debited without being credited. This declarative transaction handling — one annotation instead of manual commit and rollback code — is another slice of boilerplate the framework removes. It is a favourite interview topic precisely because getting consistency right is where real bugs hide.
Common performance traps
Spring Data JPA is easy to start with, which means it is also easy to misuse. Two problems show up constantly in real projects, and knowing them marks you out as someone who has used the tool in anger.
The first is the N+1 query problem: you load a list of entities, then access a related
collection on each one, and the ORM silently fires one extra query per element. Loading 100
orders and touching each order's items can quietly become 101 queries. The fix is to fetch the
association together, with a JOIN FETCH in a @Query or an entity graph.
The second is loading more than you need. Pulling whole entities when you only want two columns wastes memory and bandwidth; projections (interfaces or DTOs that select just the fields you use) solve it. Both traps come down to the same discipline: know what SQL your repository method actually generates. If reasoning about that feels shaky, strengthen the underlying model with what is SQL and the joins guide.
Interview note: When asked about JPA performance, name the N+1 problem unprompted and describe the fix. It is the single most common real-world JPA pitfall, and mentioning it — along with lazy versus eager loading — signals hands-on experience far more convincingly than reciting the list of
JpaRepositorymethods.
What you learned
Spring Data JPA turns database access into three declarations: an @Entity mapping a class to
a table, a repository interface extending JpaRepository for free CRUD, and derived or
@Query methods for custom lookups. Spring generates the implementation and hands you a bean
to inject. You write intent, not plumbing.
Continue from the Spring Boot learning hub by connecting the database URL and credentials in application.properties, and strengthen the SQL fundamentals underneath with the SQL learning track. Persistence done this cleanly is a large part of why Spring Boot dominates Java backend development.
Frequently Asked Questions
What is Spring Data JPA?
What is the difference between JPA and Spring Data JPA?
What does JpaRepository give you?
What are derived query methods in Spring Data JPA?
How do I write a custom query in Spring Data JPA?
Want to Build Your Career in Java Full Stack with AI?
Join CodeBegun and train with working industry engineers — Explore the Program

