eva4j 1.0.17 → 1.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/AGENTS.md +2 -0
  2. package/DOMAIN_YAML_GUIDE.md +3 -1
  3. package/QUICK_REFERENCE.md +8 -4
  4. package/bin/eva4j.js +70 -2
  5. package/config/defaults.json +1 -0
  6. package/docs/CAMUNDA_DMN_GUIDE.md +1380 -0
  7. package/docs/KAFKA_PRODUCTION_CONFIG.md +441 -0
  8. package/docs/RABBITMQ_PRODUCTION_CONFIG.md +227 -0
  9. package/docs/commands/ADD_RABBITMQ_CLIENT.md +192 -0
  10. package/docs/commands/EVALUATE_SYSTEM.md +272 -8
  11. package/docs/commands/GENERATE_RABBITMQ_EVENT.md +341 -0
  12. package/docs/commands/GENERATE_RABBITMQ_LISTENER.md +595 -0
  13. package/docs/commands/GENERATE_TEMPORAL_FLOW.md +52 -12
  14. package/docs/commands/INDEX.md +27 -3
  15. package/docs/prototype/TEMPORAL_COMMUNICATION_PATTERNS.md +731 -0
  16. package/docs/prototype/TEMPORAL_DESIGN_METHODOLOGY.md +740 -0
  17. package/docs/prototype/system/RISKS.md +277 -0
  18. package/docs/prototype/system/customers.yaml +133 -0
  19. package/docs/prototype/system/inventory.yaml +109 -0
  20. package/docs/prototype/system/notifications.yaml +131 -0
  21. package/docs/prototype/system/orders.yaml +241 -0
  22. package/docs/prototype/system/payments.yaml +256 -0
  23. package/docs/prototype/system/products.yaml +168 -0
  24. package/docs/prototype/system/system.yaml +269 -0
  25. package/examples/domain-endpoints-multi-aggregate.yaml +140 -0
  26. package/examples/domain-read-models.yaml +2 -2
  27. package/examples/system/customer.yaml +89 -0
  28. package/examples/system/orders.yaml +119 -0
  29. package/examples/system/product.yaml +27 -0
  30. package/examples/system/system.yaml +80 -0
  31. package/package.json +1 -1
  32. package/src/agents/design-gap-analyst-temporal.agent.md +452 -0
  33. package/src/agents/design-gap-analyst.agent.md +383 -0
  34. package/src/agents/design-reviewer-temporal.agent.md +412 -0
  35. package/src/agents/design-reviewer.agent.md +31 -5
  36. package/src/agents/implement-use-cases.prompt.md +179 -0
  37. package/src/agents/ux-gap-analyst.agent.md +412 -0
  38. package/src/commands/add-rabbitmq-client.js +261 -0
  39. package/src/commands/add-temporal-client.js +22 -2
  40. package/src/commands/build.js +267 -11
  41. package/src/commands/evaluate-system.js +700 -13
  42. package/src/commands/generate-entities.js +308 -16
  43. package/src/commands/generate-rabbitmq-event.js +665 -0
  44. package/src/commands/generate-rabbitmq-listener.js +205 -0
  45. package/src/commands/generate-temporal-activity.js +968 -34
  46. package/src/commands/generate-temporal-flow.js +95 -38
  47. package/src/commands/generate-temporal-system.js +708 -0
  48. package/src/skills/build-system-yaml/SKILL.md +222 -2
  49. package/src/skills/build-system-yaml/references/domain-yaml-spec.md +50 -4
  50. package/src/skills/build-system-yaml/references/module-spec.md +57 -8
  51. package/src/skills/build-temporal-system/SKILL.md +752 -0
  52. package/src/skills/build-temporal-system/references/temporal-communication-patterns.md +167 -0
  53. package/src/skills/build-temporal-system/references/temporal-domain-yaml-spec.md +449 -0
  54. package/src/skills/build-temporal-system/references/temporal-module-spec.md +353 -0
  55. package/src/skills/build-temporal-system/references/temporal-system-yaml-spec.md +326 -0
  56. package/src/skills/implement-use-case/SKILL.md +350 -0
  57. package/src/skills/implement-use-case/references/use-case-patterns.md +980 -0
  58. package/src/skills/requirements-elicitation/SKILL.md +228 -0
  59. package/src/skills/requirements-elicitation/references/interview-framework.md +260 -0
  60. package/src/skills/requirements-elicitation/references/output-templates.md +368 -0
  61. package/src/utils/bounded-context-diagram.js +844 -0
  62. package/src/utils/domain-validator.js +266 -4
  63. package/src/utils/naming.js +10 -0
  64. package/src/utils/system-validator.js +169 -11
  65. package/src/utils/system-yaml-parser.js +318 -0
  66. package/src/utils/temporal-validator.js +497 -0
  67. package/src/utils/yaml-to-entity.js +10 -7
  68. package/templates/aggregate/DomainEventHandler.java.ejs +116 -22
  69. package/templates/aggregate/JpaAggregateRoot.java.ejs +2 -2
  70. package/templates/aggregate/JpaEntity.java.ejs +2 -2
  71. package/templates/base/docker/rabbitmq-services.yaml.ejs +12 -0
  72. package/templates/base/resources/parameters/develop/kafka.yaml.ejs +5 -0
  73. package/templates/base/resources/parameters/develop/rabbitmq.yaml.ejs +15 -0
  74. package/templates/base/resources/parameters/develop/temporal.yaml.ejs +0 -3
  75. package/templates/base/resources/parameters/local/kafka.yaml.ejs +5 -0
  76. package/templates/base/resources/parameters/local/rabbitmq.yaml.ejs +15 -0
  77. package/templates/base/resources/parameters/local/temporal.yaml.ejs +0 -3
  78. package/templates/base/resources/parameters/production/kafka.yaml.ejs +39 -8
  79. package/templates/base/resources/parameters/production/rabbitmq.yaml.ejs +32 -0
  80. package/templates/base/resources/parameters/production/temporal.yaml.ejs +0 -3
  81. package/templates/base/resources/parameters/test/kafka.yaml.ejs +12 -6
  82. package/templates/base/resources/parameters/test/rabbitmq.yaml.ejs +15 -0
  83. package/templates/base/resources/parameters/test/temporal.yaml.ejs +0 -3
  84. package/templates/base/root/AGENTS.md.ejs +1 -1
  85. package/templates/crud/EndpointsController.java.ejs +1 -1
  86. package/templates/crud/ScaffoldCommand.java.ejs +5 -2
  87. package/templates/crud/ScaffoldCommandHandler.java.ejs +3 -1
  88. package/templates/crud/ScaffoldQuery.java.ejs +5 -2
  89. package/templates/crud/ScaffoldQueryHandler.java.ejs +3 -1
  90. package/templates/crud/SubEntityRemoveCommand.java.ejs +1 -1
  91. package/templates/evaluate/report.html.ejs +1447 -90
  92. package/templates/kafka-event/KafkaConfigBean.java.ejs +1 -1
  93. package/templates/kafka-event/KafkaMessageBroker.java.ejs +3 -3
  94. package/templates/ports/PortAclMapper.java.ejs +35 -0
  95. package/templates/ports/PortFeignAdapter.java.ejs +7 -22
  96. package/templates/ports/PortFeignClient.java.ejs +4 -0
  97. package/templates/ports/PortResponseDto.java.ejs +1 -1
  98. package/templates/rabbitmq-event/RabbitConfigBean.java.ejs +33 -0
  99. package/templates/rabbitmq-event/RabbitConfigExchange.java.ejs +12 -0
  100. package/templates/rabbitmq-event/RabbitMessageBroker.java.ejs +35 -0
  101. package/templates/rabbitmq-event/RabbitMessageBrokerMethod.java.ejs +9 -0
  102. package/templates/rabbitmq-listener/RabbitConfigConsumerBean.java.ejs +33 -0
  103. package/templates/rabbitmq-listener/RabbitConfigConsumerExchange.java.ejs +12 -0
  104. package/templates/rabbitmq-listener/RabbitListenerClass.java.ejs +82 -0
  105. package/templates/rabbitmq-listener/RabbitListenerSimple.java.ejs +56 -0
  106. package/templates/read-model/ReadModelJpa.java.ejs +1 -1
  107. package/templates/read-model/ReadModelJpaRepository.java.ejs +2 -0
  108. package/templates/read-model/ReadModelRabbitListener.java.ejs +71 -0
  109. package/templates/read-model/ReadModelRepositoryImpl.java.ejs +9 -5
  110. package/templates/read-model/ReadModelSyncHandler.java.ejs +2 -0
  111. package/templates/shared/configurations/kafkaConfig/KafkaConfig.java.ejs +18 -4
  112. package/templates/shared/configurations/rabbitmqConfig/RabbitMQConfig.java.ejs +100 -0
  113. package/templates/shared/configurations/temporalConfig/TemporalConfig.java.ejs +2 -64
  114. package/templates/shared/configurations/temporalConfig/TemporalWorkerFactoryLifecycle.java.ejs +41 -0
  115. package/templates/temporal-activity/ActivityImpl.java.ejs +68 -2
  116. package/templates/temporal-activity/ActivityInput.java.ejs +14 -0
  117. package/templates/temporal-activity/ActivityInterface.java.ejs +7 -1
  118. package/templates/temporal-activity/ActivityOutput.java.ejs +14 -0
  119. package/templates/temporal-activity/NestedType.java.ejs +12 -0
  120. package/templates/temporal-activity/SharedActivityInput.java.ejs +14 -0
  121. package/templates/temporal-activity/SharedActivityInterface.java.ejs +15 -0
  122. package/templates/temporal-activity/SharedActivityOutput.java.ejs +14 -0
  123. package/templates/temporal-activity/SharedNestedType.java.ejs +12 -0
  124. package/templates/temporal-flow/ModuleHeavyActivity.java.ejs +6 -0
  125. package/templates/temporal-flow/ModuleLightActivity.java.ejs +6 -0
  126. package/templates/temporal-flow/ModuleTemporalWorkerConfig.java.ejs +58 -0
  127. package/templates/temporal-flow/WorkFlowImpl.java.ejs +172 -12
  128. package/templates/temporal-flow/WorkFlowInput.java.ejs +11 -0
  129. package/templates/temporal-flow/WorkFlowInterface.java.ejs +5 -4
  130. package/templates/temporal-flow/WorkFlowService.java.ejs +42 -12
  131. package/COMMAND_EVALUATION.md +0 -911
  132. package/test-c2010.js +0 -49
  133. package/test-update-compat.js +0 -109
  134. package/test-update-lifecycle.js +0 -121
