All three annotations do the same basic job: they mark a class so that component scanning finds it and registers it as a bean in the application context. @Component is the generic one. @Service and @Repository are specialisations of it (each is itself annotated with @Component), and they name the layer the class belongs to.
The difference interviewers want you to know is that only one of the three changes runtime behaviour. @Repository turns on persistence exception translation, so a class using JPA or Hibernate directly throws Spring’s DataAccessException subclasses instead of provider-specific exceptions. @Service currently adds nothing beyond @Component; it’s documentation that tools and aspects can read. Most candidates say “they’re all the same”, which is close but misses the one point that makes the question worth asking.
Comparison at a glance
@Component |
@Service |
@Repository |
|
|---|---|---|---|
| Registers a bean via scanning | Yes | Yes (meta-annotated with @Component) |
Yes (meta-annotated with @Component) |
| Intended layer | Anything that isn’t more specific | Business logic | Data access |
| Extra runtime behaviour | None | None | Exception translation to DataAccessException |
| Wraps the bean in a proxy | No | No | Yes, added by the exception translation post-processor |
| Default bean name | Class name with first letter lower-cased | Same | Same |
| Scope | Singleton unless @Scope says otherwise |
Same | Same |
| Makes methods transactional | No | No | No |
@Controller (and @RestController) belong to the same family: they’re @Component specialisations for the web layer, and Spring MVC uses them to find request handlers. @Configuration is also meta-annotated with @Component, which is why configuration classes are picked up by the same scan.
How scanning treats them
@SpringBootApplication includes @ComponentScan, which by default scans the package of the application class and everything below it. The scanner looks for classes carrying @Component directly or through a meta-annotation, which is exactly what @Service and @Repository are:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {
@AliasFor(annotation = Component.class)
String value() default "";
}That’s (roughly) the real declaration from org.springframework.stereotype. The value attribute is an alias for the bean name, so @Service("billing") names the bean billing, just as @Component("billing") would.
Because they’re processed identically, swapping @Service for @Component on a service class changes nothing at runtime. The Spring Framework reference still recommends @Service for the service layer, partly because the stereotypes “may also carry additional semantics in future releases”, and partly because they’re useful today as markers. An aspect can target a layer with a pointcut like @within(org.springframework.stereotype.Service), and architecture tests (ArchUnit, for example) can check that classes annotated @Repository aren’t called from controllers.
The code below is illustrative Spring Boot 3 code (Java 17, Hibernate 6); it wasn’t run for this page, and exact exception subclasses can vary between versions.
What @Repository actually does
Here’s a hand-written data access class that uses EntityManager directly:
@Repository
public class CustomerDao {
@PersistenceContext
private EntityManager em;
public void insert(Customer customer) {
em.persist(customer);
em.flush(); // force the INSERT now, so a constraint violation surfaces here
}
}Suppose the customers table has a unique constraint on email, and a second customer with an existing email is inserted. Hibernate reports the violation with its own exception type, org.hibernate.exception.ConstraintViolationException (which, in Hibernate 6, is also a jakarta.persistence.PersistenceException).
With @Repository on the class, that exception never reaches the caller. Spring Boot’s auto-configuration registers a PersistenceExceptionTranslationPostProcessor, which wraps every @Repository bean in a proxy. When a method throws, the proxy asks the available PersistenceExceptionTranslators (for JPA, the entity manager factory bean acts as one) to convert it, and the caller receives a DataIntegrityViolationException (or one of its subclasses, such as DuplicateKeyException), with the Hibernate exception kept as the cause.
Change the annotation to @Component or @Service and the bean is still created and still works, but the raw Hibernate exception escapes. Anything written against Spring’s hierarchy, such as a handler for DataIntegrityViolationException or retry logic for OptimisticLockingFailureException, quietly stops matching.
Why bother translating? Because callers shouldn’t depend on which persistence technology you picked. Spring’s DataAccessException hierarchy is unchecked and organised by meaning: DataIntegrityViolationException for constraint failures, OptimisticLockingFailureException for version conflicts, CannotAcquireLockException, QueryTimeoutException and so on. A service layer can react to “duplicate” or “conflict” without importing anything from Hibernate, JDBC or a specific database driver.
Notice the flush() in the example. JPA delays SQL until it has to, often until the transaction commits, and commit happens after insert returns, outside the repository proxy. In that case the error surfaces at commit time; JpaTransactionManager translates commit failures too, but the exception now comes from the transactional boundary, not from the repository method. When you need to handle a constraint violation at a specific call, flush inside the repository method.
When you don’t need @Repository
Two very common cases already translate without it:
- Spring Data repositories. An interface like
interface CustomerRepository extends JpaRepository<Customer, Long>gets a generated implementation with exception translation built into its proxy. Adding@Repositoryto the interface is harmless but unnecessary, and Spring Data’s documentation doesn’t ask for it. JdbcTemplateandJdbcClient. They translateSQLExceptionintoDataAccessExceptionthemselves, using the database’s error codes, before the exception leaves the template.
So @Repository earns its keep mainly on classes that use EntityManager, a Hibernate Session, or another persistence API directly. Many teams still put it on any hand-written DAO, translating or not, purely for the layer marker, which is a reasonable convention.
Misconceptions interviewers listen for
- “@Service makes methods transactional.” It doesn’t. You need
@Transactionalon the class or method. The two are independent, though they often appear together. - “@Repository is required for Spring Data to find my repository.” No.
@EnableJpaRepositories(applied automatically by Spring Boot) detects interfaces extendingRepositoryand its subtypes. - “Services are singletons and components are prototypes.” Scope has nothing to do with the stereotype. Every one of them is a singleton by default.
- “I can put @Service on an interface.” You can, but scanning only registers concrete classes, so an annotation on an interface does nothing for a hand-written implementation. Put it on the implementing class.
- “If it compiles with @Component, the annotation choice doesn’t matter.” It matters for
@Repository, as shown above, and for any aspect or architecture rule keyed on stereotypes.
Exercise: the exception handler that never fires
A team’s @RestControllerAdvice maps DataIntegrityViolationException to HTTP 409 Conflict. Registering a user with an existing email should return 409, but it returns 500, and the log shows org.hibernate.exception.ConstraintViolationException thrown from UserStore.save. UserStore is annotated @Service and calls entityManager.persist(user) followed by entityManager.flush(). What’s wrong, and what are the options?
Answer
UserStore is doing data access but isn’t annotated @Repository, so no translation proxy wraps it. The Hibernate exception escapes unconverted, the advice’s DataIntegrityViolationException handler doesn’t match, and the default handler produces a 500.
Options, best first:
- Annotate
UserStorewith@Repository(it’s a data access class, so the name fits). The proxy then converts the exception toDataIntegrityViolationException, and the handler returns 409. Since the method flushes, the failure surfaces inside the proxied call. - Replace the hand-written store with a Spring Data
JpaRepositoryand callsaveAndFlush, which has translation built in. - Handle
ConstraintViolationExceptionin the advice too. It works, but it couples the web layer to Hibernate, which is precisely what translation is meant to avoid.
A complete answer also mentions checking for the race properly: rather than querying “does this email exist?” before inserting, rely on the unique constraint and translate the violation, because two concurrent requests can both pass the check.