Skip to main content

Arquitectura interna por capas + Lineamientos de nomenclatura

Este documento reemplaza la sección anterior de “Hexagonal”. Toda solución Nexus sigue la Arquitectura Base Backend Alpura de 4 capas + componente transversal Commons + Alpura-SDK:

WEB → Facade → Service → Persistence
|
Commons (transversal)
|
Alpura-SDK (cross)

Convenciones generales:

  • Capaces estrictas: una capa solo invoca la inmediatamente inferior. WEB nunca llama a Service directamente; Service nunca llama a Controllers.
  • Commons transversal: Utilities, Exceptions, Value Objects, Constants se reutilizan en todo el servicio (pueden importarse por cualquier capa).
  • Alpura-SDK cross: usado dentro de la capa Service y Persistence para clientes REST/SOAP, auth, resiliencia (CB, Bulkhead, Timeout, Retry), tracing, serialización, headers comunes, secretos, etc. No se usa desde Controllers ni Facade.
  • Nomenclatura uniforme de métodos (este documento define las reglas de nombres y parámetros).

13.1 Diagrama Arquitectura Base Backend (igual a la imagen de referencia)​


13.2 Responsabilidad por capa​

CapaClases / Interfaces esperadasResponsabilidadQué NO debe hacer
WEB@RestController, Filter (auth), Handler (telemetry), @ControllerAdvice (exceptions)Mapear HTTP ↔ DTOs; validación estructural; headers; status codes; traducción de excepiones de capas inferiores a ApiError.No contiene reglas de negocio ni acceso directo a DB.
FacadeInterfaces Java (PurchaseOrderFacade, ValidationFacade, PurchaseOrderStatusFacade).Fachadas delgadas que definen los contratos que expone la capa lógica. Una implementación en Service suele implementar N facades.No lógica de negocio; solo interface + documentación Javadoc.
Service@Service implementando Facades: PurchaseOrderService, StatusService, ValidationEngineService, OracleFusionPurchaseOrderService, OutboxPublisherService, etc.Lógica de negocio, orquestación de flujos, idempotencia, commits transaccionales, invocación a Repositories y a clientes externos vía Alpura-SDK.No parsea HTTP. No valida schema (eso es WEB/Facade).
Persistence@Repository (Spring Data JPA o JOOQ DAOs) + entidades Model (Alpura-Model).Acceso a datos y mapping de entidades ↔ modelo relacional. Consultas y mutaciones SQL.No implementa reglas, solo acceso a datos.
Commons (transversal)Utilities, Exceptions, Value Objects, Constants.Reutilización transversal: helpers, modelos de error, constantes, monedas/dinero, tipos de valores (Money, Quantity).No depende de ninguna capa funcional.
Alpura-SDK (cross)AlpuraWebClient, FusionTokenClient, PubSubPublisher, Resilience4jDecorators, OpenTelemetryTracer, SecretResolver, etc.Capacidades corporativas reutilizables.No contiene lógica de negocio Nexus.

13.3 Estructura de paquetes Spring Boot recomendada​

Se propone un esquema por componente (module) + capa. Cada micro/worker mantiene el mismo árbol para que cualquier desarrollador lo lea rápido.

src/main/java
└── com/alpura/nexus
├── purchaseorder # Componente de dominio: ingesta de OC
│ ├── web # WEB: @RestController, @ControllerAdvice
│ ├── facade # Facade: interfaces Java (PurchaseOrderFacade)
│ ├── service # Service: @Service impl (PurchaseOrderService, OutboxPublisherService)
│ └── persistence # Persistence: @Repository + model entities (mapping)
│
├── status # Componente: consulta de estado
│ ├── web
│ ├── facade
│ ├── service
│ └── persistence
│
├── validation # Componente: motor de reglas
│ ├── web # (si expone endpoints internos; normalmente usamos Config API)
│ ├── facade
│ ├── service # ValidationEngineService
│ └── persistence # ValidationRuleRepository, etc.
│
├── fusion # Componente: integración Oracle Fusion (Worker)
│ ├── consumer # WEB/Listener equiv al consumer Pub/Sub
│ ├── facade
│ ├── service # OracleFusionPurchaseOrderService
│ └── persistence # ProcessedEvent, PurchaseOrder repository, etc.
│
├── config # Componente: partners/reglas (Configuration API)
│ ├── web
│ ├── facade
│ ├── service
│ └── persistence
│
└── commons # Commons (transversal)
├── exceptions # Exceptions (BaseNexusException, IdempotencyConflictException, etc.)
├── utilities # Utilities (CorrelationIdUtils, Mappers, EnvelopeBuilders, etc.)
├── valueobjects # Value Objects (Money, Quantity, CorrelationId, IdempotencyKey, etc.)
└── constants # Constants (Topics, Scopes, Headers, etc.)

