---
name: quarkus-service-conventions
source: https://app.decimal.ai/s/quarkus-service-conventions@1/SKILL.md
source_sha256: d3617315e207
---

# Quarkus Service Conventions

## Contract
Enforces Quarkus (CDI / Panache / RESTEasy Reactive / Mutiny) idioms when writing
Java for a Quarkus service. Apply whenever the project is Quarkus — do NOT reach for
Spring Boot annotations, naming, or layout. Every rule below is mandatory.

## Rules

### 1. Naming and package layout
- A REST endpoint class is named `<Name>Resource` (e.g. `MarketResource`, `OrderResource`).
  Never `<Name>Controller`.
- REST endpoint classes live in a `resource/` package. Never `controller/`.
- Standard packages: `resource/`, `service/`, `repository/`, `domain/` (entities),
  `dto/`, `config/`, `mapper/`.

### 2. REST endpoints (JAX-RS)
- Class-level: `@Path("/markets")`. Method-level: `@GET` / `@POST` / `@PUT` / `@DELETE`
  plus a method-level `@Path("/{id}")` for sub-paths.
- Path variables: `@PathParam("id")`. Query params: `@QueryParam("page")`.
- Produce/consume JSON with `@Produces(MediaType.APPLICATION_JSON)` /
  `@Consumes(MediaType.APPLICATION_JSON)`.
- Never use Spring `@RestController`, `@GetMapping`, `@PostMapping`, `@RequestMapping`,
  `@PathVariable`, or `@RequestParam`.

### 3. CDI beans and injection
- Annotate service/bean classes `@ApplicationScoped`. Never `@Service`, `@Component`,
  `@RestController`, or `@Singleton` (`@Singleton` is non-proxyable — interception breaks).
- Inject dependencies with `@Inject` (constructor injection or field injection). Never
  `@Autowired`.

### 4. Panache entities (active record)
- Entity `extends PanacheEntity` (JPA) or `extends PanacheMongoEntity` (MongoDB,
  annotated `@MongoEntity(collection = "...")`).
- Declare `public` fields with NO getters and NO setters — Panache generates accessors at
  build time. Do not write `private` fields + boilerplate getters/setters.
- Query with static finders on the entity: `Market.find("slug", slug)`,
  `Market.findById(id)`, `Market.listAll()`. For an `Optional`, use
  `.firstResultOptional()`. Never inject a `JpaRepository`/`CrudRepository`.

### 5. Reactive (Mutiny)
- Reactive methods return `Uni<T>` (single) or `Multi<T>` (stream). Never
  `CompletableFuture`, Reactor `Mono`/`Flux`, or a blocking `T`.
- Compose with `.chain(...)`, `.onItem()...`, `.onFailure()...`. Handle absence with
  `.onItem().ifNull().failWith(() -> ...)`.
- Never make a blocking call inside a `Uni`/`Multi` pipeline (it stalls the event loop).
- To consume one cold `Uni` from two subscribers, share it with `.memoize().indefinitely()`
  — do not call `.subscribe()` twice on the same cold `Uni`.

### 6. Logging
- Use JBoss Logging: `private static final Logger log = Logger.getLogger(Foo.class);`
  (`org.jboss.logging.Logger`). Or CDI-inject it: `@Inject Logger log;`.
- Format with `log.infof("fetch slug=%s", slug)` / `log.errorf(ex, "failed slug=%s", slug)`.
- Never use SLF4J `LoggerFactory.getLogger(...)` or `{}` placeholders.

### 7. Configuration
- Bind a group of properties with `@ConfigMapping(prefix = "market")` on an INTERFACE with
  method accessors (`int maxPageSize();`). For a single value, use
  `@ConfigProperty(name = "market.max-page-size", defaultValue = "100")`.
- Never `@ConfigurationProperties` or `@Value`.
- The settings file is `src/main/resources/application.properties`. Never `application.yml`.
- DB seed/import data goes in `src/main/resources/import.sql` (Hibernate auto-loads it in dev/test).

