@dv.nghiem/flowdeck 0.2.4 → 0.3.0
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.
- package/README.md +24 -41
- package/dist/hooks/memory-hook.d.ts +21 -0
- package/dist/hooks/memory-hook.d.ts.map +1 -0
- package/dist/hooks/orchestrator-guard-hook.d.ts.map +1 -1
- package/dist/hooks/todo-hook.d.ts +1 -7
- package/dist/hooks/todo-hook.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +709 -420
- package/dist/services/memory-store.d.ts +40 -0
- package/dist/services/memory-store.d.ts.map +1 -0
- package/dist/tools/memory-search.d.ts +3 -0
- package/dist/tools/memory-search.d.ts.map +1 -0
- package/docs/commands/fd-doctor.md +21 -0
- package/docs/commands/fd-quick.md +33 -0
- package/docs/commands/fd-reflect.md +23 -0
- package/docs/commands/fd-status.md +31 -0
- package/docs/commands/fd-translate-intent.md +17 -0
- package/docs/commands.md +209 -271
- package/docs/configuration.md +1 -2
- package/docs/index.md +22 -28
- package/docs/memory.md +69 -0
- package/docs/quick-start.md +1 -1
- package/package.json +1 -1
- package/src/commands/fd-deploy-check.md +131 -11
- package/src/commands/fd-new-project.md +14 -1
- package/src/commands/fd-quick.md +60 -0
- package/src/commands/fd-reflect.md +41 -2
- package/src/commands/fd-status.md +84 -0
- package/src/rules/README.md +8 -7
- package/src/skills/agent-harness-construction/SKILL.md +227 -0
- package/src/skills/api-design/SKILL.md +5 -0
- package/src/skills/backend-patterns/SKILL.md +105 -0
- package/src/skills/clean-architecture/SKILL.md +85 -0
- package/src/skills/cqrs/SKILL.md +230 -0
- package/src/skills/ddd-architecture/SKILL.md +104 -0
- package/src/skills/django-patterns/SKILL.md +304 -0
- package/src/skills/django-tdd/SKILL.md +297 -0
- package/src/skills/event-driven-architecture/SKILL.md +152 -0
- package/src/skills/frontend-pattern/SKILL.md +159 -0
- package/src/skills/hexagonal-architecture/SKILL.md +80 -0
- package/src/skills/layered-architecture/SKILL.md +64 -0
- package/src/skills/postgres-patterns/SKILL.md +74 -0
- package/src/skills/python-patterns/SKILL.md +5 -0
- package/src/skills/saga-architecture/SKILL.md +113 -0
- package/dist/tools/run-parallel.d.ts +0 -4
- package/dist/tools/run-parallel.d.ts.map +0 -1
- package/docs/command-migration.md +0 -175
- package/docs/commands/fd-analyze-change.md +0 -107
- package/docs/commands/fd-dashboard.md +0 -11
- package/docs/commands/fd-evaluate-risk.md +0 -134
- package/docs/commands/fd-guarded-edit.md +0 -105
- package/docs/commands/fd-progress.md +0 -11
- package/docs/commands/fd-review-code.md +0 -29
- package/docs/commands/fd-roadmap.md +0 -10
- package/docs/commands/fd-settings.md +0 -10
- package/docs/parallel-execution.md +0 -255
- package/src/commands/fd-analyze-change.md +0 -57
- package/src/commands/fd-approve.md +0 -64
- package/src/commands/fd-blast-radius.md +0 -49
- package/src/commands/fd-dashboard.md +0 -57
- package/src/commands/fd-evaluate-risk.md +0 -62
- package/src/commands/fd-guarded-edit.md +0 -69
- package/src/commands/fd-impact-radar.md +0 -51
- package/src/commands/fd-learn.md +0 -36
- package/src/commands/fd-progress.md +0 -50
- package/src/commands/fd-regression-predict.md +0 -57
- package/src/commands/fd-review-code.md +0 -96
- package/src/commands/fd-review-route.md +0 -54
- package/src/commands/fd-roadmap.md +0 -46
- package/src/commands/fd-settings.md +0 -57
- package/src/commands/fd-test-gap.md +0 -54
- package/src/commands/fd-volatility-map.md +0 -64
- package/src/commands/fd-workspace-status.md +0 -34
- package/src/skills/parallel-execute/SKILL.md +0 -92
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent-harness-construction
|
|
3
|
+
description: Build autonomous agent pipelines — construct agent loops, wire multi-agent orchestration, implement self-healing retry logic, and measure agent effectiveness
|
|
4
|
+
origin: FlowDeck
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Agent Harness Construction Skill
|
|
8
|
+
|
|
9
|
+
Constructs autonomous agent pipelines that can plan, execute, self-correct, and measure their own effectiveness.
|
|
10
|
+
|
|
11
|
+
## When to Activate
|
|
12
|
+
|
|
13
|
+
Activate when:
|
|
14
|
+
- Building multi-agent orchestration systems
|
|
15
|
+
- Implementing autonomous loops (self-correcting agents)
|
|
16
|
+
- Designing agent retry and self-healing policies
|
|
17
|
+
- Wiring agent-to-agent communication
|
|
18
|
+
- Measuring and optimizing agent effectiveness
|
|
19
|
+
|
|
20
|
+
## Agent Loop Architecture
|
|
21
|
+
|
|
22
|
+
### Core Loop Pattern
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
┌─────────────────────────────────────────────┐
|
|
26
|
+
│ 1. OBSERVE → Gather context state │
|
|
27
|
+
│ 2. THINK → Plan next action │
|
|
28
|
+
│ 3. ACT → Execute tool call │
|
|
29
|
+
│ 4. EVALUATE → Check result quality │
|
|
30
|
+
│ 5. ADAPT → Retry or proceed │
|
|
31
|
+
└─────────────────────────────────────────────┘
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
interface AgentLoop {
|
|
36
|
+
observe: () => Promise<Context>;
|
|
37
|
+
think: (ctx: Context) => Promise<Plan>;
|
|
38
|
+
act: (plan: Plan) => Promise<Result>;
|
|
39
|
+
evaluate: (result: Result) => Evaluation;
|
|
40
|
+
adapt: (evaluation: Evaluation) => 'continue' | 'retry' | 'complete';
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Self-Healing Retry Logic
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
async function withRetry<T>(
|
|
48
|
+
fn: () => Promise<T>,
|
|
49
|
+
options: {
|
|
50
|
+
maxAttempts?: number;
|
|
51
|
+
backoff?: 'linear' | 'exponential';
|
|
52
|
+
onRetry?: (attempt: number, error: Error) => void;
|
|
53
|
+
} = {}
|
|
54
|
+
): Promise<T> {
|
|
55
|
+
const { maxAttempts = 3, backoff = 'exponential', onRetry } = options;
|
|
56
|
+
|
|
57
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
58
|
+
try {
|
|
59
|
+
return await fn();
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (attempt === maxAttempts) throw error;
|
|
62
|
+
const delay = backoff === 'exponential'
|
|
63
|
+
? Math.pow(2, attempt - 1) * 1000
|
|
64
|
+
: attempt * 1000;
|
|
65
|
+
onRetry?.(attempt, error as Error);
|
|
66
|
+
await sleep(delay);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
throw new Error('unreachable');
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Multi-Agent Orchestration
|
|
74
|
+
|
|
75
|
+
### Supervisor Pattern
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
interface AgentMessage {
|
|
79
|
+
from: string;
|
|
80
|
+
to: string;
|
|
81
|
+
type: 'request' | 'response' | 'delegate' | 'result';
|
|
82
|
+
payload: unknown;
|
|
83
|
+
traceId: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
class SupervisorAgent {
|
|
87
|
+
private agents: Map<string, Agent>;
|
|
88
|
+
private messageQueue: AgentMessage[] = [];
|
|
89
|
+
|
|
90
|
+
async delegate(task: Task, targetAgent: string): Promise<Result> {
|
|
91
|
+
const message: AgentMessage = {
|
|
92
|
+
from: this.id,
|
|
93
|
+
to: targetAgent,
|
|
94
|
+
type: 'delegate',
|
|
95
|
+
payload: task,
|
|
96
|
+
traceId: generateTraceId(),
|
|
97
|
+
};
|
|
98
|
+
return this.sendAndWait(message);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async parallelDelegate(tasks: Task[], agents: string[]): Promise<Result[]> {
|
|
102
|
+
return Promise.all(
|
|
103
|
+
tasks.map((task, i) => this.delegate(task, agents[i % agents.length]))
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Council Pattern
|
|
110
|
+
|
|
111
|
+
Multiple agents deliberate and vote on a decision:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
interface CouncilMember {
|
|
115
|
+
id: string;
|
|
116
|
+
specialty: 'security' | 'performance' | 'correctness';
|
|
117
|
+
vote: (proposal: Proposal) => Promise<Vote>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface CouncilDecision {
|
|
121
|
+
votes: Vote[];
|
|
122
|
+
decision: 'approve' | 'reject' | 'revise';
|
|
123
|
+
consensus: number; // 0-1
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function councilDeliberate(
|
|
127
|
+
proposal: Proposal,
|
|
128
|
+
members: CouncilMember[]
|
|
129
|
+
): Promise<CouncilDecision> {
|
|
130
|
+
const votes = await Promise.all(members.map(m => m.vote(proposal)));
|
|
131
|
+
const approvals = votes.filter(v => v.approve).length;
|
|
132
|
+
const consensus = approvals / votes.length;
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
votes,
|
|
136
|
+
decision: consensus >= 0.7 ? 'approve' : consensus >= 0.4 ? 'revise' : 'reject',
|
|
137
|
+
consensus,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Agent Effectiveness Measurement
|
|
143
|
+
|
|
144
|
+
### Trace-Based Metrics
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
interface AgentTrace {
|
|
148
|
+
traceId: string;
|
|
149
|
+
agentId: string;
|
|
150
|
+
spans: {
|
|
151
|
+
name: string;
|
|
152
|
+
startTime: number;
|
|
153
|
+
endTime: number;
|
|
154
|
+
success: boolean;
|
|
155
|
+
tokensUsed?: number;
|
|
156
|
+
error?: string;
|
|
157
|
+
}[];
|
|
158
|
+
outcome: 'success' | 'failure' | 'timeout';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Track effectiveness
|
|
162
|
+
function measureAgentEffectiveness(traces: AgentTrace[]): AgentMetrics {
|
|
163
|
+
return {
|
|
164
|
+
successRate: traces.filter(t => t.outcome === 'success').length / traces.length,
|
|
165
|
+
avgDuration: traces.reduce((sum, t) => {
|
|
166
|
+
const duration = t.spans[t.spans.length - 1].endTime - t.spans[0].startTime;
|
|
167
|
+
return sum + duration;
|
|
168
|
+
}, 0) / traces.length,
|
|
169
|
+
avgTokensPerTask: traces.reduce((sum, t) =>
|
|
170
|
+
sum + (t.spans.reduce((s, span) => s + (span.tokensUsed ?? 0), 0) / t.spans.length), 0
|
|
171
|
+
) / traces.length,
|
|
172
|
+
retryRate: traces.filter(t => t.spans.some(s => s.name === 'retry')).length / traces.length,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Error Classification
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
type ErrorCategory =
|
|
181
|
+
| 'transient' // Network blip, timeout — retry eligible
|
|
182
|
+
| 'recoverable' // Missing context, bad input — can fix with adaptation
|
|
183
|
+
| 'fatal'; // Auth failure, permission — cannot proceed
|
|
184
|
+
|
|
185
|
+
function classifyError(error: Error): ErrorCategory {
|
|
186
|
+
if (error.message.includes('timeout') || error.message.includes('ECONNREFUSED')) {
|
|
187
|
+
return 'transient';
|
|
188
|
+
}
|
|
189
|
+
if (error.message.includes('invalid input') || error.message.includes('missing context')) {
|
|
190
|
+
return 'recoverable';
|
|
191
|
+
}
|
|
192
|
+
return 'fatal';
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Self-Healing Policies
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
interface HealingPolicy {
|
|
200
|
+
trigger: (error: Error) => boolean;
|
|
201
|
+
action: (context: AgentContext) => Promise<Action>;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const healingPolicies: HealingPolicy[] = [
|
|
205
|
+
{
|
|
206
|
+
trigger: (e) => e.message.includes('rate limit'),
|
|
207
|
+
action: async (ctx) => {
|
|
208
|
+
ctx.throttleDelay = Math.min(ctx.throttleDelay * 2, 60000);
|
|
209
|
+
return { type: 'backoff', delay: ctx.throttleDelay };
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
trigger: (e) => e.message.includes('context too long'),
|
|
214
|
+
action: async (ctx) => {
|
|
215
|
+
ctx.summarizeOlderHistory();
|
|
216
|
+
return { type: 'compact' };
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
];
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Related Skills
|
|
223
|
+
|
|
224
|
+
- [self-healing-policies](self-healing-policies) — Define recovery policies
|
|
225
|
+
- [agent-introspection-debugging](agent-introspection-debugging) — Debug agent issues
|
|
226
|
+
- [eval-harness](eval-harness) — Evaluate agent performance
|
|
227
|
+
- [continuous-agent-loop](continuous-agent-loop) — Maintain persistent agent sessions
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# backend-patterns
|
|
2
|
+
|
|
3
|
+
## When to Activate
|
|
4
|
+
When implementing backend services, APIs, or server-side logic. Use when designing service layers, data access patterns, or middleware.
|
|
5
|
+
|
|
6
|
+
## Steps
|
|
7
|
+
1. **Identify the service layer** - Determine if you need a service layer to orchestrate business logic
|
|
8
|
+
2. **Apply Repository Pattern** - Encapsulate data access behind repository interfaces for testability
|
|
9
|
+
3. **Use Dependency Injection** - Pass dependencies explicitly rather than creating them inside classes
|
|
10
|
+
4. **Implement error handling** - Add comprehensive error handling with appropriate HTTP status codes
|
|
11
|
+
5. **Add middleware/logging** - Log requests, responses, and errors consistently
|
|
12
|
+
|
|
13
|
+
## Examples
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
// Service Layer with Repository Pattern
|
|
17
|
+
interface UserRepository {
|
|
18
|
+
findById(id: string): Promise<User | null>;
|
|
19
|
+
findAll(filter?: UserFilter): Promise<User[]>;
|
|
20
|
+
create(attributes: CreateUserDTO): Promise<User>;
|
|
21
|
+
update(id: string, attributes: UpdateUserDTO): Promise<User>;
|
|
22
|
+
delete(id: string): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class UserService {
|
|
26
|
+
constructor(private readonly userRepository: UserRepository) {}
|
|
27
|
+
|
|
28
|
+
async getUser(id: string): Promise<User> {
|
|
29
|
+
const user = await this.userRepository.findById(id);
|
|
30
|
+
if (!user) {
|
|
31
|
+
throw new NotFoundError(`User with id ${id} not found`);
|
|
32
|
+
}
|
|
33
|
+
return user;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async createUser(dto: CreateUserDTO): Promise<User> {
|
|
37
|
+
const existing = await this.userRepository.findByEmail(dto.email);
|
|
38
|
+
if (existing) {
|
|
39
|
+
throw new ConflictError('User with this email already exists');
|
|
40
|
+
}
|
|
41
|
+
return this.userRepository.create(dto);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Dependency Injection Container
|
|
46
|
+
const container = new Container();
|
|
47
|
+
container.register('userRepository', () => new PostgresUserRepository());
|
|
48
|
+
container.register('userService', () => new UserService(container.resolve('userRepository')));
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// Error Handling with Custom Exceptions
|
|
53
|
+
class AppError extends Error {
|
|
54
|
+
constructor(
|
|
55
|
+
message: string,
|
|
56
|
+
public readonly code: string,
|
|
57
|
+
public readonly statusCode: number = 500
|
|
58
|
+
) {
|
|
59
|
+
super(message);
|
|
60
|
+
this.name = 'AppError';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
class NotFoundError extends AppError {
|
|
65
|
+
constructor(message: string) {
|
|
66
|
+
super(message, 'NOT_FOUND', 404);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
class ValidationError extends AppError {
|
|
71
|
+
constructor(
|
|
72
|
+
message: string,
|
|
73
|
+
public readonly details?: Record<string, string[]>
|
|
74
|
+
) {
|
|
75
|
+
super(message, 'VALIDATION_ERROR', 422);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Global Error Handler Middleware
|
|
80
|
+
function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) {
|
|
81
|
+
if (err instanceof AppError) {
|
|
82
|
+
return res.status(err.statusCode).json({
|
|
83
|
+
error: {
|
|
84
|
+
code: err.code,
|
|
85
|
+
message: err.message,
|
|
86
|
+
details: err instanceof ValidationError ? err.details : undefined,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
console.error('Unhandled error:', err);
|
|
91
|
+
return res.status(500).json({
|
|
92
|
+
error: {
|
|
93
|
+
code: 'INTERNAL_ERROR',
|
|
94
|
+
message: 'An unexpected error occurred',
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Related Skills
|
|
101
|
+
- api-design
|
|
102
|
+
- postgres-patterns
|
|
103
|
+
- python-patterns
|
|
104
|
+
- layered-architecture
|
|
105
|
+
- ddd-architecture
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# clean-architecture
|
|
2
|
+
|
|
3
|
+
## When to Activate
|
|
4
|
+
When designing or implementing a new feature or service that needs clear separation of concerns, testability, and independence from frameworks, databases, or UI libraries.
|
|
5
|
+
|
|
6
|
+
## Steps
|
|
7
|
+
1. **Identify the core business logic** - Determine the essential domain rules that would exist even if the application had no UI, database, or external services.
|
|
8
|
+
2. **Define the boundary** - Draw a clear boundary between the inner circles (entities, use cases) and outer circles (interfaces, infrastructure).
|
|
9
|
+
3. **Place dependencies pointing inward** - Dependencies should always point toward the center. The inner circle knows nothing about the outer circle.
|
|
10
|
+
4. **Define ports (interfaces)** - Create interfaces in the domain layer that define how the outside world can interact with it.
|
|
11
|
+
5. **Implement adapters** - Create concrete implementations (adapters) for databases, web frameworks, external APIs, etc. in the outer layers.
|
|
12
|
+
6. **Wire everything via dependency injection** - Use a composition root or DI container to assemble the application.
|
|
13
|
+
|
|
14
|
+
## Examples
|
|
15
|
+
```typescript
|
|
16
|
+
// Domain Layer - Enterprise Business Rules (innermost circle)
|
|
17
|
+
class Order {
|
|
18
|
+
constructor(
|
|
19
|
+
private readonly id: string,
|
|
20
|
+
private readonly items: OrderItem[],
|
|
21
|
+
private readonly status: OrderStatus
|
|
22
|
+
) {}
|
|
23
|
+
|
|
24
|
+
get total(): number {
|
|
25
|
+
return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
canBeFulfilled(): boolean {
|
|
29
|
+
return this.status === 'pending' && this.items.length > 0
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Application Layer - Application Business Rules
|
|
34
|
+
interface OrderRepository {
|
|
35
|
+
findById(id: string): Promise<Order | null>
|
|
36
|
+
save(order: Order): Promise<void>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface NotificationService {
|
|
40
|
+
sendOrderConfirmation(order: Order): Promise<void>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
class PlaceOrderUseCase {
|
|
44
|
+
constructor(
|
|
45
|
+
private readonly orderRepo: OrderRepository,
|
|
46
|
+
private readonly notifier: NotificationService
|
|
47
|
+
) {}
|
|
48
|
+
|
|
49
|
+
async execute(orderData: OrderData): Promise<Order> {
|
|
50
|
+
const order = new Order(orderData.id, orderData.items, 'pending')
|
|
51
|
+
|
|
52
|
+
if (!order.canBeFulfilled()) {
|
|
53
|
+
throw new InvalidOrderError('Order cannot be fulfilled')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await this.orderRepo.save(order)
|
|
57
|
+
await this.notifier.sendOrderConfirmation(order)
|
|
58
|
+
|
|
59
|
+
return order
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Infrastructure Layer - Interface Adapters (outermost circle)
|
|
64
|
+
class PostgresOrderRepository implements OrderRepository {
|
|
65
|
+
async findById(id: string): Promise<Order | null> {
|
|
66
|
+
// Database implementation
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async save(order: Order): Promise<void> {
|
|
70
|
+
// Database implementation
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
class EmailNotificationService implements NotificationService {
|
|
75
|
+
async sendOrderConfirmation(order: Order): Promise<void> {
|
|
76
|
+
// Email implementation
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Related Skills
|
|
82
|
+
- layered-architecture
|
|
83
|
+
- hexagonal-architecture
|
|
84
|
+
- ddd-architecture
|
|
85
|
+
- backend-patterns
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# CQRS (Command Query Responsibility Segregation)
|
|
2
|
+
|
|
3
|
+
## When to Activate
|
|
4
|
+
|
|
5
|
+
Activate when:
|
|
6
|
+
- Designing read-heavy or write-heavy systems separately
|
|
7
|
+
- Implementing complex domain models with divergent read/write logic
|
|
8
|
+
- Building systems that need different data representations for reading vs. writing
|
|
9
|
+
- Scaling read and write workloads independently
|
|
10
|
+
- Implementing event sourcing alongside specialized read models
|
|
11
|
+
|
|
12
|
+
## Steps
|
|
13
|
+
|
|
14
|
+
### 1. Separate Command and Query Models
|
|
15
|
+
|
|
16
|
+
| Aspect | Command | Query |
|
|
17
|
+
|--------|---------|-------|
|
|
18
|
+
| Purpose | Modify state | Read state |
|
|
19
|
+
| Returns | Void / ACK | Data |
|
|
20
|
+
| Side Effects | Yes | No |
|
|
21
|
+
| Complexity | Business logic | Data shaping |
|
|
22
|
+
|
|
23
|
+
Commands and queries should use **different models** with different schemas optimized for their specific use case.
|
|
24
|
+
|
|
25
|
+
### 2. Design Command Side
|
|
26
|
+
|
|
27
|
+
- Commands are **intent-based** (present tense: `PlaceOrder`, `UpdatePrice`)
|
|
28
|
+
- Validate business rules **before** executing
|
|
29
|
+
- Return success/failure, not data
|
|
30
|
+
- Keep command handlers small and focused
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
interface Command {
|
|
34
|
+
id: string; // Correlation ID
|
|
35
|
+
type: string; // Command type
|
|
36
|
+
payload: unknown; // Command data
|
|
37
|
+
metadata: {
|
|
38
|
+
userId: string;
|
|
39
|
+
timestamp: string;
|
|
40
|
+
correlationId: string;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface CommandHandler<T extends Command> {
|
|
45
|
+
execute(command: T): Promise<CommandResult>;
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 3. Design Query Side
|
|
50
|
+
|
|
51
|
+
- Queries are **data-focused** (past/read tense: `GetUserOrders`, `FindActiveProducts`)
|
|
52
|
+
- Queries should be **side-effect free**
|
|
53
|
+
- Return **read-optimized** data structures (possibly denormalized)
|
|
54
|
+
- Support pagination, filtering, sorting
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
interface Query {
|
|
58
|
+
id: string;
|
|
59
|
+
type: string;
|
|
60
|
+
parameters: Record<string, unknown>;
|
|
61
|
+
pagination?: { page: number; limit: number };
|
|
62
|
+
sorting?: { field: string; direction: 'asc' | 'desc' }[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface QueryHandler<T extends Query> {
|
|
66
|
+
execute(query: T): Promise<QueryResult>;
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 4. Implement Synchronization
|
|
71
|
+
|
|
72
|
+
When commands and queries share data:
|
|
73
|
+
|
|
74
|
+
1. **Synchronous** (same DB): Update the read model transactionally
|
|
75
|
+
2. **Asynchronous** (event-driven): Project events to read models
|
|
76
|
+
3. **Dual writes**: Update both models, handle eventual consistency
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
// Synchronous synchronization
|
|
80
|
+
async function placeOrder(command: PlaceOrderCommand): Promise<void> {
|
|
81
|
+
const order = Order.create(command.payload);
|
|
82
|
+
|
|
83
|
+
await this.transactionManager.execute(async (tx) => {
|
|
84
|
+
// Write to command model
|
|
85
|
+
await this.orderRepo.save(order, tx);
|
|
86
|
+
|
|
87
|
+
// Synchronize to read model
|
|
88
|
+
const readModel = {
|
|
89
|
+
orderId: order.id,
|
|
90
|
+
customerId: order.customerId,
|
|
91
|
+
status: order.status,
|
|
92
|
+
total: order.total,
|
|
93
|
+
placedAt: order.placedAt
|
|
94
|
+
};
|
|
95
|
+
await this.orderReadRepo.save(readModel, tx);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 5. Handle Eventual Consistency
|
|
101
|
+
|
|
102
|
+
If read and write models are updated asynchronously:
|
|
103
|
+
|
|
104
|
+
- Document **expected consistency lag**
|
|
105
|
+
- Design UIs to handle stale data gracefully
|
|
106
|
+
- Implement **cache invalidation** strategies
|
|
107
|
+
- Use **version numbers** or timestamps for cache validation
|
|
108
|
+
|
|
109
|
+
## Examples
|
|
110
|
+
|
|
111
|
+
### Command Implementation
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
// commands/place-order.command.ts
|
|
115
|
+
interface PlaceOrderCommand {
|
|
116
|
+
orderId?: string; // Optional, generated if not provided
|
|
117
|
+
customerId: string;
|
|
118
|
+
items: OrderItem[];
|
|
119
|
+
paymentMethod: 'CARD' | 'PAYPAL';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
class PlaceOrderCommandHandler implements CommandHandler<PlaceOrderCommand> {
|
|
123
|
+
async execute(command: PlaceOrderCommand): Promise<CommandResult> {
|
|
124
|
+
// 1. Validate command
|
|
125
|
+
const validation = this.validate(command);
|
|
126
|
+
if (!validation.success) {
|
|
127
|
+
return CommandResult.failure(validation.errors);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 2. Check business invariants
|
|
131
|
+
const customer = await this.customerRepo.findById(command.customerId);
|
|
132
|
+
if (!customer.isActive) {
|
|
133
|
+
return CommandResult.failure('Customer account is not active');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 3. Create aggregate
|
|
137
|
+
const order = Order.create({
|
|
138
|
+
id: command.orderId,
|
|
139
|
+
customerId: command.customerId,
|
|
140
|
+
items: command.items
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// 4. Persist
|
|
144
|
+
await this.orderRepo.save(order);
|
|
145
|
+
|
|
146
|
+
// 5. Emit event for async processing
|
|
147
|
+
await this.eventBus.publish(OrderPlacedEvent.fromOrder(order));
|
|
148
|
+
|
|
149
|
+
return CommandResult.success({ orderId: order.id });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Query Implementation
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
// queries/get-order-details.query.ts
|
|
158
|
+
interface GetOrderDetailsQuery {
|
|
159
|
+
orderId: string;
|
|
160
|
+
includeItems?: boolean;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
interface OrderDetailsReadModel {
|
|
164
|
+
orderId: string;
|
|
165
|
+
customerId: string;
|
|
166
|
+
customerName: string;
|
|
167
|
+
status: string;
|
|
168
|
+
total: number;
|
|
169
|
+
placedAt: string;
|
|
170
|
+
items?: OrderItemReadModel[];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
class GetOrderDetailsQueryHandler implements QueryHandler<GetOrderDetailsQuery, OrderDetailsReadModel> {
|
|
174
|
+
async execute(query: GetOrderDetailsQuery): Promise<OrderDetailsReadModel> {
|
|
175
|
+
const order = await this.readModelRepo.findOrderWithDetails(query.orderId);
|
|
176
|
+
|
|
177
|
+
if (!order) {
|
|
178
|
+
throw new QueryNotFoundError('Order not found');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const result: OrderDetailsReadModel = {
|
|
182
|
+
orderId: order.orderId,
|
|
183
|
+
customerId: order.customerId,
|
|
184
|
+
customerName: order.customerName,
|
|
185
|
+
status: order.status,
|
|
186
|
+
total: order.total,
|
|
187
|
+
placedAt: order.placedAt
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
if (query.includeItems) {
|
|
191
|
+
result.items = await this.readModelRepo.findOrderItems(query.orderId);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Mediator Pattern for CQRS
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
class CqrsMediator {
|
|
203
|
+
private commandHandlers: Map<string, CommandHandler<any>>;
|
|
204
|
+
private queryHandlers: Map<string, QueryHandler<any>>;
|
|
205
|
+
|
|
206
|
+
async send<T>(message: Command | Query): Promise<CommandResult | QueryResult> {
|
|
207
|
+
const handler = message instanceof Command
|
|
208
|
+
? this.commandHandlers.get(message.type)
|
|
209
|
+
: this.queryHandlers.get(message.type);
|
|
210
|
+
|
|
211
|
+
if (!handler) {
|
|
212
|
+
throw new HandlerNotFoundError(message.type);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return handler.execute(message);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Usage
|
|
220
|
+
const result = await mediator.send(new PlaceOrderCommand({ ... }));
|
|
221
|
+
const orderDetails = await mediator.send(new GetOrderDetailsQuery({ orderId: '123' }));
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Related Skills
|
|
225
|
+
|
|
226
|
+
- api-design
|
|
227
|
+
- event-driven-architecture
|
|
228
|
+
- backend-patterns
|
|
229
|
+
- event-sourcing
|
|
230
|
+
- hexagonal-architecture
|