Regla estricta de dependencias​

  • com.alpura.nexus.*.web solo importa *.facade y *.commons.*. Nunca importa *.service ni *.persistence.
  • *.facade no importa de ninguna otra capa Nexus; solo define interfaces; puede importar DTOs y commons.valueobjects/constants.
  • *.service implementa *.facade; importa *.persistence y *.commons.* y alpura.sdk.*.
  • *.persistence solo importa alpura.model.* (entidades), Spring Data/JOOQ y *.commons.exceptions para lanzar excepciones traducidas.

13.4 Lineamientos de nomenclatura de métodos​

La regla aplica a todos los componentes Nexus (APIs, Workers, Validation Engine). Cumplir estas reglas reduce la entropía del código y facilita auditoría/debug.

13.4.1 Métodos públicos​

  • Estilo: CamelCase, empieza con minúscula.
  • Semántica: verbo o frase verbal precisa; evitar abreviaturas ambiguas (no usar calc, getPO, etc.). Usar verbos de dominio.
  • Número de parámetros: ideal 0 a 3. Si se requiere más, agrupar en un Value Object/DTO o aplicar Builder.
  • Ejemplos correctos:
    • createPurchaseOrder
    • getRequestStatusById
    • validateInputData
    • retryRequest
    • publishOutboxBatch
    • createInFusion
    • loadActiveRulesByScope

13.4.2 Métodos privados (o protegidos)​

  • Estilo: CamelCase, empieza con minúscula, con prefijo _ para distinguirlos de los públicos.
  • Propósito: paso interno dentro de un método público o helpers que no deben exponerse.
  • Parámetros: aplicar igual objetivo 0-3 params; agrupar en Value Objects cuando sea necesario.
  • Ejemplos correctos:
    • _verifyUserPermissions
    • _formatOrderData
    • _updateDatabaseRecord
    • _logActivity
    • _sendHttpRequest
    • _mapGeppToCanonical
    • _buildEnvelope
    • _resolveFusionConfig

13.4.3 ¿Cuándo crear un objeto para agrupar parámetros?​

Si en la firma de un método aparecen 4+ conceptos semánticos, crear un Value Object/DTO. Ejemplo:

Antes (evitar):

createRequest(partnerId, companyId, sourceSystem, externalReference, idempotencyKey, payload) // 6 params

Después (recomendado):

createRequest(CreateRequestCommand command) // 1 param

donde CreateRequestCommand agrupa los 6 campos en un Value Object inmutable.

13.4.4 Alternativas cuando exceda 3 parámetros​

EscenarioAlternativa
Parámetros con mismo origen (partner/scope)Crear ScopeContext VO (partnerId/companyId/sourceSystem).
Objeto complejo con campos opcionalesBuilder PurchaseOrderRequest.Builder en commons.valueobjects.
Método realiza varias accionesDividir en métodos privados auxiliares _paso1, _paso2, etc. y llamarlos desde el público.

13.5 Ejemplos aplicados a Nexus (públicos y privados)​

Se proveen ejemplos por componente para uniformar nombres. Usan las reglas de 0-3 params y _ en privados.

13.5.1 PurchaseOrderService (implementa PurchaseOrderFacade)​

@Service
public class PurchaseOrderService implements PurchaseOrderFacade {

// 1 param - agrupado en Command (0-3 params via VO)
@Override
@Transactional
public CreatePurchaseOrderAccepted createPurchaseOrder(CreatePurchaseOrderCommand command) {
_validateRequest(command);
_enrichScopeFromHeader(command);
PurchaseOrderRequest persisted = _persistRequest(command);
ValidationResult validated = _runValidation(command, persisted);
_writeValidationResult(persisted, validated);
if (!validated.isValid()) {
_transitionStatus(persisted, VALIDATION_FAILED, "validation engine");
_throwValidationException(validated);
}
OutboxEvent envelope = _buildOutboxEnvelope(persisted, validated, command);
_persistOutbox(envelope);
_transitionStatus(persisted, QUEUED, "outbox persisted");
_auditRequestQueued(persisted, envelope, command.context());
return _toAccepted(persisted);
}

// privados (prefijo _ )
private void _validateRequest(CreatePurchaseOrderCommand command) { /* ... */ }
private void _enrichScopeFromHeader(CreatePurchaseOrderCommand command) { /* ... */ }
private PurchaseOrderRequest _persistRequest(CreatePurchaseOrderCommand command) { /* ... */ }
private ValidationResult _runValidation(CreatePurchaseOrderCommand c, PurchaseOrderRequest p) { /* ... */ }
private void _writeValidationResult(PurchaseOrderRequest p, ValidationResult v) { /* ... */ }
private void _transitionStatus(PurchaseOrderRequest p, Status s, String reason) { /* ... */ }
private void _throwValidationException(ValidationResult v) { /* ... */ }
private OutboxEvent _buildOutboxEnvelope(PurchaseOrderRequest p, ValidationResult v, CreatePurchaseOrderCommand c) { /* ... */ }
private void _persistOutbox(OutboxEvent e) { /* ... */ }
private void _auditRequestQueued(PurchaseOrderRequest p, OutboxEvent e, ScopeContext c) { /* ... */ }
private CreatePurchaseOrderAccepted _toAccepted(PurchaseOrderRequest p) { /* ... */ }
}

