Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Spring Boot architecture patterns for REST APIs, layered services, JPA, caching, async, and exception handling.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
SKILL.md
1---2name: springboot-patterns3description: Spring Boot architecture patterns, REST API design, layered services, data access, caching, async processing, and logging. Use for Java Spring Boot backend work.4origin: ECC5---67# Spring Boot Development Patterns89Spring Boot architecture and API patterns for scalable, production-grade services.1011## When to Activate1213- Building REST APIs with Spring MVC or WebFlux14- Structuring controller → service → repository layers15- Configuring Spring Data JPA, caching, or async processing16- Adding validation, exception handling, or pagination17- Setting up profiles for dev/staging/production environments18- Implementing event-driven patterns with Spring Events or Kafka1920## REST API Structure2122```java23@RestController24@RequestMapping("/api/markets")25@Validated26class MarketController {27private final MarketService marketService;2829MarketController(MarketService marketService) {30this.marketService = marketService;31}3233@GetMapping34ResponseEntity<Page<MarketResponse>> list(35@RequestParam(defaultValue = "0") int page,36@RequestParam(defaultValue = "20") int size) {37Page<Market> markets = marketService.list(PageRequest.of(page, size));38return ResponseEntity.ok(markets.map(MarketResponse::from));39}4041@PostMapping42ResponseEntity<MarketResponse> create(@Valid @RequestBody CreateMarketRequest request) {43Market market = marketService.create(request);44return ResponseEntity.status(HttpStatus.CREATED).body(MarketResponse.from(market));45}46}47```4849## Repository Pattern (Spring Data JPA)5051```java52public interface MarketRepository extends JpaRepository<MarketEntity, Long> {53@Query("select m from MarketEntity m where m.status = :status order by m.volume desc")54List<MarketEntity> findActive(@Param("status") MarketStatus status, Pageable pageable);55}56```5758## Service Layer with Transactions5960```java61@Service62public class MarketService {63private final MarketRepository repo;6465public MarketService(MarketRepository repo) {66this.repo = repo;67}6869@Transactional70public Market create(CreateMarketRequest request) {71MarketEntity entity = MarketEntity.from(request);72MarketEntity saved = repo.save(entity);73return Market.from(saved);74}75}76```7778## DTOs and Validation7980```java81public record CreateMarketRequest(82@NotBlank @Size(max = 200) String name,83@NotBlank @Size(max = 2000) String description,84@NotNull @FutureOrPresent Instant endDate,85@NotEmpty List<@NotBlank String> categories) {}8687public record MarketResponse(Long id, String name, MarketStatus status) {88static MarketResponse from(Market market) {89return new MarketResponse(market.id(), market.name(), market.status());90}91}92```9394## Exception Handling9596```java97@ControllerAdvice98class GlobalExceptionHandler {99@ExceptionHandler(MethodArgumentNotValidException.class)100ResponseEntity<ApiError> handleValidation(MethodArgumentNotValidException ex) {101String message = ex.getBindingResult().getFieldErrors().stream()102.map(e -> e.getField() + ": " + e.getDefaultMessage())103.collect(Collectors.joining(", "));104return ResponseEntity.badRequest().body(ApiError.validation(message));105}106107@ExceptionHandler(AccessDeniedException.class)108ResponseEntity<ApiError> handleAccessDenied() {109return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ApiError.of("Forbidden"));110}111112@ExceptionHandler(Exception.class)113ResponseEntity<ApiError> handleGeneric(Exception ex) {114// Log unexpected errors with stack traces115return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)116.body(ApiError.of("Internal server error"));117}118}119```120121## Caching122123Requires `@EnableCaching` on a configuration class.124125```java126@Service127public class MarketCacheService {128private final MarketRepository repo;129130public MarketCacheService(MarketRepository repo) {131this.repo = repo;132}133134@Cacheable(value = "market", key = "#id")135public Market getById(Long id) {136return repo.findById(id)137.map(Market::from)138.orElseThrow(() -> new EntityNotFoundException("Market not found"));139}140141@CacheEvict(value = "market", key = "#id")142public void evict(Long id) {}143}144```145146## Async Processing147148Requires `@EnableAsync` on a configuration class.149150```java151@Service152public class NotificationService {153@Async154public CompletableFuture<Void> sendAsync(Notification notification) {155// send email/SMS156return CompletableFuture.completedFuture(null);157}158}159```160161## Logging (SLF4J)162163```java164@Service165public class ReportService {166private static final Logger log = LoggerFactory.getLogger(ReportService.class);167168public Report generate(Long marketId) {169log.info("generate_report marketId={}", marketId);170try {171// logic172} catch (Exception ex) {173log.error("generate_report_failed marketId={}", marketId, ex);174throw ex;175}176return new Report();177}178}179```180181## Middleware / Filters182183```java184@Component185public class RequestLoggingFilter extends OncePerRequestFilter {186private static final Logger log = LoggerFactory.getLogger(RequestLoggingFilter.class);187188@Override189protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,190FilterChain filterChain) throws ServletException, IOException {191long start = System.currentTimeMillis();192try {193filterChain.doFilter(request, response);194} finally {195long duration = System.currentTimeMillis() - start;196log.info("req method={} uri={} status={} durationMs={}",197request.getMethod(), request.getRequestURI(), response.getStatus(), duration);198}199}200}201```202203## Pagination and Sorting204205```java206PageRequest page = PageRequest.of(pageNumber, pageSize, Sort.by("createdAt").descending());207Page<Market> results = marketService.list(page);208```209210## Error-Resilient External Calls211212> **Production recommendation:** Use Resilience4j or Spring Retry for production retry logic213> with circuit breakers, metrics, and configurable policies.214215```java216public <T> T withRetry(Supplier<T> supplier, int maxRetries) {217final long maxBackoffMillis = 10_000L;218int attempts = 0;219while (true) {220try {221return supplier.get();222} catch (Exception ex) {223attempts++;224if (attempts >= maxRetries) {225throw ex;226}227try {228long backoff = Math.min((long) Math.pow(2, attempts) * 100L, maxBackoffMillis);229Thread.sleep(backoff);230} catch (InterruptedException ie) {231Thread.currentThread().interrupt();232throw ex;233}234}235}236}237```238239## Rate Limiting (Filter + Bucket4j)240241**Security Note**: The `X-Forwarded-For` header is untrusted by default because clients can spoof it.242Only use forwarded headers when:2431. Your app is behind a trusted reverse proxy (nginx, AWS ALB, etc.)2442. You have registered `ForwardedHeaderFilter` as a bean2453. You have configured `server.forward-headers-strategy=NATIVE` or `FRAMEWORK` in application properties2464. Your proxy is configured to overwrite (not append to) the `X-Forwarded-For` header247248When `ForwardedHeaderFilter` is properly configured, `request.getRemoteAddr()` will automatically249return the correct client IP from the forwarded headers. Without this configuration, use250`request.getRemoteAddr()` directly—it returns the immediate connection IP, which is the only251trustworthy value.252253```java254@Component255public class RateLimitFilter extends OncePerRequestFilter {256private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();257258/*259* SECURITY: This filter uses request.getRemoteAddr() to identify clients for rate limiting.260*261* If your application is behind a reverse proxy (nginx, AWS ALB, etc.), you MUST configure262* Spring to handle forwarded headers properly for accurate client IP detection:263*264* 1. Set server.forward-headers-strategy=NATIVE (for cloud platforms) or FRAMEWORK in265* application.properties/yaml266* 2. If using FRAMEWORK strategy, register ForwardedHeaderFilter:267*268* @Bean269* ForwardedHeaderFilter forwardedHeaderFilter() {270* return new ForwardedHeaderFilter();271* }272*273* 3. Ensure your proxy overwrites (not appends) the X-Forwarded-For header to prevent spoofing274* 4. Configure server.tomcat.remoteip.trusted-proxies or equivalent for your container275*276* Without this configuration, request.getRemoteAddr() returns the proxy IP, not the client IP.277* Do NOT read X-Forwarded-For directly—it is trivially spoofable without trusted proxy handling.278*/279@Override280protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,281FilterChain filterChain) throws ServletException, IOException {282// Use getRemoteAddr() which returns the correct client IP when ForwardedHeaderFilter283// is configured, or the direct connection IP otherwise. Never trust X-Forwarded-For284// headers directly without proper proxy configuration.285String clientIp = request.getRemoteAddr();286287Bucket bucket = buckets.computeIfAbsent(clientIp,288k -> Bucket.builder()289.addLimit(Bandwidth.classic(100, Refill.greedy(100, Duration.ofMinutes(1))))290.build());291292if (bucket.tryConsume(1)) {293filterChain.doFilter(request, response);294} else {295response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());296}297}298}299```300301## Background Jobs302303Use Spring’s `@Scheduled` or integrate with queues (e.g., Kafka, SQS, RabbitMQ). Keep handlers idempotent and observable.304305## Observability306307- Structured logging (JSON) via Logback encoder308- Metrics: Micrometer + Prometheus/OTel309- Tracing: Micrometer Tracing with OpenTelemetry or Brave backend310311## Production Defaults312313- Prefer constructor injection, avoid field injection314- Enable `spring.mvc.problemdetails.enabled=true` for RFC 7807 errors (Spring Boot 3+)315- Configure HikariCP pool sizes for workload, set timeouts316- Use `@Transactional(readOnly = true)` for queries317- Enforce null-safety via `@NonNull` and `Optional` where appropriate318319**Remember**: Keep controllers thin, services focused, repositories simple, and errors handled centrally. Optimize for maintainability and testability.320