eva4j 1.0.13 → 1.0.14

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 (44) hide show
  1. package/AGENTS.md +51 -9
  2. package/DOMAIN_YAML_GUIDE.md +150 -0
  3. package/bin/eva4j.js +31 -1
  4. package/design-system.md +797 -0
  5. package/docs/commands/EVALUATE_SYSTEM.md +542 -0
  6. package/docs/commands/GENERATE_ENTITIES.md +196 -0
  7. package/docs/commands/INDEX.md +10 -1
  8. package/examples/domain-endpoints-relations.yaml +353 -0
  9. package/examples/domain-endpoints-versioned.yaml +144 -0
  10. package/examples/domain-endpoints.yaml +135 -0
  11. package/examples/system.yaml +289 -0
  12. package/package.json +1 -1
  13. package/src/commands/create.js +6 -3
  14. package/src/commands/evaluate-system.js +384 -0
  15. package/src/commands/generate-entities.js +677 -14
  16. package/src/commands/generate-kafka-event.js +59 -5
  17. package/src/commands/generate-system.js +243 -0
  18. package/src/generators/base-generator.js +9 -1
  19. package/src/utils/naming.js +3 -2
  20. package/src/utils/system-validator.js +314 -0
  21. package/src/utils/yaml-to-entity.js +31 -2
  22. package/templates/aggregate/AggregateRepository.java.ejs +5 -0
  23. package/templates/aggregate/AggregateRepositoryImpl.java.ejs +9 -0
  24. package/templates/aggregate/DomainEventHandler.java.ejs +24 -20
  25. package/templates/aggregate/JpaRepository.java.ejs +5 -0
  26. package/templates/base/root/skill-build-domain-yaml-references-generate-entities.md.ejs +1103 -0
  27. package/templates/base/root/skill-build-domain-yaml.ejs +292 -0
  28. package/templates/base/root/skill-build-system-yaml.ejs +252 -0
  29. package/templates/base/root/system.yaml.ejs +97 -0
  30. package/templates/crud/EndpointsController.java.ejs +178 -0
  31. package/templates/crud/FindByQuery.java.ejs +17 -0
  32. package/templates/crud/FindByQueryHandler.java.ejs +57 -0
  33. package/templates/crud/ScaffoldCommand.java.ejs +12 -0
  34. package/templates/crud/ScaffoldCommandHandler.java.ejs +43 -0
  35. package/templates/crud/ScaffoldQuery.java.ejs +12 -0
  36. package/templates/crud/ScaffoldQueryHandler.java.ejs +40 -0
  37. package/templates/crud/SubEntityAddCommand.java.ejs +17 -0
  38. package/templates/crud/SubEntityAddCommandHandler.java.ejs +43 -0
  39. package/templates/crud/SubEntityRemoveCommand.java.ejs +9 -0
  40. package/templates/crud/SubEntityRemoveCommandHandler.java.ejs +42 -0
  41. package/templates/crud/TransitionCommand.java.ejs +9 -0
  42. package/templates/crud/TransitionCommandHandler.java.ejs +39 -0
  43. package/templates/evaluate/report.html.ejs +971 -0
  44. package/templates/kafka-event/Event.java.ejs +7 -0
