Skip to main content

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):

AnnotationFilesTypical use
@Getter / @Setter~294 / ~289Entities and DTOs
@NoArgsConstructor~221Entities (JPA requires a no-arg constructor)
@RequiredArgsConstructor~171Constructor injection of final fields in services/controllers — no @Autowired anywhere
@AllArgsConstructor~106DTOs
@Slf4j14Injects 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.