### 8. Exceptions
- Map a domain exception to an HTTP response with an `ExceptionMapper<E>` annotated
  `@Provider`, or a `@ServerExceptionMapper` method (RESTEasy Reactive).
- Never `@RestControllerAdvice` or `@ExceptionHandler`.

### 9. Validation
- Validate a request body or bean param with `@Valid` on the parameter, combined with Bean
  Validation constraints (`@NotNull`, `@NotBlank`) on the DTO.

### 10. Tests
- Integration test: `@QuarkusTest`; replace a bean with `@InjectMock`; assert with
  RestAssured `given().when().get("/markets/unknown").then().statusCode(404)`.
- Never `@SpringBootTest`, `@WebMvcTest`, `@MockBean`, or `MockMvc`.
- Pure unit test (no CDI): plain JUnit 5 + Mockito —
  `@ExtendWith(MockitoExtension.class)` with `@Mock` / `@InjectMocks`. No `@QuarkusTest`.

## Worked examples (before = base's Spring default → after = Quarkus)

**Resource naming + JAX-RS**
```java
// BEFORE
@RestController
@RequestMapping("/markets")
public class MarketController {
  @GetMapping("/{slug}")
  public Market find(@PathVariable String slug) { ... }
}
// AFTER
@Path("/markets")
@ApplicationScoped
public class MarketResource {
  @GET @Path("/{slug}")
  @Produces(MediaType.APPLICATION_JSON)
  public Market find(@PathParam("slug") String slug) { ... }
}
```

**CDI bean + injection**
```java
// BEFORE
@Service
public class MarketService {
  @Autowired private MarketRepository repo;
}
// AFTER
@ApplicationScoped
public class MarketService {
  private final MarketRepository repo;
  @Inject public MarketService(MarketRepository repo) { this.repo = repo; }
}
```

**Panache entity**
```java
// BEFORE
@Entity
public class Market {
  @Id @GeneratedValue private Long id;
  private String name;
  public String getName() { return name; }
  public void setName(String n) { this.name = n; }
}
// AFTER
@Entity
public class Market extends PanacheEntity {
  public String name;
  public String status;   // public fields, accessors generated at build time
}
```

**Panache query → Optional**
```java
// BEFORE
Optional<Market> m = marketRepository.findBySlug(slug);
// AFTER
Optional<Market> m = Market.find("slug", slug).firstResultOptional();
```

**Reactive method**
```java
// BEFORE
public CompletableFuture<Order> placeOrder(OrderRequest req) { ... }
// AFTER
public Uni<Order> placeOrder(OrderRequest req) {
  return validate(req).chain(this::persist).chain(this::notifyFulfillment);
}
```

**Logging**
```java
// BEFORE
private static final Logger log = LoggerFactory.getLogger(MarketService.class);
log.info("fetch slug={}", slug);
// AFTER
private static final Logger log = Logger.getLogger(MarketService.class);
log.infof("fetch slug=%s", slug);
```

**Configuration**
```java
// BEFORE
@ConfigurationProperties(prefix = "market")
public class MarketProps { private int maxPageSize; /* getters */ }
// AFTER
@ConfigMapping(prefix = "market")
public interface MarketConfig { int maxPageSize(); Duration cacheTtl(); }
```

**Exception → 404**
```java
// BEFORE
@RestControllerAdvice
class Handler {
  @ExceptionHandler(MarketNotFoundException.class)
  ResponseEntity<?> h(MarketNotFoundException e) { return ResponseEntity.status(404).build(); }
}
// AFTER
@Provider
public class MarketNotFoundMapper implements ExceptionMapper<MarketNotFoundException> {
  public Response toResponse(MarketNotFoundException e) {
    return Response.status(404).entity(ErrorResponse.from(e)).build();
  }
}
```