13.5.2 OracleFusionPurchaseOrderService (implementa PurchaseOrderCreationService)​

@Service
public class OracleFusionPurchaseOrderService implements PurchaseOrderCreationService {

private final AlpuraWebClientFactory webClients; // Alpura-SDK

@Override
public FusionCreateResponse createPurchaseOrder(PurchaseOrderRequest request) {
FusionClientConfig cfg = _resolveFusionConfig(request);
String accessToken = _issueFusionToken(cfg);
Object payload = _mapCanonicalToFusion(request);
HttpResponse resp = _callFusionCreate(cfg, accessToken, payload);
FusionCreateResponse parsed = _parseFusionResponse(resp, request);
_registerProcessedEvent(request, parsed.fusionPoId());
return parsed;
}

private FusionClientConfig _resolveFusionConfig(PurchaseOrderRequest r) { /* ... */ }
private String _issueFusionToken(FusionClientConfig cfg) { /* ... */ }
private Object _mapCanonicalToFusion(PurchaseOrderRequest r) { /* ... */ }
private HttpResponse _callFusionCreate(FusionClientConfig c, String token, Object payload) { /* ... */ }
private FusionCreateResponse _parseFusionResponse(HttpResponse r, PurchaseOrderRequest req) { /* ... */ }
private void _registerProcessedEvent(PurchaseOrderRequest r, String fusionId) { /* ... */ }
}

13.5.3 ValidationEngineService (implementa ValidationFacade)​

@Service
public class ValidationEngineService implements ValidationFacade {

@Override
public ValidationResult validatePurchaseOrder(PurchaseOrderRequest request) {
ScopeContext scope = _scopeOf(request);
List<ValidationRuleVersion> rules = _loadActiveRulesByScope(scope, request.documentType());
List<ValidationError> errors = _executeRules(rules, request);
return _buildResult(errors, rules.size());
}

private ScopeContext _scopeOf(PurchaseOrderRequest r) { /* ... */ }
private List<ValidationRuleVersion> _loadActiveRulesByScope(ScopeContext s, String docType) { /* ... */ }
private List<ValidationError> _executeRules(List<ValidationRuleVersion> rules, PurchaseOrderRequest r) { /* ... */ }
private ValidationResult _buildResult(List<ValidationError> errors, int count) { /* ... */ }
}

13.5.4 OutboxPublisherService​

@Service
public class OutboxPublisherService {

// 0 params (ideal): batch publish por polling
public int publishPendingBatch() {
List<OutboxEventRow> batch = _lockPendingBatch();
for (OutboxEventRow row : batch) {
publish(row);
}
return batch.size();
}

// 1 param
public void publish(OutboxEventRow row) {
PubsubMessage msg = _buildMessage(row);
try {
_publishToTopic(row.topic(), msg);
_markProcessed(row);
} catch (Exception ex) {
_handlePublishFailure(row, ex);
}
}

private List<OutboxEventRow> _lockPendingBatch() { /* FOR UPDATE SKIP LOCKED */ }
private PubsubMessage _buildMessage(OutboxEventRow r) { /* ... */ }
private void _publishToTopic(String topic, PubsubMessage msg) { /* Alpura-SDK */ }
private void _markProcessed(OutboxEventRow r) { /* ... */ }
private void _handlePublishFailure(OutboxEventRow r, Exception ex) { /* ... */ }
}

13.5.5 PurchaseOrderStatusService (lectura)​

@Service
public class PurchaseOrderStatusService implements PurchaseOrderStatusFacade {

@Override
public RequestStatusResponse getRequestStatusById(UUID requestId) {
PurchaseOrderRequest request = _findRequest(requestId);
List<StatusTransition> history = _loadHistory(requestId);
List<IntegrationErrorView> errors = _loadErrors(requestId);
return _toStatusResponse(request, history, errors);
}

@Override
public Page<RequestStatusView> searchRequests(RequestSearchCriteria criteria) { /* 1 param VO */ }

private PurchaseOrderRequest _findRequest(UUID id) { /* ... */ }
private List<StatusTransition> _loadHistory(UUID id) { /* ... */ }
private List<IntegrationErrorView> _loadErrors(UUID id) { /* ... */ }
private RequestStatusResponse _toStatusResponse(PurchaseOrderRequest r, List<StatusTransition> h, List<IntegrationErrorView> e) { /* 3 params - límite aceptable */ }
}

