RadhaPublication
HomeInterviewLearning PathsRole TracksMockBooksPricing
Login / Sign UpLog in

© 2026 Radha Publication. All rights reserved.

Shop on AmazonDonateContactPrivacyTerms
Spring Boot questions
MediumSpring Boot#187 min readJul 11, 2026

Difference between @Component, @Service and @Repository.

practice
learning
Practice modeTest yourself instead of reading straight through

Hook: Interviewers love this question because it quickly shows whether you see Spring annotations as random labels or as clear role markers.

Question: Difference between @Component, @Service and @Repository.

Answer: @Component is the general Spring stereotype for any managed bean. @Service is a specialized @Component for business logic, and @Repository is a specialized @Component for persistence or data access. All three can be found by component scanning, but @Repository can also activate exception translation so low-level persistence errors become Spring DataAccessException types.

Interview-Ready Answer: I’d say all three create Spring beans, but they communicate different intent. @Component is the generic choice for any Spring-managed class. @Service marks business logic, and @Repository marks the persistence layer and gives one extra benefit: Spring can translate database and JPA exceptions into its own unchecked data-access exceptions. So in practice, I use @Service for orchestration and rules, @Repository for DB access, and @Component for shared helpers.

🧠 Memory Map
Memory map — visual summary of this topic

Big picture

@Component, @Service, and @Repository are all stereotype annotations. A stereotype annotation is a label that tells Spring what role a class plays. @Service and @Repository are meta-annotated with @Component, which means they are built on top of the generic bean annotation.

How Spring uses them under the hood

  1. Spring Boot starts component scanning from the package that contains @SpringBootApplication and walks the subpackages.
  2. When it finds a class annotated with @Component, @Service, or @Repository, it registers a bean definition in the application context.
  3. At startup, Spring creates the bean, resolves constructor dependencies, and usually stores it as a singleton, which means one shared instance per application context by default.
  4. @Service does not add special runtime behavior by default; it mainly tells humans, tests, and tooling that this class holds business logic.
  5. @Repository is the special one: Spring can apply exception translation so persistence exceptions from JPA, Hibernate, or JDBC are converted into Spring’s unchecked data-access exception hierarchy.
  6. Later, controllers, other services, or scheduled jobs inject these beans and call them directly; no manual new is needed.

When to use each one

  • @Component: use for generic reusable helpers, mappers, formatters, adapters, or infrastructure code that does not clearly fit service or repository.
  • @Service: use for business operations and orchestration, such as placeOrder, calculateDiscount, or registerUser.
  • @Repository: use for persistence code, such as custom DAO classes, query objects, or adapters that talk to the database.

Comparison table

AnnotationMain meaningExtra behaviorTypical place
@ComponentGeneric beanNoneUtilities
@ServiceBusiness layerSemantic onlyUse-case logic
@RepositoryData accessException translationDAO / persistence

Performance and complexity

At runtime, the annotation choice itself has almost no cost. The main cost happens at startup during component scanning, which is roughly O(n) in the number of candidate classes Spring must inspect. Bean lookup after startup is effectively O(1) because the container keeps bean definitions in maps. In a small app, scanning is usually fast; in a very large monolith with thousands of classes and broad package scanning, startup can add noticeable time, sometimes from hundreds of milliseconds into seconds. Also remember that if a bean gets proxied for transactions or repository advice, there is a small extra startup and memory cost, but still not a per-request penalty worth worrying about in normal systems.

Important edge cases

  • @Repository does not magically fix every exception; it helps when persistence exceptions escape the repository boundary and Spring’s translation post-processor can intercept them.
  • @Service does not mean transactional by itself. You still need @Transactional when you want a transaction boundary.
  • If a class is outside component scan, none of these annotations help because Spring never sees the class.
  • Annotating a class with both @Component and @Service is redundant; use the most specific stereotype that matches the role.
  • If you create your own composed annotation, such as a custom @PaymentService built on @Service, you keep the same scanning behavior and improve readability.

Real-world story

Imagine a checkout service in an e-commerce app. The team has a PricingComponent that formats prices, an OrderService that calculates totals and applies coupons, and an OrderRepository that writes orders to the database. This separation makes testing much easier: the business rules can be tested without a database, and the repository can be tested with a smaller persistence-focused test.

Now the bug story: a developer put the database write logic directly inside @Service code and used low-level JPA calls there. When duplicate order IDs started appearing during a flash sale, the app threw a raw ConstraintViolationException and returned a generic 500 to users. Logs were noisy, retries were triggered, and some customers saw failed checkouts even though the payment had already been authorized. After the team moved the persistence code into @Repository, kept business validation in @Service, and used the formatter as a plain @Component, the duplicate-key problem became a clean Spring data-access exception that the API layer could map to a sensible client error.