**Integration test**
```java
// BEFORE
@WebMvcTest(MarketController.class)
class T { @Autowired MockMvc mvc; @MockBean MarketService svc; }
// AFTER
@QuarkusTest
class MarketResourceTest {
  @InjectMock MarketService svc;
  @Test void notFound() {
    given().when().get("/markets/unknown").then().statusCode(404);
  }
}
```

## Edge cases & exceptions
- **Single application-wide bean that must be interceptable/proxyable** → still
  `@ApplicationScoped`, not `@Singleton`. `@Singleton` exists but breaks proxying/interception.
- **MongoDB entity** → `extends PanacheMongoEntity` + `@MongoEntity(collection = "markets")`;
  same public-fields rule.
- **Repository pattern instead of active record** → a `PanacheRepository<Market>` bean is
  allowed, but do not mix active-record static finders and an injected repository in the same
  bounded context; pick one.
- **YAML config** is only available with the `quarkus-config-yaml` extension; default and
  idiomatic is `application.properties`.
- **Two cold-Uni subscribers** → `memoize()`; a hot/already-resolved `Uni` can be subscribed
  freely.
- **Field injection** is acceptable in Quarkus (package-private `@Inject` field avoids proxy
  issues) — unlike Spring where constructor injection is strongly preferred. Either is fine here.

## Do / Don't
- Never `@RestController` / `@GetMapping`; always `@Path` + `@GET` on a `*Resource`.
- Never `@Service` / `@Autowired`; always `@ApplicationScoped` + `@Inject`.
- Never `@Singleton` for a normal bean; always `@ApplicationScoped`.
- Never `private` entity fields + getters/setters; always `public` Panache fields.
- Never inject a `JpaRepository`; always static Panache finders.
- Never `CompletableFuture`/`Mono`/`Flux`; always `Uni`/`Multi`.
- Never block inside a `Uni`/`Multi`; always compose with `.chain`/`.onItem`.
- Never SLF4J + `{}`; always JBoss `Logger` + `infof`/`errorf` + `%s`.
- Never `@ConfigurationProperties`/`@Value`/`application.yml`; always
  `@ConfigMapping`/`@ConfigProperty` + `application.properties`.
- Never `@RestControllerAdvice`; always `ExceptionMapper`/`@ServerExceptionMapper`.
- Never `@SpringBootTest`/`@MockBean`/`MockMvc`; always `@QuarkusTest`/`@InjectMock`/RestAssured.

## Common mistakes (base's wrong defaults)
- Names the endpoint `*Controller` and annotates `@RestController` because "Java REST service"
  reads as Spring.
- Uses `@Service` + field `@Autowired` for the service bean.
- Writes a JPA entity with private fields and full getter/setter boilerplate.
- Injects a Spring-Data-style repository instead of calling Panache static finders.
- Returns `CompletableFuture`/`Mono` for an async method.
- Reaches for SLF4J `LoggerFactory` and `{}` placeholders.
- Defaults config to `application.yml` + `@ConfigurationProperties`.
- Handles exceptions with `@RestControllerAdvice`/`@ExceptionHandler`.
- Tests with `@SpringBootTest`/`@WebMvcTest` + `@MockBean` + `MockMvc`.

## Quick checklist
- [ ] `*Resource` in `resource/`, JAX-RS annotations only
- [ ] `@ApplicationScoped` + `@Inject` (never `@Service`/`@Autowired`/`@Singleton`)
- [ ] Panache entity: `extends PanacheEntity`, public fields, static finders
- [ ] Reactive returns `Uni`/`Multi`, no blocking inside pipeline
- [ ] JBoss `Logger` + `infof`/`errorf` + `%s`
- [ ] `@ConfigMapping`/`@ConfigProperty` + `application.properties`
- [ ] `ExceptionMapper`/`@ServerExceptionMapper` for error responses
- [ ] `@QuarkusTest` + `@InjectMock` + RestAssured; unit = Mockito only
