Spring Annotations
01 @Component, @Service and @Repository
Solution and explanation
What they are
All three are stereotype annotations: they mark a class as a Spring bean so component scanning registers it in the IoC container. @Service and @Repository are specialisations of @Component that also describe the class's role in a layered architecture.
The layers of a typical application
Controller (@RestController) – receives HTTP requests
↓
Service (@Service) – business rules, transactions
↓
Repository (@Repository) – database access
↓
Database
Each annotation
- @Component: a generic Spring-managed class (a PDF generator, an SMS client, a utility).
- @Service: holds business logic. Functionally identical to @Component today, but it documents intent and is a natural target for AOP (logging, transactions).
- @Repository: data-access classes. Spring adds persistence exception translation: low-level exceptions such as SQLException or Hibernate exceptions become Spring's unchecked DataAccessException hierarchy, so the service layer does not depend on JDBC or JPA specifics.
Example: job portal
@Component
public class SlugGenerator {
public String slug(String title) { return title.toLowerCase().replaceAll("[^a-z0-9]+", "-"); }
}
@Repository
public class JdbcJobRepository {
private final JdbcTemplate jdbc;
public JdbcJobRepository(JdbcTemplate jdbc) { this.jdbc = jdbc; }
public List<String> titles() {
return jdbc.queryForList("SELECT title FROM jobs", String.class);
}
}
@Service
public class JobService {
private final JdbcJobRepository jobs;
private final SlugGenerator slugs;
public JobService(JdbcJobRepository jobs, SlugGenerator slugs) {
this.jobs = jobs;
this.slugs = slugs;
}
public List<String> jobSlugs() {
return jobs.titles().stream().map(slugs::slug).toList();
}
}
With Spring Data JPA
You usually write only an interface; Spring generates the implementation, so the interface does not need @Repository.
public interface JobRepository extends JpaRepository<Job, Long> { }
Common mistakes
- Putting business logic in controllers or repositories instead of services.
- Annotating a class outside the scanned packages and wondering why it is not injected.
- Using @Component on everything – it works, but loses the readability and exception translation benefits.
Note: @Controller and @RestController are also stereotypes of @Component, used for the web layer.
Interview follow-ups
- Can you use @Component instead of @Service? Yes, behaviour is the same; @Service communicates the layer.
- What does @Repository add? Exception translation into DataAccessException.
@Repository
public interface JobRepository
extends JpaRepository<Job, Long> {
}
@Service
public class JobService {
}
@Component
public class JobSearchHelper {
}
Create the controller, service and repository layers for a small job-search use case.
Quick check
Which statement is correct?
02 What is @Controller?
Solution and explanation
What it is
@Controller marks a class as a Spring MVC web controller: a bean whose methods handle HTTP requests. In a classic server-rendered application, controller methods return a view name (a Thymeleaf or JSP template) and put data into a Model.
How a request flows
Browser → DispatcherServlet → @Controller method → returns "jobs/list"
→ ViewResolver finds templates/jobs/list.html → HTML response
Example: rendering a page
@Controller
@RequestMapping("/jobs")
public class JobPageController {
private final JobService jobs;
public JobPageController(JobService jobs) { this.jobs = jobs; }
@GetMapping
public String list(Model model) {
model.addAttribute("jobs", jobs.findOpenJobs());
return "jobs/list"; // view name, not text
}
@GetMapping("/{id}")
public String show(@PathVariable Long id, Model model) {
model.addAttribute("job", jobs.find(id));
return "jobs/show";
}
}
<!-- src/main/resources/templates/jobs/list.html (Thymeleaf) -->
<ul>
<li th:each="job : ${jobs}" th:text="${job.title}">Job title</li>
</ul>
Returning data instead of a view
Adding @ResponseBody to a method makes Spring write the return value (JSON/text) directly to the response instead of resolving a view.
@GetMapping("/count")
@ResponseBody
public long count() { return jobs.count(); }
Form handling (POST-redirect-GET)
@PostMapping
public String create(@Valid @ModelAttribute JobForm form, BindingResult errors) {
if (errors.hasErrors()) return "jobs/new";
jobs.create(form);
return "redirect:/jobs"; // avoids double submission on refresh
}
Note: use @Controller for server-rendered pages; for JSON APIs use @RestController.
Interview follow-ups
- What is the DispatcherServlet? The front controller that receives every request and routes it to the right controller method.
- What does "redirect:" do? Sends an HTTP 302 so the browser makes a new GET request.
@Controller
public class HomeController {
@GetMapping("/")
public String home() {
return "home";
}
}
Create a controller that returns a Thymeleaf home view with one model attribute.
Quick check
Which statement is correct?
03 What is @RestController?
Solution and explanation
What it is
@RestController is a convenience annotation equal to @Controller + @ResponseBody. Every method's return value is serialised directly into the HTTP response body – typically as JSON by Jackson – instead of being treated as a view name. It is the standard choice for REST APIs consumed by React, mobile apps or other services.
Example: jobs API
@RestController
@RequestMapping("/api/v1/jobs")
public class JobApiController {
private final JobService jobs;
public JobApiController(JobService jobs) { this.jobs = jobs; }
@GetMapping
public List<JobDto> list(@RequestParam(defaultValue = "0") int page) {
return jobs.page(page);
}
@GetMapping("/{id}")
public JobDto get(@PathVariable Long id) {
return jobs.find(id);
}
@PostMapping
public ResponseEntity<JobDto> create(@Valid @RequestBody CreateJobRequest request) {
JobDto created = jobs.create(request);
return ResponseEntity.created(URI.create("/api/v1/jobs/" + created.id())).body(created);
}
}
public record JobDto(Long id, String title, String company, String location) { }
GET /api/v1/jobs/7
HTTP/1.1 200 OK
Content-Type: application/json
{"id":7,"title":"Java Developer","company":"Ascendia","location":"Pune"}
How the conversion works
- Spring finds the method for the URL and HTTP method.
- @RequestBody JSON is converted to Java objects by HttpMessageConverters (Jackson).
- The method returns an object.
- Jackson writes it as JSON, using the Accept header to pick the format.
Best practices
- Return DTOs (records), not JPA entities – avoids leaking internal fields and lazy-loading errors.
- Use ResponseEntity when you need specific status codes or headers.
- Version the API in the path (/api/v1).
- Validate input with @Valid and handle errors centrally with @RestControllerAdvice.
@Controller vs @RestController
- @Controller: returns view names; add @ResponseBody per method for data.
- @RestController: every method returns data; no views.
@RestController
@RequestMapping("/api/jobs")
public class JobController {
@GetMapping
public List<JobDto> index() {
return List.of();
}
}
Create a GET endpoint and verify that the response is returned as JSON.
Quick check
Which statement is correct?
04 What is @RequestMapping?
Solution and explanation
What it is
@RequestMapping maps HTTP requests to controller classes or methods. It can match on path, HTTP method, request parameters, headers, and content types.
At class and method level
A class-level mapping sets a common prefix; method-level mappings add to it.
@RestController
@RequestMapping("/api/v1/applications")
public class ApplicationController {
@RequestMapping(method = RequestMethod.GET) // GET /api/v1/applications
public List<ApplicationDto> all() { ... }
@RequestMapping(value = "/{id}", method = RequestMethod.GET) // GET /api/v1/applications/5
public ApplicationDto one(@PathVariable Long id) { ... }
@RequestMapping(method = RequestMethod.POST,
consumes = "application/json", produces = "application/json")
public ApplicationDto apply(@RequestBody ApplyRequest req) { ... }
}
Shortcut annotations (preferred)
- @GetMapping = @RequestMapping(method = GET)
- @PostMapping, @PutMapping, @PatchMapping, @DeleteMapping
@GetMapping("/{id}")
public ApplicationDto one(@PathVariable Long id) { ... }
Advanced matching
@GetMapping(value = "/export", params = "format=csv") // only when ?format=csv
public ResponseEntity<byte[]> exportCsv() { ... }
@GetMapping(value = "/export", headers = "X-Admin=true") // only with this header
public ResponseEntity<byte[]> exportAdmin() { ... }
@PostMapping(value = "/resume", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public void upload(@RequestParam MultipartFile file) { ... }
Path patterns
- /files/{name} – one segment.
- /files/** – any depth.
- /jobs/{id:\\d+} – regular expression constraint (digits only).
Note: without a method attribute, @RequestMapping matches every HTTP method, which is rarely what you want on an API.
Interview follow-ups
- What happens when two methods match the same URL and method? Startup fails with an "ambiguous mapping" error.
- What returns 405? A path that exists but not for the requested HTTP method.
@RestController
@RequestMapping(
value = "/api/v1/jobs",
produces = MediaType.APPLICATION_JSON_VALUE
)
public class JobController {
}
Create a shared /api/v1/jobs prefix for multiple job endpoints.
Quick check
Which statement is correct?
05 @GetMapping versus @PostMapping
Solution and explanation
The difference
Both are shortcuts of @RequestMapping for specific HTTP methods, and they follow HTTP semantics:
- @GetMapping handles GET: read data. Safe (no changes on the server) and idempotent (repeating it gives the same result). Parameters travel in the URL and can be cached and bookmarked.
- @PostMapping handles POST: create a resource or trigger an action. Not idempotent (sending twice may create two records). Data travels in the request body.
Example: job applications
@RestController
@RequestMapping("/api/v1/jobs/{jobId}/applications")
public class JobApplicationController {
@GetMapping // list applications for a job
public List<ApplicationDto> list(@PathVariable Long jobId,
@RequestParam(defaultValue = "applied") String status) {
return service.list(jobId, status);
}
@PostMapping // apply to the job
public ResponseEntity<ApplicationDto> apply(@PathVariable Long jobId,
@Valid @RequestBody ApplyRequest request) {
ApplicationDto created = service.apply(jobId, request);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
}
GET /api/v1/jobs/7/applications?status=shortlisted → 200 OK, list
POST /api/v1/jobs/7/applications {"resumeId": 12} → 201 Created
Side-by-side
- Purpose: GET reads; POST creates/acts.
- Data: GET uses path and query parameters; POST uses a JSON or form body.
- Idempotent: GET yes; POST no.
- Cacheable/bookmarkable: GET yes; POST no.
- Security: never put passwords or tokens in GET URLs (they appear in logs and browser history).
Other mappings
- @PutMapping: replace a resource (idempotent).
- @PatchMapping: partially update.
- @DeleteMapping: remove (idempotent).
Note: a GET request must never change data – crawlers and browser prefetching may call it automatically.
@GetMapping
public List<JobDto> all() {
return service.all();
}
@PostMapping
public ResponseEntity<JobDto> create(
@Valid @RequestBody CreateJobRequest body
) {
JobDto job = service.create(body);
return ResponseEntity
.status(201)
.body(job);
}
Implement GET and POST endpoints for a jobs collection.
Quick check
Which statement is correct?
06 What is @PathVariable?
Solution and explanation
What it is
@PathVariable binds a value from the URL path (a URI template variable) to a method parameter. It identifies which resource is being addressed.
@GetMapping("/api/v1/companies/{companyId}/jobs/{jobId}")
public JobDto job(@PathVariable Long companyId, @PathVariable Long jobId) {
return jobs.findForCompany(companyId, jobId);
}
GET /api/v1/companies/12/jobs/345 → companyId = 12, jobId = 345
Naming and type conversion
- If the parameter name differs: @PathVariable("id") Long jobId.
- Spring converts the text to the parameter type (Long, UUID, enums). Invalid input such as /jobs/abc for a Long produces 400 Bad Request (MethodArgumentTypeMismatchException).
@GetMapping("/orders/{orderNumber}")
public OrderDto byNumber(@PathVariable("orderNumber") String number) { ... }
@GetMapping("/jobs/{id:\\d+}") // regex: only digits match this endpoint
public JobDto byId(@PathVariable Long id) { ... }
Optional path variables and maps
@GetMapping({"/reports", "/reports/{year}"})
public Report report(@PathVariable(required = false) Integer year) { ... }
@GetMapping("/a/{x}/b/{y}")
public String all(@PathVariable Map<String, String> vars) { return vars.toString(); } // {x=..., y=...}
@PathVariable vs @RequestParam
- Path variable: identifies a resource (/jobs/345).
- Request parameter: filters, sorts or pages a collection (/jobs?city=Pune&page=2).
Note: never trust a path variable for authorization – check that the logged-in user may access company 12 before returning its data.
@GetMapping("/{id}")
public JobDto show(
@PathVariable Long id
) {
return service.find(id);
}
Create an endpoint for /api/jobs/42 and return 404 when the job does not exist.
Quick check
Which statement is correct?
07 What is @RequestParam?
Solution and explanation
What it is
@RequestParam binds query-string parameters (or form fields) to method parameters. It is used for optional inputs like filters, search terms, sorting and pagination.
@GetMapping("/api/v1/jobs")
public Page<JobDto> search(
@RequestParam(required = false) String q,
@RequestParam(defaultValue = "Pune") String city,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
return jobs.search(q, city, PageRequest.of(page, size));
}
GET /api/v1/jobs?q=java&city=Noida&page=1
→ q = "java", city = "Noida", page = 1, size = 20 (default)
Behaviour
- Required by default: a missing parameter without defaultValue gives 400 Bad Request.
- defaultValue makes it optional.
- Type conversion is automatic (int, LocalDate with @DateTimeFormat, enums).
- Multiple values: ?skill=java&skill=sql → @RequestParam List<String> skill.
- All parameters: @RequestParam Map<String, String> params.
@GetMapping("/applications")
public List<ApplicationDto> filter(
@RequestParam List<String> status,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from) { ... }
// /applications?status=applied&status=shortlisted&from=2026-10-01
Uploading files
@PostMapping(value = "/resume", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public void upload(@RequestParam("file") MultipartFile file) { ... }
Tip: group many filters into an object
public record JobFilter(String q, String city, Integer minSalary) { }
@GetMapping("/api/v1/jobs")
public List<JobDto> search(JobFilter filter) { ... } // bound from query parameters automatically
Note: validate numeric limits (size ≤ 100) so a client cannot request a million rows.
@GetMapping
public Page<JobDto> search(
@RequestParam String skill,
@RequestParam(defaultValue = "0")
int page
) {
return service.search(skill, page);
}
Support /api/jobs?skill=java&page=0.
Quick check
Which statement is correct?
08 What is @RequestBody?
Solution and explanation
What it is
@RequestBody tells Spring to read the HTTP request body and convert it into a Java object using an HttpMessageConverter – Jackson for JSON. It is used for POST, PUT and PATCH requests that send structured data.
public record CreateJobRequest(
@NotBlank String title,
@NotNull Long companyId,
@Min(0) Integer minSalary,
List<@NotBlank String> skills) { }
@PostMapping("/api/v1/jobs")
public ResponseEntity<JobDto> create(@Valid @RequestBody CreateJobRequest request) {
return ResponseEntity.status(HttpStatus.CREATED).body(jobs.create(request));
}
POST /api/v1/jobs
Content-Type: application/json
{"title":"Spring Boot Developer","companyId":3,"minSalary":800000,"skills":["Java","Spring"]}
What happens step by step
- The Content-Type header (application/json) selects the Jackson converter.
- Jackson creates the CreateJobRequest object from the JSON.
- @Valid runs Bean Validation; violations throw MethodArgumentNotValidException → 400 with field errors.
- Your method runs with a fully populated object.
Common errors
- 415 Unsupported Media Type: the client did not send Content-Type: application/json.
- 400 Bad Request: malformed JSON or a type mismatch ("abc" for an Integer).
- Unknown JSON fields are ignored by default in Spring Boot (configurable with spring.jackson.deserialization.fail-on-unknown-properties).
Security tip: never bind directly to entities
// Risky: a client could send {"id": 9, "verified": true}
@PostMapping public Job create(@RequestBody Job job) { ... }
Use a request DTO that contains only the fields a client may set (protection against mass assignment).
Note: only one @RequestBody is allowed per method, because the body stream can be read once.
public record CreateJobRequest(
@NotBlank String title,
@NotBlank String location
) {
}
@PostMapping
public JobDto create(
@Valid @RequestBody CreateJobRequest request
) {
return service.create(request);
}
Submit valid and invalid JSON to a POST endpoint and compare the responses.
Quick check
Which statement is correct?
09 What is @ResponseBody?
Solution and explanation
What it is
@ResponseBody tells Spring to write a method's return value directly into the HTTP response body (converted to JSON, XML or text) instead of treating it as a view name. @RestController applies it to every method automatically.
@Controller
public class HealthController {
@GetMapping("/status")
@ResponseBody
public Map<String, Object> status() {
return Map.of("status", "UP", "time", Instant.now().toString());
}
}
GET /status → {"status":"UP","time":"2026-10-12T10:15:30Z"}
How the format is chosen
Content negotiation: Spring looks at the client's Accept header and the available HttpMessageConverters.
- Objects → JSON via Jackson (default in Boot).
- String → text/plain.
- byte[] / Resource → files and downloads.
- XML if jackson-dataformat-xml is on the classpath and the client asks for application/xml.
Mixing views and data in one @Controller
@Controller
public class JobController {
@GetMapping("/jobs")
public String page(Model model) { return "jobs/list"; } // HTML view
@GetMapping("/jobs/suggest")
@ResponseBody
public List<String> suggest(@RequestParam String q) { return service.suggest(q); } // JSON
}
Controlling status and headers
@ResponseBody returns 200 by default. Use @ResponseStatus(HttpStatus.CREATED) or return ResponseEntity for full control.
Note: a String returned from a @RestController method is sent as text, not as a view name – a common surprise when moving code between controller types.
@Controller
public class HealthController {
@ResponseBody
@GetMapping("/health-text")
public Map<String, String> health() {
return Map.of(
"status",
"UP"
);
}
}
Return a JSON health response from a regular @Controller.
Quick check
Which statement is correct?
10 What is @Qualifier?
Solution and explanation
The problem
When more than one bean of the same type exists, Spring cannot decide which to inject and fails with NoUniqueBeanDefinitionException.
public interface PaymentGateway { String charge(long paise); }
@Component("razorpay")
public class RazorpayGateway implements PaymentGateway { public String charge(long p) { return "rzp_" + p; } }
@Component("stripe")
public class StripeGateway implements PaymentGateway { public String charge(long p) { return "stripe_" + p; } }
@Service
public class CheckoutService {
public CheckoutService(PaymentGateway gateway) { } // ✗ two candidates
}
NoUniqueBeanDefinitionException: expected single matching bean but found 2: razorpay,stripe
The fix: @Qualifier names the bean you want
@Service
public class DomesticCheckoutService {
private final PaymentGateway gateway;
public DomesticCheckoutService(@Qualifier("razorpay") PaymentGateway gateway) {
this.gateway = gateway;
}
}
@Service
public class InternationalCheckoutService {
private final PaymentGateway gateway;
public InternationalCheckoutService(@Qualifier("stripe") PaymentGateway gateway) {
this.gateway = gateway;
}
}
Other ways to resolve ambiguity
- @Primary on the default implementation (used when no qualifier is given).
- Matching the parameter name to the bean name (PaymentGateway razorpay).
- Injecting all of them: List<PaymentGateway> or Map<String, PaymentGateway> (bean name → bean), useful for a strategy pattern:
@Service
public class PaymentRouter {
private final Map<String, PaymentGateway> gateways;
public PaymentRouter(Map<String, PaymentGateway> gateways) { this.gateways = gateways; }
public String pay(String provider, long paise) { return gateways.get(provider).charge(paise); }
}
@Qualifier vs @Primary
- @Primary: set once on a bean; it wins by default everywhere.
- @Qualifier: chosen at each injection point; it always wins over @Primary.
Note: custom qualifier annotations (e.g. @Domestic) avoid typo-prone string names.
public interface PaymentGateway {
}
@Component("stripe")
public class StripeGateway
implements PaymentGateway {
}
@Component("razorpay")
public class RazorpayGateway
implements PaymentGateway {
}
@Service
public class BillingService {
private final PaymentGateway gateway;
public BillingService(
@Qualifier("stripe")
PaymentGateway gateway
) {
this.gateway = gateway;
}
}
Create two payment gateway beans and inject one specific implementation using @Qualifier.
Quick check
Which statement is correct?
Spring Quiz: Lessons 1 – 10
Test what you have learned before continuing to the next learning block.
Which statement is correct?
Which statement is correct?
Which statement is correct?
Which statement is correct?
Which statement is correct?
Which statement is correct?
Which statement is correct?
Which statement is correct?
Which statement is correct?
Which statement is correct?