@@ -0,0 +1,731 @@
1
+ # Patrones de Comunicación entre Microservicios con Temporal
2
+
3
+ ## 📋 Propósito
4
+
5
+ Cuando Temporal actúa como broker de orquestación, los microservicios se comunican de formas distintas según el caso de uso. Este documento describe los **4 patrones principales**, cuándo usar cada uno, y ejemplos concretos aplicados a un e-commerce.
6
+
7
+ ---
8
+
9
+ ## 🔄 Resumen Comparativo
10
+
11
+ | Patrón | Acoplamiento | Dirección | Espera respuesta | Caso de uso |
12
+ |--------|-------------|-----------|------------------|-------------|
13
+ | **Remote Activity** | Medio | Request → Response | ✅ Sí (bloqueante) | Operaciones atómicas cross-service |
14
+ | **Remote Activity + Async** | Medio | Request → Response (paralelo) | ✅ Sí (no bloqueante) | Múltiples operaciones independientes en paralelo |
15
+ | **Child Workflow** | Alto | Parent → Child | ✅ Sí | Subprocesos con ciclo de vida propio |
16
+ | **Signal External** | Bajo | Fire & Forget | ❌ No | Notificaciones entre workflows activos |
17
+ | **Signal + Await** | Bajo | Bidireccional | ✅ Sí (con espera) | Esperar evento externo con timeout |
18
+
19
+ ---
20
+
21
+ ## 1. Remote Activity
22
+
23
+ ### Concepto
24
+
25
+ Un workflow en el **Servicio A** invoca una actividad cuya implementación vive en el **Servicio B**. Temporal enruta la tarea al worker correcto a través del **Task Queue**. No hay HTTP entre servicios.
26
+
27
+ ```
28
+ Servicio A (workflow) Temporal Server Servicio B (worker)
29
+ │ │ │
30
+ │ stub.doSomething(data) │ │
31
+ │───────────────────────────►│ │
32
+ │ │ tarea en QUEUE_B ─────►│
33
+ │ │ │ ejecuta con acceso a BD
34
+ │ │ resultado ◄────────────│
35
+ │ resultado ◄───────────────│ │
36
+ ```
37
+
38
+ ### Cuándo usarlo
39
+
40
+ - Operaciones **atómicas** y **sin ciclo de vida propio**
41
+ - El workflow necesita el resultado para continuar
42
+ - Lectura de datos de otro servicio
43
+ - Escritura simple en otro servicio
44
+
45
+ ### Ejemplo: Orders invoca una actividad de Inventory
46
+
47
+ **Interfaz compartida** (en módulo `shared` o artefacto de contrato):
48
+
49
+ ```java
50
+ @ActivityInterface
51
+ public interface InventoryActivity {
52
+ ReserveResult reserveStock(String orderId, List<ItemRequest> items);
53
+ void releaseStock(String reservationId);
54
+ }
55
+ ```
56
+
57
+ **Implementación** (vive en el microservicio `inventory`):
58
+
59
+ ```java
60
+ public class InventoryActivityImpl implements InventoryActivity {
61
+
62
+ private final InventoryRepository inventoryRepository;
63
+
64
+ @Override
65
+ public ReserveResult reserveStock(String orderId, List<ItemRequest> items) {
66
+ // Acceso directo a la BD de inventory
67
+ for (ItemRequest item : items) {
68
+ Product product = inventoryRepository.findById(item.getProductId())
69
+ .orElseThrow(() -> new ProductNotFoundException(item.getProductId()));
70
+ product.reserve(item.getQuantity());
71
+ inventoryRepository.save(product);
72
+ }
73
+ String reservationId = UUID.randomUUID().toString();
74
+ return new ReserveResult(reservationId, orderId);
75
+ }
76
+
77
+ @Override
78
+ public void releaseStock(String reservationId) {
79
+ // Compensación: liberar stock reservado
80
+ }
81
+ }
82
+ ```
83
+
84
+ **Worker** (registra la activity en el microservicio `inventory`):
85
+
86
+ ```java
87
+ Worker inventoryWorker = workerFactory.newWorker("INVENTORY_QUEUE");
88
+ inventoryWorker.registerActivitiesImplementations(
89
+ new InventoryActivityImpl(inventoryRepository)
90
+ );
91
+ ```
92
+
93
+ **Invocación desde el workflow** (en el microservicio `orders`):
94
+
95
+ ```java
96
+ public class PlaceOrderWorkFlowImpl implements PlaceOrderWorkFlow {
97
+
98
+ // Stub que apunta al queue de inventory — NO es HTTP
99
+ private final InventoryActivity inventoryActivities = Workflow.newActivityStub(
100
+ InventoryActivity.class,
101
+ ActivityOptions.newBuilder()
102
+ .setTaskQueue("INVENTORY_QUEUE")
103
+ .setStartToCloseTimeout(Duration.ofSeconds(30))
104
+ .setRetryOptions(RetryOptions.newBuilder()
105
+ .setMaximumAttempts(3)
106
+ .build())
107
+ .build()
108
+ );
109
+
110
+ @Override
111
+ public void start(String orderId) {
112
+ // Esto viaja por Temporal, no por HTTP
113
+ ReserveResult reserved = inventoryActivities.reserveStock(orderId, items);
114
+
115
+ // Registrar compensación en la Saga
116
+ saga.addCompensation(() -> inventoryActivities.releaseStock(reserved.getReservationId()));
117
+
118
+ // Continuar con el siguiente paso...
119
+ }
120
+ }
121
+ ```
122
+
123
+ ### Qué necesita cada microservicio
124
+
125
+ ```
126
+ orders (invocador):
127
+ ├── InventoryActivity.java ← interfaz (solo el contrato)
128
+ ├── ReserveResult.java ← DTO de respuesta
129
+ └── ItemRequest.java ← DTO de entrada
130
+
131
+ inventory (ejecutor):
132
+ ├── InventoryActivity.java ← interfaz (misma)
133
+ ├── InventoryActivityImpl.java ← implementación con acceso a BD
134
+ └── InventoryWorker.java ← registra la activity en el queue
135
+ ```
136
+
137
+ ### Variante: Ejecución Paralela con `Async.function()`
138
+
139
+ Por defecto, Remote Activity es **bloqueante** — el workflow se detiene hasta recibir el resultado. Sin embargo, cuando necesitas invocar múltiples actividades que son **independientes entre sí**, puedes ejecutarlas en paralelo usando `Async.function()`.
140
+
141
+ ```
142
+ Secuencial (default): Paralelo (Async.function):
143
+
144
+ reserveStock() ──────► 2s reserveStock() ───► 2s ──┐
145
+ validateAddress() ─────► 1s validateAddress() ──► 1s ──┤ max(2s, 1s, 1.5s) = 2s
146
+ checkFraud() ──────────► 1.5s checkFraud() ───────► 1.5s┘
147
+ ───────────────────────── ─────────────────────────
148
+ Total: 4.5s Total: 2s
149
+ ```
150
+
151
+ **Ejemplo: Validaciones paralelas antes de confirmar una orden**
152
+
153
+ ```java
154
+ public class PlaceOrderWorkFlowImpl implements PlaceOrderWorkFlow {
155
+
156
+ private final InventoryActivity inventoryActivities = Workflow.newActivityStub(
157
+ InventoryActivity.class,
158
+ ActivityOptions.newBuilder()
159
+ .setTaskQueue("INVENTORY_HEAVY_TASK_QUEUE")
160
+ .setStartToCloseTimeout(Duration.ofSeconds(30))
161
+ .build()
162
+ );
163
+
164
+ private final ShippingActivity shippingActivities = Workflow.newActivityStub(
165
+ ShippingActivity.class,
166
+ ActivityOptions.newBuilder()
167
+ .setTaskQueue("SHIPPING_LIGHT_TASK_QUEUE")
168
+ .setStartToCloseTimeout(Duration.ofSeconds(10))
169
+ .build()
170
+ );
171
+
172
+ private final RiskActivity riskActivities = Workflow.newActivityStub(
173
+ RiskActivity.class,
174
+ ActivityOptions.newBuilder()
175
+ .setTaskQueue("RISK_LIGHT_TASK_QUEUE")
176
+ .setStartToCloseTimeout(Duration.ofSeconds(15))
177
+ .build()
178
+ );
179
+
180
+ @Override
181
+ public void start(String orderId) {
182
+ // Lanzar las 3 actividades en paralelo — NO bloquean individualmente
183
+ Promise<ReserveResult> stockPromise = Async.function(
184
+ inventoryActivities::reserveStock, orderId, items
185
+ );
186
+ Promise<AddressValidation> addressPromise = Async.function(
187
+ shippingActivities::validateAddress, shippingAddress
188
+ );
189
+ Promise<FraudScore> fraudPromise = Async.function(
190
+ riskActivities::quickCheck, orderId, totalAmount
191
+ );
192
+
193
+ // Esperar TODAS — el workflow se bloquea aquí hasta que las 3 terminen
194
+ ReserveResult reserved = stockPromise.get();
195
+ AddressValidation address = addressPromise.get();
196
+ FraudScore fraud = fraudPromise.get();
197
+
198
+ // Con los 3 resultados, tomar decisión
199
+ if (fraud.isRejected()) {
200
+ inventoryActivities.releaseStock(reserved.getReservationId());
201
+ throw ApplicationFailure.newFailure("Fraud detected", "FRAUD");
202
+ }
203
+
204
+ // Continuar con el flujo...
205
+ }
206
+ }
207
+ ```
208
+
209
+ **API de `Promise`:**
210
+
211
+ | Método | Comportamiento |
212
+ |--------|---------------|
213
+ | `promise.get()` | Bloquea el workflow hasta que la actividad termine. Propaga excepciones. |
214
+ | `Promise.allOf(p1, p2, p3)` | Retorna un `Promise<Void>` que se completa cuando **todas** terminan. |
215
+ | `Promise.anyOf(p1, p2, p3)` | Retorna un `Promise<Object>` que se completa cuando **la primera** termina. |
216
+
217
+ **`Promise.allOf` — espera explícita con manejo unificado:**
218
+
219
+ ```java
220
+ Promise.allOf(stockPromise, addressPromise, fraudPromise).get(); // espera las 3
221
+
222
+ // Ahora todos los .get() son inmediatos (ya completaron)
223
+ ReserveResult reserved = stockPromise.get();
224
+ AddressValidation address = addressPromise.get();
225
+ FraudScore fraud = fraudPromise.get();
226
+ ```
227
+
228
+ **`Promise.anyOf` — patrón race (el primero gana):**
229
+
230
+ ```java
231
+ // Útil para failover: consultar múltiples proveedores, usar el primero que responda
232
+ Promise<ShippingRate> dhlPromise = Async.function(dhlActivities::getRate, pkg);
233
+ Promise<ShippingRate> fedexPromise = Async.function(fedexActivities::getRate, pkg);
234
+
235
+ Promise<Object> first = Promise.anyOf(dhlPromise, fedexPromise);
236
+ first.get(); // espera al primero
237
+
238
+ ShippingRate rate = dhlPromise.isCompleted()
239
+ ? dhlPromise.get()
240
+ : fedexPromise.get();
241
+ ```
242
+
243
+ ### Cuándo usar `Async.function()` vs secuencial
244
+
245
+ | Criterio | Secuencial (default) | Paralelo (`Async.function`) |
246
+ |----------|---------------------|------------------------------|
247
+ | Actividades **dependen** una de otra | ✅ Usar | ❌ No aplica |
248
+ | Actividades **independientes** | ❌ Desperdicia tiempo | ✅ Usar |
249
+ | Compensación en Saga | Registrar después de cada `.get()` | Registrar después de `Promise.allOf()` |
250
+ | Manejo de errores | Falla en el paso, compensa lo anterior | Una falla cancela las pendientes si usas `CancellationScope` |
251
+
252
+ > **Nota importante:** `Async.function()` es parte del SDK de Temporal y es **determinista** — seguro de usar dentro de workflows. No confundir con `CompletableFuture` de Java, que NO es determinista y está prohibido en workflows Temporal.
253
+
254
+ ### En Temporal Web UI
255
+
256
+ Aparecen como una **actividad normal** dentro del workflow. No se distingue de una actividad local. Cuando se usa `Async.function()`, las actividades paralelas aparecen como múltiples `ActivityTaskScheduled` events casi simultáneos en el historial.
257
+
258
+ ---
259
+
260
+ ## 2. Child Workflow
261
+
262
+ ### Concepto
263
+
264
+ Un workflow en el **Servicio A** lanza un **sub-workflow** que puede vivir en el **Servicio B**. El child tiene su propio historial, puede ser cancelado independientemente, y el padre puede esperar su resultado o continuar sin esperarlo.
265
+
266
+ ```
267
+ Servicio A (parent workflow) Temporal Server Servicio B (child workflow)
268
+ │ │ │
269
+ │ childStub.start(data) │ │
270
+ │───────────────────────────────►│ │
271
+ │ │ nueva ejecución ──────►│
272
+ │ │ │ ejecuta pasos
273
+ │ │ │ propios (activities,
274
+ │ │ │ signals, timers)
275
+ │ │ resultado ◄────────────│
276
+ │ resultado ◄───────────────────│ │
277
+ ```
278
+
279
+ ### Cuándo usarlo
280
+
281
+ - El subproceso tiene **ciclo de vida propio** (puede cancelarse, consultarse, reintentar)
282
+ - El subproceso tiene **lógica compleja** con múltiples pasos internos
283
+ - Necesitas **limitar el alcance de la cancelación** (cancelar el child sin cancelar el parent)
284
+ - El historial del parent sería demasiado largo si incluyera todo inline
285
+
286
+ ### Ejemplo: PlaceOrder lanza ProcessPayment como child
287
+
288
+ **Interfaz del child** (compartida):
289
+
290
+ ```java
291
+ @WorkflowInterface
292
+ public interface ProcessPaymentWorkFlow {
293
+
294
+ @WorkflowMethod
295
+ PaymentResult start(PaymentRequest request);
296
+
297
+ @SignalMethod
298
+ void onThreeDSecureCompleted(String verificationToken);
299
+
300
+ @QueryMethod
301
+ String getPaymentStatus();
302
+ }
303
+ ```
304
+
305
+ **Implementación** (vive en el microservicio `payments`):
306
+
307
+ ```java
308
+ public class ProcessPaymentWorkFlowImpl implements ProcessPaymentWorkFlow {
309
+
310
+ private final PaymentActivity paymentActivities = Workflow.newActivityStub(
311
+ PaymentActivity.class,
312
+ ActivityOptions.newBuilder()
313
+ .setTaskQueue("PAYMENT_QUEUE")
314
+ .setStartToCloseTimeout(Duration.ofSeconds(60))
315
+ .build()
316
+ );
317
+
318
+ private String status = "PENDING";
319
+ private String verificationToken = null;
320
+
321
+ @Override
322
+ public PaymentResult start(PaymentRequest request) {
323
+
324
+ // 1. Intentar cobro
325
+ ChargeResult charge = paymentActivities.charge(request);
326
+
327
+ // 2. Si requiere 3D Secure, esperar verificación del usuario
328
+ if (charge.isRequires3DSecure()) {
329
+ status = "AWAITING_VERIFICATION";
330
+
331
+ // Espera hasta 10 minutos que el usuario complete 3D Secure
332
+ boolean verified = Workflow.await(
333
+ Duration.ofMinutes(10),
334
+ () -> this.verificationToken != null
335
+ );
336
+
337
+ if (!verified) {
338
+ status = "TIMEOUT";
339
+ return new PaymentResult(null, "TIMEOUT");
340
+ }
341
+
342
+ // Confirmar con el token de verificación
343
+ charge = paymentActivities.confirmWithVerification(
344
+ charge.getTransactionId(), verificationToken
345
+ );
346
+ }
347
+
348
+ status = "COMPLETED";
349
+ return new PaymentResult(charge.getTransactionId(), "SUCCESS");
350
+ }
351
+
352
+ @Override
353
+ public void onThreeDSecureCompleted(String token) {
354
+ this.verificationToken = token;
355
+ }
356
+
357
+ @Override
358
+ public String getPaymentStatus() {
359
+ return status;
360
+ }
361
+ }
362
+ ```
363
+
364
+ **Invocación desde el parent** (en el microservicio `orders`):
365
+
366
+ ```java
367
+ public class PlaceOrderWorkFlowImpl implements PlaceOrderWorkFlow {
368
+
369
+ @Override
370
+ public void start(String orderId) {
371
+ // ... reservar stock ...
372
+
373
+ // Lanzar pago como child workflow
374
+ ProcessPaymentWorkFlow paymentWorkflow = Workflow.newChildWorkflowStub(
375
+ ProcessPaymentWorkFlow.class,
376
+ ChildWorkflowOptions.newBuilder()
377
+ .setWorkflowId("payment-" + orderId)
378
+ .setTaskQueue("PAYMENT_FLOW_QUEUE")
379
+ .build()
380
+ );
381
+
382
+ // Espera el resultado del child (bloqueante para el parent)
383
+ PaymentResult result = paymentWorkflow.start(
384
+ new PaymentRequest(orderId, totalAmount, paymentToken)
385
+ );
386
+
387
+ if (!"SUCCESS".equals(result.getStatus())) {
388
+ saga.compensate(); // Fallo en pago → compensar stock
389
+ return;
390
+ }
391
+
392
+ saga.addCompensation(() -> paymentActivities.refund(result.getPaymentId()));
393
+
394
+ // Continuar: confirmar orden...
395
+ }
396
+ }
397
+ ```
398
+
399
+ ### Diferencia clave con Remote Activity
400
+
401
+ ```
402
+ Remote Activity:
403
+ - Una operación: charge() → resultado
404
+ - Sin estado interno
405
+ - Sin signals ni queries
406
+
407
+ Child Workflow:
408
+ - Múltiples pasos: charge() → esperar 3D Secure → confirm()
409
+ - Estado interno (status, verificationToken)
410
+ - Acepta signals (onThreeDSecureCompleted)
411
+ - Se puede consultar (getPaymentStatus)
412
+ - Historial propio en Temporal Web
413
+ ```
414
+
415
+ ### En Temporal Web UI
416
+
417
+ Aparecen como **dos ejecuciones vinculadas**: el parent muestra un evento `ChildWorkflowExecutionStarted` y el child tiene su propio historial completo con todos sus pasos internos.
418
+
419
+ ---
420
+
421
+ ## 3. Signal External (Fire & Forget)
422
+
423
+ ### Concepto
424
+
425
+ Un servicio envía una **señal** a un workflow que ya está corriendo en otro servicio. No espera respuesta. El emisor solo necesita conocer el `workflowId` del receptor.
426
+
427
+ ```
428
+ Servicio A Temporal Server Servicio B (workflow activo)
429
+ │ │ │
430
+ │ signal(workflowId, data) │ │
431
+ │───────────────────────────►│ │
432
+ │ │ entrega el signal ─────────►│
433
+ │ (continúa sin esperar) │ │ procesa el signal
434
+ │ │ │
435
+ ```
436
+
437
+ ### Cuándo usarlo
438
+
439
+ - Avisar a un workflow activo que **algo pasó**
440
+ - No necesitas respuesta del receptor
441
+ - Los servicios son **completamente independientes**
442
+ - Comunicación basada en eventos entre workflows
443
+
444
+ ### Ejemplo: Webhook de pagos notifica al workflow de orders
445
+
446
+ **Escenario:** El usuario completó el pago en una pasarela externa (PayPal, 3D Secure). La pasarela llama a un webhook en el microservicio `payments`. Este debe avisar al workflow `PlaceOrder` en el microservicio `orders` que el pago fue exitoso.
447
+
448
+ **Receptor — workflow en orders** (ya está corriendo y esperando):
449
+
450
+ ```java
451
+ @WorkflowInterface
452
+ public interface PlaceOrderWorkFlow {
453
+
454
+ @WorkflowMethod
455
+ void start(String cartId, String paymentToken);
456
+
457
+ @SignalMethod
458
+ void paymentCompleted(String paymentId);
459
+ }
460
+
461
+ public class PlaceOrderWorkFlowImpl implements PlaceOrderWorkFlow {
462
+
463
+ private String paymentId = null;
464
+
465
+ @Override
466
+ public void start(String cartId, String paymentToken) {
467
+ // ... pasos previos (reservar stock, etc.) ...
468
+
469
+ // Esperar señal de pago (máximo 30 minutos)
470
+ boolean paid = Workflow.await(
471
+ Duration.ofMinutes(30),
472
+ () -> this.paymentId != null
473
+ );
474
+
475
+ if (!paid) {
476
+ saga.compensate(); // timeout → liberar stock
477
+ return;
478
+ }
479
+
480
+ // Pago recibido, continuar
481
+ orderActivities.confirmOrder(orderId, this.paymentId);
482
+ }
483
+
484
+ @Override
485
+ public void paymentCompleted(String paymentId) {
486
+ this.paymentId = paymentId; // desbloquea el await
487
+ }
488
+ }
489
+ ```
490
+
491
+ **Emisor — webhook controller en payments:**
492
+
493
+ ```java
494
+ @RestController
495
+ public class PaymentWebhookController {
496
+
497
+ private final WorkflowClient workflowClient;
498
+
499
+ // POST /webhooks/payments (llamado por la pasarela externa)
500
+ @PostMapping("/webhooks/payments")
501
+ public ResponseEntity<Void> handlePaymentWebhook(@RequestBody PaymentNotification notification) {
502
+
503
+ // El workflowId se guardó cuando se inició el checkout
504
+ String workflowId = notification.getMetadata().get("workflowId");
505
+
506
+ // Enviar signal al workflow de orders — fire & forget
507
+ PlaceOrderWorkFlow workflow = workflowClient.newUntypedWorkflowStub(workflowId)
508
+ .signal("paymentCompleted", notification.getPaymentId());
509
+
510
+ return ResponseEntity.ok().build();
511
+ }
512
+ }
513
+ ```
514
+
515
+ **Alternativa sin conocer la interfaz** (acoplamiento mínimo):
516
+
517
+ ```java
518
+ // Signal sin tipar — solo necesitas el workflowId y el nombre del signal
519
+ workflowClient.newUntypedWorkflowStub(workflowId)
520
+ .signal("paymentCompleted", paymentId);
521
+ ```
522
+
523
+ ### Qué necesita cada microservicio
524
+
525
+ ```
526
+ payments (emisor):
527
+ ├── WorkflowClient ← del SDK de Temporal
528
+ └── workflowId ← guardado cuando se inició el checkout
529
+
530
+ orders (receptor):
531
+ └── @SignalMethod en su workflow ← define qué signals acepta
532
+ ```
533
+
534
+ **El emisor NO necesita la interfaz del receptor** si usa `newUntypedWorkflowStub`. Es el patrón de menor acoplamiento.
535
+
536
+ ### En Temporal Web UI
537
+
538
+ Aparece como un evento `WorkflowSignaled` en el historial del workflow receptor. En el emisor no queda registro (es fire & forget desde su perspectiva).
539
+
540
+ ---
541
+
542
+ ## 4. Signal + Await (Espera con Timeout)
543
+
544
+ ### Concepto
545
+
546
+ Combina Signal External con `Workflow.await()` para crear un patrón **request-wait** entre servicios. Un workflow envía un signal y luego espera que el otro le responda con otro signal.
547
+
548
+ ```
549
+ Servicio A (workflow) Temporal Server Servicio B (workflow)
550
+ │ │ │
551
+ │ signal → B │ │
552
+ │───────────────────────────►│────────────────────────►│
553
+ │ │ │
554
+ │ await(timeout) │ │ procesa...
555
+ │ zzz... │ │
556
+ │ │ │
557
+ │ │ signal → A ◄───────────│
558
+ │ ◄─────────────────────────│ │
559
+ │ (despierta y continúa) │ │
560
+ ```
561
+
562
+ ### Cuándo usarlo
563
+
564
+ - Necesitas respuesta de otro servicio pero **no quieres bloquear con una activity**
565
+ - El otro servicio puede tardar minutos, horas o días
566
+ - Necesitas un **timeout** si la respuesta no llega
567
+ - Útil para aprobaciones humanas, verificaciones externas, procesos batch
568
+
569
+ ### Ejemplo: Orders solicita aprobación de fraude a Risk
570
+
571
+ ```java
572
+ // En PlaceOrderWorkFlowImpl (orders)
573
+ private FraudCheckResult fraudResult = null;
574
+
575
+ @Override
576
+ public void start(String orderId) {
577
+ // ... crear orden, reservar stock ...
578
+
579
+ // Solicitar análisis de fraude (signal al servicio de risk)
580
+ String riskWorkflowId = "risk-check-" + orderId;
581
+
582
+ // Iniciar el workflow de risk assessment
583
+ RiskAssessmentWorkFlow riskWorkflow = workflowClient.newWorkflowStub(
584
+ RiskAssessmentWorkFlow.class,
585
+ WorkflowOptions.newBuilder()
586
+ .setWorkflowId(riskWorkflowId)
587
+ .setTaskQueue("RISK_QUEUE")
588
+ .build()
589
+ );
590
+ WorkflowClient.start(riskWorkflow::assess, orderId, orderAmount);
591
+
592
+ // Esperar resultado (máximo 5 minutos)
593
+ boolean received = Workflow.await(
594
+ Duration.ofMinutes(5),
595
+ () -> this.fraudResult != null
596
+ );
597
+
598
+ if (!received) {
599
+ // Timeout: decidir política (aprobar por defecto o rechazar)
600
+ orderActivities.flagForManualReview(orderId);
601
+ return;
602
+ }
603
+
604
+ if (fraudResult.isRejected()) {
605
+ saga.compensate();
606
+ return;
607
+ }
608
+
609
+ // Aprobado → continuar con cobro
610
+ paymentActivities.charge(orderId, totalAmount, paymentToken);
611
+ }
612
+
613
+ @SignalMethod
614
+ public void onFraudCheckCompleted(FraudCheckResult result) {
615
+ this.fraudResult = result;
616
+ }
617
+ ```
618
+
619
+ ```java
620
+ // En RiskAssessmentWorkFlowImpl (risk)
621
+ @Override
622
+ public void assess(String orderId, BigDecimal amount) {
623
+ // Análisis que puede tardar...
624
+ FraudScore score = riskActivities.analyzeTransaction(orderId, amount);
625
+
626
+ FraudCheckResult result = new FraudCheckResult(
627
+ score.isAboveThreshold() ? "REJECTED" : "APPROVED"
628
+ );
629
+
630
+ // Enviar resultado de vuelta al workflow de orders
631
+ String orderWorkflowId = "PlaceOrderWorkFlow-" + orderId;
632
+ workflowClient.newUntypedWorkflowStub(orderWorkflowId)
633
+ .signal("onFraudCheckCompleted", result);
634
+ }
635
+ ```
636
+
637
+ ---
638
+
639
+ ## 🎯 Árbol de Decisión
640
+
641
+ ```
642
+ ¿Necesito datos/resultado del otro servicio?
643
+
644
+ ├─ NO → Signal External (fire & forget)
645
+ │ Ejemplo: avisar que un pago se completó
646
+
647
+ └─ SÍ
648
+
649
+ ├─ ¿La operación es simple y rápida (< 2 min)?
650
+ │ │
651
+ │ ├─ ¿Solo una operación, o varias que dependen entre sí?
652
+ │ │ └─ SÍ → Remote Activity (secuencial)
653
+ │ │ Ejemplo: reservar stock, leer un carrito, crear un registro
654
+ │ │
655
+ │ └─ ¿Múltiples operaciones independientes que puedo lanzar a la vez?
656
+ │ └─ SÍ → Remote Activity + Async.function() (paralelo)
657
+ │ Ejemplo: reservar stock + validar dirección + check fraude
658
+
659
+ └─ ¿La operación tiene múltiples pasos, estado interno, o puede tardar mucho?
660
+
661
+ ├─ ¿Necesito controlar su ciclo de vida (cancelar, consultar estado)?
662
+ │ └─ SÍ → Child Workflow
663
+ │ Ejemplo: proceso de pago con 3D Secure, fulfillment multi-paso
664
+
665
+ └─ ¿Solo necesito esperar una respuesta eventual?
666
+ └─ SÍ → Signal + Await
667
+ Ejemplo: aprobación de fraude, validación humana
668
+ ```
669
+
670
+ ---
671
+
672
+ ## 📊 E-commerce: Qué patrón usa cada comunicación
673
+
674
+ ```
675
+ PlaceOrderWorkFlow (orders)
676
+
677
+ ├── cartActivities.fetchCart() → Remote Activity (carts)
678
+ ├── orderActivities.createDraft() → Remote Activity (orders, local)
679
+ ├── inventoryActivities.reserveStock() → Remote Activity (inventory)
680
+
681
+ ├── ProcessPaymentWorkFlow → Child Workflow (payments)
682
+ │ ├── paymentActivities.charge() → Remote Activity (payments, local)
683
+ │ └── await 3DSecure signal → Signal + Await (webhook externo)
684
+
685
+ ├── orderActivities.confirmOrder() → Remote Activity (orders, local)
686
+ ├── cartActivities.markCheckedOut() → Remote Activity (carts)
687
+ ├── notificationActivities.sendConfirmation() → Remote Activity (notifications)
688
+
689
+ └── [si falla] saga.compensate()
690
+ ├── inventoryActivities.releaseStock() → Remote Activity (inventory)
691
+ └── paymentActivities.refund() → Remote Activity (payments)
692
+ ```
693
+
694
+ ---
695
+
696
+ ## 🔧 Notas Técnicas
697
+
698
+ ### Task Queues
699
+
700
+ Each microservicio escucha en sus propias colas con prefijo de módulo:
701
+
702
+ ```
703
+ orders: ORDER_WORKFLOW_QUEUE, ORDER_LIGHT_TASK_QUEUE, ORDER_HEAVY_TASK_QUEUE
704
+ carts: CART_LIGHT_TASK_QUEUE, CART_HEAVY_TASK_QUEUE
705
+ inventory: INVENTORY_LIGHT_TASK_QUEUE, INVENTORY_HEAVY_TASK_QUEUE
706
+ payments: PAYMENT_WORKFLOW_QUEUE, PAYMENT_LIGHT_TASK_QUEUE, PAYMENT_HEAVY_TASK_QUEUE
707
+ notifications: NOTIFICATION_WORKFLOW_QUEUE, NOTIFICATION_LIGHT_TASK_QUEUE
708
+ shipping: SHIPPING_LIGHT_TASK_QUEUE, SHIPPING_HEAVY_TASK_QUEUE
709
+ ```
710
+
711
+ ### Qué comparten los servicios
712
+
713
+ ```
714
+ Remote Activity: Interfaz (@ActivityInterface) + DTOs
715
+ Child Workflow: Interfaz (@WorkflowInterface) + DTOs
716
+ Signal External: Solo el workflowId (y opcionalmente el nombre del signal)
717
+ Signal + Await: workflowId + nombre del signal + DTOs del payload
718
+ ```
719
+
720
+ ### Reintentos y resiliencia
721
+
722
+ | Patrón | Reintentos | Qué pasa si el servicio remoto cae |
723
+ |--------|-----------|-------------------------------------|
724
+ | Remote Activity | Configurables vía `RetryOptions` | Temporal reintenta hasta que el worker vuelva o se agoten intentos |
725
+ | Child Workflow | El child tiene sus propios reintentos | El parent espera; el child retoma cuando el worker vuelve |
726
+ | Signal External | No aplica (fire & forget) | El signal se encola y se entrega cuando el workflow lo procese |
727
+ | Signal + Await | El await tiene timeout | Si no llega el signal, el timeout dispara lógica alternativa |
728
+
729
+ ---
730
+
731
+ **Última actualización:** 2026-04-07