13.5.6 Repositories (Persistence layer)​

  • Mantener el mismo estilo; 1-3 params. No exponer más de 3 parámetros sin SearchCriteria VO.
public interface PurchaseOrderRequestRepository extends JpaRepository<PurchaseOrderRequest, UUID> {

// publicos (no usa _ )
Optional<PurchaseOrderRequest> findByIdempotencyKey(String key);
Optional<PurchaseOrderRequest> findByExternalScope(String partnerId, String sourceSystem, String externalReference);

// paginada con 1 param VO
Page<PurchaseOrderRequestRow> search(RequestSearchCriteria criteria);
}

13.6 Value Objects sugeridos (Commons · Value Objects)​

Usar Value Objects (inmutables) para agrupar conceptos. Ayudan a mantener 0-3 params.

VOCampos
ScopeContextpartnerId, companyId, sourceSystem
CorrelationrequestId, eventId, traceparent
Moneyamount, currencyCode
QuantityUomvalue, unitOfMeasure
IdempotencyKeypartnerId, value
CreatePurchaseOrderCommandscope, correlation, idempotencyKey, payload, submittedAt
RequestSearchCriteriapartnerId, status, externalReference, from, to, page
FusionClientConfigbaseUrl, tokenUrl, clientIdRef, clientSecretRef, scopes

13.7 Exceptions (Commons · Exceptions)​

Se reutiliza la librería Alpura Exceptions y se extiende con Nexus. Todas llevan code, message, requestId opcional.

ClaseCategoría errorHTTP (mappeado en WEB)
NexusValidationExceptionVALIDATION_ERROR422
NexusConflictExceptionBUSINESS_ERROR409
NexusNotFoundExceptionBUSINESS_ERROR404
NexusAuthenticationExceptionAUTHENTICATION_ERROR401
NexusAuthorizationExceptionAUTHORIZATION_ERROR403
NexusTemporaryExceptionTEMPORARY_ERROR502 / 504 (decide WebClient Adapter)
NexusIntegrationExceptionINTEGRATION_ERROR500 / 424 Failed Dependency
NexusTechnicalExceptionTECHNICAL_ERROR500
NexusNonRetryableExceptionNON_RETRYABLE_ERROR500

13.8 Utilities (Commons · Utilities)​

Mantener helpers estáticos sin estado. Nombre público sin _; helpers internos privados con _.

UtilidadMétodos
CorrelationUtilsgenerateRequestId(), generateEventId(), _toTraceMap()
EnvelopeBuilderbuildRequestedEnvelope(RequestScope, Correlation, CanonicalPO) + _addTraceAttrs()
MapperstoStatusResponse(request, history, errors) + _mapHistory()
RuleDslEvaluatorevaluate(rule, canonical) + _compile() + _bindParams()
FusionMappingUtilstoFusionPayload(canonical, cfg) + _mapLine() + _normalizeSupplier()

13.9 Bounded Contexts (lenguaje ubicuo, sin ceremonial)​

Se mantienen 5 bounded contexts como marco del lenguaje. No requieren Hexagonal ni separación de paquetes rígida; pero cada contexto corresponde a un paquete en la estructura anterior.

ContextoDescripciónAgregados / Roots
Purchase Order ManagementCiclo de vida de la solicitud de OCPurchaseOrderRequest (agg root id=requestId), PurchaseOrder, PurchaseOrderLine
Validation ManagementReglas versionadas y su ejecuciónValidationRule (root), ValidationRuleVersion
Partner ConfigurationPartners y configuraciónPartner (root), PartnerConfiguration
Integration ManagementInteracciones con Fusion/Pub/Sub/OutboxOutboxEvent, ProcessedEvent, IntegrationError, Fusion Client Config
Tracking & AuditHistorial, auditoría, búsquedaTransactionStatusHistory, AuditEvent, búsquedas de status

13.10 Checklist de revisión por Pull Request​

  1. Estructura de capas: ¿El Controller llama a Facade? ¿Service implementa Facade? ¿Service llama Repositories/Alpura-SDK?
  2. Nombres de métodos: ¿Públicos CamelCase sin prefijo? ¿Privados con _?
  3. Cantidad de parámetros: ¿≤ 3? Si supera, ¿hay VO/DTO o Builder?
  4. Exceptions: ¿Lanza de commons.exceptions? No usa RuntimeException genérico.
  5. Commons/Alpura-SDK: ¿Reutiliza en lugar de duplicar?
  6. Transacciones: ¿@Transactional vive en Service? No en Controller ni Repository.
  7. Trazabilidad: ¿Todo método público recibe/propaga requestId y eventId cuando aplica?