azcodr 1.5.0 → 1.5.1

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 (75) hide show
  1. package/.agents/hooks.json +42 -0
  2. package/.agents/hooks.json.example +42 -42
  3. package/.agents/mcp_config.json.example +6 -1
  4. package/.agents/scripts/safety_guard.sh +34 -16
  5. package/.agents/scripts/verify_completion.sh +27 -13
  6. package/.agents/skills/agentic-architect/SKILL.md +125 -125
  7. package/.agents/skills/agentic-architect/references/agents_md_template.md +62 -62
  8. package/.agents/skills/agentic-architect/references/refinement_workflow.md +32 -32
  9. package/.agents/skills/agentic-architect/references/skill_architecture_inquiry.md +63 -63
  10. package/.agents/skills/agentic-architect/references/skill_template.md +56 -56
  11. package/.agents/skills/agentic-architect/scripts/validate_agentic_configs.sh +401 -362
  12. package/.agents/skills/clean-code-refactor/SKILL.md +91 -91
  13. package/.agents/skills/clean-code-refactor/references/clean_code_smells.md +27 -27
  14. package/.agents/skills/clean-code-refactor/references/design_patterns_ts.md +65 -65
  15. package/.agents/skills/compliance-audit/SKILL.md +120 -120
  16. package/.agents/skills/compliance-audit/references/owasp_top10_controls.md +16 -16
  17. package/.agents/skills/compliance-audit/references/soc2_iso_controls.md +28 -28
  18. package/.agents/skills/lets-build/SKILL.md +173 -172
  19. package/.agents/skills/lets-build/references/architecture_interview_matrix.md +115 -115
  20. package/.agents/skills/lets-build/references/hexagonal_bootstrap_scaffolds.md +160 -160
  21. package/.agents/skills/lets-build/references/project_readme_template.md +79 -79
  22. package/.agents/skills/lets-build/scripts/bootstrap_workspace.sh +255 -253
  23. package/.agents/skills/product-analyst/SKILL.md +154 -154
  24. package/.agents/skills/product-analyst/references/backlog_ordering_techniques.md +107 -107
  25. package/.agents/skills/product-analyst/references/gherkin_patterns.md +46 -46
  26. package/.agents/skills/product-analyst/references/invest_checklist.md +38 -38
  27. package/.agents/skills/product-analyst/references/okr_alignment_guide.md +76 -76
  28. package/.agents/skills/product-analyst/references/smart_tasks.md +59 -59
  29. package/.agents/skills/relentless-questioner/SKILL.md +128 -128
  30. package/.agents/skills/relentless-questioner/references/adaptive_question_trees.md +102 -102
  31. package/.editorconfig +19 -19
  32. package/.github/copilot-instructions.md +1 -0
  33. package/.github/workflows/ci.yml +56 -0
  34. package/.gitignore +25 -25
  35. package/AGENTS.md +102 -102
  36. package/LICENSE +21 -21
  37. package/README.md +154 -154
  38. package/bin/azcodr.js +228 -228
  39. package/data/.gitkeep +0 -0
  40. package/docs/knowledge/ubiquitous_language.md +18 -18
  41. package/docs/rules/agentic_configuration.md +259 -259
  42. package/docs/rules/api_architecture.md +179 -179
  43. package/docs/rules/authentication.md +76 -76
  44. package/docs/rules/authorization.md +75 -75
  45. package/docs/rules/caching.md +69 -69
  46. package/docs/rules/clean_code.md +62 -62
  47. package/docs/rules/cloud_native.md +41 -41
  48. package/docs/rules/cqrs.md +203 -203
  49. package/docs/rules/database_design.md +125 -125
  50. package/docs/rules/database_operations.md +69 -69
  51. package/docs/rules/design_patterns.md +98 -98
  52. package/docs/rules/devops_ci_cd.md +76 -76
  53. package/docs/rules/domain_driven_design.md +122 -122
  54. package/docs/rules/error_handling.md +52 -52
  55. package/docs/rules/feature_flags.md +59 -59
  56. package/docs/rules/frontend_architecture.md +157 -157
  57. package/docs/rules/multitenancy_architecture.md +98 -98
  58. package/docs/rules/product_ownership.md +127 -127
  59. package/docs/rules/project_management.md +49 -49
  60. package/docs/rules/relentless_questioning.md +52 -52
  61. package/docs/rules/requirements_engineering.md +98 -98
  62. package/docs/rules/security_compliance.md +53 -53
  63. package/docs/rules/server_driven_ui.md +88 -88
  64. package/docs/rules/test_driven_development.md +185 -185
  65. package/docs/rules/transactional_email.md +27 -27
  66. package/docs/rules/type_safety.md +65 -65
  67. package/docs/rules/ui_ux_architecture.md +150 -150
  68. package/docs/rules/workflow_state_machines.md +117 -117
  69. package/lib/index.d.ts +134 -123
  70. package/lib/index.js +5 -5
  71. package/lib/scaffold.js +399 -351
  72. package/memory.md +36 -36
  73. package/package.json +62 -59
  74. package/scripts/test_coverage.js +38 -0
  75. package/scripts/validate.js +246 -0
