Quarkus 3.27 LTS Java 17 Apache Camel Premium

InvoiceFlow — Guía de Patrones Quarkus

Arquitectura de referencia para microservicios Java cloud-native y event-driven. Cubre REST API, Panache, Camel/RabbitMQ, CDI y configuración multi-entorno.

Cliente: InvoiceFlow SaaS Stack: Quarkus 3.x + PostgreSQL + RabbitMQ Equipo: 4 devs backend Volumen: 50.000 facturas/mes Generado: CULTIVA IA · 2026-06-18
10
Patrones cubiertos
5
Capas arquitectura
3
Entornos config
4
Rutas Camel
GraalVM
Native ready

⬡ Arquitectura en Capas — Resource → Service → Repository

HTTP Client
JAX-RS
DocumentResource
@Path + @Valid
DocumentService
@Transactional
DocumentRepository
Panache
PostgreSQL
Hibernate ORM
ASYNC
EventService
Camel Route
RabbitMQ
Patrones de Código

🌐
REST API Resource — InvoiceResource

JAX-RS @Valid Paginación
@Path("/api/invoices")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@RequiredArgsConstructor
public class InvoiceResource {
  private final InvoiceService invoiceService;

  @GET
  public Response list(
      @QueryParam("page") @DefaultValue("0") int page,
      @QueryParam("size") @DefaultValue("20") int size) {
    List<Invoice> invoices = invoiceService.list(page, size);
    return Response.ok(invoices).build();
  }

  @POST
  public Response create(
      @Valid CreateInvoiceRequest request,
      @Context UriInfo uriInfo) {
    Invoice invoice = invoiceService.create(request);
    URI location = uriInfo.getAbsolutePathBuilder()
        .path(String.valueOf(invoice.id)).build();
    return Response.created(location)
        .entity(InvoiceResponse.from(invoice)).build();
  }

  @GET
  @Path("/{id}")
  public Response getById(@PathParam("id") Long id) {
    return invoiceService.findById(id)
        .map(InvoiceResponse::from)
        .map(Response::ok)
        .orElse(Response.status(NOT_FOUND))
        .build();
  }
}

Puntos clave

  • @RequiredArgsConstructor — inyección por constructor via Lombok
  • @Valid — Bean Validation activado en request body
  • 201 Created + header Location tras POST exitoso
  • Optional.map chain — 404 limpio sin null checks

Service Layer — InvoiceService

@Transactional CDI EventService
@Slf4j
@ApplicationScoped
@RequiredArgsConstructor
public class InvoiceService {
  private final InvoiceRepository repo;
  private final EventService eventService;
  private final InvoiceValidator validator;
  private final ProcessingPublisher publisher;

  @Transactional
  public Invoice create(CreateInvoiceRequest request) {
    ValidationResult v = validator.validate(request);
    if (!v.valid()) {
      eventService.createErrorEvent(
          request, "INVOICE_REJECTED", v.message());
      throw new WebApplicationException(
          v.message(), BAD_REQUEST);
    }
    Invoice inv = Invoice.from(request);
    repo.persist(inv);

    InvoiceReceipt receipt = InvoiceReceipt.from(inv);
    publisher.publishAsync(receipt);
    eventService.createSuccessEvent(
        receipt, "INVOICE_CREATED");

    log.info("Invoice {} created", inv.id);
    return inv;
  }

  public Optional<Invoice> findById(Long id) {
    return repo.findByIdOptional(id);
  }

  public List<Invoice> list(int page, int size) {
    return repo.findAll()
        .page(page, size).list();
  }
}

Puntos clave

  • Validación antes de persist — nunca guardar datos inválidos
  • EventService tracking — auditoría de éxito y error
  • publishAsync fuera de la TX — evitar tx largas bloqueantes
  • @Transactional solo en métodos que modifican datos

🗄
Repository — Panache Pattern

