Spring BootSpring Data JPAbeginner
Updated:

Spring Data JPA: Getting Started

7 min read

Spring Data JPA lets you talk to a database by declaring an interface — no SQL, no boilerplate. Here is how entities, repositories and derived queries fit together, with code.

TL;DR – Quick Answer

Spring Data JPA is a Spring module that removes most of the boilerplate of database access. You map a Java class to a table with @Entity, then declare a repository interface that extends JpaRepository. Spring generates the implementation at runtime, giving you save, find, delete and paging methods for free, plus custom queries derived from method names like findByEmail. You write almost no SQL and no DAO code.

On This Page

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 JpaRepository methods.

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?
Spring Data JPA is a layer on top of JPA and Hibernate that generates data-access code for you. You define an @Entity and a repository interface extending JpaRepository, and Spring provides the CRUD implementation automatically. It reduces database boilerplate dramatically compared with writing DAOs by hand.
What is the difference between JPA and Spring Data JPA?
JPA is the Java specification for mapping objects to relational tables, and Hibernate is a popular implementation of it. Spring Data JPA sits above them and removes boilerplate by auto-generating repository implementations. JPA defines the mapping; Spring Data JPA saves you from writing repetitive persistence code.
What does JpaRepository give you?
Extending JpaRepository gives you ready-made methods such as save, saveAll, findById, findAll, count, delete and paginated queries, without writing any implementation. Spring generates the concrete class at runtime. You add only the custom finders your domain needs.
What are derived query methods in Spring Data JPA?
Derived query methods are repository methods whose SQL Spring infers from the method name. For example findByEmail generates a query filtering on the email column, and findByAgeGreaterThan filters on age. You just declare the method signature and Spring writes the query.
How do I write a custom query in Spring Data JPA?
Annotate a repository method with @Query and provide JPQL or native SQL, using named or positional parameters. This is the escape hatch when a query is too complex for a derived method name. You keep the interface declaration and Spring binds your query to it.

Want to Build Your Career in Java Full Stack with AI?

Join CodeBegun and train with working industry engineers — Explore the Program

Apply for Demo Class →
Siva Prasad Galaba
Founder, CodeBegun · Staff Engineer

Founder of CodeBegun. 15+ years building Java systems at companies like Crunchyroll. Teaches Java, Spring Boot and system design the way the industry actually works, and mentors students through projects, mock interviews and placement preparation.

Technically reviewed by CodeBegun Technical TeamLast reviewed 15 July 2026 LinkedIn
Chat with us