Repositories & @Repository

Reviewed & published by Brayan K

The repository pattern gives you a collection-like abstraction over persistence, and @Repository marks that data-access bean — the clean boundary between your business logic and the data store.

Part of the free Java course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

Before You Start

You should understand the service layer and interfaces, since a repository is fundamentally an interface that services depend on.

What You'll Learn

The Big Idea — A Librarian at the Counter

💡 Analogy: A repository is like a librarian. You ask "find book #42" or "store this book", and the librarian handles it — you never see whether the books are on shelves, in a basement archive, or scanned into a database. The librarian is the single counter (boundary) between you and the storage. Swap the basement for a digital archive and your request doesn't change at all.

That counter is the persistence boundary: callers stay blissfully ignorant of how storage works.

1️⃣ The Repository Pattern

Define an interface with collection-like operations — save, findById, findAll, deleteById. Callers depend on this abstraction, never on the storage details behind it.

// (Conceptual — illustrates the repository PATTERN.)
package com.example.demo;

import java.util.List;
import java.util.Optional;

// The repository is a COLLECTION-LIKE abstraction over persistence.
// Callers depend on THIS interface, not on how data is actually stored.
public interface UserRepository {
    User save(User user);
    Optional<User> findById(long id);
    List<User> findAll();
    void deleteById(long id);
}

2️⃣ The @Repository Bean

@Repository registers the data-access bean and, crucially, enables persistence-exception translation into Spring's unchecked DataAccessException hierarchy.

package com.example.demo;

import org.springframework.stereotype.Repository;
import java.util.*;

// @Repository: a data-access bean. Besides registering it for injection, it
// enables PERSISTENCE-EXCEPTION TRANSLATION into Spring's DataAccessException.
@Repository
public class InMemoryUserRepository implements UserRepository {
    private final Map<Long, User> store = new HashMap<>();

    public User save(User user) { store.put(user.id(), user); return user; }
    public Optional<User> findById(long id) { return Optional.ofNullable(store.get(id)); }
    public List<User> findAll() { return new ArrayList<>(store.values()); }
    public void deleteById(long id) { store.remove(id); }
}

3️⃣ Services Depend on Repositories

The service injects the repository interface and uses it. Business logic stays persistence-ignorant — and you can inject an in-memory fake in tests.

package com.example.demo;

import org.springframework.stereotype.Service;
import java.util.Optional;

@Service
public class UserService {

    private final UserRepository users;   // depend on the REPOSITORY abstraction

    public UserService(UserRepository users) {   // injected by Spring
        this.users = users;
    }

    // Business logic stays persistence-IGNORANT: it just uses the repository.
    public User register(User user) {
        return users.save(user);
    }
    public Optional<User> find(long id) {
        return users.findById(id);
    }
}

4️⃣ The Pattern, Made Concrete

Here is the whole pattern in plain Java with real output: an interface, an in-memory implementation, and callers that could swap to a DB-backed version without changing.

import java.util.*;

public class Main {
    record User(long id, String name) {}

    // The repository PATTERN in plain Java: a collection-like abstraction.
    interface UserRepository {
        User save(User u);
        Optional<User> findById(long id);
        List<User> findAll();
    }
    static class InMemoryUserRepository implements UserRepository {
        private final Map<Long, User> store = new HashMap<>();
        public User save(User u) { store.put(u.id(), u); return u; }
        public Optional<User> findById(long id) { return Optional.ofNullable(store.get(id)); }
        public List<User> findAll() { return new ArrayList<>(store.values()); }
    }

    public static void main(String[] args) {
        // Callers stay persistence-ignorant — swap this impl for a DB-backed one
        // later WITHOUT changing any caller code.
        UserRepository repo = new InMemoryUserRepository();
        repo.save(new User(1, "Ada"));
        repo.save(new User(2, "Linus"));

        System.out.println("count:   " + repo.findAll().size());
        System.out.println("findById 1: " + repo.findById(1).map(User::name).orElse("none"));
        System.out.println("findById 9: " + repo.findById(9).map(User::name).orElse("none"));
    }
}
import java.util.*;

public class Main {
    // 🎯 YOUR TURN — replace each ___ using the hint beside it.
    //
    // Spring Data writes this class for you. Writing it once by hand is the
    // fastest way to stop it feeling like magic.