@@ -1,203 +1,203 @@
1
- # CQRS (Command Query Responsibility Segregation) & YAGNI Defense
2
-
3
- > **Core Mandate:** Enforce evolutionary graduation across the CQRS spectrum, protect against premature abstraction (YAGNI), bound CQRS strictly to high-contention subdomains, decouple write aggregates from read projections, and prevent eventual consistency hazards.
4
-
5
- ---
6
-
7
- ## 1. The YAGNI Defense: Default Architecture vs. CQRS
8
-
9
- CQRS separates the data model used for state mutations (**Commands**) from the data model used for read operations (**Queries**). While powerful, **CQRS is one of the most frequently over-engineered patterns in modern software**.
10
-
11
- ```mermaid
12
- flowchart LR
13
- subgraph DefaultModel["Default Single-Model (CRUD) - Start Here"]
14
- D1["• 1 Unified Data Schema (ACID)<br/>• Immediate Strong Consistency<br/>• Zero Projection Infrastructure<br/>• Minimal Mental Overhead"]
15
- end
16
-
17
- subgraph DistributedCQRS["Distributed CQRS (Level 3) - Graduate on Proof"]
18
- C1["• Dual Schemas & Dual Databases<br/>• Eventual Consistency & Lag<br/>• Outbox, Message Bus, CDC Workers<br/>• Projection Versioning & Replays"]
19
- end
20
-
21
- DefaultModel -.->|Graduate ONLY on high read-write asymmetry or SLA breaches| DistributedCQRS
22
- ```
23
-
24
- ### The YAGNI Rule for CQRS
25
- 1. **Default to Single Model / Single Database**: Start every application, microservice, or bounded context with a standard single data model (Hexagonal/DDD) where repositories handle both reads and writes.
26
- 2. **Never Use CQRS as Top-Level System Architecture**: CQRS must never be applied globally across an entire system. It is strictly a bounded-context tactical pattern. Generic subdomains (Auth, Settings, Billing, Organizations) must remain simple transactional CRUD.
27
- 3. **Explicit Graduation Tipping Points**: Do not introduce segregated read databases or asynchronous projections until empirical metrics prove that a unified relational model cannot meet operational constraints:
28
- - **Read/Write Asymmetry**: Read volume exceeds writes by > 50:1, and heavy read queries cause lock contention that starves transactional write operations.
29
- - **Impedance Mismatch**: Constructing UI views requires joining 10+ relational tables across multiple consistency boundaries, causing query latency to violate SLAs (> 500ms).
30
- - **Polyglot Query Topology**: Complex full-text search, spatial filtering, or columnar aggregation fundamentally requires specialized search engines (Elasticsearch, Meilisearch, ClickHouse) that cannot act as the primary write store.
31
-
32
- ---
33
-
34
- ## 2. The 4-Tier Evolutionary CQRS Spectrum
35
-
36
- Rather than treating CQRS as a binary switch, systems must graduate incrementally across four explicit architectural tiers:
37
-
38
- ```mermaid
39
- flowchart TD
40
- L0["Level 0: Method CQS (Bertrand Meyer)<br/>Functions either mutate state or return data; zero architectural overhead. Always mandatory."]
41
- L1["Level 1: Segregated Handlers in Code (Single DB, Single Schema)<br/>Command Handlers load Aggregates; Query Handlers bypass domain model for flat DTOs."]
42
- L2["Level 2: Segregated Read Models / Materialized Views (Single DB)<br/>Read queries query indexed SQL views or JSON cache tables populated synchronously via ACID transactions."]
43
- L3["Level 3: Polyglot Persistence & Asynchronous Projections (Multi-Store)<br/>Write DB (Postgres) + Read DB (Elastic/Redis). Synchronized asynchronously via Transactional Outbox + CDC."]
44
-
45
- L0 --> L1 --> L2 --> L3
46
- ```
47
-
48
- ### Level 0: Method-Level CQS (Command-Query Separation)
49
- - Mandatory across all codebases (see [`docs/rules/clean_code.md`](./clean_code.md)).
50
- - Any function that modifies state must return `void` or a `Result<void, DomainError>`. It must never return the mutated entity.
51
- - Any function that queries state must be pure and cause zero observable side-effects.
52
-
53
- ### Level 1: Segregated Handlers in Code (Single DB, Single Schema)
54
- - When domain aggregates become rich with business logic, hydrating deep aggregate graphs (with children, value objects, and invariant checks) simply to render a flat summary list is wasteful.
55
- - **Commands**: Flow through `CommandHandler` ➔ loads `AggregateRoot` from `Repository` ➔ executes domain methods ➔ commits transaction.
56
- - **Queries**: Flow through `QueryHandler` ➔ bypasses domain repositories ➔ issues direct projection queries (SQL `SELECT` or ORM raw select) ➔ returns read-only `DTO`.
57
- - *Zero distributed complexity; 100% ACID consistency.*
58
-
59
- ### Level 2: Segregated Read Models (Single DB, Synchronous)
60
- - When read queries require expensive aggregations or multi-table joins, create dedicated read tables, database views, or PostgreSQL JSONB projection columns in the same database.
61
- - Projections are updated **synchronously within the same database transaction** as the command mutation (or via database triggers).
62
- - *Guarantees immediate read-your-own-writes consistency without distributed outbox pipelines.*
63
-
64
- ### Level 3: Polyglot Persistence & Asynchronous Projections
65
- - Writes commit to the transactional primary database (e.g. PostgreSQL).
66
- - Events are emitted via the Transactional Outbox pattern and streamed to secondary read stores (e.g. Elasticsearch for search, Redis for caches, ClickHouse for telemetry).
67
- - *Requires strict handling of eventual consistency, projection replay, and outbox delivery.*
68
-
69
- ---
70
-
71
- ## 3. Tactical Implementation Patterns (TypeScript)
72
-
73
- ### 1. Command Side: Enforcing Invariants via Aggregates
74
- Commands express user intent, encapsulate validation, and mutate state through the Aggregate Root:
75
-
76
- ```typescript
77
- // commands/create_order.command.ts
78
- export interface CreateOrderCommand {
79
- readonly orderId: string;
80
- readonly customerId: string;
81
- readonly items: ReadonlyArray<{ productId: string; quantity: number; unitPriceCents: number }>;
82
- }
83
-
84
- // handlers/create_order.handler.ts
85
- export class CreateOrderCommandHandler {
86
- constructor(
87
- private readonly orderRepo: OrderRepositoryPort,
88
- private readonly outbox: TransactionalOutboxPort
89
- ) {}
90
-
91
- async execute(command: CreateOrderCommand): Promise<Result<void, OrderDomainError>> {
92
- // 1. Load or instantiate Aggregate Root
93
- const orderResult = OrderAggregate.create({
94
- id: command.orderId,
95
- customerId: command.customerId,
96
- items: command.items
97
- });
98
-
99
- if (orderResult.isFailure) {
100
- return Result.fail(orderResult.error);
101
- }
102
-
103
- const order = orderResult.value;
104
-
105
- // 2. Commit aggregate mutation and outbox event atomically in 1 transaction
106
- await this.orderRepo.transaction(async (tx) => {
107
- await this.orderRepo.save(order, tx);
108
- for (const event of order.pullDomainEvents()) {
109
- await this.outbox.stageEvent(event, tx);
110
- }
111
- });
112
-
113
- return Result.ok();
114
- }
115
- }
116
- ```
117
-
118
- ### 2. Query Side: Fast Direct Projection DTOs
119
- Queries bypass domain entities and ORM aggregate hydration entirely, executing direct, index-optimized reads:
120
-
121
- ```typescript
122
- // queries/get_customer_orders.query.ts
123
- export interface GetCustomerOrdersQuery {
124
- readonly customerId: string;
125
- readonly limit: number;
126
- readonly cursor?: string;
127
- }
128
-
129
- export interface CustomerOrderSummaryDTO {
130
- readonly orderId: string;
131
- readonly totalCents: number;
132
- readonly status: string;
133
- readonly itemCount: number;
134
- readonly createdAt: string;
135
- }
136
-
137
- // handlers/get_customer_orders.handler.ts
138
- export class GetCustomerOrdersQueryHandler {
139
- constructor(private readonly db: ReadDatabaseClient) {}
140
-
141
- async execute(query: GetCustomerOrdersQuery): Promise<CustomerOrderSummaryDTO[]> {
142
- // Direct SQL projection - zero domain aggregate hydration overhead
143
- return await this.db.query<CustomerOrderSummaryDTO>(
144
- `SELECT order_id as "orderId", total_cents as "totalCents",
145
- status, item_count as "itemCount", created_at as "createdAt"
146
- FROM order_summaries_view
147
- WHERE customer_id = $1
148
- ORDER BY created_at DESC
149
- LIMIT $2`,
150
- [query.customerId, query.limit]
151
- );
152
- }
153
- }
154
- ```
155
-
156
- ---
157
-
158
- ## 4. Asynchronous Projection Invariants (Level 3 CQRS)
159
-
160
- When graduating to Level 3 (asynchronous read stores), you must prevent data corruption, drift, and race conditions:
161
-
162
- ### 1. Zero Dual-Writes (Strict Outbox Mandate)
163
- Never write to the command database and then publish to a message broker (RabbitMQ/Kafka) in two independent operations. Network failure between the two operations corrupts read models. Always use the **Transactional Outbox Pattern** (see [`docs/rules/database_design.md`](./database_design.md)).
164
-
165
- ### 2. Monotonic Sequence & Idempotent Projectors
166
- Every domain event must carry an aggregate version number. Read projectors must discard duplicate or out-of-order events:
167
- ```typescript
168
- export async function projectOrderUpdated(event: OrderUpdatedEvent, db: ReadDb): Promise<void> {
169
- // Idempotent upsert guarded by monotonic version sequence
170
- await db.query(
171
- `UPDATE order_read_models
172
- SET status = $1, version = $2, updated_at = $3
173
- WHERE order_id = $4 AND version < $2`,
174
- [event.newStatus, event.version, event.occurredAt, event.orderId]
175
- );
176
- }
177
- ```
178
-
179
- ---
180
-
181
- ## 5. Defending Read-Your-Own-Writes Consistency
182
-
183
- The biggest hazard of asynchronous CQRS is **eventual consistency lag**: a user submits a mutation (e.g. updating their display name), receives an HTTP `200 OK`, gets redirected to their profile, but still sees their old name because the background projection worker is lagging.
184
-
185
- ### Strategies to Eliminate User-Perceived Lag
186
- 1. **Optimistic UI Updates**: The frontend immediately updates the client cache with the submitted values upon HTTP 200 response, without refetching from the query API.
187
- 2. **Monotonic Version Headers**:
188
- - The command response returns the new entity version: `ETag: "v14"`.
189
- - Subsequent query requests include `If-None-Match: "v14"` or `X-Min-Version: 14`.
190
- - If the read projection has not caught up to `v14`, the query service waits (up to 500ms) or falls back to querying the primary write database.
191
- 3. **Session Read-Model Pinning**: Direct read requests from the mutating user's session to the primary database for 5 seconds after a command execution, while general traffic continues reading from the read replica.
192
-
193
- ---
194
-
195
- ## 6. Gotchas & Anti-Patterns
196
-
197
- | Anti-Pattern | Why It Fails | Mandated Correction |
198
- |---|---|---|
199
- | **CQRS as Default Architecture** | Multiplies boilerplate, schemas, and cognitive load 3x on simple applications (YAGNI violation). | Default to Single Model / Single Database. Graduate only on measured bottlenecks. |
200
- | **Conflating CQRS with Event Sourcing** | Event Sourcing stores all state as event streams; CQRS only separates read and write pathways. ES adds severe schema migration complexity. | Implement CQRS with standard relational state. Only add Event Sourcing if the business requires legal/temporal audit ledgers. |
201
- | **Leaking Entities into Queries** | Hydrating full Domain Aggregates inside Query Handlers causes severe N+1 query cascades and memory bloat. | Query Handlers must bypass entities and return flat, serializable DTOs directly from the database. |
202
- | **Dual-Write Projections** | Emitting HTTP calls or message queue pushes directly inside mutation services causes silent sync failures. | Write events to a local `outbox` table in the same DB transaction as the aggregate state change. |
203
- | **CRUD Queries via Aggregate Roots** | Loading an entire Aggregate Root just to read 2 fields creates unnecessary lock contention and memory footprint. | Use direct projection queries (Level 1 CQRS) for read-only flows. |
1
+ # CQRS (Command Query Responsibility Segregation) & YAGNI Defense
2
+
3
+ > **Core Mandate:** Enforce evolutionary graduation across the CQRS spectrum, protect against premature abstraction (YAGNI), bound CQRS strictly to high-contention subdomains, decouple write aggregates from read projections, and prevent eventual consistency hazards.
4
+
5
+ ---
6
+
7
+ ## 1. The YAGNI Defense: Default Architecture vs. CQRS
8
+
9
+ CQRS separates the data model used for state mutations (**Commands**) from the data model used for read operations (**Queries**). While powerful, **CQRS is one of the most frequently over-engineered patterns in modern software**.
10
+
11
+ ```mermaid
12
+ flowchart LR
13
+ subgraph DefaultModel["Default Single-Model (CRUD) - Start Here"]
14
+ D1["• 1 Unified Data Schema (ACID)<br/>• Immediate Strong Consistency<br/>• Zero Projection Infrastructure<br/>• Minimal Mental Overhead"]
15
+ end
16
+
17
+ subgraph DistributedCQRS["Distributed CQRS (Level 3) - Graduate on Proof"]
18
+ C1["• Dual Schemas & Dual Databases<br/>• Eventual Consistency & Lag<br/>• Outbox, Message Bus, CDC Workers<br/>• Projection Versioning & Replays"]
19
+ end
20
+
21
+ DefaultModel -.->|Graduate ONLY on high read-write asymmetry or SLA breaches| DistributedCQRS
22
+ ```
23
+
24
+ ### The YAGNI Rule for CQRS
25
+ 1. **Default to Single Model / Single Database**: Start every application, microservice, or bounded context with a standard single data model (Hexagonal/DDD) where repositories handle both reads and writes.
26
+ 2. **Never Use CQRS as Top-Level System Architecture**: CQRS must never be applied globally across an entire system. It is strictly a bounded-context tactical pattern. Generic subdomains (Auth, Settings, Billing, Organizations) must remain simple transactional CRUD.
27
+ 3. **Explicit Graduation Tipping Points**: Do not introduce segregated read databases or asynchronous projections until empirical metrics prove that a unified relational model cannot meet operational constraints:
28
+ - **Read/Write Asymmetry**: Read volume exceeds writes by > 50:1, and heavy read queries cause lock contention that starves transactional write operations.
29
+ - **Impedance Mismatch**: Constructing UI views requires joining 10+ relational tables across multiple consistency boundaries, causing query latency to violate SLAs (> 500ms).
30
+ - **Polyglot Query Topology**: Complex full-text search, spatial filtering, or columnar aggregation fundamentally requires specialized search engines (Elasticsearch, Meilisearch, ClickHouse) that cannot act as the primary write store.
31
+
32
+ ---
33
+
34
+ ## 2. The 4-Tier Evolutionary CQRS Spectrum
35
+
36
+ Rather than treating CQRS as a binary switch, systems must graduate incrementally across four explicit architectural tiers:
37
+
38
+ ```mermaid
39
+ flowchart TD
40
+ L0["Level 0: Method CQS (Bertrand Meyer)<br/>Functions either mutate state or return data; zero architectural overhead. Always mandatory."]
41
+ L1["Level 1: Segregated Handlers in Code (Single DB, Single Schema)<br/>Command Handlers load Aggregates; Query Handlers bypass domain model for flat DTOs."]
42
+ L2["Level 2: Segregated Read Models / Materialized Views (Single DB)<br/>Read queries query indexed SQL views or JSON cache tables populated synchronously via ACID transactions."]
43
+ L3["Level 3: Polyglot Persistence & Asynchronous Projections (Multi-Store)<br/>Write DB (Postgres) + Read DB (Elastic/Redis). Synchronized asynchronously via Transactional Outbox + CDC."]
44
+
45
+ L0 --> L1 --> L2 --> L3
46
+ ```
47
+
48
+ ### Level 0: Method-Level CQS (Command-Query Separation)
49
+ - Mandatory across all codebases (see [`docs/rules/clean_code.md`](./clean_code.md)).
50
+ - Any function that modifies state must return `void` or a `Result<void, DomainError>`. It must never return the mutated entity.
51
+ - Any function that queries state must be pure and cause zero observable side-effects.
52
+
53
+ ### Level 1: Segregated Handlers in Code (Single DB, Single Schema)
54
+ - When domain aggregates become rich with business logic, hydrating deep aggregate graphs (with children, value objects, and invariant checks) simply to render a flat summary list is wasteful.
55
+ - **Commands**: Flow through `CommandHandler` ➔ loads `AggregateRoot` from `Repository` ➔ executes domain methods ➔ commits transaction.
56
+ - **Queries**: Flow through `QueryHandler` ➔ bypasses domain repositories ➔ issues direct projection queries (SQL `SELECT` or ORM raw select) ➔ returns read-only `DTO`.
57
+ - *Zero distributed complexity; 100% ACID consistency.*
58
+
59
+ ### Level 2: Segregated Read Models (Single DB, Synchronous)
60
+ - When read queries require expensive aggregations or multi-table joins, create dedicated read tables, database views, or PostgreSQL JSONB projection columns in the same database.
61
+ - Projections are updated **synchronously within the same database transaction** as the command mutation (or via database triggers).
62
+ - *Guarantees immediate read-your-own-writes consistency without distributed outbox pipelines.*
63
+
64
+ ### Level 3: Polyglot Persistence & Asynchronous Projections
65
+ - Writes commit to the transactional primary database (e.g. PostgreSQL).
66
+ - Events are emitted via the Transactional Outbox pattern and streamed to secondary read stores (e.g. Elasticsearch for search, Redis for caches, ClickHouse for telemetry).
67
+ - *Requires strict handling of eventual consistency, projection replay, and outbox delivery.*
68
+
69
+ ---
70
+
71
+ ## 3. Tactical Implementation Patterns (TypeScript)
72
+
73
+ ### 1. Command Side: Enforcing Invariants via Aggregates
74
+ Commands express user intent, encapsulate validation, and mutate state through the Aggregate Root:
75
+
76
+ ```typescript
77
+ // commands/create_order.command.ts
78
+ export interface CreateOrderCommand {
79
+ readonly orderId: string;
80
+ readonly customerId: string;
81
+ readonly items: ReadonlyArray<{ productId: string; quantity: number; unitPriceCents: number }>;
82
+ }
83
+
84
+ // handlers/create_order.handler.ts
85
+ export class CreateOrderCommandHandler {
86
+ constructor(
87
+ private readonly orderRepo: OrderRepositoryPort,
88
+ private readonly outbox: TransactionalOutboxPort
89
+ ) {}
90
+
91
+ async execute(command: CreateOrderCommand): Promise<Result<void, OrderDomainError>> {
92
+ // 1. Load or instantiate Aggregate Root
93
+ const orderResult = OrderAggregate.create({
94
+ id: command.orderId,
95
+ customerId: command.customerId,
96
+ items: command.items
97
+ });
98
+
99
+ if (orderResult.isFailure) {
100
+ return Result.fail(orderResult.error);
101
+ }
102
+
103
+ const order = orderResult.value;
104
+
105
+ // 2. Commit aggregate mutation and outbox event atomically in 1 transaction
106
+ await this.orderRepo.transaction(async (tx) => {
107
+ await this.orderRepo.save(order, tx);
108
+ for (const event of order.pullDomainEvents()) {
109
+ await this.outbox.stageEvent(event, tx);
110
+ }
111
+ });
112
+
113
+ return Result.ok();
114
+ }
115
+ }
116
+ ```
117
+
118
+ ### 2. Query Side: Fast Direct Projection DTOs
119
+ Queries bypass domain entities and ORM aggregate hydration entirely, executing direct, index-optimized reads:
120
+
121
+ ```typescript
122
+ // queries/get_customer_orders.query.ts
123
+ export interface GetCustomerOrdersQuery {
124
+ readonly customerId: string;
125
+ readonly limit: number;
126
+ readonly cursor?: string;
127
+ }
128
+
129
+ export interface CustomerOrderSummaryDTO {
130
+ readonly orderId: string;
131
+ readonly totalCents: number;
132
+ readonly status: string;
133
+ readonly itemCount: number;
134
+ readonly createdAt: string;
135
+ }
136
+
137
+ // handlers/get_customer_orders.handler.ts
138
+ export class GetCustomerOrdersQueryHandler {
139
+ constructor(private readonly db: ReadDatabaseClient) {}
140
+
141
+ async execute(query: GetCustomerOrdersQuery): Promise<CustomerOrderSummaryDTO[]> {
142
+ // Direct SQL projection - zero domain aggregate hydration overhead
143
+ return await this.db.query<CustomerOrderSummaryDTO>(
144
+ `SELECT order_id as "orderId", total_cents as "totalCents",
145
+ status, item_count as "itemCount", created_at as "createdAt"
146
+ FROM order_summaries_view
147
+ WHERE customer_id = $1
148
+ ORDER BY created_at DESC
149
+ LIMIT $2`,
150
+ [query.customerId, query.limit]
151
+ );
152
+ }
153
+ }
154
+ ```
155
+
156
+ ---
157
+
158
+ ## 4. Asynchronous Projection Invariants (Level 3 CQRS)
159
+
160
+ When graduating to Level 3 (asynchronous read stores), you must prevent data corruption, drift, and race conditions:
161
+
162
+ ### 1. Zero Dual-Writes (Strict Outbox Mandate)
163
+ Never write to the command database and then publish to a message broker (RabbitMQ/Kafka) in two independent operations. Network failure between the two operations corrupts read models. Always use the **Transactional Outbox Pattern** (see [`docs/rules/database_design.md`](./database_design.md)).
164
+
165
+ ### 2. Monotonic Sequence & Idempotent Projectors
166
+ Every domain event must carry an aggregate version number. Read projectors must discard duplicate or out-of-order events:
167
+ ```typescript
168
+ export async function projectOrderUpdated(event: OrderUpdatedEvent, db: ReadDb): Promise<void> {
169
+ // Idempotent upsert guarded by monotonic version sequence
170
+ await db.query(
171
+ `UPDATE order_read_models
172
+ SET status = $1, version = $2, updated_at = $3
173
+ WHERE order_id = $4 AND version < $2`,
174
+ [event.newStatus, event.version, event.occurredAt, event.orderId]
175
+ );
176
+ }
177
+ ```
178
+
179
+ ---
180
+
181
+ ## 5. Defending Read-Your-Own-Writes Consistency
182
+
183
+ The biggest hazard of asynchronous CQRS is **eventual consistency lag**: a user submits a mutation (e.g. updating their display name), receives an HTTP `200 OK`, gets redirected to their profile, but still sees their old name because the background projection worker is lagging.
184
+
185
+ ### Strategies to Eliminate User-Perceived Lag
186
+ 1. **Optimistic UI Updates**: The frontend immediately updates the client cache with the submitted values upon HTTP 200 response, without refetching from the query API.
187
+ 2. **Monotonic Version Headers**:
188
+ - The command response returns the new entity version: `ETag: "v14"`.
189
+ - Subsequent query requests include `If-None-Match: "v14"` or `X-Min-Version: 14`.
190
+ - If the read projection has not caught up to `v14`, the query service waits (up to 500ms) or falls back to querying the primary write database.
191
+ 3. **Session Read-Model Pinning**: Direct read requests from the mutating user's session to the primary database for 5 seconds after a command execution, while general traffic continues reading from the read replica.
192
+
193
+ ---
194
+
195
+ ## 6. Gotchas & Anti-Patterns
196
+
197
+ | Anti-Pattern | Why It Fails | Mandated Correction |
198
+ |---|---|---|
199
+ | **CQRS as Default Architecture** | Multiplies boilerplate, schemas, and cognitive load 3x on simple applications (YAGNI violation). | Default to Single Model / Single Database. Graduate only on measured bottlenecks. |
200
+ | **Conflating CQRS with Event Sourcing** | Event Sourcing stores all state as event streams; CQRS only separates read and write pathways. ES adds severe schema migration complexity. | Implement CQRS with standard relational state. Only add Event Sourcing if the business requires legal/temporal audit ledgers. |
201
+ | **Leaking Entities into Queries** | Hydrating full Domain Aggregates inside Query Handlers causes severe N+1 query cascades and memory bloat. | Query Handlers must bypass entities and return flat, serializable DTOs directly from the database. |
202
+ | **Dual-Write Projections** | Emitting HTTP calls or message queue pushes directly inside mutation services causes silent sync failures. | Write events to a local `outbox` table in the same DB transaction as the aggregate state change. |
203
+ | **CRUD Queries via Aggregate Roots** | Loading an entire Aggregate Root just to read 2 fields creates unnecessary lock contention and memory footprint. | Use direct projection queries (Level 1 CQRS) for read-only flows. |