Skip to content
Development
Skill

/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.

From plugin
spring-boot-skills
33733 skills

Claude Code

$ npx -y skills add rrezartprebreza/spring-boot-skills --skill layered-architecture --agent claude-code

SKILL.md

layered-architecture.SKILL.md
name: layered-architecture
description: >
  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.

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 `@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`
// 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 (RestTemplate, ObjectMapper, 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 `
Read more
@rrezartprebreza
Ships withspring-boot-skills

Production-grade Claude Code and Codex skills for Spring Boot developers

Stars on GitHub

Momentum

stars in total
337
Star history of rrezartprebreza/spring-boot-skills on GitHub, a chart by star-history.com

Other skills on spring-boot-skills.

More skills from other plugins.

brainstorming
Auto-invoked

brainstorming

You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…

@obra@obraInstall Skill
ask-matt

ask-matt

Ask which skill or flow fits your situation. A router over the skills in this repo.

skills10d309k
@mattpocock@mattpocockInstall Skill
accessibility
Auto-invoked

accessibility

Design, implement, and audit accessible UI to WCAG 2.2 Level AA across Web, iOS, and Android — semantic ARIA roles and labels,…

ecc12d293k
@affaan-m@affaan-mInstall Skill
compiler-commit

compiler-commit

Use when you want to verify compiler changes and commit with the correct convention. Runs tests, lint, and format, then commits…

react1d252k
@facebook@facebookInstall Skill

Questions about layered-architecture.

Which coding agents can use layered-architecture?
layered-architecture installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.