In Java backend, most annotations belong to one of these groups:
1. Web Layer (API entry)
These annotations define how requests enter your system.
@RestController
@RestController = @Controller + @ResponseBody- Marks this class as a web API controller
- All methods return JSON directly, not HTML
Example
@RestController
@RequestMapping("/coupon")
public class CouponController {
@GetMapping("/{id}")
public Coupon getCoupon(@PathVariable Long id) {
return couponService.getById(id);
}
}Runtime behavior
- HTTP request → Spring DispatcherServlet
- Finds this controller
- Executes method
- Automatically converts return value → JSON
Common bug
- Forget
@RestController→ returns view instead of JSON
@RequestMapping
Defines base URL path
@RequestMapping("/coupon")All methods inside become:
/coupon/xxxCan also define method type:
@RequestMapping(value = "/test", method = RequestMethod.GET)But usually replaced by @GetMapping, @PostMapping.
@GetMapping / @PostMapping
Shortcut annotations:
@GetMapping("/list")
@PostMapping("/create")Meaning
- GET → query data
- POST → create/change data
Real flow
@PostMapping("/claim")
public void claim(@RequestBody ClaimRequest req) {
couponService.claim(req);
}@RequestBody
Convert JSON → Java object
Frontend sends:
{
"userId": 1,
"campaignId": 100
}Backend:
public void claim(@RequestBody ClaimRequest req)Spring automatically:
- parse JSON
- map fields → object
Common bug
- field name mismatch → null value
- missing setter/getter
@PathVariable
Used for URL path parameters:
@GetMapping("/coupon/{id}")
public Coupon get(@PathVariable Long id)Request:
GET /coupon/10→ id = 10
Common bug
- name mismatch:
@GetMapping("/coupon/{id}")
public Coupon get(@PathVariable("couponId") Long id) // wrong2. Service / Bean Management (Spring IOC core)
Spring manages objects for you.
@Component
Basic annotation:
@Component
public class MyUtil {}→ Spring creates this object automatically.
@Service
Same as @Component, but semantic meaning:
@Service
public class CouponService {}Meaning:
- this class contains business logic
@Autowired
Inject dependency automatically
@Autowired
private CouponService couponService;Spring:
- finds matching bean
- injects it
@Resource
Similar to @Autowired, but different rule:
@Resource
private CouponService couponService;Difference
| Annotation | Match rule |
|---|---|
@Autowired | by type |
@Resource | by name first |
Real bug
If multiple beans exist:
@Autowired // may fail
@Resource // works if name matches3. Transaction
@Transactional
This is very important for business correctness
@Transactional
public void claimCoupon() {
deductStock();
insertUserCoupon();
}Meaning
- All DB operations = one unit
- If one fails → all rollback
Example problem
Without transaction:
deductStock(); // success
insertUserCoupon(); // fail→ stock reduced but no coupon → data inconsistency
With @Transactional:
→ both rollback
Common bugs
- Transaction not working
this.claimCoupon(); // self-call → transaction NOT effective- Wrong propagation
- Exception swallowed → no rollback
4. Validation
Used to validate input automatically.
@Valid
public void create(@Valid @RequestBody CouponDTO dto)→ triggers validation
@NotNull
@NotNull
private Long userId;→ must not be null
@NotBlank
@NotBlank
private String name;→ must not be:
- null
- ""
- ” “
Real flow
If invalid:
{
"name": ""
}→ Spring throws exception BEFORE entering service
Common bug
- Forgot
@Valid→ validation not triggered
5. Persistence (DB mapping)
@Table
@Table(name = "coupon")→ maps class → DB table
@Column
@Column(name = "coupon_name")
private String name;→ field → column
@Id
@Id
private Long id;→ primary key
MyBatis mapper
Example:
<insert id="insert">
INSERT INTO coupon(name) VALUES(#{name})
</insert>or
@Insert("INSERT INTO coupon(name) VALUES(#{name})")Real bug sources
- field mismatch
- SQL syntax error
- parameter not passed correctly
- batch insert error (you saw this before)
6. Async / Scheduling / Messaging
These are non-request entry points
@Scheduled
Runs automatically:
@Scheduled(cron = "0 0 0 * * ?")
public void expireCoupons() {}→ runs every day
Used for:
- expire coupons
- cleanup jobs
@Async
@Async
public void sendNotification() {}→ runs in another thread
Used for:
- sending email
- logging
- slow operations
Common bug
- no thread pool config → not working
@KafkaListener
@KafkaListener(topics = "payment-success")
public void handlePayment(String msg) {}→ listens to Kafka message
@RabbitListener
Same idea for RabbitMQ.
Real business example (from your coupon system)
Payment success:
- Payment system → send message
- Your system receives:
@KafkaListener(topics = "payment-success")
public void onPaymentSuccess(...) {
couponService.useCoupon(...);
}Common bugs here
- message duplicated → must be idempotent
- order arrives late → coupon already unlocked
- consumer fails → retry issues
7. How all of them connect (VERY IMPORTANT)
A full real flow looks like this:
HTTP Request
↓
@RestController
↓
@RequestMapping / @PostMapping
↓
@RequestBody / @PathVariable
↓
@Service
↓
@Transactional
↓
Mapper / DB (@Table / SQL)
↓
Return JSONOR async flow:
Message Queue
↓
@KafkaListener
↓
@Service
↓
@Transactional
↓
DB update8. One mental model to remember everything
Think like this:
| Layer | Responsibility |
|---|---|
| Controller | receive request |
| Service | business logic |
| Transaction | ensure consistency |
| Mapper | DB operation |
| Validation | protect input |
| Async/MQ | background or external trigger |