Panache HQL Paginación
@ApplicationScoped
public class InvoiceRepository
    implements PanacheRepository<Invoice> {

  // Buscar por estado con paginación
  public List<Invoice> findByStatus(
      InvoiceStatus status, int page, int size) {
    return find("status = ?1 order by createdAt desc",
        status).page(page, size).list();
  }

  // Buscar por número de referencia único
  public Optional<Invoice> findByReference(
      String ref) {
    return find("referenceNumber", ref)
        .firstResultOptional();
  }

  // Contar facturas pendientes del día
  public long countPendingToday(LocalDate date) {
    return count(
        "status = ?1 and createdAt >= ?2",
        InvoiceStatus.PENDING,
        date.atStartOfDay());
  }

  // Facturas por cliente con importe > umbral
  public List<Invoice> findHighValue(
      String clientId, BigDecimal minAmount) {
    return list(
        "clientId = ?1 and amount >= ?2 " +
        "order by amount desc",
        clientId, minAmount);
  }
}

Puntos clave

  • PanacheRepository — separa lógica de datos del dominio
  • HQL paramétrico — previene SQL injection, tipado seguro
  • .page(page, size) — paginación con offset gestionado por Panache
  • firstResultOptional() — nunca lanza NPE en búsquedas únicas

📋
DTOs + Validación + Exception Mapping

Records Bean Validation @Provider
// DTO de entrada con validaciones
public record CreateInvoiceRequest(
    @NotBlank @Size(max = 50)  String referenceNumber,
    @NotNull                     String clientId,
    @NotNull @Positive           BigDecimal amount,
    @NotNull @FutureOrPresent    Instant dueDate,
    @NotEmpty                    List<@NotBlank String> lines) {}

// DTO de respuesta con factory method
public record InvoiceResponse(
    Long id, String referenceNumber,
    BigDecimal amount, InvoiceStatus status) {
  public static InvoiceResponse from(Invoice inv) {
    return new InvoiceResponse(
        inv.getId(), inv.getReferenceNumber(),
        inv.getAmount(), inv.getStatus());
  }
}

// Mapper de violaciones de validación → 400
@Provider
public class ValidationExceptionMapper
    implements ExceptionMapper<ConstraintViolationException> {
  @Override
  public Response toResponse(ConstraintViolationException ex) {
    String msg = ex.getConstraintViolations().stream()
        .map(cv -> cv.getPropertyPath() +
                 ": " + cv.getMessage())
        .collect(Collectors.joining(", "));
    return Response.status(BAD_REQUEST)
        .entity(Map.of("error", "validation_error",
                       "message", msg)).build();
  }
}

Puntos clave

  • Java Records — DTOs inmutables, verbosidad cero
  • @NotBlank/@Positive/@FutureOrPresent — validación declarativa
  • @Provider ExceptionMapper — respuestas de error estandarizadas
  • factory method from(Entity) — conversión limpia sin MapStruct

🔀
Apache Camel — Rutas Event-Driven (InvoiceFlow)

direct: spring-rabbitmq choice/when onException
@ApplicationScoped
public class InvoiceProcessingRoute
    extends RouteBuilder {

  @Override
  public void configure() {
    // Manejo de errores de validación
    onException(ValidationException.class)
        .handled(true)
        .to("direct:validation-error")
        .log("Error: ${exception.message}");

    // Ruta principal de procesamiento
    from("direct:process-invoice")
        .routeId("invoice-processing")
        .log("Invoice: ${header.invoiceId}")
        .bean(InvoiceValidator.class, "validate")
        .bean(InvoiceTransformer.class, "enrich")
        // Routing condicional por tipo
        .choice()
          .when(header("type")
              .isEqualTo("CREDIT_NOTE"))
            .to("direct:process-credit")
          .when(header("type")
              .isEqualTo("PROFORMA"))
            .to("direct:process-proforma")
          .otherwise()
            .to("direct:process-standard")
        .end();
  }
}
// Publisher asíncrono a RabbitMQ
@ApplicationScoped
public class ProcessingPublisher {
  private final ProducerTemplate producer;

  public void publishAsync(InvoiceReceipt receipt) {
    producer.asyncSendBody(
        "direct:invoice-publisher", receipt);
  }
}

