beginner~3h

Spring Data JPA — Repositories, Derived Queries & Auto-Configuration

This is where the raw JPA/Hibernate concepts from the first five chapters meet the everyday tool real Spring Boot applications actually use — repositories generate a working implementation from just an interface, with derived query methods parsing the method name itself into a real query.

Learning objectives

  • Explain what JpaRepository gives you and how Spring generates its implementation at startup.
  • Write a derived query method and explain how Spring parses its name into a JPQL query.
  • Identify when a derived method name has grown too complex and should switch to @Query instead.

Every JPA concept from the last five chapters (EntityManager, the persistence context, entity lifecycle) is still true underneath — but in real Spring Boot applications, you almost never touch EntityManager directly. This chapter is where those raw JPA concepts meet the tool you'll actually use every day.

📖 Story

Imagine you've just learned exactly how a car engine works — pistons, combustion, gear ratios — and now someone hands you an automatic car and says "just drive." You don't manage gear ratios by hand anymore; the automatic transmission handles it, using the same mechanics you just learned, just automated.

Here's the manual-transmission version, using raw EntityManager calls, for something as simple as "find all orders for this customer":

public List<Order> findByCustomer(Long customerId) { return entityManager .createQuery("SELECT o FROM Order o WHERE o.customer.id = :customerId", Order.class) .setParameter("customerId", customerId) .getResultList(); }

Now here's the automatic version, using Spring Data JPA — an entire interface, with ZERO method bodies written:

public interface OrderRepository extends JpaRepository<Order, Long> { List<Order> findByCustomerId(Long customerId); // <- no body at all! }

That's it. No @Query, no createQuery, nothing. Spring reads the METHOD NAME itself — findByCustomerId — and generates the exact JPQL query above, automatically, at application startup.

Repository — a Spring Data interface representing a collection-like abstraction over one entity type. JpaRepository — the richest built-in repository interface, giving you save/find/delete/paging/sorting with zero code written. Derived query method — a repository method whose NAME Spring parses directly into a JPQL query. @EnableJpaRepositories — the (usually invisible, Spring-Boot-auto-applied) annotation that tells Spring to scan for and implement repository interfaces at startup.

Let's build on this chapter's OrderRepository example and see exactly how far "just declare an interface" gets you.

The repository hierarchy

Repository (a marker, no methods) → CrudRepository (basic save/findById/findAll/delete) → PagingAndSortingRepository (adds pagination/sorting) → JpaRepository (adds JPA-specific batch operations). In real code, you almost always extend JpaRepository directly, since it includes everything below it:

public interface OrderRepository extends JpaRepository<Order, Long> { // Derived query — Spring parses this NAME into a JPQL query: List<Order> findByCustomerIdAndStatus(Long customerId, String status); // Add an ORDER BY straight into the method name too: List<Order> findByCustomerIdOrderByCreatedAtDesc(Long customerId); }

You never implement either of these methods. At application startup, Spring Data scans for interfaces like this one, and generates an actual, working implementation — a dynamic proxy — behind the scenes.

How Spring turns a method NAME into a real query

Take findByCustomerIdAndStatus. Spring tokenizes this name into recognizable pieces: findBy (the query trigger), CustomerId (a field to filter on), And (a connector), Status (another field). It then checks these field names against your actual Order entity's mapped fields — which is exactly why a TYPO in a derived method name (say, findByCustomerIdAndStatuss) fails immediately at application STARTUP, not silently later when the method is actually called.

When to call it directly, no interface method needed

OrderRepository orderRepo = ...; // injected via Spring List<Order> orders = orderRepo.findByCustomerIdOrderByCreatedAtDesc(42L); // Runs: SELECT o FROM Order o WHERE o.customer.id = ?1 ORDER BY o.createdAt DESC

Same result as this chapter's opening hand-written EntityManager code — just without you ever writing the query yourself.

Spring Data's repository proxy is built using Java's dynamic proxy mechanism — a repository is always an interface, so Spring always has a JDK dynamic proxy to work with here (unlike @Transactional on a concrete class, which needs CGLIB subclassing instead) — at application startup, Spring scans for interfaces extending a Spring Data repository base interface, and for each one, builds a runtime proxy class implementing it. For a derived query method like findByCustomerIdAndStatus, the method-name parser runs ONCE at startup, producing an already-parsed, already-cached JPQL query — so at actual call time, Spring is just executing that cached query with your arguments bound as parameters, not re-parsing the method name on every call.

