Spring Boot Skills
GitHub
Architecture

Layered Architecture

Use when generating or modifying any Spring Boot class — controllers, services, repositories, DTOs, mappers, or configuration. Enforces strict layer separation and prevents business logic from leaking across boundaries.

View SKILL.md

Layer Rules

@RestController        ← HTTP only. No business logic. No JPA entities in responses.
      ↓ DTOs
@Service               ← All business logic lives here. Orchestrates repositories.
      ↓ Domain objects / Entities
@Repository            ← Data access only. No business logic. Returns entities or projections.
      ↓ JPA / JDBC
Database

Controller Layer

  • Handles HTTP: parsing requests, validating input (@Valid), returning responses
  • Calls ONE service method per endpoint — no orchestration in controllers
  • Never returns @Entity classes directly — always map to response DTOs
  • Never injects @Repository — always goes through a @Service
  • Exception handling via @ControllerAdvice, never try/catch in controllers
// ✅ GOOD
@PostMapping("/orders")
public ResponseEntity<OrderResponse> createOrder(@Valid @RequestBody CreateOrderRequest request) {
    Order order = orderService.createOrder(request);
    return ResponseEntity.status(HttpStatus.CREATED).body(OrderResponse.from(order));
}

// ❌ BAD — business logic in controller
@PostMapping("/orders")
public ResponseEntity<Order> createOrder(@RequestBody CreateOrderRequest request) {
    if (request.getItems().isEmpty()) throw new RuntimeException("No items");
    Order order = orderRepository.save(new Order(request)); // direct repo access
    return ResponseEntity.ok(order); // returning entity
}

Service Layer

  • Contains all business logic, validation rules, and orchestration
  • @Transactional lives here, not in controllers or repositories
  • Constructor injection only — never @Autowired field injection
  • One service per aggregate root (OrderService, not OrderAndPaymentService)
  • Returns domain objects or DTOs — never HttpServletRequest / HttpServletResponse
  • Retries and concurrency caps on service methods via Framework 7's @Retryable / @ConcurrencyLimit (enable with @EnableResilientMethods) — no spring-retry dependency
// ✅ GOOD
@Service
@RequiredArgsConstructor
public class OrderService {
    private final OrderRepository orderRepository;
    private final InventoryService inventoryService;

    @Transactional
    public Order createOrder(CreateOrderRequest request) {
        inventoryService.reserve(request.items()); // records use accessor names, not getX()
        Order order = Order.from(request);
        return orderRepository.save(order);
    }
}

// ❌ BAD — field injection, HTTP concern in service
@Service
public class OrderService {
    @Autowired private OrderRepository orderRepository;

    public ResponseEntity<Order> createOrder(...) { ... } // HTTP type in service
}

Repository Layer

  • Extends JpaRepository<Entity, ID> or CrudRepository
  • Custom queries via @Query or query derivation — no raw SQL unless unavoidable
  • Returns entities or Spring Data Projections — never raw Object[]
  • No business logic — pure data access

DTOs

  • Separate Request / Response DTOs — never use the same class for both
  • Validation annotations (@NotNull, @Size, etc.) on Request DTOs only
  • Static factory method ResponseDto.from(Entity entity) for mapping
  • Use records for immutable DTOs (Java 16+)
// ✅ GOOD
public record OrderResponse(UUID id, String status, List<LineItemResponse> items, Instant createdAt) {
    public static OrderResponse from(Order order) {
        return new OrderResponse(order.getId(), order.getStatus().name(),
            order.getItems().stream().map(LineItemResponse::from).toList(),
            order.getCreatedAt());
    }
}

Mapper Pattern

  • Keep mapping logic out of controllers and services — use dedicated mapper classes or static factory methods
  • Mapper is a plain class or utility — not a Spring bean unless it needs injected dependencies
  • Entity → Response DTO: static method on the response DTO (OrderResponse.from(order))
  • Request DTO → Entity: static factory on the entity (Order.from(request)) or a mapper class
  • Collection mapping: use .stream().map(OrderResponse::from).toList() — never manual loops
// ✅ GOOD — dedicated mapper for complex mappings
public class OrderMapper {

    public static OrderResponse toResponse(Order order) {
        return new OrderResponse(
            order.getId(),
            order.getStatus().name(),
            order.getItems().stream().map(OrderMapper::toLineItem).toList(),
            order.getCreatedAt()
        );
    }

    public static Order toEntity(CreateOrderRequest request, User user) {
        Order order = Order.create(request.customerEmail(), user);
        request.items().forEach(item ->
            order.addItem(item.productId(), item.quantity()));
        return order;
    }

    private static LineItemResponse toLineItem(OrderItem item) {
        return new LineItemResponse(item.getProductId(), item.getQuantity(), item.getPrice());
    }
}

Configuration Layer

  • @Configuration classes live in a config/ package — never in service/ or controller/
  • Configuration never imports service or controller classes
  • Use @ConfigurationProperties for type-safe config — never raw @Value for groups of related settings
  • Bean definitions for infrastructure concerns only (RestClient, JsonMapper, SecurityFilterChain)

Cross-Cutting Concerns

  • Logging: use @Slf4j — never System.out.println
  • Validation: @Valid on controller parameters, custom validators as @Component
  • Exception handling: single @RestControllerAdvice class, never try/catch in controllers
  • Auditing: @CreatedDate / @LastModifiedDate with @EnableJpaAuditing