// Ruta de publicación con marshal JSON
@ApplicationScoped
public class InvoicePublisherRoute
    extends RouteBuilder {

  @ConfigProperty(name = "camel.rabbitmq.queue.invoices")
  String invoiceQueue;

  @ConfigProperty(name = "rabbitmq.host")
  String rabbitHost;

  @Override
  public void configure() {
    from("direct:invoice-publisher")
        .routeId("invoice-publisher")
        .log("Publishing to RabbitMQ: ${body}")
        .marshal().json(JsonLibrary.Jackson)
        .toF("spring-rabbitmq:%s?hostname=%s",
             invoiceQueue, rabbitHost);
  }
}
Flujo de rutas
direct:process-invoice
InvoiceValidator.validate
InvoiceTransformer.enrich
choice (type)
direct:process-standard / credit / proforma
direct:invoice-publisher
marshal().json(Jackson)
spring-rabbitmq:invoice-queue
async — no bloquea la TX
onException(ValidationException)
direct:validation-error
EventService.createErrorEvent
handled(true) — no re-throw
Mejores Prácticas por Área
Área Patrón Implementación Estado
Arquitectura Constructor injection via Lombok @RequiredArgsConstructor en todas las clases CDI Adoptado
Arquitectura Resource → Service → Repository 3 capas estrictas, sin saltar niveles ni acceder al repo desde resource Adoptado
Event-Driven EventService tracking Toda operación registra evento SUCCESS o ERROR en tabla events Adoptado
Event-Driven Publicación async fuera de TX publishAsync() se llama tras repo.persist() y commit Adoptado
Logging Logback + Logstash encoder JSON estructurado, MDC context propagation para tracing de request Configurar
Logging LogContext en operaciones async SafeAutoCloseable scope en CompletableFuture para tracing Pendiente
Async CompletableFuture para S3/storage Upload a S3/MinIO en pool separado, no bloquea hilo HTTP Pendiente
Config YAML multi-profile %dev/%test/%prod con env vars para secretos, nunca hardcoded Adoptado
Transacciones @Transactional scope mínimo TX cortas en service, nunca llamar async dentro de la TX Adoptado
Testing camel-quarkus-junit5 Tests de rutas con mock de beans, AssertJ para assertions fluidas En progreso
Dependencias Maven (pom.xml)
quarkus-bom
3.27.0 LTS
BOM principal — gestiona todas las versiones de extensiones Quarkus
quarkus-camel-bom
3.27.0
BOM de Apache Camel para Quarkus — rutas, direct, bean, spring-rabbitmq
quarkus-arc
via BOM
CDI container de Quarkus — @ApplicationScoped, inyección de dependencias
camel-quarkus-spring-rabbitmq
via Camel BOM
Integración Camel con RabbitMQ via protocolo AMQP Spring
camel-quarkus-direct
via Camel BOM
Rutas in-memory direct: para comunicación intra-servicio sin red
lombok
1.18.42
@RequiredArgsConstructor, @Slf4j, @Builder — reduce boilerplate
quarkus-logging-logback
via BOM
Logback como implementación de logging (override del JULI por defecto)
logstash-logback-encoder
via BOM
Encoder JSON estructurado para logs — compatible con ELK/Datadog
quarkus-hibernate-orm-panache
via BOM
ORM + pattern Repository — queries HQL, paginación, transacciones
Configuración Multi-Entorno (application.yml)
# application.yml — InvoiceFlow SaaS
"%dev":
  quarkus:
    datasource:
      jdbc:
        url: jdbc:postgresql://localhost:5432/invoiceflow_dev
      username: dev_user
      password: ${DB_PASSWORD}
    hibernate-orm:
      database:
        generation: drop-and-create
      log:
        sql: true

  rabbitmq:
    host: localhost
    port: 5672
    username: ${RABBITMQ_USER}
    password: ${RABBITMQ_PASSWORD}

"%test":
  quarkus:
    datasource:
      jdbc:
        url: jdbc:h2:mem:invoiceflow_test;DB_CLOSE_DELAY=-1
    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   # nunca drop en prod

  rabbitmq:
    host: ${RABBITMQ_HOST}
    port: ${RABBITMQ_PORT}

# Colas Camel
camel:
  rabbitmq:
    queue:
      invoices: invoiceflow-processing-queue
      notifications: invoiceflow-notifications-queue

# Health checks activos
quarkus:
  smallrye-health:
    ui:
      enable: true

Reglas de configuración

  • %dev/%test/%prod — un solo fichero, perfiles Quarkus activos por variable QUARKUS_PROFILE
  • ${ENV_VAR} — secretos siempre en variables de entorno, nunca hardcoded
  • generation: validate en prod — nunca drop-and-create en producción
  • H2 en test — sin dependencias externas, tests rápidos en CI