@Repository
Marks a data access class as a bean and turns database exceptions thrown from it into Spring's DataAccessException hierarchy.
@Repository | @Repository("beanName")- Added in Spring Boot Spring Framework 2.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$valueoptional | String= "" | Explicit bean name. When empty, the simple class name with a lower-case first letter is used. |
Return value
n/aRegisters the class through component scanning and marks it for persistence exception translation, so technology-specific exceptions surface as DataAccessException subclasses.
Errors
Exceptions thrown by the bean are translated into subclasses of org.springframework.dao.DataAccessException (all unchecked).
@Repository marks a class that loads and stores data. Like @Service, it is meta-annotated with @Component, so scanning registers it. Unlike @Service, it also changes behaviour: Spring Boot registers a PersistenceExceptionTranslationPostProcessor, which wraps every @Repository bean in a proxy that translates persistence exceptions into Spring's DataAccessException hierarchy.
Why translation matters
Without it, a service that calls JPA directly would catch jakarta.persistence.PersistenceException or a Hibernate exception, and one that uses JDBC would see SQLException. With it, callers see the same unchecked types whatever the technology: DuplicateKeyException, DataIntegrityViolationException, OptimisticLockingFailureException, EmptyResultDataAccessException. Your error handler can map DataIntegrityViolationException to a 409 once, and that mapping keeps working if a repository moves from JPA to JdbcClient.
Spring's JDBC classes (JdbcClient, JdbcTemplate) already translate their own exceptions, so for them the annotation mainly documents the role. Where it really changes behaviour is a class that uses an EntityManager directly. Bear in mind that persist() usually only queues an insert: a constraint violation tends to appear at flush or commit, outside the repository method, where the transaction manager translates it instead. The translation can be switched off with spring.dao.exceptiontranslation.enabled=false, but there is rarely a reason to.
Spring Data interfaces do not need it
An interface that extends JpaRepository or CrudRepository is detected by Spring Data, not by component scanning, and its generated implementation already translates exceptions. Adding @Repository to it does no harm but is redundant. Use the annotation on classes you write yourself.
Mistakes to avoid
- Putting business rules in repositories. A repository answers "load this" and "store that"; deciding whether an order may be cancelled belongs in a service.
- Catching
DataAccessExceptioninside the repository and returningnull. The caller loses the cause, and a database outage looks like "not found". - Leaking the exceptions to clients. A raw
DataIntegrityViolationExceptionmessage contains constraint and table names; translate it to a problem response without the SQL detail.
Repositories are also where transactions are commonly misunderstood. A @Repository method is not transactional on its own; Spring Data's built-in methods are, but your own class needs @Transactional on the service that calls it.
Example
import java.util.List;
import java.util.Optional;
import org.springframework.jdbc.core.simple.JdbcClient;
import org.springframework.stereotype.Repository;
record Customer(long id, String email, String name) {
}
public class CustomerRepository {
private final JdbcClient jdbc;
public CustomerRepository(JdbcClient jdbc) {
this.jdbc = jdbc;
}
public Optional<Customer> findById(long id) {
return jdbc.sql("select id, email, name from customer where id = :id")
.param("id", id)
.query(Customer.class)
.optional();
}
public List<Customer> findByNamePrefix(String prefix) {
return jdbc.sql("select id, email, name from customer where name like :p order by name")
.param("p", prefix + "%")
.query(Customer.class)
.list();
}
public int insert(String email, String name) {
return jdbc.sql("insert into customer (email, name) values (:email, :name)")
.param("email", email)
.param("name", name)
.update();
}
}When to use it
- A JdbcClient-based repository for reporting queries that JPA handles poorly
- Wrapping direct EntityManager access so callers see DataAccessException types
- Turning a unique-constraint violation into a 409 Conflict through a domain exception
Frequently asked questions
Do I need @Repository on an interface that extends JpaRepository?
No. Spring Data finds those interfaces itself and its implementations already translate exceptions. Use @Repository on classes you write.
Does @Repository make methods transactional?
No. It only registers the bean and adds exception translation. Put @Transactional on the service method that defines the unit of work.
More examples
An EntityManager repository, where translation matters most
Because the class carries @Repository, exceptions thrown inside its methods, such as a NonUniqueResultException from a query or an EntityExistsException from persist(), reach the caller as DataAccessException subclasses. A unique-constraint violation usually surfaces later, at flush or commit, where JpaTransactionManager translates it in the same way. persist() also needs a transaction, so call save() from a @Transactional service method.
import java.util.List;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityManager;
import jakarta.persistence.Id;
import jakarta.persistence.PersistenceContext;
import org.springframework.stereotype.Repository;
@Entity
class Invoice {
@Id
Long id;
String number;
boolean paid;
}
@Repository
public class InvoiceRepository {
@PersistenceContext
private EntityManager em;
public List<Invoice> findUnpaid() {
return em.createQuery("select i from Invoice i where i.paid = false order by i.number", Invoice.class)
.getResultList();
}
public void save(Invoice invoice) {
em.persist(invoice);
}
}Handling a translated exception in the service
Relying on the unique constraint instead of a select-then-insert check avoids a race between two simultaneous sign-ups. The service turns the translated exception into a domain exception that an error handler can map to 409.
import org.springframework.dao.DuplicateKeyException;
import org.springframework.jdbc.core.simple.JdbcClient;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
@Repository
class UserRepository {
private final JdbcClient jdbc;
UserRepository(JdbcClient jdbc) {
this.jdbc = jdbc;
}
void insert(String email) {
jdbc.sql("insert into app_user (email) values (?)").param(email).update();
}
}
class EmailTakenException extends RuntimeException {
EmailTakenException(String email) {
super("Email already registered: " + email);
}
}
@Service
public class SignUpService {
private final UserRepository users;
public SignUpService(UserRepository users) {
this.users = users;
}
public void register(String email) {
try {
users.insert(email);
} catch (DuplicateKeyException e) {
throw new EmailTakenException(email);
}
}
}A Spring Data repository needs no annotation
Spring Data creates the implementation and applies exception translation to it. Adding @Repository here would be harmless but redundant.
import java.util.List;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import org.springframework.data.jpa.repository.JpaRepository;
@Entity
class Product {
@Id
@GeneratedValue
Long id;
String sku;
boolean active;
}
// Detected by Spring Data's repository scanning, not by @Repository.
public interface ProductRepository extends JpaRepository<Product, Long> {
List<Product> findByActiveTrueOrderBySku();
}
Discussion