Backend Annotations, Explained
A newcomer's guide to every annotation you'll meet repeatedly in
backend/src/main/java/com/motorph/payroll/.
Each entry: what it does, and a real example from this codebase. How the
pieces fit at request time is in README.md.
@RestController + @RequestMapping
@RestController marks a class as a web endpoint whose return values are
serialized straight to JSON (no view templates). @RequestMapping at class
level sets the URL prefix; @GetMapping/@PostMapping/@PutMapping/
@PatchMapping/@DeleteMapping map methods to verbs + subpaths.
@RestController
@RequestMapping("/api/employees")
public class EmployeeController { ... }
— EmployeeController.java.
All 77 REST controllers follow this shape. The one non-REST controller,
TypingController,
uses plain @Controller with @MessageMapping because it handles STOMP
WebSocket frames, not HTTP (scheduling-and-websockets.md).
@PreAuthorize
Method-level authorization, evaluated before the method runs. This codebase
uses exactly one idiom — hasAuthority(<permission constant>):
@PostMapping
@PreAuthorize("hasAuthority('" + PermissionConstants.HR_EMPLOYEES_CREATE + "')")
public ResponseEntity<EmployeeDto> create(@Valid @RequestBody EmployeeCreateRequest request) {
— EmployeeController.java.
Authorities are permission names (hr.employees.create), never role names —
all defined in
PermissionConstants.
A failing check throws AccessDeniedException → 403. It only works because
@EnableMethodSecurity is switched on (see below).
@Service, @Repository, @Component
All three make Spring instantiate and inject the class as a bean; they differ
only in intent. @Service marks business logic
(EmployeeServiceImpl),
@Component is the generic form used for infrastructure like
AuditInterceptor
or
HolidayGenerationScheduler.
You will not find @Repository in this codebase: repositories are
interfaces extending JpaRepository/JpaSpecificationExecutor
(EmployeeRepository),
and Spring Data registers those automatically.
@Transactional(readOnly = true)
Wraps the method (or every public method, when class-level) in a database
transaction. The convention here: class-level
@Transactional(readOnly = true), and each write method overrides with plain
@Transactional:
@Service
@Transactional(readOnly = true)
public class EmployeeServiceImpl implements EmployeeService {
...
@Override
@Transactional // write path overrides the read-only default
public EmployeeDto create(EmployeeCreateRequest request) { ... }
— EmployeeServiceImpl.java.
The one deliberate exception is
RefreshTokenService
— see README.md §6 before touching it.
@Valid + jakarta.validation constraints
@Valid on a @RequestBody parameter triggers the constraint annotations
declared on the DTO's fields; violations become a 400 with joined field
messages (via GlobalControllerAdvice). Real example:
@NotBlank(message = "Last name is required")
private String lastName;
@NotNull(message = "Birthday is required")
private LocalDate birthday;
@Pattern(regexp = "PROBATIONARY|REGULAR|INACTIVE",
message = "Status must be one of PROBATIONARY, REGULAR, INACTIVE")
private String status;
— EmployeeCreateRequest.java.
@NotBlank (non-null, non-whitespace string), @NotNull (any type), and
@Pattern (regex) are the workhorses across the 287 DTOs.
@RestControllerAdvice + @ExceptionHandler
One class that catches exceptions from every controller and turns them
into consistent JSON error bodies. Each @ExceptionHandler method claims one
exception type:
@RestControllerAdvice
public class GlobalControllerAdvice {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ApiExceptionResponse> handleResourceNotFound(ResourceNotFoundException ex) {
return buildResponse(ex.getMessage(), HttpStatus.NOT_FOUND);
}
— GlobalControllerAdvice.java.
The full status mapping table is in README.md §4.
@ConfigurationProperties
Binds a prefix of application.yml (and its environment-variable overrides)
onto a typed bean, instead of sprinkling @Value strings around:
@Component
@ConfigurationProperties(prefix = "jwt")
@Getter
@Setter
public class JwtProperties {
private String secretKey;
private long accessTokenExpirationMinute;
...
@PostConstruct
void validateSecret() { ... } // refuses to boot on a missing/weak JWT_SECRET
— JwtProperties.java.
Same pattern in
AltchaProperties
(which only enforces its HMAC key when altcha.enabled=true, so fresh clones
boot with zero config) and
BillingProperties.
Note the @PostConstruct fail-fast validation — configuration errors should
kill the boot, not surface as runtime 500s.
@Scheduled
Runs a method on a schedule — enabled globally by @EnableScheduling on
PayrollBackendApplication.
Two styles in this codebase:
@Scheduled(cron = "0 0 3 1 12 *", zone = "Asia/Manila") // 03:00 every Dec 1, PH time
public void seedUpcomingYear() { ... }
— HolidayGenerationScheduler.java
(always pass zone for calendar-meaningful crons — server clocks are UTC), and
@Scheduled(fixedDelay = SWEEP_DELAY_MS) // every 5 minutes, after the previous run finishes
void sweepExpiredSignatures() { ... }
— AltchaService.java.
All jobs are catalogued in scheduling-and-websockets.md.
JPA mapping annotations (one entity, annotated)
From Employee.java:
@Entity // this class maps to a table
@Table(name = "employee") // ...this table
public class Employee {
@Id // primary key
@GeneratedValue(strategy = GenerationType.IDENTITY) // DB identity column assigns it
@Column(name = "employee_number")
private Integer employeeNumber;
@Column(name = "last_name", nullable = false) // column name + constraint
private String lastName;
@Column(name = "sss_number", nullable = false, unique = true)
private String sssNumber;
@ManyToOne(fetch = FetchType.LAZY) // FK side of employee *→1 position
@JoinColumn(name = "position_id", nullable = false)
private Position position;
Conventions to copy: explicit name = "snake_case" on every column, and
FetchType.LAZY on every @ManyToOne (the default is EAGER, which
silently multiplies queries). Two more you'll meet in
PayrollSettings:
@Enumerated(EnumType.STRING) (store the enum's name, never its ordinal)
and @Convert(converter = ...) for custom column ↔ type conversion
(RestDaysConverter packs a Set<DayOfWeek> into one varchar).
Remember: Hibernate validates these mappings against the Flyway-owned schema at boot (entities-and-migrations.md) — the annotations describe the schema, they never create it.
MapStruct @Mapper
Generates the mapping implementation at compile time:
@Mapper(componentModel = "spring") // generated impl is an injectable bean
public interface EmployeeMapper {
@Mapping(target = "employeeNumber", ignore = true)
@Mapping(target = "position", ignore = true)
Employee toEntity(EmployeeCreateRequest request);
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void updateEntity(EmployeeUpdateRequest request, @MappingTarget Employee employee);
— EmployeeMapper.java.
@MappingTarget maps onto an existing object (for updates), and the
IGNORE null-strategy makes partial updates skip absent fields. Only this and
TimesheetMapper
use MapStruct — everything else maps by hand (README.md §1).
@EnableWebSecurity + @EnableMethodSecurity
Both sit on
SecurityConfiguration.
@EnableWebSecurity activates the servlet security filter chain the class
then defines (URL rules, stateless sessions, the three custom filters);
@EnableMethodSecurity is what makes @PreAuthorize actually evaluate —
without it, those annotations are silently ignored. The full pipeline is
documented in ../security/request-pipeline.md.
Lombok
Lombok generates boilerplate at compile time. What this codebase actually uses (approximate file counts, so you know what's idiomatic here):
| Annotation | Files | Typical use |
|---|---|---|
@Getter / @Setter | ~294 / ~289 | Entities and DTOs |
@NoArgsConstructor | ~221 | Entities (JPA requires a no-arg constructor) |
@RequiredArgsConstructor | ~171 | Constructor injection of final fields in services/controllers — no @Autowired anywhere |
@AllArgsConstructor | ~106 | DTOs |
@Slf4j | 14 | Injects a log field (seeders, schedulers, interceptors) |
Not used: @Data, @Builder, @EqualsAndHashCode (zero occurrences).
@Data in particular is avoided on entities on purpose — its generated
equals/hashCode/toString traverse lazy relations and break JPA in
subtle ways. Stick to the @Getter @Setter @NoArgsConstructor trio you see in
Employee.java.