Gotchas

  • Agent tends to put @Transactional on controllers — move it to services
  • Agent uses @Autowired field injection — always use constructor injection (@RequiredArgsConstructor)
  • Agent returns List<Entity> from controllers — always map to List<ResponseDto>
  • Agent creates OrderAndInventoryService god classes — split by aggregate
  • Agent puts mapping logic inside controllers — extract to mapper class or DTO factory method
  • Agent creates @Configuration classes that depend on @Service beans — configuration should only wire infrastructure
  • Agent defines an ObjectMapper bean to customize JSON — Boot 4 uses Jackson 3 (tools.jackson): define JsonMapper beans, and @JsonComponent is now @JacksonComponent
  • Agent adds spring-retry for service-level retries — built into Framework 7 (@Retryable, @EnableResilientMethods)
FROM THE REPOSITORY

Examples & templates

bad-service.javaexamples
Source on GitHub
// ❌ BAD — every common layering violation in one class

@Service
public class OrderService {

    @Autowired                                   // field injection instead of constructor
    private OrderRepository orderRepository;

    @Autowired
    private HttpServletRequest httpRequest;       // HTTP concern in service layer

    public ResponseEntity<Order> createOrder(CreateOrderRequest request) {  // returns HTTP type
        String token = httpRequest.getHeader("Authorization");              // reads HTTP headers in service
        if (request.getItems().isEmpty()) {
            return ResponseEntity.badRequest().build();                     // HTTP status logic in service
        }
        Order order = new Order();                                          // no factory method
        order.setEmail(request.getEmail());
        order.setStatus("PENDING");                                         // string instead of enum
        Order saved = orderRepository.save(order);                          // no @Transactional
        return ResponseEntity.ok(saved);                                    // returns entity directly
    }

    @Transactional
    public Order findById(Long id) {             // Long ID instead of UUID
        return orderRepository.findById(id)
            .orElse(null);                       // returns null instead of throwing
    }

    public List<Order> findAll() {               // no pagination, no readOnly
        return orderRepository.findAll();        // unbounded query
    }
}
good-service.javaexamples
Source on GitHub
// ✅ GOOD — proper layer separation, constructor injection, @Transactional on service

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class OrderService {

    private final OrderRepository orderRepository;
    private final InventoryService inventoryService;

    public OrderResponse findById(UUID id) {
        Order order = orderRepository.findById(id)
            .orElseThrow(() -> new OrderNotFoundException(id));
        return OrderResponse.from(order);
    }

    public Page<OrderResponse> findAll(Pageable pageable) {
        return orderRepository.findAll(pageable).map(OrderResponse::from);
    }

    @Transactional
    public Order createOrder(CreateOrderRequest request) {
        inventoryService.reserve(request.items());
        Order order = Order.create(request.customerEmail());
        request.items().forEach(item ->
            order.addItem(item.productId(), item.quantity()));
        return orderRepository.save(order);
    }

    @Transactional
    public void cancel(UUID id) {
        Order order = orderRepository.findById(id)
            .orElseThrow(() -> new OrderNotFoundException(id));
        order.cancel();
        inventoryService.release(order.getItems());
    }
}
CreateOrderRequest.javatemplates
Source on GitHub
package com.example.order.dto.request;

import jakarta.validation.Valid;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;

import java.util.List;
import java.util.UUID;

public record CreateOrderRequest(

    @NotBlank(message = "Customer email is required")
    @Email(message = "Must be a valid email address")
    String customerEmail,

    @NotEmpty(message = "Order must have at least one item")
    @Valid
    List<OrderItemRequest> items

) {
    public record OrderItemRequest(

        @NotNull(message = "Product ID is required") // @NotBlank only supports CharSequence
        UUID productId,

        @Positive(message = "Quantity must be positive")
        int quantity
    ) {}
}
OrderMapper.javatemplates
Source on GitHub
package com.example.order.dto;

import com.example.order.entity.Order;
import com.example.order.entity.OrderItem;
import com.example.order.dto.request.CreateOrderRequest;
import com.example.order.dto.response.OrderResponse;
import com.example.order.dto.response.OrderResponse.LineItemResponse;

import java.util.List;

public class OrderMapper {

    private OrderMapper() {} // utility class

    public static OrderResponse toResponse(Order order) {
        return new OrderResponse(
            order.getId(),
            order.getStatus().name(),
            toLineItems(order.getItems()),
            order.getCreatedAt()
        );
    }

    public static List<LineItemResponse> toLineItems(List<OrderItem> items) {
        return items.stream()
            .map(OrderMapper::toLineItem)
            .toList();
    }

    private static LineItemResponse toLineItem(OrderItem item) {
        return new LineItemResponse(
            item.getProductId(),
            item.getProductName(),
            item.getQuantity(),
            item.getPrice()
        );
    }
}
OrderResponse.javatemplates
Source on GitHub
package com.example.order.dto.response;

import com.example.order.entity.Order;

import java.time.Instant;
import java.util.List;
import java.util.UUID;

public record OrderResponse(
    UUID id,
    String status,
    List<LineItemResponse> items,
    Instant createdAt
) {
    public static OrderResponse from(Order order) {
        return new OrderResponse(
            order.getId(),
            order.getStatus().name(),
            order.getItems().stream()
                .map(item -> new LineItemResponse(
                    item.getProductId(),
                    item.getProductName(),
                    item.getQuantity(),
                    item.getPrice()))
                .toList(),
            order.getCreatedAt()
        );
    }

    public record LineItemResponse(
        UUID productId,
        String productName,
        int quantity,
        java.math.BigDecimal price
    ) {}
}

Verification scope

Compiled sources
CreateOrderRequest, both trees
Runtime checks
Bean Validation and HTTP validation; not the full architecture

Coverage describes checked-in fixtures, not a guarantee for every snippet or project.

Coverage matrix & agent evidence
ADD TO YOUR AGENT

Install Boot 4 skills

claude plugin marketplace add rrezartprebreza/spring-boot-skills
claude plugin install spring-boot-4-skills@spring-boot-skills