What went wrong: the team blurred layers. The result was harder testing, poor exception mapping, and unclear ownership of business rules versus database rules.

Spring Boot
package com.example.demo;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityManager;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    @Bean
    HibernatePropertiesCustomizer hibernateCustomizer() {
        // This keeps the demo runnable without an external schema file.
        return props -> props.put("hibernate.hbm2ddl.auto", "create-drop");
    }

    @Bean
    PersistenceExceptionTranslationPostProcessor exceptionTranslationPostProcessor() {
        // This is what lets @Repository turn persistence errors into Spring's DataAccessException types.
        return new PersistenceExceptionTranslationPostProcessor();
    }

    @Bean
    CommandLineRunner demo(RegistrationService registrationService) {
        return args -> {
            System.out.println("First registration succeeds");
            AppUser saved = registrationService.register("alice@example.com", "alice");
            System.out.println("Saved user id=" + saved.getId() + ", email=" + saved.getEmail());

            System.out.println();
            System.out.println("Duplicate email shows repository exception translation");
            try {
                registrationService.register("alice@example.com", "alice clone");
            } catch (DataIntegrityViolationException ex) {
                System.out.println("Caught translated Spring exception: " + ex.getClass().getSimpleName());
                System.out.println("Most specific cause: " + ex.getMostSpecificCause().getClass().getSimpleName());
            }

            System.out.println();
            System.out.println("Invalid input is handled in the service layer");
            try {
                registrationService.register("   ", "bad");
            } catch (IllegalArgumentException ex) {
                System.out.println("Service validation failed: " + ex.getMessage());
            }
        };
    }
}

@Component
class NameFormatter {
    // Generic reusable helper: not business logic, not persistence.
    String format(String rawName) {
        if (rawName == null || rawName.isBlank()) {
            return "Guest";
        }
        String cleaned = rawName.trim();
        return cleaned.substring(0, 1).toUpperCase() + cleaned.substring(1);
    }
}

@Service
class RegistrationService {
    private final AppUserRepository repository;
    private final NameFormatter formatter;

    RegistrationService(AppUserRepository repository, NameFormatter formatter) {
        this.repository = repository;
        this.formatter = formatter;
    }

    @Transactional
    AppUser register(String email, String rawName) {
        // Business rule belongs here so controllers stay thin and repositories stay focused.
        if (email == null || email.isBlank()) {
            throw new IllegalArgumentException("Email must not be blank");
        }

        AppUser user = new AppUser();
        user.setEmail(email.trim().toLowerCase());
        user.setDisplayName(formatter.format(rawName));
        return repository.save(user);
    }
}

@Repository
class AppUserRepository {
    @PersistenceContext
    private EntityManager entityManager;

    AppUser save(AppUser user) {
        // Force the insert now so the duplicate-key failure appears inside this method.
        entityManager.persist(user);
        entityManager.flush();
        return user;
    }
}

@Entity
@Table(name = "app_users", uniqueConstraints = @UniqueConstraint(name = "uk_app_users_email", columnNames = "email"))
class AppUser {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String email;

    @Column(nullable = false)
    private String displayName;

    protected AppUser() {
        // JPA needs a no-arg constructor.
    }

    Long getId() {
        return id;
    }

    String getEmail() {
        return email;
    }

    void setEmail(String email) {
        this.email = email;
    }

    String getDisplayName() {
        return displayName;
    }

    void setDisplayName(String displayName) {
        this.displayName = displayName;
    }
}

Follow-up & Tricky Questions:

  • Q: Can I replace all three with @Component?
    A: Yes, the app will still work in most cases, but you lose the layer meaning that helps teams read the code. You also lose the special persistence exception translation that comes with @Repository.
  • Q: Is @Service required for @Transactional?
    A: No. @Transactional works on any Spring bean, not only on services. Teams usually put it on services because that is where business operations naturally start and end.
  • Q: Should repository methods contain business rules?
    A: Usually no. Repositories should focus on loading and saving data; business decisions belong in services so the rules stay reusable and easier to test.
  • Q: What happens if the package is not scanned?
    A: Spring will not create the bean at all, so dependency injection fails with a missing bean error. The annotation only helps if the class is in a scanned package.
  • Q: Can I create my own stereotype annotation?
    A: Yes. You can make a custom annotation meta-annotated with @Component or one of its specializations to keep intent clear across a large codebase.
  • Tricky: Is there any runtime difference between @Component and @Service?
    A: Usually no. The main difference is semantic: @Service says this bean belongs to the business layer, which helps humans and tooling.
  • Tricky: Does @Repository translate every exception automatically?
    A: No. It mainly helps with persistence exceptions that leave the repository boundary and can be intercepted by Spring’s exception translation mechanism.
  • Tricky: If I annotate an interface with @Service, does Spring create the implementation?
    A: No. Spring still needs a concrete class to instantiate, unless a framework like Spring Data creates a proxy for the interface.