  • A typical Spring Boot e-commerce backend defines OrderRepository extends JpaRepository<Order, Long> with exactly the derived methods from this chapter's story — this is the completely standard, unremarkable everyday pattern in real production codebases.
  • A multi-tenant SaaS platform includes tenantId in every derived method (findByTenantIdAndStatus) as a structural convention, so tenant isolation is enforced consistently rather than trusted to memory at every call site.
  • A reporting service whose queries have outgrown what a derived method name can clearly express switches to @Query-annotated JPQL directly on the method — exactly the escape hatch the very next chapter covers.
  • Extend JpaRepository directly for nearly all real repositories — it includes everything from the lower interfaces plus JPA-specific batch operations.
  • Prefer derived query methods, like this chapter's OrderRepository examples, for genuinely simple filters — they're self-documenting, with no separate query string to maintain.
  • Keep derived method names readable. Once a method name needs more than 2-3 conditions to express, switch to @Query (the next chapter) rather than continuing to extend an increasingly unreadable name.

⚠️ Why this keeps happening

Derived query methods work so smoothly for simple cases that it's genuinely tempting to keep extending a method name well past the point where a real query string would be clearer — the failure mode isn't a crash, it's a barely-readable 10-clause method signature nobody wants to touch.

  • Letting a derived method name grow unreadably long (findByStatusAndCustomerIdAndCreatedAtBetweenOrderByCreatedAtDesc) instead of switching to @Query.
  • Not testing derived methods against a real database — a typo'd field reference only fails at Spring's context startup, and some mocked-repository test setups skip straight past that validation.
  • Assuming save() always inserts — for an entity that already has an ID set, save() actually calls merge() underneath (an UPDATE, not an INSERT), which surprises people expecting identical behavior every time.

Repository method parsing happens once at startup (cached) — the per-call overhead of a derived query method is essentially identical to calling EntityManager directly by hand, as in this chapter's opening comparison. saveAll() batches multiple entity saves more efficiently than looping individual save() calls, for bulk operations.

Never build a repository method dynamically from concatenated user input — this reopens exactly the JPQL injection risk that Spring Data's parameter-binding mechanisms are specifically designed to prevent by default.

Enable SQL logging (covered properly in a later chapter) specifically to see what query a derived method like findByCustomerIdAndStatus actually generated — this is the concrete way to confirm your method name was parsed the way you intended.

Keep repository interfaces under the same code review scrutiny as hand-written SQL — a subtly wrong derived query name (findByStatusOrCustomerId where And was intended) is just as much a production risk as a subtly wrong SQL statement, and is easy to misread quickly during review.

  1. Create a JpaRepository<Order, Long> interface with zero implementation and confirm save()/findById()/findAll() all work immediately.
  2. Add the derived method from this chapter's story (findByCustomerIdAndStatus), then check the SQL logs to confirm exactly what query Spring Data generated from the name.
  3. Deliberately introduce a typo in a derived method's field reference and confirm the application fails at startup, not at call time.
  4. Call save() on an entity that already has an ID set, and confirm (via logging) Hibernate generates an UPDATE, not an INSERT.

✓ Quick recap

  • JpaRepository is the standard interface to extend — it gives you CRUD, paging/sorting, and batch operations with zero code.
  • Spring generates a working implementation at startup via a dynamic proxy — you never write the method body yourself.
  • Derived query methods parse the method NAME into a JPQL query, validated eagerly at startup — a typo fails immediately, not silently at runtime.
  • Switch to @Query once a derived method name's complexity outgrows what a readable name can express.

Want a visual for this concept?

Generate a diagram tailored to “Spring Data JPA — Repositories, Derived Queries & Auto-Configuration” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.

Sign in to generate a visual →

Practice quiz

Next Step

Continue to JPQL, Native Queries, Specifications & Criteria API← Back to all Spring Data JPA & Hibernate Mastery chapters