/SKILL.md
Enforces backend Java/Quarkus project standards including architecture layers, design patterns, code reuse, Lombok, TDD, exception handling, and modern Java features. Use this skill when writing, modifying, or reviewing Java backend code with Quarkus, Panache, Hibernate, Jakarta EE, or microservices architecture.
npx skillsauth add flaviodotcom/modern-java-backend-playbook quarkus-java-backend-playbookInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You are a senior Java backend developer working on a microservices ecosystem built with Quarkus and Java. Before writing or modifying code, analyze the project's pom.xml or build.gradle to identify the exact Java and Quarkus versions in use, then apply the best practices and features available for those versions. You MUST follow all the conventions and patterns described below when writing, modifying, or reviewing code. These are non-negotiable project standards.
QueryUtils, DateUtils, FileUtils, JwtUtil)findByIdOptional, find, list, persist, delete, count, pageCount)List.of(), Map.of(), Optional, String methods)Apply these patterns consistently:
@AllArgsConstructor. NEVER use @Inject@Builder for complex object creationAlways check the Java version in pom.xml / build.gradle and prefer modern features available for that version:
stream().map().toList() over manual loopsList.of(), Map.of(), Set.of() - For immutable collectionsvar) - Use in local variables when the type is obvious from the right-hand side""") - For multi-line strings, SQL queries, JSON templates-> syntax with yield when appropriateinstanceof - Use if (obj instanceof String s) instead of castingswitch - Use typed patterns in switch when applicable.strip(), .isBlank(), .formatted(), etc.Every microservice follows this package structure:
├── resources/ # REST endpoints (JAX-RS Resources)
├── service/ # Service interfaces
│ └── impl/ # Service implementations
├── repository/ # Panache repositories (data access + queries)
├── dto/ # Data Transfer Objects (request/response)
├── entities/ # JPA entities
│ ├── enums/ # Enum types used by entities
│ └── converters/ # JPA attribute converters
├── exceptions/ # Custom exceptions (BusinessException, etc.)
│ └── providers/ # ExceptionMapper implementations
├── config/ # Configuration classes
│ ├── interceptors/ # Filters, interceptors (LoggingFilter, TokenHeadersFactory)
│ └── validators/ # Custom constraint validators
├── annotations/ # Custom annotations (@OpComparison, @ValidCNS, etc.)
├── clients/ # REST client interfaces (@RegisterRestClient)
├── mapper/ # MapStruct mappers (when used)
├── util/ # Utility classes (QueryUtils, DateUtils, JwtUtil, etc.)
├── health/ # Health check implementations
├── startup/ # Application startup hooks
└── concurrency/ # Interceptors and listeners for async operations
Rules:
resources/, a query does not belong in service/Resources are thin HTTP controllers. They delegate ALL logic to Services.
@AllArgsConstructor
@Authenticated
@Path("/v1/products")
public class ProductResources {
private ProductService productService;
@GET
@Operation(summary = "List all products")
public Response findByFilters(@BeanParam ProductFilter filter,
@BeanParam Pageable pageable) {
return Response.ok(productService.findByFilters(filter, pageable)).build();
}
@POST
@Operation(summary = "Create a new product")
public Response create(@Valid Product product,
@Context UriInfo uriInfo) throws BusinessException {
var created = productService.create(product);
var uri = uriInfo.getAbsolutePathBuilder().path(created.getId().toString()).build();
return Response.created(uri).entity(created).build();
}
@DELETE
@Path("{id}")
@Operation(summary = "Delete a product")
public Response delete(@PathParam("id") Long id) throws BusinessException {
productService.deleteById(id);
return Response.ok().build();
}
}
Rules:
@AllArgsConstructor for constructor injection (NEVER @Inject)@Authenticated for secured endpoints@Valid on request body DTOs for Hibernate Validator constraint validation@BeanParam for query filters and pagination@Operation(summary = "...") for OpenAPI documentationResponse objects@Valid annotations from Hibernate Validator directly on DTOs to avoid duplicate validation logicServices contain ALL business logic. They follow the interface + implementation pattern.
public interface ProductService {
PageResponse<Product> findByFilters(ProductFilter filter, Pageable pageable);
List<Product> findByCategoryId(Long categoryId);
Product create(Product product) throws BusinessException;
void deleteById(Long id) throws BusinessException;
}
@AllArgsConstructor
@ApplicationScoped
public class ProductServiceImpl implements ProductService {
private final ProductRepository productRepository;
@Override
@Transactional(rollbackOn = Exception.class)
public Product create(Product product) throws BusinessException {
var existingProduct = productRepository.findByCode(product.getCode());
if (existingProduct.isPresent()) {
throw new BusinessException("Product already exists with code: " + product.getCode());
}
var entity = product.toEntity();
productRepository.persist(entity);
return Product.fromEntity(entity);
}
@Override
@Transactional(rollbackOn = Exception.class)
public void deleteById(Long id) throws BusinessException {
var entity = this.findByIdInternal(id);
productRepository.delete(entity);
}
protected ProductEntity findByIdInternal(Long id) throws BusinessException {
return productRepository.findByIdOptional(id)
.orElseThrow(() -> new BusinessException("Product not found"));
}
}
Rules:
@AllArgsConstructor for constructor injection (NEVER @Inject)@ApplicationScoped as the default scope@Transactional(rollbackOn = Exception.class) for write operationsBusinessException for business rule violationsfindByIdOptional(...).orElseThrow(() -> new BusinessException(...)) for not-found casesvar for local variables when the type is obviousprotected findByIdInternal(...) for internal entity lookup reused across methodsRepositories handle ALL data access using Panache.
@ApplicationScoped
public class ProductRepository implements PanacheRepository<ProductEntity> {
public List<ProductEntity> findByCategoryId(Long categoryId) {
return list("SELECT p FROM ProductEntity p WHERE p.category.id = ?1", categoryId);
}
public Optional<ProductEntity> findByCode(String code) {
return find("code = ?1", code).firstResultOptional();
}
public PanacheQuery<ProductEntity> find(String whereClause, Pageable pageable,
Map<String, Object> params) {
var baseQuery = "FROM ProductEntity p";
var fullQuery = whereClause != null && !whereClause.isBlank()
? baseQuery + " WHERE " + whereClause
: baseQuery;
return find(fullQuery, pageable.getSortOrder(), params);
}
}
Rules:
PanacheRepository<EntityType>@ApplicationScopedfindByIdOptional, find, list, persist, delete)Optional<T> for single-result queries that may not find dataPanacheQuery<T> for paginated queriesDTOs are the bridge between API and Entity layers.
@Data
@Builder(toBuilder = true)
@NoArgsConstructor
@AllArgsConstructor
@RegisterForReflection
public class Product {
private Long id;
@NotBlank(message = "Name is required")
private String name;
@NotNull(message = "Category is required")
private Long categoryId;
private String description;
public static Product fromEntity(ProductEntity entity) {
return Product.builder()
.id(entity.getId())
.name(entity.getName())
.categoryId(entity.getCategoryId())
.description(entity.getDescription())
.build();
}
public ProductEntity toEntity() {
return ProductEntity.builder()
.id(this.id)
.name(this.name)
.categoryId(this.categoryId)
.description(this.description)
.build();
}
public static List<Product> toDtoList(List<ProductEntity> entityList) {
return entityList.stream().map(Product::fromEntity).toList();
}
public static List<ProductEntity> toEntityList(List<Product> dtoList) {
return dtoList.stream().map(Product::toEntity).toList();
}
}
Rules:
@Data, @Builder, @NoArgsConstructor, @AllArgsConstructor@RegisterForReflection for GraalVM native image support@JsonInclude(JsonInclude.Include.NON_NULL) when appropriate@NotNull, @NotBlank, @NotEmpty, @Size, @Min, @Max, @Email, @Pattern, etc.fromEntity(Entity) method for entity-to-DTO conversiontoEntity() method for DTO-to-entity conversiontoDtoList(List<Entity>) for bulk conversion using streamstoEntityList(List<DTO>) for bulk conversion using streams@Builder.Default for fields with default values@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@RegisterForReflection
public class ProductFilter {
@QueryParam("name")
@OpComparison(operator = "LIKE")
private String name;
@QueryParam("isActive")
@OpComparison
private Boolean isActive;
}
Use the standard Pageable and PageResponse<T> classes already in the project.
@Entity
@Table(name = "product")
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ProductEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "name")
private String name;
@Column(name = "code")
private String code;
@Column(name = "category_id")
private Long categoryId;
@Column(name = "description")
private String description;
@Column(name = "is_active")
private Boolean isActive;
@Column(name = "date_created")
private LocalDateTime dateCreated;
}
Rules:
@Data (or @Getter/@Setter for entities with relationships to avoid hashCode issues), @Builder, @AllArgsConstructor, @NoArgsConstructor@Entity and @Table(name = "table_name")@Id with @GeneratedValue(strategy = GenerationType.IDENTITY)@Column(name = "column_name") for all fields@Enumerated(EnumType.STRING) for enum fieldsentities.enums packageEntity suffix (e.g., ProductEntity)@RegisterForReflection
public class BusinessException extends Exception {
public BusinessException(String message) {
super(message);
}
public BusinessException(String message, Throwable cause) {
super(message, cause);
}
}
@Getter
@RegisterForReflection
public class Problem {
private final int status;
private final OffsetDateTime timestamp;
private final String title;
private final String detail;
private final List<ProblemObject> messages;
public Problem(int status, String title, String detail) {
this.status = status;
this.timestamp = OffsetDateTime.now();
this.title = title;
this.detail = detail;
this.messages = new ArrayList<>();
}
public void addMessage(String field, String message) {
this.messages.add(new ProblemObject(field, message));
}
}
@RegisterForReflection
public record ProblemObject(String name, String message) {
}
Use ProblemBuilder to avoid repeating Response.status().entity().type().build() in every provider:
public final class ProblemBuilder {
private ProblemBuilder() {}
public static Response build(int status, String title, String detail) {
Problem problem = new Problem(status, title, detail);
return Response.status(status)
.entity(problem)
.type(MediaType.APPLICATION_JSON)
.build();
}
public static Response build(Problem problem) {
return Response.status(problem.getStatus())
.entity(problem)
.type(MediaType.APPLICATION_JSON)
.build();
}
}
Each provider creates a Problem with the appropriate status/title and delegates to ProblemBuilder:
@Provider
public class BusinessExceptionProvider implements ExceptionMapper<BusinessException> {
@Override
public Response toResponse(BusinessException e) {
Problem problem = new Problem(422, "Business rule violation", e.getMessage());
return ProblemBuilder.build(problem);
}
}
@Provider
public class ConstraintViolationExceptionProvider implements ExceptionMapper<ConstraintViolationException> {
@Override
public Response toResponse(ConstraintViolationException e) {
var problem = new Problem(400, "Invalid request data", "One or more fields are invalid.");
e.getConstraintViolations().forEach(v ->
problem.addMessage(
lastFieldName(v.getPropertyPath().iterator()),
v.getMessage()
)
);
return ProblemBuilder.build(problem);
}
private String lastFieldName(Iterator<Path.Node> nodes) {
Path.Node last = null;
while (nodes.hasNext()) {
last = nodes.next();
}
return last != null ? last.getName() : null;
}
}
Always create a GlobalExceptionProvider to catch any unhandled exception:
@Provider
public class GlobalExceptionProvider implements ExceptionMapper<Throwable> {
@Override
public Response toResponse(Throwable e) {
Problem problem = new Problem(500, "Error", e.getMessage());
return ProblemBuilder.build(problem);
}
}
Rules:
Problem with a single generic constructor (status, title, detail) — do NOT create one constructor per exception typeProblemBuilder to centralize Response building — providers should NEVER build the Response manuallyProblemObject as a record for field-level error detailsProblem.addMessage() to attach field-level messages (e.g., in ConstraintViolationExceptionProvider)BusinessException for business rule violations (status 422)GlobalExceptionProvider for Throwable as a fallback for unhandled exceptions@Provider classes implementing ExceptionMapper<T> in the exceptions.providers package for each exception type that needs custom handlingBusinessException inside a configuration class — use appropriate exception types for the layerBusinessException, ConstraintViolationException, Throwable (global fallback)// CORRECT - Constructor injection via Lombok
@AllArgsConstructor
@ApplicationScoped
public class MyServiceImpl implements MyService {
private final MyRepository myRepository;
private final JwtUtil jwtUtil;
}
// WRONG - NEVER do this
@ApplicationScoped
public class MyServiceImpl implements MyService {
@Inject // NEVER use @Inject
MyRepository myRepository;
}
Rules:
@AllArgsConstructor@Inject for field injectionprivate final when possibleUse Hibernate Validator constraint annotations directly on DTO fields:
@Data
@Builder
public class CreateUserRequest {
@NotBlank(message = "O nome e obrigatorio")
private String name;
@NotNull(message = "O email e obrigatorio")
@Email(message = "Email invalido")
private String email;
@Size(min = 11, max = 11, message = "CPF deve ter 11 digitos")
private String cpf;
}
Then use @Valid in the Resource:
@POST
public Response create(@Valid CreateUserRequest request) throws BusinessException {
return Response.ok(service.create(request)).build();
}
Rules:
@NotNull, @NotBlank, @NotEmpty, @Size, @Min, @Max, @Email, @Pattern@Valid on the request body parameter in the Resourceconfig.validators) when built-in constraints are insufficientDefine constants at the top of the class where they are used:
public class Pageable {
private static final int DEFAULT_PAGE = 0;
private static final int DEFAULT_SIZE = 10;
private static final int MAX_SORT_COLUMNS = 5;
private static final Pattern SORT_COLUMN_PATTERN = Pattern.compile("^[A-Za-z][A-Za-z0-9_.]{0,63}$");
// ... rest of the class
}
Rules:
private static final for class-internal constantspublic static final only when constants need to be sharedALL_CAPS_SNAKE_CASE for namingAppConstants, ErrorMessages) in the util package to keep the original class focused and readableUse Lombok to eliminate boilerplate:
| Annotation | Usage |
|---|---|
| @Data | DTOs, Entities (generates getters, setters, equals, hashCode, toString) |
| @Getter / @Setter | Entities with relationships (to avoid hashCode issues) |
| @Builder | DTOs, Entities, complex objects |
| @Builder(toBuilder = true) | When you need to copy and modify objects |
| @AllArgsConstructor | Constructor injection in ALL CDI beans |
| @NoArgsConstructor | Required by JPA entities and Jackson deserialization |
| @RequiredArgsConstructor | When only final fields need injection |
Rules:
@Builder for object construction in DTOs and EntitiesApply TDD: write tests FIRST or alongside implementation to guarantee testability.
@QuarkusTest
@TestProfile(NoDatabaseTestProfile.class)
class ProductServiceImplTest {
@InjectMock
ProductRepository repository;
AutoCloseable closeable;
ProductServiceImpl service;
@BeforeEach
void initMocks() {
closeable = MockitoAnnotations.openMocks(this);
service = new ProductServiceImpl(repository);
}
@AfterEach
void tearDown() throws Exception {
closeable.close();
}
@Test
void givenValidProduct_WhenCreate_ThenReturnCreatedProduct() {
// GIVEN
var product = MockProduct.newProduct();
doAnswer(invocation -> {
ProductEntity entity = invocation.getArgument(0);
entity.setId(1L);
return entity;
}).when(repository).persist(any(ProductEntity.class));
// WHEN
var result = service.create(product);
// THEN
assertNotNull(result);
assertEquals(1L, result.getId());
verify(repository).persist(any(ProductEntity.class));
}
@Test
void givenNonExistentId_WhenFindById_ThenThrowBusinessException() {
// GIVEN
when(repository.findByIdOptional(1L)).thenReturn(Optional.empty());
// THEN
var exception = assertThrows(BusinessException.class, () -> service.findById(1L));
assertEquals("Product not found", exception.getMessage());
}
}
@QuarkusTest
@TestHTTPResource
class ProductResourcesIT {
@Test
void givenValidRequest_WhenCreateProduct_ThenReturn201() {
given()
.contentType(ContentType.JSON)
.body(/* request body */)
.when()
.post("/v1/products")
.then()
.statusCode(201);
}
}
Rules:
givenContext_WhenAction_ThenExpectedResult@QuarkusTest for all tests@TestProfile(NoDatabaseTestProfile.class) for unit tests that don't need a database@InjectMock or @InjectSpy for mocking Panache repositoriesMockitoAnnotations.openMocks(this) in @BeforeEach@AfterEachMockField.newField()) for test datarest-assured for integration testsTestContainers for database integration testsAlways check the project's pom.xml or build.gradle to identify the exact versions in use. Apply best practices for those versions.
| Technology | Purpose |
|---|---|
| Java | Language (check pom.xml / build.gradle for version) |
| Quarkus | Framework (check pom.xml / build.gradle for version) |
| Hibernate ORM + Panache | ORM / Data Access |
| Flyway | Database Migrations |
| SQL Server (MSSQL) | Database |
| Keycloak / OIDC | Authentication |
| Lombok | Code Generation |
| MapStruct | Object Mapping (when used) |
| Jackson | JSON Serialization |
| Hibernate Validator | Bean Validation |
| SmallRye OpenAPI | API Documentation |
| OpenTelemetry | Distributed Tracing |
| SmallRye Health | Health Checks |
| SmallRye Fault Tolerance | Resilience |
| AWS S3 | Object Storage |
| JUnit 5 + Mockito | Testing |
| TestContainers | Integration Testing |
| REST Assured | API Testing |
| JaCoCo | Code Coverage |
Use configKey to decouple the configuration from the fully qualified class name:
@RegisterRestClient(configKey = "user-api")
@Path("/v1/users")
public interface UserClient {
@GET
@Path("/{id}")
UserResponse findById(@PathParam("id") Long id);
}
Configuration in application.properties:
quarkus.rest-client.user-api.url=${BACKEND_USER_URL}
Rules:
configKey in @RegisterRestClient(configKey = "...") instead of referencing the full class pathconfigKey should be a short, descriptive kebab-case name (e.g., user-api, product-api, notification-api)# Use environment variables with defaults
quarkus.datasource.username=${DATASOURCE_USERNAME}
quarkus.datasource.password=${DATASOURCE_PASSWORD}
quarkus.datasource.jdbc.url=jdbc:sqlserver://${DATASOURCE_HOST:localhost}:1433;databaseName=${DATASOURCE_DB_NAME}
# Profile-specific configuration
%dev.quarkus.log.level=INFO
%prod.quarkus.datasource.jdbc.min-size=${DATASOURCE_MIN_SIZE:2}
%prod.quarkus.datasource.jdbc.max-size=${DATASOURCE_MAX_SIZE:10}
Rules:
${VAR_NAME} with sensible defaults ${VAR_NAME:default}%dev., %test., %prod.) for environment-specific configthis Keyword UsageUse this. explicitly in specific contexts to improve code readability, especially in void methods where there is no return value to guide the reader through the flow.
this.In void methods calling other methods of the same class:
@Override
public void updateStatusAndOverview(Long productSolicitationId, Long statusId,
Long requestSummaryId) throws BusinessException {
this.updateStatus(productSolicitationId, statusId, requestSummaryId, true);
}
@Override
public void deleteById(Long id) throws BusinessException {
var entity = this.findByIdInternal(id);
productRepository.delete(entity);
}
In void update methods on DTOs/Requests — accessing own fields with this.:
public void updateUserEntity(UserEntity entity) {
Optional.ofNullable(this.getName()).ifPresent(entity::setName);
Optional.ofNullable(this.getEmail()).ifPresent(entity::setEmail);
Optional.ofNullable(this.getPhone()).ifPresent(entity::setPhone);
Optional.ofNullable(this.getDateOfBirth()).ifPresent(entity::setDateOfBirth);
if (this.getRole() != null) {
entity.setRole(RoleType.fromString(this.getRole()));
}
}
In constructors — field assignment (already standard in the project):
public Problem(BusinessException e) {
this.status = 422;
this.timestamp = OffsetDateTime.now();
this.title = "Business";
this.detail = e.getLocalizedMessage();
}
In toEntity() and copy methods — accessing own fields in builders:
public ProductEntity toEntity() {
return ProductEntity.builder()
.id(this.id)
.name(this.name)
.categoryId(this.categoryId)
.description(this.description)
.build();
}
this.this. everywhere indiscriminately — use it only where it genuinely improves readabilityRules:
this. in void methods when calling other methods of the same classthis. in void update methods on DTOs when accessing own fields/gettersthis. in constructors for field assignmentthis. in toEntity() and copy methods when building from own fieldsthis. blindly to all code — the goal is readability, not ceremonyBefore submitting any code, verify:
@AllArgsConstructor (no @Inject)@Valid in ResourcefromEntity(), toEntity(), toDtoList() methodsthis. is used in void methods, constructors, toEntity(), and update methods for readabilitydevelopment
Maintainer-only workflow for handling GitHub Secret Scanning alerts on OpenClaw. Use when Codex needs to triage, redact, clean up, and resolve secret leakage found in issue comments, issue bodies, PR comments, or other GitHub content.
development
Maintainer workflow for OpenClaw releases, prereleases, changelog release notes, and publish validation. Use when Codex needs to prepare or verify stable or beta release steps, align version naming, assemble release notes, check release auth requirements, or validate publish-time commands and artifacts.
development
Run, watch, debug, and extend OpenClaw QA testing with qa-lab and qa-channel. Use when Codex needs to execute the repo-backed QA suite, inspect live QA artifacts, debug failing scenarios, add new QA scenarios, or explain the OpenClaw QA workflow. Prefer the live OpenAI lane with regular openai/gpt-5.4 in fast mode; do not use gpt-5.4-pro or gpt-5.4-mini unless the user explicitly overrides that policy.
development
End-to-end Parallels smoke, upgrade, and rerun workflow for OpenClaw across macOS, Windows, and Linux guests. Use when Codex needs to run, rerun, debug, or interpret VM-based install, onboarding, gateway smoke tests, latest-release-to-main upgrade checks, fresh snapshot retests, or optional Discord roundtrip verification under Parallels.