    record Book(long id, String title, int pages) {}

    // 1) The contract the rest of the app depends on — not the class.
    ___ BookRepository {                          // 👉 replace ___ with interface
        Book save(Book b);
        Optional<Book> findById(long id);
        List<Book> findAll();
        boolean deleteById(long id);
    }

    // 2) One possible implementation. Swap it for a JDBC one and nothing
    //    above has to change.
    static class InMemoryBookRepository ___ BookRepository {   // 👉 replace ___ with implements
        private final Map<Long, Book> rows = new LinkedHashMap<>();
        private long nextId = 1;

        public Book save(Book b) {
            Book stored = b.id() == 0 ? new Book(nextId++, b.title(), b.pages()) : b;
            rows.put(stored.id(), stored);
            return stored;
        }

        // 3) A row that may not be there. Optional says so in the type,
        //    where returning null says nothing at all.
        public Optional<Book> findById(long id) { return Optional.___(rows.get(id)); }  // 👉 replace ___ with ofNullable

        public List<Book> findAll() { return List.copyOf(rows.values()); }

        // 4) remove() gives back what it removed, or null if there was nothing.
        public boolean deleteById(long id) { return rows.remove(id) ___ null; }   // 👉 replace ___ with !=
    }

    public static void main(String[] args) {
        BookRepository repo = new InMemoryBookRepository();
        Book dune = repo.save(new Book(0, "Dune", 412));
        repo.save(new Book(0, "Emma", 474));

        System.out.println("Saved with id " + dune.id());
        System.out.println("All: " + repo.findAll().size());

        // 5) Unwrap the Optional with a fallback instead of testing for null.
        System.out.println("Found: " + repo.findById(1).map(Book::title).___("nothing"));  // 👉 replace ___ with orElse
        System.out.println("Missing: " + repo.findById(99).map(Book::title).orElse("nothing"));

        System.out.println("Deleted 2? " + repo.deleteById(2));
        System.out.println("Deleted 2 again? " + repo.deleteById(2));
        System.out.println("Left: " + repo.findAll());
    }
}

🧠 Quick Recall

Predict before you peek.

Answer: a collection-like interface over persistence, hiding how data is stored.

Answer: persistence-exception translation into Spring's unchecked DataAccessException hierarchy.

Answer: the service layer — it uses the repository to read and write data.

Common Errors

Pro Tips

📋 Quick Reference

ConceptSyntax / ideaNote
patterncollection-like APIOver persistence
data bean@RepositoryPersistence layer
stereotype of@Component+ exception translation
errorsDataAccessExceptionUnchecked, agnostic
boundarylogic ⟷ data storePersistence ignorance
depends on itthe service layerVia the interface

📚 Resources / Keep Learning

🎉 Lesson Complete!

You now understand the repository pattern as a collection-like abstraction over persistence, what @Repository marks and the exception translation it adds, and why the repository is the clean boundary between business logic and the data store.

Practice quiz

What does the repository pattern provide?

  • A collection-like abstraction over data access / persistence
  • A UI framework
  • A logging system
  • A thread pool

Answer: A collection-like abstraction over data access / persistence. The repository pattern abstracts persistence behind a collection-like interface, hiding how data is actually stored.

What does @Repository mark?

  • A controller
  • A bean in the persistence (data access) layer
  • A config class
  • A scheduled task

Answer: A bean in the persistence (data access) layer. @Repository is a stereotype of @Component marking a data-access bean in the persistence layer.

Besides registering a bean, @Repository enables...

  • HTTP routing
  • JSON serialisation
  • persistence-exception translation into Spring's DataAccessException
  • transaction creation

Answer: persistence-exception translation into Spring's DataAccessException. @Repository triggers exception translation, converting provider-specific exceptions into Spring's unchecked DataAccessException hierarchy.

The repository is the boundary between...

  • the controller and the view
  • business logic and the data store
  • Java and the JVM
  • the client and the network

Answer: business logic and the data store. Repositories form the persistence boundary, separating business logic from the details of the underlying data store.

Which layer typically depends on a repository?

  • The controller directly, always
  • application.properties
  • The main method
  • The service layer

Answer: The service layer. Services depend on repositories to read and write data, keeping persistence behind that boundary.

