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.
WEBnunca llama aServicedirectamente;Servicenunca llama aControllers. - Commons transversal:
Utilities,Exceptions,Value Objects,Constantsse reutilizan en todo el servicio (pueden importarse por cualquier capa). - Alpura-SDK cross: usado dentro de la capa
ServiceyPersistencepara clientes REST/SOAP, auth, resiliencia (CB, Bulkhead, Timeout, Retry), tracing, serialización, headers comunes, secretos, etc. No se usa desdeControllersniFacade. - 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
| Capa | Clases / Interfaces esperadas | Responsabilidad | Qué 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. |
| Facade | Interfaces 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.*.websolo importa*.facadey*.commons.*. Nunca importa*.serviceni*.persistence.*.facadeno importa de ninguna otra capa Nexus; solo define interfaces; puede importar DTOs ycommons.valueobjects/constants.*.serviceimplementa*.facade; importa*.persistencey*.commons.*yalpura.sdk.*.*.persistencesolo importaalpura.model.*(entidades), Spring Data/JOOQ y*.commons.exceptionspara 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:
createPurchaseOrdergetRequestStatusByIdvalidateInputDataretryRequestpublishOutboxBatchcreateInFusionloadActiveRulesByScope
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
| Escenario | Alternativa |
|---|---|
| Parámetros con mismo origen (partner/scope) | Crear ScopeContext VO (partnerId/companyId/sourceSystem). |
| Objeto complejo con campos opcionales | Builder PurchaseOrderRequest.Builder en commons.valueobjects. |
| Método realiza varias acciones | Dividir 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.
| VO | Campos |
|---|---|
ScopeContext | partnerId, companyId, sourceSystem |
Correlation | requestId, eventId, traceparent |
Money | amount, currencyCode |
QuantityUom | value, unitOfMeasure |
IdempotencyKey | partnerId, value |
CreatePurchaseOrderCommand | scope, correlation, idempotencyKey, payload, submittedAt |
RequestSearchCriteria | partnerId, status, externalReference, from, to, page |
FusionClientConfig | baseUrl, 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.
| Clase | Categoría error | HTTP (mappeado en WEB) |
|---|---|---|
NexusValidationException | VALIDATION_ERROR | 422 |
NexusConflictException | BUSINESS_ERROR | 409 |
NexusNotFoundException | BUSINESS_ERROR | 404 |
NexusAuthenticationException | AUTHENTICATION_ERROR | 401 |
NexusAuthorizationException | AUTHORIZATION_ERROR | 403 |
NexusTemporaryException | TEMPORARY_ERROR | 502 / 504 (decide WebClient Adapter) |
NexusIntegrationException | INTEGRATION_ERROR | 500 / 424 Failed Dependency |
NexusTechnicalException | TECHNICAL_ERROR | 500 |
NexusNonRetryableException | NON_RETRYABLE_ERROR | 500 |
13.8 Utilities (Commons · Utilities)
Mantener helpers estáticos sin estado. Nombre público sin _; helpers internos privados con _.
| Utilidad | Métodos |
|---|---|
CorrelationUtils | generateRequestId(), generateEventId(), _toTraceMap() |
EnvelopeBuilder | buildRequestedEnvelope(RequestScope, Correlation, CanonicalPO) + _addTraceAttrs() |
Mappers | toStatusResponse(request, history, errors) + _mapHistory() |
RuleDslEvaluator | evaluate(rule, canonical) + _compile() + _bindParams() |
FusionMappingUtils | toFusionPayload(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.
| Contexto | Descripción | Agregados / Roots |
|---|---|---|
| Purchase Order Management | Ciclo de vida de la solicitud de OC | PurchaseOrderRequest (agg root id=requestId), PurchaseOrder, PurchaseOrderLine |
| Validation Management | Reglas versionadas y su ejecución | ValidationRule (root), ValidationRuleVersion |
| Partner Configuration | Partners y configuración | Partner (root), PartnerConfiguration |
| Integration Management | Interacciones con Fusion/Pub/Sub/Outbox | OutboxEvent, ProcessedEvent, IntegrationError, Fusion Client Config |
| Tracking & Audit | Historial, auditoría, búsqueda | TransactionStatusHistory, AuditEvent, búsquedas de status |
13.10 Checklist de revisión por Pull Request
- Estructura de capas: ¿El Controller llama a Facade? ¿Service implementa Facade? ¿Service llama Repositories/Alpura-SDK?
- Nombres de métodos: ¿Públicos CamelCase sin prefijo? ¿Privados con
_? - Cantidad de parámetros: ¿≤ 3? Si supera, ¿hay VO/DTO o Builder?
- Exceptions: ¿Lanza de
commons.exceptions? No usaRuntimeExceptiongenérico. - Commons/Alpura-SDK: ¿Reutiliza en lugar de duplicar?
- Transacciones: ¿
@Transactionalvive en Service? No en Controller ni Repository. - Trazabilidad: ¿Todo método público recibe/propaga
requestIdyeventIdcuando aplica?