mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-18 07:37:59 +02:00
* fix(skills): move version into metadata and normalize to semver 29 skills declared `version` at the top level of their frontmatter. The schema reads it from `metadata`, so tooling that follows the schema either misses it or has to special-case the top level. Three motion skills also declared `version: 1.0`, which is not a valid semantic version; normalized to `1.0.0`. No behavioral change — frontmatter metadata only. * fix(skills): state activation triggers in skill descriptions 148 skills described what they cover but never named the situation that should trigger them. Since the description is what Claude matches against to decide whether to load a skill, a description without a trigger makes activation guesswork — the skill is either missed or loaded at the wrong time. Added a "Use when ..." clause to each, derived from the skill's own body (most already stated the trigger under "## When to Use" or in the opening line; that intent is now reflected in the frontmatter where it is actually read from). Descriptions were only appended to; no existing wording was removed. * fix(skills): sync activation triggers into the Codex skill mirror 10 of the skills whose descriptions changed are also mirrored under `.agents/skills/`, where the description was previously a verbatim copy. Left alone, the two surfaces would disagree about when the skill applies. Only the description line is synced; the Codex copies keep their reduced frontmatter, since that validator accepts only name, description, metadata, license, and allowed-tools. * fix(skills): correct three activation clauses from review - autonomous-loops: the clause pulled new loop work into a skill that its own body marks as a compatibility shim retained for one release. It now points at the canonical continuous-agent-loop instead. - continuous-learning: the description carried the v1 routing directive twice; collapsed to one. - homelab-pihole-dns: the clause fired on any broken home DNS. Narrowed to tasks that actually involve Pi-hole. * chore: retain current main lockfile --------- Co-authored-by: Çağrı Solakoğlu <cagri.solakoglu@vtcenerji.com> Co-authored-by: haelyra <49814733+haelyra@users.noreply.github.com>
724 lines
21 KiB
Markdown
724 lines
21 KiB
Markdown
---
|
|
name: quarkus-patterns
|
|
description: Quarkus 3.x LTS architecture patterns with Camel for messaging, RESTful API design, CDI services, data access with Panache, and async processing. Use for Java Quarkus backend work with event-driven architectures. Use when building or reviewing a Quarkus service, especially with Camel messaging or Panache data access.
|
|
metadata:
|
|
origin: ECC
|
|
---
|
|
|
|
# Quarkus Development Patterns
|
|
|
|
Quarkus 3.x architecture and API patterns for cloud-native, event-driven services with Apache Camel.
|
|
|
|
## When to Activate
|
|
|
|
- Building REST APIs with JAX-RS or RESTEasy Reactive
|
|
- Structuring resource → service → repository layers
|
|
- Implementing event-driven patterns with Apache Camel and RabbitMQ
|
|
- Configuring Hibernate Panache, caching, or reactive streams
|
|
- Adding validation, exception mapping, or pagination
|
|
- Setting up profiles for dev/staging/production environments (YAML config)
|
|
- Custom logging with LogContext and Logback/Logstash encoder
|
|
- Working with CompletableFuture for async operations
|
|
- Implementing conditional flow processing
|
|
- Working with GraalVM native compilation
|
|
|
|
## Service Layer with Multiple Dependencies
|
|
|
|
```java
|
|
@Slf4j
|
|
@ApplicationScoped
|
|
@RequiredArgsConstructor
|
|
public class OrderProcessingService {
|
|
|
|
private final OrderValidator orderValidator;
|
|
private final EventService eventService;
|
|
private final OrderRepository orderRepository;
|
|
private final FulfillmentPublisher fulfillmentPublisher;
|
|
private final AuditPublisher auditPublisher;
|
|
|
|
@Transactional
|
|
public OrderReceipt process(CreateOrderCommand command) {
|
|
ValidationResult validation = orderValidator.validate(command);
|
|
if (!validation.valid()) {
|
|
eventService.createErrorEvent(command, "ORDER_REJECTED", validation.message());
|
|
throw new WebApplicationException(validation.message(), Response.Status.BAD_REQUEST);
|
|
}
|
|
|
|
Order order = Order.from(command);
|
|
orderRepository.persist(order);
|
|
|
|
OrderReceipt receipt = OrderReceipt.from(order);
|
|
fulfillmentPublisher.publishAsync(receipt);
|
|
auditPublisher.publish("ORDER_ACCEPTED", receipt);
|
|
eventService.createSuccessEvent(receipt, "ORDER_ACCEPTED");
|
|
|
|
log.info("Processed order {}", order.id);
|
|
return receipt;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Key Patterns:**
|
|
- `@RequiredArgsConstructor` for constructor injection via Lombok
|
|
- `@Slf4j` for Logback logging
|
|
- `@Transactional` on service methods that write through Panache or repositories
|
|
- Validate input before persistence or message publication
|
|
- Event tracking for success/error scenarios
|
|
- Async Camel message publishing
|
|
|
|
## Custom Logging Context Pattern (Logback)
|
|
|
|
```java
|
|
@ApplicationScoped
|
|
public class ProcessingService {
|
|
|
|
public void processDocument(Document doc) {
|
|
LogContext logContext = CustomLog.getCurrentContext();
|
|
try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) {
|
|
// Add context to all log statements
|
|
logContext.put("documentId", doc.getId().toString());
|
|
logContext.put("documentType", doc.getType());
|
|
logContext.put("userId", SecurityContext.getUserId());
|
|
|
|
log.info("Starting document processing");
|
|
|
|
// All logs within this scope inherit the context
|
|
processInternal(doc);
|
|
|
|
log.info("Document processing completed");
|
|
} catch (Exception e) {
|
|
log.error("Document processing failed", e);
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Logback Configuration (logback.xml):**
|
|
|
|
```xml
|
|
<configuration>
|
|
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
|
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
|
|
<includeContext>true</includeContext>
|
|
<includeMdc>true</includeMdc>
|
|
</encoder>
|
|
</appender>
|
|
|
|
<logger name="com.example" level="INFO"/>
|
|
<root level="WARN">
|
|
<appender-ref ref="CONSOLE"/>
|
|
</root>
|
|
</configuration>
|
|
```
|
|
|
|
## Event Service Pattern
|
|
|
|
```java
|
|
@Slf4j
|
|
@ApplicationScoped
|
|
@RequiredArgsConstructor
|
|
public class EventService {
|
|
private final EventRepository eventRepository;
|
|
private final ObjectMapper objectMapper;
|
|
|
|
public void createSuccessEvent(Object payload, String eventType) {
|
|
Objects.requireNonNull(payload, "Payload cannot be null");
|
|
Event event = new Event();
|
|
event.setType(eventType);
|
|
event.setStatus(EventStatus.SUCCESS);
|
|
event.setPayload(serializePayload(payload));
|
|
event.setTimestamp(Instant.now());
|
|
|
|
eventRepository.persist(event);
|
|
log.info("Success event created: {}", eventType);
|
|
}
|
|
|
|
public void createErrorEvent(Object payload, String eventType, String errorMessage) {
|
|
Objects.requireNonNull(payload, "Payload cannot be null");
|
|
if (errorMessage == null || errorMessage.isBlank()) {
|
|
throw new IllegalArgumentException("Error message cannot be blank");
|
|
}
|
|
Event event = new Event();
|
|
event.setType(eventType);
|
|
event.setStatus(EventStatus.ERROR);
|
|
event.setErrorMessage(errorMessage);
|
|
event.setPayload(serializePayload(payload));
|
|
event.setTimestamp(Instant.now());
|
|
|
|
eventRepository.persist(event);
|
|
log.error("Error event created: {} - {}", eventType, errorMessage);
|
|
}
|
|
|
|
private String serializePayload(Object payload) {
|
|
try {
|
|
return objectMapper.writeValueAsString(payload);
|
|
} catch (JsonProcessingException e) {
|
|
throw new IllegalStateException("Failed to serialize event payload", e);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Camel Message Publishing (RabbitMQ)
|
|
|
|
```java
|
|
@Slf4j
|
|
@ApplicationScoped
|
|
@RequiredArgsConstructor
|
|
public class BusinessRulesPublisher {
|
|
private final ProducerTemplate producerTemplate;
|
|
|
|
public void publishSync(BusinessRulesPayload payload) {
|
|
producerTemplate.sendBody(
|
|
"direct:business-rules-publisher",
|
|
payload
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
**Camel Route Configuration:**
|
|
|
|
```java
|
|
@ApplicationScoped
|
|
public class BusinessRulesRoute extends RouteBuilder {
|
|
|
|
@ConfigProperty(name = "camel.rabbitmq.queue.business-rules")
|
|
String businessRulesQueue;
|
|
|
|
@ConfigProperty(name = "rabbitmq.host")
|
|
String rabbitHost;
|
|
|
|
@ConfigProperty(name = "rabbitmq.port")
|
|
Integer rabbitPort;
|
|
|
|
@Override
|
|
public void configure() {
|
|
from("direct:business-rules-publisher")
|
|
.routeId("business-rules-publisher")
|
|
.log("Publishing message to RabbitMQ: ${body}")
|
|
.marshal().json(JsonLibrary.Jackson)
|
|
.toF("spring-rabbitmq:%s?hostname=%s&portNumber=%d",
|
|
businessRulesQueue, rabbitHost, rabbitPort);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Camel Direct Routes (In-Memory)
|
|
|
|
```java
|
|
@ApplicationScoped
|
|
public class DocumentProcessingRoute extends RouteBuilder {
|
|
|
|
@Override
|
|
public void configure() {
|
|
// Error handling
|
|
onException(ValidationException.class)
|
|
.handled(true)
|
|
.to("direct:validation-error-handler")
|
|
.log("Validation error: ${exception.message}");
|
|
|
|
// Main processing route
|
|
from("direct:process-document")
|
|
.routeId("document-processing")
|
|
.log("Processing document: ${header.documentId}")
|
|
.bean(DocumentValidator.class, "validate")
|
|
.bean(DocumentTransformer.class, "transform")
|
|
.choice()
|
|
.when(header("documentType").isEqualTo("INVOICE"))
|
|
.to("direct:process-invoice")
|
|
.when(header("documentType").isEqualTo("CREDIT_NOTE"))
|
|
.to("direct:process-credit-note")
|
|
.otherwise()
|
|
.to("direct:process-generic")
|
|
.end();
|
|
|
|
from("direct:validation-error-handler")
|
|
.bean(EventService.class, "createErrorEvent")
|
|
.log("Validation error handled");
|
|
}
|
|
}
|
|
```
|
|
|
|
## Camel File Processing
|
|
|
|
```java
|
|
@ApplicationScoped
|
|
public class FileMonitoringRoute extends RouteBuilder {
|
|
|
|
@ConfigProperty(name = "file.input.directory")
|
|
String inputDirectory;
|
|
|
|
@ConfigProperty(name = "file.processed.directory")
|
|
String processedDirectory;
|
|
|
|
@ConfigProperty(name = "file.error.directory")
|
|
String errorDirectory;
|
|
|
|
@Override
|
|
public void configure() {
|
|
from("file:" + inputDirectory + "?move=" + processedDirectory +
|
|
"&moveFailed=" + errorDirectory + "&delay=5000")
|
|
.routeId("file-monitor")
|
|
.log("Processing file: ${header.CamelFileName}")
|
|
.to("direct:process-file");
|
|
|
|
from("direct:process-file")
|
|
.bean(OrderProcessingService.class, "processFile")
|
|
.log("File processing completed");
|
|
}
|
|
}
|
|
```
|
|
|
|
## Camel Bean Invocation
|
|
|
|
```java
|
|
@ApplicationScoped
|
|
public class InvoiceRoute extends RouteBuilder {
|
|
|
|
@Override
|
|
public void configure() {
|
|
from("direct:invoice-validation")
|
|
.bean(InvoiceFlowValidator.class, "validateFlowWithConfig")
|
|
.log("Validation result: ${body}");
|
|
|
|
from("direct:persist-and-publish")
|
|
.bean(DocumentJobService.class, "createDocumentAndJobEntities")
|
|
.bean(BusinessRulesPublisher.class, "publishAsync")
|
|
.bean(EventService.class, "createSuccessEvent(${body}, 'PUBLISHED')");
|
|
}
|
|
}
|
|
```
|
|
|
|
## REST API Structure
|
|
|
|
```java
|
|
@Path("/api/documents")
|
|
@Produces(MediaType.APPLICATION_JSON)
|
|
@Consumes(MediaType.APPLICATION_JSON)
|
|
@RequiredArgsConstructor
|
|
public class DocumentResource {
|
|
private final DocumentService documentService;
|
|
|
|
@GET
|
|
public Response list(
|
|
@QueryParam("page") @DefaultValue("0") int page,
|
|
@QueryParam("size") @DefaultValue("20") int size) {
|
|
List<Document> documents = documentService.list(page, size);
|
|
return Response.ok(documents).build();
|
|
}
|
|
|
|
@POST
|
|
public Response create(@Valid CreateDocumentRequest request, @Context UriInfo uriInfo) {
|
|
Document document = documentService.create(request);
|
|
URI location = uriInfo.getAbsolutePathBuilder()
|
|
.path(String.valueOf(document.id))
|
|
.build();
|
|
return Response.created(location).entity(DocumentResponse.from(document)).build();
|
|
}
|
|
|
|
@GET
|
|
@Path("/{id}")
|
|
public Response getById(@PathParam("id") Long id) {
|
|
return documentService.findById(id)
|
|
.map(DocumentResponse::from)
|
|
.map(Response::ok)
|
|
.orElse(Response.status(Response.Status.NOT_FOUND))
|
|
.build();
|
|
}
|
|
}
|
|
```
|
|
|
|
## Repository Pattern (Panache Repository)
|
|
|
|
```java
|
|
@ApplicationScoped
|
|
public class DocumentRepository implements PanacheRepository<Document> {
|
|
|
|
public List<Document> findByStatus(DocumentStatus status, int page, int size) {
|
|
return find("status = ?1 order by createdAt desc", status)
|
|
.page(page, size)
|
|
.list();
|
|
}
|
|
|
|
public Optional<Document> findByReferenceNumber(String referenceNumber) {
|
|
return find("referenceNumber", referenceNumber).firstResultOptional();
|
|
}
|
|
|
|
public long countByStatusAndDate(DocumentStatus status, LocalDate date) {
|
|
return count("status = ?1 and createdAt >= ?2", status, date.atStartOfDay());
|
|
}
|
|
}
|
|
```
|
|
|
|
## Service Layer with Transactions
|
|
|
|
```java
|
|
@ApplicationScoped
|
|
@RequiredArgsConstructor
|
|
public class DocumentService {
|
|
private final DocumentRepository repo;
|
|
private final EventService eventService;
|
|
|
|
@Transactional
|
|
public Document create(CreateDocumentRequest request) {
|
|
Document document = new Document();
|
|
document.setReferenceNumber(request.referenceNumber());
|
|
document.setDescription(request.description());
|
|
document.setStatus(DocumentStatus.PENDING);
|
|
document.setCreatedAt(Instant.now());
|
|
|
|
repo.persist(document);
|
|
|
|
eventService.createSuccessEvent(document, "DOCUMENT_CREATED");
|
|
|
|
return document;
|
|
}
|
|
|
|
public Optional<Document> findById(Long id) {
|
|
return repo.findByIdOptional(id);
|
|
}
|
|
|
|
public List<Document> list(int page, int size) {
|
|
return repo.findAll()
|
|
.page(page, size)
|
|
.list();
|
|
}
|
|
}
|
|
```
|
|
|
|
## DTOs and Validation
|
|
|
|
```java
|
|
public record CreateDocumentRequest(
|
|
@NotBlank @Size(max = 200) String referenceNumber,
|
|
@NotBlank @Size(max = 2000) String description,
|
|
@NotNull @FutureOrPresent Instant validUntil,
|
|
@NotEmpty List<@NotBlank String> categories) {}
|
|
|
|
public record DocumentResponse(Long id, String referenceNumber, DocumentStatus status) {
|
|
public static DocumentResponse from(Document document) {
|
|
return new DocumentResponse(document.getId(), document.getReferenceNumber(),
|
|
document.getStatus());
|
|
}
|
|
}
|
|
```
|
|
|
|
## Exception Mapping
|
|
|
|
```java
|
|
@Provider
|
|
public class ValidationExceptionMapper implements ExceptionMapper<ConstraintViolationException> {
|
|
@Override
|
|
public Response toResponse(ConstraintViolationException exception) {
|
|
String message = exception.getConstraintViolations().stream()
|
|
.map(cv -> cv.getPropertyPath() + ": " + cv.getMessage())
|
|
.collect(Collectors.joining(", "));
|
|
|
|
return Response.status(Response.Status.BAD_REQUEST)
|
|
.entity(Map.of("error", "validation_error", "message", message))
|
|
.build();
|
|
}
|
|
}
|
|
|
|
@Provider
|
|
@Slf4j
|
|
public class GenericExceptionMapper implements ExceptionMapper<Exception> {
|
|
|
|
@Override
|
|
public Response toResponse(Exception exception) {
|
|
log.error("Unhandled exception", exception);
|
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
|
.entity(Map.of("error", "internal_error", "message", "An unexpected error occurred"))
|
|
.build();
|
|
}
|
|
}
|
|
```
|
|
|
|
## CompletableFuture Async Operations
|
|
|
|
```java
|
|
@Slf4j
|
|
@ApplicationScoped
|
|
@RequiredArgsConstructor
|
|
public class FileStorageService {
|
|
private final S3Client s3Client;
|
|
private final ExecutorService executorService;
|
|
|
|
@ConfigProperty(name = "storage.bucket-name")
|
|
String bucketName;
|
|
|
|
public CompletableFuture<StoredDocumentInfo> uploadOriginalFile(
|
|
InputStream inputStream,
|
|
long size,
|
|
LogContext logContext,
|
|
InvoiceFormat format) {
|
|
|
|
return CompletableFuture.supplyAsync(() -> {
|
|
try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) {
|
|
String path = generateStoragePath(format);
|
|
|
|
PutObjectRequest request = PutObjectRequest.builder()
|
|
.bucket(bucketName)
|
|
.key(path)
|
|
.contentLength(size)
|
|
.build();
|
|
|
|
s3Client.putObject(request, RequestBody.fromInputStream(inputStream, size));
|
|
|
|
log.info("File uploaded to S3: {}", path);
|
|
|
|
return new StoredDocumentInfo(path, size, Instant.now());
|
|
} catch (Exception e) {
|
|
log.error("Failed to upload file to S3", e);
|
|
throw new StorageException("Upload failed", e);
|
|
}
|
|
}, executorService);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Caching
|
|
|
|
```java
|
|
@ApplicationScoped
|
|
@RequiredArgsConstructor
|
|
public class DocumentCacheService {
|
|
private final DocumentRepository repo;
|
|
|
|
@CacheResult(cacheName = "document-cache")
|
|
public Optional<Document> getById(@CacheKey Long id) {
|
|
return repo.findByIdOptional(id);
|
|
}
|
|
|
|
@CacheInvalidate(cacheName = "document-cache")
|
|
public void evict(@CacheKey Long id) {}
|
|
|
|
@CacheInvalidateAll(cacheName = "document-cache")
|
|
public void evictAll() {}
|
|
}
|
|
```
|
|
|
|
## Configuration as YAML
|
|
|
|
```yaml
|
|
# application.yml
|
|
"%dev":
|
|
quarkus:
|
|
datasource:
|
|
jdbc:
|
|
url: jdbc:postgresql://localhost:5432/dev_db
|
|
username: dev_user
|
|
password: ${DB_PASSWORD}
|
|
hibernate-orm:
|
|
database:
|
|
generation: drop-and-create
|
|
|
|
rabbitmq:
|
|
host: localhost
|
|
port: 5672
|
|
username: ${RABBITMQ_USER}
|
|
password: ${RABBITMQ_PASSWORD}
|
|
|
|
"%test":
|
|
quarkus:
|
|
datasource:
|
|
jdbc:
|
|
url: jdbc:h2:mem:test
|
|
hibernate-orm:
|
|
database:
|
|
generation: drop-and-create
|
|
|
|
"%prod":
|
|
quarkus:
|
|
datasource:
|
|
jdbc:
|
|
url: ${DATABASE_URL}
|
|
username: ${DB_USER}
|
|
password: ${DB_PASSWORD}
|
|
hibernate-orm:
|
|
database:
|
|
generation: validate
|
|
|
|
rabbitmq:
|
|
host: ${RABBITMQ_HOST}
|
|
port: ${RABBITMQ_PORT}
|
|
username: ${RABBITMQ_USER}
|
|
password: ${RABBITMQ_PASSWORD}
|
|
|
|
# Camel configuration
|
|
camel:
|
|
rabbitmq:
|
|
queue:
|
|
business-rules: business-rules-queue
|
|
invoice-processing: invoice-processing-queue
|
|
```
|
|
|
|
## Health Checks
|
|
|
|
```java
|
|
@Readiness
|
|
@ApplicationScoped
|
|
@RequiredArgsConstructor
|
|
public class DatabaseHealthCheck implements HealthCheck {
|
|
private final AgroalDataSource dataSource;
|
|
|
|
@Override
|
|
public HealthCheckResponse call() {
|
|
try (Connection conn = dataSource.getConnection()) {
|
|
boolean valid = conn.isValid(2);
|
|
return HealthCheckResponse.named("Database connection")
|
|
.status(valid)
|
|
.build();
|
|
} catch (SQLException e) {
|
|
return HealthCheckResponse.down("Database connection");
|
|
}
|
|
}
|
|
}
|
|
|
|
@Liveness
|
|
@ApplicationScoped
|
|
public class CamelHealthCheck implements HealthCheck {
|
|
@Inject
|
|
CamelContext camelContext;
|
|
|
|
@Override
|
|
public HealthCheckResponse call() {
|
|
boolean isStarted = camelContext.getStatus().isStarted();
|
|
return HealthCheckResponse.named("Camel Context")
|
|
.status(isStarted)
|
|
.build();
|
|
}
|
|
}
|
|
```
|
|
|
|
## Dependencies (Maven)
|
|
|
|
```xml
|
|
<properties>
|
|
<quarkus.platform.version>3.27.0</quarkus.platform.version>
|
|
<lombok.version>1.18.42</lombok.version>
|
|
<assertj-core.version>3.24.2</assertj-core.version>
|
|
<jacoco-maven-plugin.version>0.8.13</jacoco-maven-plugin.version>
|
|
<maven.compiler.release>17</maven.compiler.release>
|
|
</properties>
|
|
|
|
<dependencyManagement>
|
|
<dependencies>
|
|
<dependency>
|
|
<groupId>io.quarkus.platform</groupId>
|
|
<artifactId>quarkus-bom</artifactId>
|
|
<version>${quarkus.platform.version}</version>
|
|
<type>pom</type>
|
|
<scope>import</scope>
|
|
</dependency>
|
|
<dependency>
|
|
<groupId>io.quarkus.platform</groupId>
|
|
<artifactId>quarkus-camel-bom</artifactId>
|
|
<version>${quarkus.platform.version}</version>
|
|
<type>pom</type>
|
|
<scope>import</scope>
|
|
</dependency>
|
|
</dependencies>
|
|
</dependencyManagement>
|
|
|
|
<dependencies>
|
|
<!-- Quarkus Core -->
|
|
<dependency>
|
|
<groupId>io.quarkus</groupId>
|
|
<artifactId>quarkus-arc</artifactId>
|
|
</dependency>
|
|
<dependency>
|
|
<groupId>io.quarkus</groupId>
|
|
<artifactId>quarkus-config-yaml</artifactId>
|
|
</dependency>
|
|
|
|
<!-- Camel Extensions -->
|
|
<dependency>
|
|
<groupId>org.apache.camel.quarkus</groupId>
|
|
<artifactId>camel-quarkus-spring-rabbitmq</artifactId>
|
|
</dependency>
|
|
<dependency>
|
|
<groupId>org.apache.camel.quarkus</groupId>
|
|
<artifactId>camel-quarkus-direct</artifactId>
|
|
</dependency>
|
|
<dependency>
|
|
<groupId>org.apache.camel.quarkus</groupId>
|
|
<artifactId>camel-quarkus-bean</artifactId>
|
|
</dependency>
|
|
|
|
<!-- Lombok -->
|
|
<dependency>
|
|
<groupId>org.projectlombok</groupId>
|
|
<artifactId>lombok</artifactId>
|
|
<version>${lombok.version}</version>
|
|
<scope>provided</scope>
|
|
</dependency>
|
|
|
|
<!-- Logging -->
|
|
<dependency>
|
|
<groupId>io.quarkiverse.logging.logback</groupId>
|
|
<artifactId>quarkus-logging-logback</artifactId>
|
|
</dependency>
|
|
<dependency>
|
|
<groupId>net.logstash.logback</groupId>
|
|
<artifactId>logstash-logback-encoder</artifactId>
|
|
</dependency>
|
|
</dependencies>
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### Architecture
|
|
- Use `@RequiredArgsConstructor` with Lombok for constructor injection
|
|
- Keep service layer thin; delegate complex logic to specialized classes
|
|
- Use Camel routes for message routing and integration patterns
|
|
- Prefer Panache Repository pattern for data access
|
|
|
|
### Event-Driven
|
|
- Always track operations with EventService (success/error events)
|
|
- Use Camel `direct:` endpoints for in-memory routing
|
|
- Use `spring-rabbitmq` component for RabbitMQ integration
|
|
- Implement async publishing with `ProducerTemplate.asyncSendBody()`
|
|
|
|
### Logging
|
|
- Use Logback with Logstash encoder for structured logging
|
|
- Propagate LogContext through service calls with `SafeAutoCloseable`
|
|
- Add contextual information to LogContext for request tracing
|
|
- Use `@Slf4j` instead of manual logger instantiation
|
|
|
|
### Async Operations
|
|
- Use CompletableFuture for non-blocking I/O operations
|
|
- Call `.join()` when you need to wait for completion
|
|
- Handle exceptions from CompletableFuture properly
|
|
- Pass LogContext to async operations for tracing
|
|
|
|
### Configuration
|
|
- Use YAML configuration (`quarkus-config-yaml`)
|
|
- Profile-aware configuration for dev/test/prod environments
|
|
- Externalize sensitive configuration to environment variables
|
|
- Use `@ConfigProperty` for type-safe config injection
|
|
|
|
### Validation
|
|
- Validate at resource layer with `@Valid`
|
|
- Use Bean Validation annotations on DTOs
|
|
- Map exceptions to proper HTTP responses with `@Provider`
|
|
|
|
### Transactions
|
|
- Use `@Transactional` on service methods that modify data
|
|
- Keep transactions short and focused
|
|
- Avoid calling async operations within transactions
|
|
|
|
### Testing
|
|
- Use `camel-quarkus-junit5` for route testing
|
|
- Use AssertJ for assertions
|
|
- Mock all external dependencies
|
|
- Test conditional flow logic thoroughly
|
|
|
|
### Quarkus-Specific
|
|
- Stay on latest LTS version (3.x)
|
|
- Use Quarkus dev mode for hot reload
|
|
- Add health checks for production readiness
|
|
- Test native compilation compatibility periodically
|