A key benefit of programming to a repository interface is...

  • faster compilation
  • swapping the implementation (e.g. in-memory for tests) without changing callers
  • smaller jars
  • automatic UI

Answer: swapping the implementation (e.g. in-memory for tests) without changing callers. Coding against an interface lets you substitute an in-memory fake in tests or change the storage technology freely.

Is @Repository required for component scanning to find the bean?

  • No — but it documents intent and adds exception translation
  • Yes, scanning ignores @Component
  • Only in tests
  • Only for JPA

Answer: No — but it documents intent and adds exception translation. Any @Component is scanned; @Repository additionally conveys the data-access role and enables exception translation.

What does 'persistence ignorance' in callers mean?

  • Callers know the SQL dialect
  • Callers disable persistence
  • Callers use the repository without knowing the storage mechanism
  • Callers cache everything

Answer: Callers use the repository without knowing the storage mechanism. Callers depend on the repository abstraction and stay ignorant of whether it's JDBC, JPA, or in-memory.

Which is NOT a responsibility of a repository?

  • Saving entities
  • Finding entities by id
  • Defining HTTP status codes
  • Deleting entities

Answer: Defining HTTP status codes. HTTP status codes are a web-layer concern; a repository handles persistence operations only.

Spring's DataAccessException hierarchy is...

  • checked exceptions you must catch
  • unchecked (runtime) and storage-technology-agnostic
  • only for JDBC
  • deprecated

Answer: unchecked (runtime) and storage-technology-agnostic. DataAccessException is an unchecked, consistent abstraction over different persistence technologies' exceptions.

Continue this course

Frequently asked questions

What is the repository pattern?

The repository pattern is a design pattern that provides a collection-like abstraction over data access. Instead of scattering database calls throughout your code, you define a repository — an interface with methods such as save, findById, findAll and deleteById — that callers use as if it were an in-memory collection of domain objects. The actual persistence mechanism (JDBC, JPA/Hibernate, an in-memory map, a remote API) lives behind that interface and is invisible to callers. This centralises data-access logic, makes the rest of the application 'persistence ignorant', and lets you change or mock the storage technology without touching the business code that depends on the repository.

What does the @Repository annotation do?

@Repository is a Spring stereotype, meta-annotated with @Component, that marks a class as a data-access bean in the persistence layer. Like any component it is detected by scanning and registered for injection, but it adds one important behaviour: persistence-exception translation. When a @Repository bean is wrapped by Spring's infrastructure, provider-specific exceptions (such as a JPA or JDBC exception) are translated into Spring's consistent, unchecked DataAccessException hierarchy. This means your service layer can handle data errors uniformly without coupling to the underlying technology's exception types. Semantically, @Repository also documents that the class belongs to the persistence layer, clarifying your architecture.

Why is the repository called a 'persistence boundary'?

Because it is the single, well-defined seam between your business logic and the data store. Everything above the boundary — services and controllers — speaks only in domain terms (save this user, find that order) through the repository interface, and knows nothing about tables, SQL, connections or an ORM. Everything below the boundary — the repository implementation — deals with the storage details. Concentrating all persistence concerns at this boundary keeps the rest of the codebase clean and decoupled, makes it possible to swap an in-memory implementation in for fast tests, and means a change of database technology is contained to the repository layer rather than rippling through the whole application.

How does the repository fit with the service layer?

They form adjacent layers: services depend on repositories, not the other way around. A service holds business rules and orchestration and, when it needs to read or write data, delegates to an injected repository. The repository performs the persistence operation and returns domain objects (or Optionals/collections of them). This keeps a clean flow — controller delegates to service, service uses repository, repository talks to the store — with each layer focused on one responsibility. Because the service depends on the repository interface, you can unit-test the service by injecting a fake in-memory repository, with no database required.

Do I always have to write the repository implementation myself?

Not with Spring Data. While this lesson focuses on the pattern and the @Repository stereotype using a hand-written in-memory implementation to make the idea concrete, Spring Data JPA can generate the implementation for you: you declare an interface extending JpaRepository (or CrudRepository) for your entity, and Spring provides save, findById, findAll, delete and more at runtime, plus derived query methods from method names. That is the subject of the next lesson. The repository pattern and @Repository semantics you learn here are the foundation that makes Spring Data's generated repositories feel natural — they are the same abstraction, automated.

Related lessons