Common Mistakes:

  • Mistake: Thinking @Service has special runtime behavior. Correction: It is mostly a semantic specialization of @Component.
  • Mistake: Using @Repository for any random helper that happens to call a database once. Correction: Use it for the persistence boundary, where exception translation and data access concerns belong.
  • Mistake: Putting business rules in repositories. Correction: Keep rules in services so the design stays testable and clean.
  • Mistake: Forgetting that component scanning controls whether the annotation is even seen. Correction: Place classes under the scanned root package or add explicit scanning configuration.

Memory Hook: Think of a company building: @Component is any office worker, @Service is the team that solves customer problems, and @Repository is the secure archive that stores records and handles storage errors.

Cheat Sheet:

  • @Component = generic Spring bean.
  • @Service = business layer stereotype.
  • @Repository = persistence layer stereotype.
  • All three are discovered by component scanning.
  • @Repository adds exception translation.
  • @Service is mainly for clarity and design, not extra runtime magic.

Practice Tasks:

  • Take a small Spring Boot app and rename one helper class from @Component to @Service; explain why the name is more accurate.
  • Create a custom repository that throws a duplicate-key error and watch how @Repository changes the exception type.
  • Refactor one controller so it calls only a service, and move all persistence code into a repository.
Previous
Back to Questions←→to navigate
Next

Recommended Resources

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

package com.example.demo; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EntityManager; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.PersistenceContext; import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor; import org.springframework.stereotype.Component; import org.springframework.stereotype.Repository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } @Bean HibernatePropertiesCustomizer hibernateCustomizer() { // This keeps the demo runnable without an external schema file. return props -> props.put("hibernate.hbm2ddl.auto", "create-drop"); } @Bean PersistenceExceptionTranslationPostProcessor exceptionTranslationPostProcessor() { // This is what lets @Repository turn persistence errors into Spring's DataAccessException types. return new PersistenceExceptionTranslationPostProcessor(); } @Bean CommandLineRunner demo(RegistrationService registrationService) { return args -> { System.out.println("First registration succeeds"); AppUser saved = registrationService.register("alice@example.com", "alice"); System.out.println("Saved user id=" + saved.getId() + ", email=" + saved.getEmail()); System.out.println(); System.out.println("Duplicate email shows repository exception translation"); try { registrationService.register("alice@example.com", "alice clone"); } catch (DataIntegrityViolationException ex) { System.out.println("Caught translated Spring exception: " + ex.getClass().getSimpleName()); System.out.println("Most specific cause: " + ex.getMostSpecificCause().getClass().getSimpleName()); } System.out.println(); System.out.println("Invalid input is handled in the service layer"); try { registrationService.register(" ", "bad"); } catch (IllegalArgumentException ex) { System.out.println("Service validation failed: " + ex.getMessage()); } }; } } @Component class NameFormatter { // Generic reusable helper: not business logic, not persistence. String format(String rawName) { if (rawName == null || rawName.isBlank()) { return "Guest"; } String cleaned = rawName.trim(); return cleaned.substring(0, 1).toUpperCase() + cleaned.substring(1); } } @Service class RegistrationService { private final AppUserRepository repository; private final NameFormatter formatter; RegistrationService(AppUserRepository repository, NameFormatter formatter) { this.repository = repository; this.formatter = formatter; } @Transactional AppUser register(String email, String rawName) { // Business rule belongs here so controllers stay thin and repositories stay focused. if (email == null || email.isBlank()) { throw new IllegalArgumentException("Email must not be blank"); } AppUser user = new AppUser(); user.setEmail(email.trim().toLowerCase()); user.setDisplayName(formatter.format(rawName)); return repository.save(user); } } @Repository class AppUserRepository { @PersistenceContext private EntityManager entityManager; AppUser save(AppUser user) { // Force the insert now so the duplicate-key failure appears inside this method. entityManager.persist(user); entityManager.flush(); return user; } } @Entity @Table(name = "app_users", uniqueConstraints = @UniqueConstraint(name = "uk_app_users_email", columnNames = "email")) class AppUser { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, unique = true) private String email; @Column(nullable = false) private String displayName; protected AppUser() { // JPA needs a no-arg constructor. } Long getId() { return id; } String getEmail() { return email; } void setEmail(String email) { this.email = email; } String getDisplayName() { return displayName; } void setDisplayName(String displayName) { this.displayName = displayName; } }