@@ -0,0 +1,542 @@
1
+ # Command `evaluate system`
2
+
3
+ ---
4
+
5
+ ## Table of Contents
6
+
7
+ 1. [Description and purpose](#1-description-and-purpose)
8
+ 2. [Syntax and options](#2-syntax-and-options)
9
+ 3. [system.yaml structure required](#3-systemyaml-structure-required)
10
+ 4. [Evaluation criteria](#4-evaluation-criteria)
11
+ - [Check 1 — Referential integrity](#check-1--referential-integrity)
12
+ - [Check 2 — Sync cycle detection](#check-2--sync-cycle-detection)
13
+ - [Check 3 — Module role analysis](#check-3--module-role-analysis)
14
+ - [Check 4 — Behavior gaps](#check-4--behavior-gaps)
15
+ - [Check 5 — Coupling patterns](#check-5--coupling-patterns)
16
+ 5. [Score calculation](#5-score-calculation)
17
+ 6. [Report output](#6-report-output)
18
+ 7. [Practical examples with real findings](#7-practical-examples-with-real-findings)
19
+ 8. [Common errors and how to fix them](#8-common-errors-and-how-to-fix-them)
20
+
21
+ ---
22
+
23
+ ## 1. Description and purpose
24
+
25
+ `evaluate system` statically analyzes a `system.yaml` file to detect architectural problems in a microservices design **before writing a single line of Java code**.
26
+
27
+ The command is domain-agnostic: it works for any system described in `system.yaml`, whether it's a cinema booking platform, an e-commerce system, a fintech application, or any other domain.
28
+
29
+ **What it produces:**
30
+
31
+ - A **quality score** (0–100%) based on checks passed vs. total
32
+ - A list of **critical errors** (broken references, hard cycles)
33
+ - A list of **warnings** (potential design problems that aren't necessarily wrong)
34
+ - A list of **passed validations** (proof that good practices are in place)
35
+ - An **interactive HTML report** with three tabs:
36
+ - **Validation** — errors, warnings, score
37
+ - **Flow Simulator** — step-by-step visualization of each async event flow
38
+ - **Architecture** — per-module dependency explorer + sync dependency map + Kafka topic map + interactive network diagram
39
+
40
+ ---
41
+
42
+ ## 2. Syntax and options
43
+
44
+ ```bash
45
+ eva evaluate system
46
+ eva evaluate system --port 8080 # serve the report on a custom port (default: 3000)
47
+ eva evaluate system --output ./report.html # write HTML to a custom path
48
+ ```
49
+
50
+ ### Parameters
51
+
52
+ None. The command always reads `system.yaml` from the current working directory.
53
+
54
+ ### Options
55
+
56
+ | Option | Default | Description |
57
+ |--------|---------|-------------|
58
+ | `--port <n>` | `3000` | Port for the local HTTP preview server |
59
+ | `--output <path>` | `./system-report.html` | Where to write the generated HTML file |
60
+
61
+ ### Requirements
62
+
63
+ - Must be run from a directory containing a `system.yaml` file
64
+ - No eva4j project scaffold is required — `system.yaml` can be a standalone design file
65
+
66
+ ---
67
+
68
+ ## 3. system.yaml structure required
69
+
70
+ The minimal structure the evaluator expects:
71
+
72
+ ```yaml
73
+ system:
74
+ name: my-system # used as report title
75
+
76
+ modules:
77
+ - name: orders # unique module identifier (camelCase or kebab-case)
78
+ description: "..." # shown in the report
79
+ exposes: # REST endpoints this module offers
80
+ - method: POST
81
+ path: /orders
82
+ useCase: CreateOrder
83
+ description: "..."
84
+ - method: GET
85
+ path: /orders/{id}
86
+ useCase: GetOrder
87
+ description: "..."
88
+
89
+ integrations:
90
+ async: # async event flows (Kafka, RabbitMQ, etc.)
91
+ - event: OrderCreatedEvent
92
+ producer: orders
93
+ topic: ORDER_CREATED
94
+ consumers:
95
+ - module: payments
96
+ - module: notifications
97
+
98
+ sync: # synchronous HTTP calls between modules
99
+ - caller: payments
100
+ calls: orders
101
+ port: OrderService # Java interface name that will be generated
102
+ using:
103
+ - GET /orders/{id}
104
+ ```
105
+
106
+ All fields are optional except `modules[].name`. The evaluator gracefully handles missing sections (no async events, no sync calls, etc.).
107
+
108
+ ---
109
+
110
+ ## 4. Evaluation criteria
111
+
112
+ The evaluator runs **5 independent checks** against the parsed YAML. Each check produces errors, warnings, or passing validations.
113
+
114
+ ---
115
+
116
+ ### Check 1 — Referential integrity
117
+
118
+ **What it validates:** Every name referenced anywhere in `integrations` points to a module or endpoint actually declared in `modules[]`.
119
+
120
+ #### 1a. Event producers
121
+
122
+ Every `integrations.async[].producer` must be a module declared in `modules[]`.
123
+
124
+ ```yaml
125
+ # ✅ PASSES — 'orders' is declared in modules[]
126
+ - event: OrderCreatedEvent
127
+ producer: orders
128
+
129
+ # ❌ ERROR — 'billing' is not declared in modules[]
130
+ - event: InvoiceCreatedEvent
131
+ producer: billing
132
+ ```
133
+
134
+ **Error message:**
135
+ ```
136
+ Integridad referencial: el productor 'billing' del evento 'InvoiceCreatedEvent'
137
+ no está declarado en modules[]
138
+ ```
139
+
140
+ #### 1b. Event consumers
141
+
142
+ Every module listed in `integrations.async[].consumers` must be declared in `modules[]`.
143
+
144
+ ```yaml
145
+ consumers:
146
+ - module: payments # ✅ must exist in modules[]
147
+ - module: ghost-service # ❌ ERROR if not declared
148
+ ```
149
+
150
+ **Error message:**
151
+ ```
152
+ Integridad referencial: el consumidor 'ghost-service' del evento 'OrderCreatedEvent'
153
+ no está declarado en modules[]
154
+ ```
155
+
156
+ #### 1c. Sync caller / callee existence
157
+
158
+ Both `caller` and `calls` in every `integrations.sync[]` entry must be declared modules.
159
+
160
+ #### 1d. Sync endpoints exist in target module's `exposes[]`
161
+
162
+ Every endpoint listed in `sync[].using` must match an endpoint declared in the target module's `exposes[]`. Matching uses path templating awareness (`/orders/{id}` matches `GET /orders/{id}`).
163
+
164
+ ```yaml
165
+ # payments calls orders
166
+ sync:
167
+ - caller: payments
168
+ calls: orders
169
+ port: OrderService
170
+ using:
171
+ - GET /orders/{id} # ✅ must appear in orders.exposes[]
172
+ - GET /orders/{id}/total # ❌ ERROR if not declared in orders.exposes[]
173
+ ```
174
+
175
+ **Error message:**
176
+ ```
177
+ Integridad referencial: el endpoint 'GET /orders/{id}/total' usado por 'payments'
178
+ no está declarado en el exposes[] de 'orders'
179
+ ```
180
+
181
+ **Why this matters:** Undeclared endpoints indicate either a missing API design or a stale reference — both are common sources of integration bugs discovered late in development.
182
+
183
+ ---
184
+
185
+ ### Check 2 — Sync cycle detection
186
+
187
+ **What it validates:** The directed graph of synchronous calls contains no cycles.
188
+
189
+ The evaluator builds a directed graph where each node is a module and each edge `A → B` means "A calls B synchronously". It then runs DFS to detect:
190
+
191
+ 1. **Direct bidirectional coupling** (`A → B` and `B → A`) — immediate deadlock risk
192
+ 2. **Transitive cycles** (`A → B → C → A`) — detected via full DFS path traversal
193
+
194
+ ```yaml
195
+ # ❌ CRITICAL — direct bidirectional sync coupling
196
+ sync:
197
+ - caller: orders
198
+ calls: inventory
199
+ - caller: inventory
200
+ calls: orders # ← creates orders ↔ inventory cycle
201
+ ```
202
+
203
+ **Error message:**
204
+ ```
205
+ Acoplamiento circular síncrono: 'orders' e 'inventory' se llaman mutuamente
206
+ de forma síncrona. Esto puede causar deadlocks.
207
+ ```
208
+
209
+ ```yaml
210
+ # ❌ CRITICAL — transitive cycle
211
+ sync:
212
+ - caller: A
213
+ calls: B
214
+ - caller: B
215
+ calls: C
216
+ - caller: C
217
+ calls: A # ← A → B → C → A
218
+ ```
219
+
220
+ **Error message:**
221
+ ```
222
+ Ciclo síncrono detectado: A → B → C → A
223
+ ```
224
+
225
+ **If no cycles are found:**
226
+ ```
227
+ No se detectaron ciclos ni acoplamiento síncrono bidireccional ✓
228
+ ```
229
+
230
+ **Why this matters:** Synchronous circular dependencies in distributed systems can cause deadlocks under load, timeout cascades, and thread-pool exhaustion. This check catches the problem at design time.
231
+
232
+ ---
233
+
234
+ ### Check 3 — Module role analysis
235
+
236
+ **What it validates:** Each module has a coherent role — it either exposes endpoints, participates in integrations, or both. Isolated modules are flagged.
237
+
238
+ #### 3a. Completely isolated modules
239
+
240
+ A module with no `exposes[]` AND no participation in `integrations` (neither as producer/consumer nor as caller/callee) in an integration is suspicious.
241
+
242
+ **Warning:**
243
+ ```
244
+ Módulo aislado: 'reporting' no tiene endpoints expuestos ni integraciones declaradas
245
+ ```
246
+
247
+ #### 3b. Modules without `exposes[]`
248
+
249
+ A module with integrations but no REST endpoints is noted. This is often intentional (pure consumers, background processors) but is surfaced as a warning for review.
250
+
251
+ **Warning:**
252
+ ```
253
+ 'notifications' no tiene endpoints expuestos (exposes[] vacío o ausente)
254
+ ```
255
+
256
+ If `notifications` only consumes events and doesn't expose REST endpoints, it's also marked as a passing note:
257
+ ```
258
+ 'notifications' es consumidor puro de eventos (correcto: no produce eventos propios) ✓
259
+ 'notifications' no expone endpoints REST directamente (módulo de integración) ✓
260
+ ```
261
+
262
+ #### 3c. Autonomous modules
263
+
264
+ Modules with endpoints but no integrations are flagged as autonomous — useful for spotting modules that should be integrated but aren't yet.
265
+
266
+ **Passing:**
267
+ ```
268
+ 'movies' es un módulo autónomo sin dependencias de integración ✓
269
+ ```
270
+
271
+ ---
272
+
273
+ ### Check 4 — Behavior gaps
274
+
275
+ **What it validates:** Every state-mutating endpoint (PUT, POST, PATCH, DELETE) that uses a "scheduler-like" verb has at least one identifiable trigger — an incoming event or a sync call.
276
+
277
+ #### Trigger verbs detected
278
+
279
+ The evaluator looks for these verbs in the `useCase` name:
280
+
281
+ | Verb | Typical pattern |
282
+ |------|----------------|
283
+ | `expire` | `ExpireReservation`, `ExpireSession` |
284
+ | `clean` | `CleanExpiredTokens` |
285
+ | `close` | `CloseBatch` |
286
+ | `archive` | `ArchiveOldOrders` |
287
+ | `timeout` | `TimeoutPendingPayments` |
288
+ | `process` | `ProcessRefund`, `ProcessPendingItems` |
289
+ | `purge` | `PurgeDeletedUsers` |
290
+ | `flush` | `FlushQueue` |
291
+
292
+ #### What constitutes a valid trigger
293
+
294
+ - **Async event:** another module produces an event consumed by this module, and the event name contains the same verb
295
+ - **Sync call:** another module calls this endpoint via `sync[].using`
296
+
297
+ If neither is found, the evaluator raises a warning:
298
+
299
+ **Warning:**
300
+ ```
301
+ Gap de comportamiento: 'ExpireReservation' (PUT /reservations/{id}/expire) en 'reservations'
302
+ no tiene ningún evento ni llamada síncrona que lo active.
303
+ Puede necesitar un scheduler o job periódico.
304
+ ```
305
+
306
+ #### How to fix a behavior gap
307
+
308
+ Option A — Add an async trigger event:
309
+ ```yaml
310
+ integrations:
311
+ async:
312
+ - event: ReservationExpiredEvent
313
+ producer: reservations # self-triggered via scheduler
314
+ topic: RESERVATION_EXPIRED
315
+ consumers:
316
+ - module: reservations # acts on it
317
+ ```
318
+
319
+ Option B — Document the scheduler explicitly (informational, suppresses the warning by convention):
320
+ ```yaml
321
+ exposes:
322
+ - method: PUT
323
+ path: /reservations/{id}/expire
324
+ useCase: ExpireReservation
325
+ description: "Invocado por Spring @Scheduled cada minuto. No tiene trigger externo."
326
+ ```
327
+
328
+ Option C — Accept the warning; it's a legitimate scheduler endpoint.
329
+
330
+ **Why this matters:** These gaps represent operations that exist in the design but have no automated trigger. In production they either never run (dead code risk) or require manual invocation (operational risk). Surfacing them early allows deliberate decisions about scheduling strategies.
331
+
332
+ ---
333
+
334
+ ### Check 5 — Coupling patterns
335
+
336
+ **What it validates:** The relationship between synchronous calls and asynchronous events to detect asymmetric coupling that increases fragility.
337
+
338
+ #### Asymmetric coupling pattern
339
+
340
+ This pattern occurs when:
341
+ - Module A calls Module B **synchronously**
342
+ - Module B also publishes events that Module A **consumes asynchronously**
343
+
344
+ This creates a hybrid dependency — A depends on B both at request time (sync call) and at event time (async consumer). While not a hard error, it often indicates that the data needed in the sync call could instead travel inside the event, eliminating the coupling entirely.
345
+
346
+ ```yaml
347
+ # payments calls reservations synchronously (to get amount)
348
+ sync:
349
+ - caller: payments
350
+ calls: reservations
351
+ port: ReservationService
352
+ using:
353
+ - GET /reservations/{id}
354
+
355
+ # reservations also sends events that payments consumes
356
+ async:
357
+ - event: ReservationCreatedEvent
358
+ producer: reservations
359
+ consumers:
360
+ - module: payments # ← asymmetric: payments ↔ reservations in both directions
361
+ ```
362
+
363
+ **Warning:**
364
+ ```
365
+ Acoplamiento asimétrico: 'payments' llama síncronamente a 'reservations',
366
+ mientras 'reservations' responde vía eventos asíncronos (ReservationCreatedEvent,
367
+ ReservationCancelledEvent). Considerar pasar los datos necesarios directamente
368
+ en el evento para eliminar la llamada síncrona.
369
+ ```
370
+
371
+ #### How to eliminate asymmetric coupling
372
+
373
+ Embed the needed data in the event payload:
374
+
375
+ ```yaml
376
+ # Before (asymmetric): payments calls GET /reservations/{id} to get the amount
377
+ # After (decoupled): amount travels inside the event
378
+ - event: ReservationCreatedEvent
379
+ producer: reservations
380
+ topic: RESERVATION_CREATED
381
+ # Event payload would include: reservationId, customerId, amount, seatCount
382
+ consumers:
383
+ - module: payments # payments now has amount without a sync call
384
+ ```
385
+
386
+ Once `amount` travels in the event, the sync call from `payments → reservations` becomes unnecessary and can be removed.
387
+
388
+ #### Dual-trigger pattern (intentional — passing)
389
+
390
+ When a module exposes an endpoint that can be triggered both via REST call AND by consuming an event, the evaluator recognizes this as an intentional dual-trigger design and marks it as passing:
391
+
392
+ ```
393
+ 'screenings' tiene endpoints accesibles tanto síncronamente como vía eventos
394
+ (diseño dual — intencional) ✓
395
+ ```
396
+
397
+ This pattern is valid when an operation needs to be invocable both manually (admin REST call) and automatically (event-driven).
398
+
399
+ ---
400
+
401
+ ## 5. Score calculation
402
+
403
+ The score is calculated as:
404
+
405
+ ```
406
+ score = round((passed_count / (passed_count + errors_count + warnings_count)) * 100)
407
+ ```
408
+
409
+ Where:
410
+ - `errors_count` — number of critical errors (each error counts as 1)
411
+ - `warnings_count` — number of warnings (each warning counts as 1)
412
+ - `passed_count` — number of passing validations
413
+
414
+ **Score thresholds:**
415
+
416
+ | Score | Color | Interpretation |
417
+ |-------|-------|----------------|
418
+ | > 80% | 🟢 Green | Good architecture — minor issues only |
419
+ | 60–80% | 🟡 Yellow | Moderate issues — review warnings before coding |
420
+ | < 60% | 🔴 Red | Significant problems — resolve errors before proceeding |
421
+
422
+ A score of 100% means zero errors, zero warnings, and at least some passing validations.
423
+
424
+ ---
425
+
426
+ ## 6. Report output
427
+
428
+ The command writes a self-contained HTML file (no external dependencies at runtime) and starts a local HTTP server for preview.
429
+
430
+ ```
431
+ ✔ Analysis complete!
432
+
433
+ 📊 Validation Summary
434
+ ────────────────────────────────────────
435
+ 🔴 Errors: 0
436
+ 🟡 Warnings: 5
437
+ 🟢 Passed: 17
438
+ 📈 Score: 87%
439
+
440
+ 🌐 Server running at: http://localhost:3000
441
+ ```
442
+
443
+ The HTML report contains three interactive tabs:
444
+
445
+ | Tab | Contents |
446
+ |-----|----------|
447
+ | **Validación** | Score cards, collapsible sections for errors / warnings / passed |
448
+ | **Simulador de flujos** | Step-by-step playback of each async event flow, with sync sub-calls shown inline |
449
+ | **Arquitectura** | Module dependency explorer (click any module), sync dependency cards, Kafka topic map, interactive network diagram (Vis.js) with hover highlights per event group |
450
+
451
+ The HTML is a **single self-contained file** — embeds all data as base64, uses React 18 from CDN. Can be shared as-is without a server.
452
+
453
+ ---
454
+
455
+ ## 7. Practical examples with real findings
456
+
457
+ ### Example: cinema booking system
458
+
459
+ Running `eva evaluate system` on a cinema booking `system.yaml` with 7 modules and 8 async events produced:
460
+
461
+ **Score: 87% (0 errors, 5 warnings, 17 passed)**
462
+
463
+ | Finding | Type | Check | Recommendation |
464
+ |---------|------|-------|----------------|
465
+ | `notifications` has no `exposes[]` | ⚠️ Warning | Check 3 | Intentional — pure event consumer. Acceptable. |
466
+ | `ExpireReservation` has no trigger | ⚠️ Warning | Check 4 | Add a Spring `@Scheduled` job or a Temporal workflow to call this endpoint every minute |
467
+ | `ProcessRefund` has no trigger | ⚠️ Warning | Check 4 | Add a `ReservationCancelledEvent` consumer in `payments` that calls this endpoint |
468
+ | `payments → reservations` asymmetric | ⚠️ Warning | Check 5 | Embed `amount` in `ReservationCreatedEvent` payload; remove the sync call |
469
+ | `reservations → screenings` asymmetric | ⚠️ Warning | Check 5 | Embed screening data in `PrivateEventReservationCreatedEvent`; remove the sync GET call |
470
+
471
+ None of these required code changes before the score improved — they document deliberate design decisions and explicit technical debt.
472
+
473
+ ---
474
+
475
+ ## 8. Common errors and how to fix them
476
+
477
+ ### Error: event producer not declared
478
+
479
+ ```
480
+ Integridad referencial: el productor 'inventory' del evento 'StockUpdatedEvent'
481
+ no está declarado en modules[]
482
+ ```
483
+
484
+ **Fix:** Add the missing module to `modules[]`, or correct the producer name typo.
485
+
486
+ ---
487
+
488
+ ### Error: sync endpoint not declared in target module
489
+
490
+ ```
491
+ Integridad referencial: el endpoint 'GET /orders/{id}/items' usado por 'shipping'
492
+ no está declarado en el exposes[] de 'orders'
493
+ ```
494
+
495
+ **Fix:** Add the missing endpoint to `orders.exposes[]`:
496
+
497
+ ```yaml
498
+ - name: orders
499
+ exposes:
500
+ - method: GET
501
+ path: /orders/{id}/items # ← add this
502
+ useCase: GetOrderItems
503
+ ```
504
+
505
+ ---
506
+
507
+ ### Error: synchronous bidirectional cycle
508
+
509
+ ```
510
+ Acoplamiento circular síncrono: 'A' y 'B' se llaman mutuamente de forma síncrona.
511
+ ```
512
+
513
+ **Fix options:**
514
+ 1. Remove one of the sync calls and replace it with an event
515
+ 2. Extract the shared data into a third module that both A and B can query
516
+ 3. Pass the needed data from A to B via the initial request payload, avoiding the reverse call
517
+
518
+ ---
519
+
520
+ ### Warning: behavior gap (scheduler verb with no trigger)
521
+
522
+ ```
523
+ Gap de comportamiento: 'ArchiveOldOrders' (PUT /orders/archive) en 'orders'
524
+ no tiene ningún evento ni llamada síncrona que lo active.
525
+ ```
526
+
527
+ **Fix:** Document the scheduling strategy. Options:
528
+ - Spring `@Scheduled(cron = "0 0 2 * * *")` in the `orders` module
529
+ - A dedicated `scheduler` module that calls this endpoint via sync
530
+ - A Temporal workflow (`eva add temporal-client` + `eva g temporal-flow orders`)
531
+ - Accept the warning if the endpoint is intentionally manual-only
532
+
533
+ ---
534
+
535
+ ### Warning: asymmetric coupling
536
+
537
+ ```
538
+ Acoplamiento asimétrico: 'A' llama síncronamente a 'B', mientras 'B' responde
539
+ vía eventos asíncronos.
540
+ ```
541
+
542
+ **Fix:** Audit the sync call. Ask: "Does A need this data at request time, or could it arrive via the event?" If via event — embed the field in the event payload and delete the sync call entry from `system.yaml`.