Layered architecture
Skill rrezartprebreza/spring-boot-skills/skills/spring-boot-4/layered-architecture
Production-grade Claude Code and Codex skills for Spring Boot developers
npx -y skills add rrezartprebreza/spring-boot-skills --skill layered-architectureAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
What its author says it does
Copied from the file, not written here
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.
SKILL.md
6.6 KB, as published. Nobody here has run it
Layered Architecture
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
@Entityclasses 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
@Transactionallives here, not in controllers or repositories- Constructor injection only — never
@Autowiredfield 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.getItems());
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>orCrudRepository - Custom queries via
@Queryor 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) {
public static OrderResponse from(Order order) {
return new OrderResponse(order.getId(), order.getStatus().name(),
order.getItems().stream().map(LineItemResponse::from).toList());
}
}
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
@Configurationclasses live in aconfig/package — never inservice/orcontroller/- Configuration never imports service or controller classes
- Use
@ConfigurationPropertiesfor type-safe config — never raw@Valuefor groups of related settings - Bean definitions for infrastructure concerns only (RestClient, JsonMapper, SecurityFilterChain)
Cross-Cutting Concerns
- Logging: use
@Slf4j— neverSystem.out.println - Validation:
@Validon controller parameters, custom validators as@Component - Exception handling: single
@RestControllerAdviceclass, never try/catch in controllers - Auditing:
@CreatedDate/@LastModifiedDatewith@EnableJpaAuditing
Gotchas
- Agent tends to put
@Transactionalon controllers — move it to services - Agent uses
@Autowiredfield injection — always use constructor injection (@RequiredArgsConstructor) - Agent returns
List<Entity>from controllers — always map toList<ResponseDto> - Agent creates
OrderAndInventoryServicegod classes — split by aggregate - Agent puts mapping logic inside controllers — extract to mapper class or DTO factory method
- Agent creates
@Configurationclasses that depend on@Servicebeans — configuration should only wire infrastructure - Agent defines an
ObjectMapperbean to customize JSON — Boot 4 uses Jackson 3 (tools.jackson): defineJsonMapperbeans, and@JsonComponentis now@JacksonComponent - Agent adds spring-retry for service-level retries — built into Framework 7 (
@Retryable,@EnableResilientMethods)