@aicgen/aicgen 1.0.0-beta.2 → 1.0.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.
- package/.agent/rules/api-design.md +649 -0
- package/.agent/rules/architecture.md +2507 -0
- package/.agent/rules/best-practices.md +622 -0
- package/.agent/rules/code-style.md +308 -0
- package/.agent/rules/design-patterns.md +577 -0
- package/.agent/rules/devops.md +230 -0
- package/.agent/rules/error-handling.md +417 -0
- package/.agent/rules/instructions.md +28 -0
- package/.agent/rules/language.md +786 -0
- package/.agent/rules/performance.md +710 -0
- package/.agent/rules/security.md +587 -0
- package/.agent/rules/testing.md +572 -0
- package/.agent/workflows/add-documentation.md +10 -0
- package/.agent/workflows/generate-integration-tests.md +10 -0
- package/.agent/workflows/generate-unit-tests.md +11 -0
- package/.agent/workflows/performance-audit.md +11 -0
- package/.agent/workflows/refactor-extract-module.md +12 -0
- package/.agent/workflows/security-audit.md +12 -0
- package/.gemini/instructions.md +4843 -0
- package/AGENTS.md +9 -11
- package/bun.lock +755 -4
- package/claude.md +2 -2
- package/config.example.yml +129 -0
- package/config.yml +38 -0
- package/data/guideline-mappings.yml +128 -0
- package/data/language/dart/async.md +289 -0
- package/data/language/dart/basics.md +280 -0
- package/data/language/dart/error-handling.md +355 -0
- package/data/language/dart/index.md +10 -0
- package/data/language/dart/testing.md +352 -0
- package/data/language/swift/basics.md +477 -0
- package/data/language/swift/concurrency.md +654 -0
- package/data/language/swift/error-handling.md +679 -0
- package/data/language/swift/swiftui-mvvm.md +795 -0
- package/data/language/swift/testing.md +708 -0
- package/data/version.json +10 -8
- package/dist/index.js +50295 -29101
- package/jest.config.js +46 -0
- package/package.json +13 -2
|
@@ -0,0 +1,2507 @@
|
|
|
1
|
+
# Architecture Rules
|
|
2
|
+
|
|
3
|
+
# Service Boundaries
|
|
4
|
+
|
|
5
|
+
## Defining Service Boundaries
|
|
6
|
+
|
|
7
|
+
Each service should own a specific business capability:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
✅ Good Boundaries:
|
|
11
|
+
- User Service: Authentication, profiles, preferences
|
|
12
|
+
- Order Service: Order processing, fulfillment
|
|
13
|
+
- Payment Service: Payment processing, billing
|
|
14
|
+
- Notification Service: Emails, SMS, push notifications
|
|
15
|
+
|
|
16
|
+
❌ Bad Boundaries:
|
|
17
|
+
- Data Access Service (technical, not business)
|
|
18
|
+
- Utility Service (too generic)
|
|
19
|
+
- God Service (does everything)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Bounded Contexts
|
|
23
|
+
|
|
24
|
+
Use Domain-Driven Design to identify boundaries:
|
|
25
|
+
|
|
26
|
+
- Each service represents a bounded context
|
|
27
|
+
- Services are organized around business domains
|
|
28
|
+
- Clear ownership of data and logic
|
|
29
|
+
- Services should be independently deployable
|
|
30
|
+
|
|
31
|
+
## Ownership Rules
|
|
32
|
+
|
|
33
|
+
**Each service:**
|
|
34
|
+
- Owns its own database (no shared databases)
|
|
35
|
+
- Owns its domain logic
|
|
36
|
+
- Exposes well-defined APIs
|
|
37
|
+
- Can be developed by autonomous teams
|
|
38
|
+
|
|
39
|
+
## Communication Rules
|
|
40
|
+
|
|
41
|
+
**Avoid:**
|
|
42
|
+
- Direct database access between services
|
|
43
|
+
- Chatty communication (N+1 service calls)
|
|
44
|
+
- Tight coupling through shared libraries
|
|
45
|
+
|
|
46
|
+
**Prefer:**
|
|
47
|
+
- API-based communication
|
|
48
|
+
- Event-driven for data synchronization
|
|
49
|
+
- Async messaging where possible
|
|
50
|
+
|
|
51
|
+
## Data Ownership
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
// ✅ Good - Service owns its data
|
|
55
|
+
Class OrderService:
|
|
56
|
+
Method CreateOrder(data):
|
|
57
|
+
# Order service owns order data
|
|
58
|
+
Order = OrderRepository.Save(data)
|
|
59
|
+
|
|
60
|
+
# Publish event for other services
|
|
61
|
+
EventBus.Publish("order.created", {
|
|
62
|
+
orderId: Order.id,
|
|
63
|
+
userId: Order.userId,
|
|
64
|
+
total: Order.total
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
Return Order
|
|
68
|
+
|
|
69
|
+
// ❌ Bad - Direct access to another service's database
|
|
70
|
+
Class OrderService:
|
|
71
|
+
Method CreateOrder(data):
|
|
72
|
+
# Don't do this!
|
|
73
|
+
User = UserDatabase.FindOne({ id: data.userId })
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Sizing Guidelines
|
|
77
|
+
|
|
78
|
+
Keep services:
|
|
79
|
+
- Small enough to be maintained by a small team (2-3 developers)
|
|
80
|
+
- Large enough to provide business value
|
|
81
|
+
- Focused on a single bounded context
|
|
82
|
+
- Independently deployable and scalable
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
# Microservices Communication
|
|
88
|
+
|
|
89
|
+
## Synchronous vs Asynchronous
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
# ⚠️ Synchronous creates coupling and multiplicative downtime
|
|
93
|
+
# If Service A calls B calls C, any failure breaks the chain
|
|
94
|
+
|
|
95
|
+
# ✅ Prefer asynchronous messaging for most inter-service communication
|
|
96
|
+
# Limit synchronous calls to one per user request
|
|
97
|
+
|
|
98
|
+
# Async Message Format Example
|
|
99
|
+
Event: "order.created"
|
|
100
|
+
Data: {
|
|
101
|
+
orderId: "ord_123",
|
|
102
|
+
userId: "user_456",
|
|
103
|
+
items: [...]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# Subscribers process independently
|
|
107
|
+
Service Inventory -> ReserveItems(items)
|
|
108
|
+
Service Notification -> SendEmail(user)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## API-Based Communication
|
|
112
|
+
|
|
113
|
+
```text
|
|
114
|
+
# ✅ Well-defined REST APIs between services
|
|
115
|
+
GET /users/{userId}
|
|
116
|
+
|
|
117
|
+
# ✅ Use circuit breaker for resilience
|
|
118
|
+
Function getUserSafe(userId):
|
|
119
|
+
Try:
|
|
120
|
+
return UserClient.getUser(userId)
|
|
121
|
+
Catch Error:
|
|
122
|
+
return getCachedUser(userId) # Fallback
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Event-Driven Integration
|
|
126
|
+
|
|
127
|
+
```text
|
|
128
|
+
# ✅ Publish events for state changes
|
|
129
|
+
Function CreateOrder(data):
|
|
130
|
+
order = Repository.Save(data)
|
|
131
|
+
|
|
132
|
+
# Failures here don't block the user
|
|
133
|
+
EventBus.Publish("order.created", {
|
|
134
|
+
orderId: order.id,
|
|
135
|
+
userId: order.userId,
|
|
136
|
+
timestamp: Now()
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
return order
|
|
140
|
+
|
|
141
|
+
# ✅ Consumers handle events independently (Decoupled)
|
|
142
|
+
Service Notification:
|
|
143
|
+
On("order.created"): SendConfirmation(event)
|
|
144
|
+
|
|
145
|
+
Service Inventory:
|
|
146
|
+
On("order.created"): ReserveInventory(event)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Tolerant Reader Pattern
|
|
150
|
+
|
|
151
|
+
```text
|
|
152
|
+
# ✅ Don't fail on unknown fields - enables independent evolution
|
|
153
|
+
Structure UserResponse:
|
|
154
|
+
id: string
|
|
155
|
+
name: string
|
|
156
|
+
...ignore other fields...
|
|
157
|
+
|
|
158
|
+
# ✅ Use sensible defaults for missing optional fields
|
|
159
|
+
Function ParseUser(data):
|
|
160
|
+
return User {
|
|
161
|
+
id: data.id,
|
|
162
|
+
name: data.name,
|
|
163
|
+
role: data.role OR 'user', # Default
|
|
164
|
+
avatar: data.avatar OR null
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Anti-Patterns
|
|
169
|
+
|
|
170
|
+
```text
|
|
171
|
+
# ❌ Chatty communication (N+1 service calls)
|
|
172
|
+
For each orderId in orderIds:
|
|
173
|
+
GetOrder(orderId) # N network calls!
|
|
174
|
+
|
|
175
|
+
# ✅ Batch requests
|
|
176
|
+
GetOrders(orderIds) # 1 network call
|
|
177
|
+
|
|
178
|
+
# ❌ Tight coupling via shared databases
|
|
179
|
+
# Service A directly queries Service B's tables
|
|
180
|
+
|
|
181
|
+
# ✅ API-based communication
|
|
182
|
+
UserClient.GetUsers(userIds)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
# Microservices Data Management
|
|
189
|
+
|
|
190
|
+
## Database Per Service
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
Each service owns its database:
|
|
194
|
+
|
|
195
|
+
✅ Order Service → order_db (PostgreSQL)
|
|
196
|
+
✅ User Service → user_db (PostgreSQL)
|
|
197
|
+
✅ Catalog Service → catalog_db (MongoDB)
|
|
198
|
+
✅ Search Service → search_index (Elasticsearch)
|
|
199
|
+
|
|
200
|
+
❌ Never share databases between services
|
|
201
|
+
❌ Never query another service's tables directly
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Polyglot Persistence
|
|
205
|
+
|
|
206
|
+
```text
|
|
207
|
+
# Each service uses the best database for its needs
|
|
208
|
+
|
|
209
|
+
# User Service -> Relational (ACID, relationships)
|
|
210
|
+
Repository UserRepository:
|
|
211
|
+
Method Create(user):
|
|
212
|
+
SQL "INSERT INTO users..."
|
|
213
|
+
|
|
214
|
+
# Catalog Service -> Document (Flexible schema)
|
|
215
|
+
Repository ProductRepository:
|
|
216
|
+
Method Create(product):
|
|
217
|
+
Collection("products").Insert(product)
|
|
218
|
+
|
|
219
|
+
# Analytics Service -> Time-Series (High write volume)
|
|
220
|
+
Repository MetricsRepository:
|
|
221
|
+
Method Record(metric):
|
|
222
|
+
InfluxDB.Write(metric)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Eventual Consistency
|
|
226
|
+
|
|
227
|
+
```text
|
|
228
|
+
# ✅ Embrace eventual consistency for cross-service data
|
|
229
|
+
|
|
230
|
+
1. Order Service: Save Order -> Publish "order.created"
|
|
231
|
+
2. Inventory Service: Listen "order.created" -> Reserve Inventory
|
|
232
|
+
|
|
233
|
+
# Data may be temporarily inconsistent - that's OK
|
|
234
|
+
|
|
235
|
+
# ✅ Use compensating actions for failures
|
|
236
|
+
Function ProcessOrder(order):
|
|
237
|
+
Try:
|
|
238
|
+
InventoryService.Reserve(order.items)
|
|
239
|
+
PaymentService.Charge(order.total)
|
|
240
|
+
Catch Error:
|
|
241
|
+
# Compensate: undo previous actions
|
|
242
|
+
InventoryService.Release(order.items)
|
|
243
|
+
Throw Error
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Data Synchronization Patterns
|
|
247
|
+
|
|
248
|
+
```text
|
|
249
|
+
# Pattern: Event Sourcing / CQRS
|
|
250
|
+
Service OrderQuery:
|
|
251
|
+
On("product.updated"):
|
|
252
|
+
# Update local read-optimized copy
|
|
253
|
+
Cache.Set(event.productId, { name: event.name, price: event.price })
|
|
254
|
+
|
|
255
|
+
# Pattern: Saga for distributed transactions
|
|
256
|
+
Saga CreateOrder:
|
|
257
|
+
Step 1:
|
|
258
|
+
Action: Inventory.Reserve()
|
|
259
|
+
Compensate: Inventory.Release()
|
|
260
|
+
Step 2:
|
|
261
|
+
Action: Payment.Charge()
|
|
262
|
+
Compensate: Payment.Refund()
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Data Ownership
|
|
266
|
+
|
|
267
|
+
```text
|
|
268
|
+
# ✅ Each service is the source of truth for its data
|
|
269
|
+
Service User:
|
|
270
|
+
Function UpdateEmail(userId, email):
|
|
271
|
+
Database.Update(userId, email)
|
|
272
|
+
EventBus.Publish("user.email.changed", { userId, email })
|
|
273
|
+
|
|
274
|
+
# Other services maintain their own copies (projections)
|
|
275
|
+
Service Order:
|
|
276
|
+
On("user.email.changed"):
|
|
277
|
+
# Update local cache, never query User DB directly
|
|
278
|
+
LocalUserCache.Update(event.userId, event.email)
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
# Microservices Resilience
|
|
285
|
+
|
|
286
|
+
## Circuit Breaker Pattern
|
|
287
|
+
|
|
288
|
+
```text
|
|
289
|
+
Class CircuitBreaker:
|
|
290
|
+
State: CLOSED | OPEN | HALF_OPEN
|
|
291
|
+
|
|
292
|
+
Method Execute(operation):
|
|
293
|
+
If State is OPEN:
|
|
294
|
+
If TimeoutExpired:
|
|
295
|
+
State = HALF_OPEN
|
|
296
|
+
Else:
|
|
297
|
+
Throw Error("Circuit Open")
|
|
298
|
+
|
|
299
|
+
Try:
|
|
300
|
+
Result = operation()
|
|
301
|
+
OnSuccess()
|
|
302
|
+
Return Result
|
|
303
|
+
Catch Error:
|
|
304
|
+
OnFailure()
|
|
305
|
+
Throw Error
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## Retry with Exponential Backoff
|
|
309
|
+
|
|
310
|
+
```text
|
|
311
|
+
Function Retry(operation, maxAttempts, baseDelay):
|
|
312
|
+
For attempt in 1..maxAttempts:
|
|
313
|
+
Try:
|
|
314
|
+
return operation()
|
|
315
|
+
Catch Error:
|
|
316
|
+
If attempt == maxAttempts: Throw Error
|
|
317
|
+
|
|
318
|
+
# Exponential Backoff + Jitter
|
|
319
|
+
delay = baseDelay * (2 ^ attempt) + RandomJitter()
|
|
320
|
+
Sleep(delay)
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
## Bulkhead Pattern
|
|
324
|
+
|
|
325
|
+
```text
|
|
326
|
+
# Isolate resources to prevent cascading failures
|
|
327
|
+
Class Bulkhead:
|
|
328
|
+
MaxConcurrent = 5
|
|
329
|
+
Active = 0
|
|
330
|
+
|
|
331
|
+
Method Execute(operation):
|
|
332
|
+
If Active >= MaxConcurrent:
|
|
333
|
+
Throw Error("Bulkhead Full")
|
|
334
|
+
|
|
335
|
+
Active++
|
|
336
|
+
Try:
|
|
337
|
+
return operation()
|
|
338
|
+
Finally:
|
|
339
|
+
Active--
|
|
340
|
+
|
|
341
|
+
# Usage: Separate bulkheads per dependency
|
|
342
|
+
PaymentBulkhead = New Bulkhead(5)
|
|
343
|
+
EmailBulkhead = New Bulkhead(10)
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## Timeouts
|
|
347
|
+
|
|
348
|
+
```text
|
|
349
|
+
Function WithTimeout(operation, timeoutMs):
|
|
350
|
+
Race:
|
|
351
|
+
1. Result = operation()
|
|
352
|
+
2. Sleep(timeoutMs) -> Throw Error("Timeout")
|
|
353
|
+
|
|
354
|
+
# Always set timeouts for external calls
|
|
355
|
+
Result = WithTimeout(UserService.GetUser(id), 5000)
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
## Graceful Degradation
|
|
359
|
+
|
|
360
|
+
```text
|
|
361
|
+
Function GetProductRecommendations(userId):
|
|
362
|
+
Try:
|
|
363
|
+
return RecommendationService.GetPersonalized(userId)
|
|
364
|
+
Catch Error:
|
|
365
|
+
# Fallback to cached popular items
|
|
366
|
+
Log("Recommendation service unavailable")
|
|
367
|
+
return GetPopularProducts()
|
|
368
|
+
|
|
369
|
+
# Partial responses instead of complete failure
|
|
370
|
+
Function GetDashboard(userId):
|
|
371
|
+
User = GetUser(userId) OR null
|
|
372
|
+
Orders = GetOrders(userId) OR []
|
|
373
|
+
Stats = GetStats(userId) OR null
|
|
374
|
+
|
|
375
|
+
return { User, Orders, Stats }
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
## Health Checks
|
|
379
|
+
|
|
380
|
+
```text
|
|
381
|
+
Endpoint GET /health:
|
|
382
|
+
Checks = [
|
|
383
|
+
CheckDatabase(),
|
|
384
|
+
CheckRedis(),
|
|
385
|
+
CheckExternalAPI()
|
|
386
|
+
]
|
|
387
|
+
|
|
388
|
+
Healthy = All(Checks) passed
|
|
389
|
+
|
|
390
|
+
Return HTTP 200/503 {
|
|
391
|
+
status: Healthy ? "healthy" : "degraded",
|
|
392
|
+
checks: { ...details... }
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
# API Gateway
|
|
400
|
+
|
|
401
|
+
## Overview
|
|
402
|
+
|
|
403
|
+
An API Gateway acts as a single entry point for a group of microservices. It handles cross-cutting concerns and routes requests to the appropriate backend services.
|
|
404
|
+
|
|
405
|
+
## Core Responsibilities
|
|
406
|
+
|
|
407
|
+
1. **Routing**: Forwarding requests to the correct service (e.g., `/api/users` -> User Service).
|
|
408
|
+
2. **Authentication & Authorization**: Verifying identity and permissions at the edge.
|
|
409
|
+
3. **Rate Limiting**: Protecting services from abuse.
|
|
410
|
+
4. **Protocol Translation**: Converting public HTTP/REST to internal gRPC or AMQP.
|
|
411
|
+
5. **Response Aggregation**: Combining data from multiple services into a single response.
|
|
412
|
+
|
|
413
|
+
## Patterns
|
|
414
|
+
|
|
415
|
+
### Backend for Frontend (BFF)
|
|
416
|
+
|
|
417
|
+
Create separate gateways for different client types (Mobile, Web, 3rd Party) to optimize the API for each consumer.
|
|
418
|
+
|
|
419
|
+
```mermaid
|
|
420
|
+
graph TD
|
|
421
|
+
Web[Web App] --> WebBFF[Web BFF]
|
|
422
|
+
Mobile[Mobile App] --> MobileBFF[Mobile BFF]
|
|
423
|
+
|
|
424
|
+
WebBFF --> SvcA[Service A]
|
|
425
|
+
WebBFF --> SvcB[Service B]
|
|
426
|
+
|
|
427
|
+
MobileBFF --> SvcA
|
|
428
|
+
MobileBFF --> SvcC[Service C]
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
## Implementation
|
|
432
|
+
|
|
433
|
+
### Cross-Cutting Concerns
|
|
434
|
+
|
|
435
|
+
Handle these at the gateway to keep microservices focused on business logic:
|
|
436
|
+
|
|
437
|
+
- **SSL Termination**: Decrypt HTTPS at the gateway.
|
|
438
|
+
- **CORS**: Handle Cross-Origin Resource Sharing headers.
|
|
439
|
+
- **Request Validation**: Basic schema validation before hitting services.
|
|
440
|
+
- **Caching**: Cache common responses.
|
|
441
|
+
|
|
442
|
+
### When to Use
|
|
443
|
+
|
|
444
|
+
| Use API Gateway When... | Avoid API Gateway When... |
|
|
445
|
+
|-------------------------|---------------------------|
|
|
446
|
+
| You have multiple microservices | You have a monolithic application |
|
|
447
|
+
| You need centralized auth/security | You need ultra-low latency (extra hop) |
|
|
448
|
+
| You have diverse clients (Web, Mobile) | Your architecture is very simple |
|
|
449
|
+
|
|
450
|
+
## Best Practices
|
|
451
|
+
|
|
452
|
+
- **Keep it Logic-Free**: Don't put business logic in the gateway. It should be a router, not a processor.
|
|
453
|
+
- **High Availability**: The gateway is a single point of failure; deploy it in a cluster.
|
|
454
|
+
- **Observability**: Ensure the gateway generates trace IDs and logs all traffic.
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
# Modular Monolith Structure
|
|
460
|
+
|
|
461
|
+
## Project Organization
|
|
462
|
+
|
|
463
|
+
```
|
|
464
|
+
project-root/
|
|
465
|
+
├── apps/
|
|
466
|
+
│ └── api/
|
|
467
|
+
│ ├── src/
|
|
468
|
+
│ │ ├── app/ # Application bootstrap
|
|
469
|
+
│ │ ├── modules/ # Business modules
|
|
470
|
+
│ │ │ ├── auth/
|
|
471
|
+
│ │ │ ├── user/
|
|
472
|
+
│ │ │ ├── booking/
|
|
473
|
+
│ │ │ ├── payment/
|
|
474
|
+
│ │ │ └── notification/
|
|
475
|
+
│ │ ├── common/ # Shared infrastructure
|
|
476
|
+
│ │ │ ├── decorators/
|
|
477
|
+
│ │ │ ├── guards/
|
|
478
|
+
│ │ │ └── interceptors/
|
|
479
|
+
│ │ └── prisma/ # Database service
|
|
480
|
+
│ └── main.ts
|
|
481
|
+
├── libs/ # Shared libraries
|
|
482
|
+
│ └── shared-types/
|
|
483
|
+
└── package.json
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
## Module Structure
|
|
487
|
+
|
|
488
|
+
```
|
|
489
|
+
modules/booking/
|
|
490
|
+
├── entities/ # Domain models and DTOs
|
|
491
|
+
│ ├── booking.entity.ts
|
|
492
|
+
│ ├── create-booking.dto.ts
|
|
493
|
+
│ └── booking-response.dto.ts
|
|
494
|
+
├── repositories/ # Data access layer
|
|
495
|
+
│ └── booking.repository.ts
|
|
496
|
+
├── services/ # Business logic
|
|
497
|
+
│ ├── booking.service.ts
|
|
498
|
+
│ └── availability.service.ts
|
|
499
|
+
├── controllers/ # HTTP/API layer
|
|
500
|
+
│ └── bookings.controller.ts
|
|
501
|
+
└── booking.module.ts # Module definition
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
## Module Definition
|
|
505
|
+
|
|
506
|
+
```typescript
|
|
507
|
+
@Module({
|
|
508
|
+
imports: [
|
|
509
|
+
PrismaModule,
|
|
510
|
+
forwardRef(() => AuthModule),
|
|
511
|
+
NotificationsModule,
|
|
512
|
+
],
|
|
513
|
+
controllers: [BookingsController],
|
|
514
|
+
providers: [
|
|
515
|
+
BookingService,
|
|
516
|
+
AvailabilityService,
|
|
517
|
+
BookingRepository,
|
|
518
|
+
],
|
|
519
|
+
exports: [BookingService], // Only export public API
|
|
520
|
+
})
|
|
521
|
+
export class BookingsModule {}
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
## Layered Architecture Within Modules
|
|
525
|
+
|
|
526
|
+
```typescript
|
|
527
|
+
// Controller - HTTP layer
|
|
528
|
+
@Controller('api/v1/bookings')
|
|
529
|
+
export class BookingsController {
|
|
530
|
+
constructor(private bookingService: BookingService) {}
|
|
531
|
+
|
|
532
|
+
@Get('calendar')
|
|
533
|
+
async getCalendarBookings(@Query() dto: GetBookingsDto) {
|
|
534
|
+
return this.bookingService.getBookingsForCalendar(dto);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Service - Business logic
|
|
539
|
+
@Injectable()
|
|
540
|
+
export class BookingService {
|
|
541
|
+
constructor(
|
|
542
|
+
private bookingRepository: BookingRepository,
|
|
543
|
+
private availabilityService: AvailabilityService,
|
|
544
|
+
) {}
|
|
545
|
+
|
|
546
|
+
async getBookingsForCalendar(dto: GetBookingsDto) {
|
|
547
|
+
const bookings = await this.bookingRepository.findByDateRange(
|
|
548
|
+
dto.startDate,
|
|
549
|
+
dto.endDate
|
|
550
|
+
);
|
|
551
|
+
return bookings.map(this.mapToCalendarDto);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Repository - Data access
|
|
556
|
+
@Injectable()
|
|
557
|
+
export class BookingRepository {
|
|
558
|
+
constructor(private prisma: PrismaService) {}
|
|
559
|
+
|
|
560
|
+
async findByDateRange(start: Date, end: Date) {
|
|
561
|
+
return this.prisma.booking.findMany({
|
|
562
|
+
where: {
|
|
563
|
+
startTime: { gte: start },
|
|
564
|
+
endTime: { lte: end }
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
## Shared Infrastructure
|
|
572
|
+
|
|
573
|
+
```typescript
|
|
574
|
+
// common/guards/jwt-auth.guard.ts
|
|
575
|
+
@Injectable()
|
|
576
|
+
export class JwtAuthGuard extends AuthGuard('jwt') {
|
|
577
|
+
canActivate(context: ExecutionContext) {
|
|
578
|
+
const isPublic = this.reflector.get<boolean>('isPublic', context.getHandler());
|
|
579
|
+
return isPublic ? true : super.canActivate(context);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// common/decorators/current-user.decorator.ts
|
|
584
|
+
export const CurrentUser = createParamDecorator(
|
|
585
|
+
(data: unknown, ctx: ExecutionContext) => {
|
|
586
|
+
return ctx.switchToHttp().getRequest().user;
|
|
587
|
+
}
|
|
588
|
+
);
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
---
|
|
593
|
+
|
|
594
|
+
# Modular Monolith Boundaries
|
|
595
|
+
|
|
596
|
+
## High Cohesion, Low Coupling
|
|
597
|
+
|
|
598
|
+
```typescript
|
|
599
|
+
// ❌ Bad: Tight coupling - direct repository access
|
|
600
|
+
@Injectable()
|
|
601
|
+
export class OrderService {
|
|
602
|
+
constructor(private userRepo: UserRepository) {} // Crosses module boundary
|
|
603
|
+
|
|
604
|
+
async createOrder(userId: string) {
|
|
605
|
+
const user = await this.userRepo.findById(userId); // Direct access
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// ✅ Good: Loose coupling via service
|
|
610
|
+
@Injectable()
|
|
611
|
+
export class OrderService {
|
|
612
|
+
constructor(private userService: UserService) {} // Service dependency
|
|
613
|
+
|
|
614
|
+
async createOrder(userId: string) {
|
|
615
|
+
const user = await this.userService.findById(userId); // Through public API
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
## No Direct Cross-Module Database Access
|
|
621
|
+
|
|
622
|
+
```typescript
|
|
623
|
+
// ❌ Never query another module's tables directly
|
|
624
|
+
class BookingService {
|
|
625
|
+
async createBooking(data: CreateBookingDto) {
|
|
626
|
+
const user = await this.prisma.user.findUnique({ where: { id: data.userId } });
|
|
627
|
+
// This bypasses the User module!
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// ✅ Use the module's public service API
|
|
632
|
+
class BookingService {
|
|
633
|
+
constructor(private userService: UserService) {}
|
|
634
|
+
|
|
635
|
+
async createBooking(data: CreateBookingDto) {
|
|
636
|
+
const user = await this.userService.findById(data.userId);
|
|
637
|
+
// Properly goes through User module
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
## Separated Interface Pattern
|
|
643
|
+
|
|
644
|
+
```typescript
|
|
645
|
+
// Define interface in consuming module
|
|
646
|
+
// modules/order/interfaces/user-provider.interface.ts
|
|
647
|
+
export interface UserProvider {
|
|
648
|
+
findById(id: string): Promise<User>;
|
|
649
|
+
validateUser(id: string): Promise<boolean>;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Implement in providing module
|
|
653
|
+
// modules/user/user.service.ts
|
|
654
|
+
@Injectable()
|
|
655
|
+
export class UserService implements UserProvider {
|
|
656
|
+
async findById(id: string): Promise<User> {
|
|
657
|
+
return this.userRepo.findById(id);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
async validateUser(id: string): Promise<boolean> {
|
|
661
|
+
const user = await this.findById(id);
|
|
662
|
+
return user && user.isActive;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
## Domain Events for Loose Coupling
|
|
668
|
+
|
|
669
|
+
```typescript
|
|
670
|
+
// ✅ Publish events instead of direct calls
|
|
671
|
+
@Injectable()
|
|
672
|
+
export class UserService {
|
|
673
|
+
constructor(private eventEmitter: EventEmitter2) {}
|
|
674
|
+
|
|
675
|
+
async createUser(dto: CreateUserDto): Promise<User> {
|
|
676
|
+
const user = await this.userRepo.create(dto);
|
|
677
|
+
|
|
678
|
+
this.eventEmitter.emit('user.created', new UserCreatedEvent(user.id, user.email));
|
|
679
|
+
|
|
680
|
+
return user;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Other modules subscribe to events
|
|
685
|
+
@Injectable()
|
|
686
|
+
export class NotificationListener {
|
|
687
|
+
@OnEvent('user.created')
|
|
688
|
+
async handleUserCreated(event: UserCreatedEvent) {
|
|
689
|
+
await this.notificationService.sendWelcomeEmail(event.email);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
## Handling Circular Dependencies
|
|
695
|
+
|
|
696
|
+
```typescript
|
|
697
|
+
// Use forwardRef() when modules depend on each other
|
|
698
|
+
@Module({
|
|
699
|
+
imports: [
|
|
700
|
+
forwardRef(() => AuthModule), // Break circular dependency
|
|
701
|
+
UserModule,
|
|
702
|
+
],
|
|
703
|
+
})
|
|
704
|
+
export class UserModule {}
|
|
705
|
+
|
|
706
|
+
@Module({
|
|
707
|
+
imports: [
|
|
708
|
+
forwardRef(() => UserModule),
|
|
709
|
+
],
|
|
710
|
+
})
|
|
711
|
+
export class AuthModule {}
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
## Export Only What's Necessary
|
|
715
|
+
|
|
716
|
+
```typescript
|
|
717
|
+
@Module({
|
|
718
|
+
providers: [
|
|
719
|
+
UserService, // Public service
|
|
720
|
+
UserRepository, // Internal
|
|
721
|
+
PasswordHasher, // Internal
|
|
722
|
+
],
|
|
723
|
+
exports: [UserService], // Only export the service, not internals
|
|
724
|
+
})
|
|
725
|
+
export class UserModule {}
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
---
|
|
730
|
+
|
|
731
|
+
# SOLID Principles
|
|
732
|
+
|
|
733
|
+
## Single Responsibility Principle (SRP)
|
|
734
|
+
|
|
735
|
+
A class should have only one reason to change.
|
|
736
|
+
|
|
737
|
+
**Bad:**
|
|
738
|
+
```typescript
|
|
739
|
+
class UserService {
|
|
740
|
+
createUser(data: UserData): User { /* ... */ }
|
|
741
|
+
sendWelcomeEmail(user: User): void { /* ... */ }
|
|
742
|
+
generateReport(users: User[]): Report { /* ... */ }
|
|
743
|
+
}
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
**Good:**
|
|
747
|
+
```typescript
|
|
748
|
+
class UserService {
|
|
749
|
+
createUser(data: UserData): User { /* ... */ }
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
class EmailService {
|
|
753
|
+
sendWelcomeEmail(user: User): void { /* ... */ }
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
class ReportService {
|
|
757
|
+
generateUserReport(users: User[]): Report { /* ... */ }
|
|
758
|
+
}
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
## Open/Closed Principle (OCP)
|
|
762
|
+
|
|
763
|
+
Open for extension, closed for modification.
|
|
764
|
+
|
|
765
|
+
**Bad:**
|
|
766
|
+
```typescript
|
|
767
|
+
class PaymentProcessor {
|
|
768
|
+
process(payment: Payment): void {
|
|
769
|
+
if (payment.type === 'credit') { /* credit logic */ }
|
|
770
|
+
else if (payment.type === 'paypal') { /* paypal logic */ }
|
|
771
|
+
// Must modify class to add new payment types
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
**Good:**
|
|
777
|
+
```typescript
|
|
778
|
+
interface PaymentHandler {
|
|
779
|
+
process(payment: Payment): void;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
class CreditCardHandler implements PaymentHandler {
|
|
783
|
+
process(payment: Payment): void { /* credit logic */ }
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
class PayPalHandler implements PaymentHandler {
|
|
787
|
+
process(payment: Payment): void { /* paypal logic */ }
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
class PaymentProcessor {
|
|
791
|
+
constructor(private handlers: Map<string, PaymentHandler>) {}
|
|
792
|
+
|
|
793
|
+
process(payment: Payment): void {
|
|
794
|
+
this.handlers.get(payment.type)?.process(payment);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
## Liskov Substitution Principle (LSP)
|
|
800
|
+
|
|
801
|
+
Subtypes must be substitutable for their base types.
|
|
802
|
+
|
|
803
|
+
**Bad:**
|
|
804
|
+
```typescript
|
|
805
|
+
class Bird {
|
|
806
|
+
fly(): void { /* flying logic */ }
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
class Penguin extends Bird {
|
|
810
|
+
fly(): void {
|
|
811
|
+
throw new Error("Penguins can't fly!"); // Violates LSP
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
**Good:**
|
|
817
|
+
```typescript
|
|
818
|
+
interface Bird {
|
|
819
|
+
move(): void;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
class FlyingBird implements Bird {
|
|
823
|
+
move(): void { this.fly(); }
|
|
824
|
+
private fly(): void { /* flying logic */ }
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
class Penguin implements Bird {
|
|
828
|
+
move(): void { this.swim(); }
|
|
829
|
+
private swim(): void { /* swimming logic */ }
|
|
830
|
+
}
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
## Interface Segregation Principle (ISP)
|
|
834
|
+
|
|
835
|
+
Clients shouldn't depend on interfaces they don't use.
|
|
836
|
+
|
|
837
|
+
**Bad:**
|
|
838
|
+
```typescript
|
|
839
|
+
interface Worker {
|
|
840
|
+
work(): void;
|
|
841
|
+
eat(): void;
|
|
842
|
+
sleep(): void;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
class Robot implements Worker {
|
|
846
|
+
work(): void { /* ... */ }
|
|
847
|
+
eat(): void { throw new Error("Robots don't eat"); }
|
|
848
|
+
sleep(): void { throw new Error("Robots don't sleep"); }
|
|
849
|
+
}
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
**Good:**
|
|
853
|
+
```typescript
|
|
854
|
+
interface Workable {
|
|
855
|
+
work(): void;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
interface Eatable {
|
|
859
|
+
eat(): void;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
interface Sleepable {
|
|
863
|
+
sleep(): void;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
class Human implements Workable, Eatable, Sleepable {
|
|
867
|
+
work(): void { /* ... */ }
|
|
868
|
+
eat(): void { /* ... */ }
|
|
869
|
+
sleep(): void { /* ... */ }
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
class Robot implements Workable {
|
|
873
|
+
work(): void { /* ... */ }
|
|
874
|
+
}
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
## Dependency Inversion Principle (DIP)
|
|
878
|
+
|
|
879
|
+
Depend on abstractions, not concretions.
|
|
880
|
+
|
|
881
|
+
**Bad:**
|
|
882
|
+
```typescript
|
|
883
|
+
class UserService {
|
|
884
|
+
private database = new MySQLDatabase();
|
|
885
|
+
|
|
886
|
+
getUser(id: string): User {
|
|
887
|
+
return this.database.query(`SELECT * FROM users WHERE id = '${id}'`);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
**Good:**
|
|
893
|
+
```typescript
|
|
894
|
+
interface Database {
|
|
895
|
+
query(sql: string): any;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
class UserService {
|
|
899
|
+
constructor(private database: Database) {}
|
|
900
|
+
|
|
901
|
+
getUser(id: string): User {
|
|
902
|
+
return this.database.query(`SELECT * FROM users WHERE id = '${id}'`);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// Can inject any database implementation
|
|
907
|
+
const userService = new UserService(new MySQLDatabase());
|
|
908
|
+
const testService = new UserService(new InMemoryDatabase());
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
## Best Practices
|
|
912
|
+
|
|
913
|
+
- Apply SRP at class, method, and module levels
|
|
914
|
+
- Use interfaces and dependency injection for flexibility
|
|
915
|
+
- Prefer composition over inheritance
|
|
916
|
+
- Design small, focused interfaces
|
|
917
|
+
- Inject dependencies rather than creating them internally
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
---
|
|
921
|
+
|
|
922
|
+
# Clean Architecture
|
|
923
|
+
|
|
924
|
+
## Core Principle
|
|
925
|
+
|
|
926
|
+
Dependencies point inward. Inner layers know nothing about outer layers.
|
|
927
|
+
|
|
928
|
+
```
|
|
929
|
+
┌─────────────────────────────────────────────┐
|
|
930
|
+
│ Frameworks & Drivers │
|
|
931
|
+
│ ┌─────────────────────────────────────┐ │
|
|
932
|
+
│ │ Interface Adapters │ │
|
|
933
|
+
│ │ ┌─────────────────────────────┐ │ │
|
|
934
|
+
│ │ │ Application Business │ │ │
|
|
935
|
+
│ │ │ ┌─────────────────────┐ │ │ │
|
|
936
|
+
│ │ │ │ Enterprise Business│ │ │ │
|
|
937
|
+
│ │ │ │ (Entities) │ │ │ │
|
|
938
|
+
│ │ │ └─────────────────────┘ │ │ │
|
|
939
|
+
│ │ │ (Use Cases) │ │ │
|
|
940
|
+
│ │ └─────────────────────────────┘ │ │
|
|
941
|
+
│ │ (Controllers, Gateways) │ │
|
|
942
|
+
│ └─────────────────────────────────────┘ │
|
|
943
|
+
│ (Web, DB, External APIs) │
|
|
944
|
+
└─────────────────────────────────────────────┘
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
## The Dependency Rule
|
|
948
|
+
|
|
949
|
+
Source code dependencies only point inward.
|
|
950
|
+
|
|
951
|
+
## Layer Structure
|
|
952
|
+
|
|
953
|
+
### Entities (Enterprise Business Rules)
|
|
954
|
+
|
|
955
|
+
```typescript
|
|
956
|
+
class Order {
|
|
957
|
+
constructor(
|
|
958
|
+
public readonly id: string,
|
|
959
|
+
private items: OrderItem[],
|
|
960
|
+
private status: OrderStatus
|
|
961
|
+
) {}
|
|
962
|
+
|
|
963
|
+
calculateTotal(): Money {
|
|
964
|
+
return this.items.reduce(
|
|
965
|
+
(sum, item) => sum.add(item.subtotal()),
|
|
966
|
+
Money.zero()
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
canBeCancelled(): boolean {
|
|
971
|
+
return this.status === OrderStatus.Pending;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
```
|
|
975
|
+
|
|
976
|
+
### Use Cases (Application Business Rules)
|
|
977
|
+
|
|
978
|
+
```typescript
|
|
979
|
+
class CreateOrderUseCase {
|
|
980
|
+
constructor(
|
|
981
|
+
private orderRepository: OrderRepository,
|
|
982
|
+
private productRepository: ProductRepository
|
|
983
|
+
) {}
|
|
984
|
+
|
|
985
|
+
async execute(request: CreateOrderRequest): Promise<CreateOrderResponse> {
|
|
986
|
+
const products = await this.productRepository.findByIds(request.productIds);
|
|
987
|
+
const order = new Order(generateId(), this.createItems(products));
|
|
988
|
+
await this.orderRepository.save(order);
|
|
989
|
+
return { orderId: order.id };
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
```
|
|
993
|
+
|
|
994
|
+
### Interface Adapters
|
|
995
|
+
|
|
996
|
+
```typescript
|
|
997
|
+
// Controller (adapts HTTP to use case)
|
|
998
|
+
class OrderController {
|
|
999
|
+
constructor(private createOrder: CreateOrderUseCase) {}
|
|
1000
|
+
|
|
1001
|
+
async create(req: Request, res: Response) {
|
|
1002
|
+
const result = await this.createOrder.execute(req.body);
|
|
1003
|
+
res.json(result);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Repository Implementation (adapts use case to database)
|
|
1008
|
+
class PostgreSQLOrderRepository implements OrderRepository {
|
|
1009
|
+
async save(order: Order): Promise<void> {
|
|
1010
|
+
await this.db.query('INSERT INTO orders...');
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
### Frameworks & Drivers
|
|
1016
|
+
|
|
1017
|
+
```typescript
|
|
1018
|
+
// Express setup
|
|
1019
|
+
const app = express();
|
|
1020
|
+
app.post('/orders', (req, res) => orderController.create(req, res));
|
|
1021
|
+
|
|
1022
|
+
// Database connection
|
|
1023
|
+
const db = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
## Best Practices
|
|
1027
|
+
|
|
1028
|
+
- Keep entities pure with no framework dependencies
|
|
1029
|
+
- Use cases orchestrate domain logic
|
|
1030
|
+
- Interfaces defined in inner layers, implemented in outer layers
|
|
1031
|
+
- Cross boundaries with simple data structures
|
|
1032
|
+
- Test use cases independently of frameworks
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
---
|
|
1036
|
+
|
|
1037
|
+
# DDD Tactical Patterns
|
|
1038
|
+
|
|
1039
|
+
## Entities
|
|
1040
|
+
|
|
1041
|
+
Objects with identity that persists through state changes.
|
|
1042
|
+
|
|
1043
|
+
```typescript
|
|
1044
|
+
class User {
|
|
1045
|
+
constructor(
|
|
1046
|
+
public readonly id: UserId,
|
|
1047
|
+
private email: Email,
|
|
1048
|
+
private name: string
|
|
1049
|
+
) {}
|
|
1050
|
+
|
|
1051
|
+
changeEmail(newEmail: Email): void {
|
|
1052
|
+
this.email = newEmail;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
equals(other: User): boolean {
|
|
1056
|
+
return this.id.equals(other.id);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
```
|
|
1060
|
+
|
|
1061
|
+
## Value Objects
|
|
1062
|
+
|
|
1063
|
+
Immutable objects defined by their attributes.
|
|
1064
|
+
|
|
1065
|
+
```typescript
|
|
1066
|
+
class Email {
|
|
1067
|
+
private readonly value: string;
|
|
1068
|
+
|
|
1069
|
+
constructor(email: string) {
|
|
1070
|
+
if (!this.isValid(email)) {
|
|
1071
|
+
throw new InvalidEmailError(email);
|
|
1072
|
+
}
|
|
1073
|
+
this.value = email.toLowerCase();
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
equals(other: Email): boolean {
|
|
1077
|
+
return this.value === other.value;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
class Money {
|
|
1082
|
+
constructor(
|
|
1083
|
+
public readonly amount: number,
|
|
1084
|
+
public readonly currency: Currency
|
|
1085
|
+
) {
|
|
1086
|
+
Object.freeze(this);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
add(other: Money): Money {
|
|
1090
|
+
this.assertSameCurrency(other);
|
|
1091
|
+
return new Money(this.amount + other.amount, this.currency);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
```
|
|
1095
|
+
|
|
1096
|
+
## Aggregates
|
|
1097
|
+
|
|
1098
|
+
Cluster of entities and value objects with a root entity.
|
|
1099
|
+
|
|
1100
|
+
```typescript
|
|
1101
|
+
class Order {
|
|
1102
|
+
private items: OrderItem[] = [];
|
|
1103
|
+
|
|
1104
|
+
constructor(
|
|
1105
|
+
public readonly id: OrderId,
|
|
1106
|
+
private customerId: CustomerId
|
|
1107
|
+
) {}
|
|
1108
|
+
|
|
1109
|
+
addItem(product: Product, quantity: number): void {
|
|
1110
|
+
const item = new OrderItem(product.id, product.price, quantity);
|
|
1111
|
+
this.items.push(item);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// All modifications go through aggregate root
|
|
1115
|
+
removeItem(productId: ProductId): void {
|
|
1116
|
+
this.items = this.items.filter(item => !item.productId.equals(productId));
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
```
|
|
1120
|
+
|
|
1121
|
+
## Domain Events
|
|
1122
|
+
|
|
1123
|
+
Capture something that happened in the domain.
|
|
1124
|
+
|
|
1125
|
+
```typescript
|
|
1126
|
+
class OrderPlaced implements DomainEvent {
|
|
1127
|
+
constructor(
|
|
1128
|
+
public readonly orderId: OrderId,
|
|
1129
|
+
public readonly customerId: CustomerId,
|
|
1130
|
+
public readonly occurredOn: Date = new Date()
|
|
1131
|
+
) {}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
class Order {
|
|
1135
|
+
private events: DomainEvent[] = [];
|
|
1136
|
+
|
|
1137
|
+
place(): void {
|
|
1138
|
+
this.status = OrderStatus.Placed;
|
|
1139
|
+
this.events.push(new OrderPlaced(this.id, this.customerId));
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
pullEvents(): DomainEvent[] {
|
|
1143
|
+
const events = [...this.events];
|
|
1144
|
+
this.events = [];
|
|
1145
|
+
return events;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
```
|
|
1149
|
+
|
|
1150
|
+
## Repositories
|
|
1151
|
+
|
|
1152
|
+
Abstract persistence for aggregates.
|
|
1153
|
+
|
|
1154
|
+
```typescript
|
|
1155
|
+
interface OrderRepository {
|
|
1156
|
+
findById(id: OrderId): Promise<Order | null>;
|
|
1157
|
+
save(order: Order): Promise<void>;
|
|
1158
|
+
nextId(): OrderId;
|
|
1159
|
+
}
|
|
1160
|
+
```
|
|
1161
|
+
|
|
1162
|
+
## Best Practices
|
|
1163
|
+
|
|
1164
|
+
- One repository per aggregate root
|
|
1165
|
+
- Aggregates should be small
|
|
1166
|
+
- Reference other aggregates by ID
|
|
1167
|
+
- Publish domain events for cross-aggregate communication
|
|
1168
|
+
- Keep value objects immutable
|
|
1169
|
+
|
|
1170
|
+
|
|
1171
|
+
---
|
|
1172
|
+
|
|
1173
|
+
# DDD Strategic Patterns
|
|
1174
|
+
|
|
1175
|
+
## Ubiquitous Language
|
|
1176
|
+
|
|
1177
|
+
Use the same terminology in code, documentation, and conversations.
|
|
1178
|
+
|
|
1179
|
+
```typescript
|
|
1180
|
+
// Domain experts say "place an order"
|
|
1181
|
+
class Order {
|
|
1182
|
+
place(): void { /* not submit(), not create() */ }
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// Domain experts say "items are added to cart"
|
|
1186
|
+
class ShoppingCart {
|
|
1187
|
+
addItem(product: Product): void { /* not insert(), not push() */ }
|
|
1188
|
+
}
|
|
1189
|
+
```
|
|
1190
|
+
|
|
1191
|
+
## Bounded Contexts
|
|
1192
|
+
|
|
1193
|
+
Explicit boundaries where a model applies consistently.
|
|
1194
|
+
|
|
1195
|
+
```
|
|
1196
|
+
┌─────────────────┐ ┌─────────────────┐
|
|
1197
|
+
│ Sales │ │ Warehouse │
|
|
1198
|
+
│ Context │ │ Context │
|
|
1199
|
+
├─────────────────┤ ├─────────────────┤
|
|
1200
|
+
│ Order │ │ Order │
|
|
1201
|
+
│ - customerId │ │ - shipmentId │
|
|
1202
|
+
│ - items[] │ │ - pickingList │
|
|
1203
|
+
│ - total │ │ - status │
|
|
1204
|
+
└─────────────────┘ └─────────────────┘
|
|
1205
|
+
Same term, different model
|
|
1206
|
+
```
|
|
1207
|
+
|
|
1208
|
+
## Context Mapping Patterns
|
|
1209
|
+
|
|
1210
|
+
### Shared Kernel
|
|
1211
|
+
Two contexts share a subset of the model.
|
|
1212
|
+
|
|
1213
|
+
### Customer/Supplier
|
|
1214
|
+
Upstream context provides what downstream needs.
|
|
1215
|
+
|
|
1216
|
+
### Conformist
|
|
1217
|
+
Downstream adopts upstream's model entirely.
|
|
1218
|
+
|
|
1219
|
+
### Anti-Corruption Layer
|
|
1220
|
+
Translate between contexts to protect domain model.
|
|
1221
|
+
|
|
1222
|
+
```typescript
|
|
1223
|
+
class InventoryAntiCorruptionLayer {
|
|
1224
|
+
constructor(private legacyInventorySystem: LegacyInventory) {}
|
|
1225
|
+
|
|
1226
|
+
checkAvailability(productId: ProductId): Promise<boolean> {
|
|
1227
|
+
// Translate from legacy format to domain model
|
|
1228
|
+
const legacyResult = await this.legacyInventorySystem.getStock(
|
|
1229
|
+
productId.toString()
|
|
1230
|
+
);
|
|
1231
|
+
return legacyResult.qty > 0;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
```
|
|
1235
|
+
|
|
1236
|
+
## Module Organization
|
|
1237
|
+
|
|
1238
|
+
```
|
|
1239
|
+
src/
|
|
1240
|
+
├── sales/ # Sales bounded context
|
|
1241
|
+
│ ├── domain/
|
|
1242
|
+
│ │ ├── order.ts
|
|
1243
|
+
│ │ └── customer.ts
|
|
1244
|
+
│ ├── application/
|
|
1245
|
+
│ │ └── place-order.ts
|
|
1246
|
+
│ └── infrastructure/
|
|
1247
|
+
│ └── order-repository.ts
|
|
1248
|
+
├── warehouse/ # Warehouse bounded context
|
|
1249
|
+
│ ├── domain/
|
|
1250
|
+
│ │ └── shipment.ts
|
|
1251
|
+
│ └── ...
|
|
1252
|
+
└── shared/ # Shared kernel
|
|
1253
|
+
└── money.ts
|
|
1254
|
+
```
|
|
1255
|
+
|
|
1256
|
+
## Best Practices
|
|
1257
|
+
|
|
1258
|
+
- Define context boundaries based on team structure and business capabilities
|
|
1259
|
+
- Use ubiquitous language within each context
|
|
1260
|
+
- Communicate between contexts via events or explicit APIs
|
|
1261
|
+
- Protect domain model with anti-corruption layers when integrating legacy systems
|
|
1262
|
+
|
|
1263
|
+
|
|
1264
|
+
---
|
|
1265
|
+
|
|
1266
|
+
# Event-Driven Architecture
|
|
1267
|
+
|
|
1268
|
+
## Event Sourcing
|
|
1269
|
+
|
|
1270
|
+
Store state as a sequence of events.
|
|
1271
|
+
|
|
1272
|
+
```typescript
|
|
1273
|
+
interface Event {
|
|
1274
|
+
id: string;
|
|
1275
|
+
aggregateId: string;
|
|
1276
|
+
type: string;
|
|
1277
|
+
data: unknown;
|
|
1278
|
+
timestamp: Date;
|
|
1279
|
+
version: number;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
class Account {
|
|
1283
|
+
private balance = 0;
|
|
1284
|
+
private version = 0;
|
|
1285
|
+
|
|
1286
|
+
static fromEvents(events: Event[]): Account {
|
|
1287
|
+
const account = new Account();
|
|
1288
|
+
events.forEach(event => account.apply(event));
|
|
1289
|
+
return account;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
private apply(event: Event): void {
|
|
1293
|
+
switch (event.type) {
|
|
1294
|
+
case 'MoneyDeposited':
|
|
1295
|
+
this.balance += (event.data as { amount: number }).amount;
|
|
1296
|
+
break;
|
|
1297
|
+
case 'MoneyWithdrawn':
|
|
1298
|
+
this.balance -= (event.data as { amount: number }).amount;
|
|
1299
|
+
break;
|
|
1300
|
+
}
|
|
1301
|
+
this.version = event.version;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
```
|
|
1305
|
+
|
|
1306
|
+
## CQRS (Command Query Responsibility Segregation)
|
|
1307
|
+
|
|
1308
|
+
Separate read and write models.
|
|
1309
|
+
|
|
1310
|
+
```typescript
|
|
1311
|
+
// Write Model (Commands)
|
|
1312
|
+
class OrderCommandHandler {
|
|
1313
|
+
async handle(cmd: PlaceOrderCommand): Promise<void> {
|
|
1314
|
+
const order = new Order(cmd.orderId, cmd.items);
|
|
1315
|
+
await this.eventStore.save(order.changes());
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// Read Model (Queries)
|
|
1320
|
+
class OrderQueryService {
|
|
1321
|
+
async getOrderSummary(orderId: string): Promise<OrderSummaryDTO> {
|
|
1322
|
+
return this.readDb.query('SELECT * FROM order_summaries WHERE id = $1', [orderId]);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// Projection updates read model from events
|
|
1327
|
+
class OrderProjection {
|
|
1328
|
+
async handle(event: OrderPlaced): Promise<void> {
|
|
1329
|
+
await this.readDb.insert('order_summaries', {
|
|
1330
|
+
id: event.orderId,
|
|
1331
|
+
status: 'placed',
|
|
1332
|
+
total: event.total
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
```
|
|
1337
|
+
|
|
1338
|
+
## Saga Pattern
|
|
1339
|
+
|
|
1340
|
+
Manage long-running transactions across services.
|
|
1341
|
+
|
|
1342
|
+
```typescript
|
|
1343
|
+
class OrderSaga {
|
|
1344
|
+
async execute(orderId: string): Promise<void> {
|
|
1345
|
+
try {
|
|
1346
|
+
await this.paymentService.charge(orderId);
|
|
1347
|
+
await this.inventoryService.reserve(orderId);
|
|
1348
|
+
await this.shippingService.schedule(orderId);
|
|
1349
|
+
} catch (error) {
|
|
1350
|
+
await this.compensate(orderId, error);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
private async compensate(orderId: string, error: Error): Promise<void> {
|
|
1355
|
+
await this.shippingService.cancel(orderId);
|
|
1356
|
+
await this.inventoryService.release(orderId);
|
|
1357
|
+
await this.paymentService.refund(orderId);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
```
|
|
1361
|
+
|
|
1362
|
+
## Event Versioning
|
|
1363
|
+
|
|
1364
|
+
Handle schema changes gracefully.
|
|
1365
|
+
|
|
1366
|
+
```typescript
|
|
1367
|
+
interface EventUpgrader {
|
|
1368
|
+
upgrade(event: Event): Event;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
class OrderPlacedV1ToV2 implements EventUpgrader {
|
|
1372
|
+
upgrade(event: Event): Event {
|
|
1373
|
+
const oldData = event.data as OrderPlacedV1Data;
|
|
1374
|
+
return {
|
|
1375
|
+
...event,
|
|
1376
|
+
type: 'OrderPlaced',
|
|
1377
|
+
version: 2,
|
|
1378
|
+
data: {
|
|
1379
|
+
...oldData,
|
|
1380
|
+
currency: 'USD' // New field with default
|
|
1381
|
+
}
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
```
|
|
1386
|
+
|
|
1387
|
+
## Best Practices
|
|
1388
|
+
|
|
1389
|
+
- Events are immutable facts
|
|
1390
|
+
- Include enough context in events for consumers
|
|
1391
|
+
- Version events from the start
|
|
1392
|
+
- Use idempotent event handlers
|
|
1393
|
+
- Design for eventual consistency
|
|
1394
|
+
- Consider snapshots for aggregates with many events
|
|
1395
|
+
|
|
1396
|
+
|
|
1397
|
+
---
|
|
1398
|
+
|
|
1399
|
+
# Event-Driven Messaging
|
|
1400
|
+
|
|
1401
|
+
## Message Types
|
|
1402
|
+
|
|
1403
|
+
### Commands
|
|
1404
|
+
Request to perform an action. Directed to a single handler.
|
|
1405
|
+
|
|
1406
|
+
```typescript
|
|
1407
|
+
interface CreateOrderCommand {
|
|
1408
|
+
type: 'CreateOrder';
|
|
1409
|
+
orderId: string;
|
|
1410
|
+
customerId: string;
|
|
1411
|
+
items: OrderItem[];
|
|
1412
|
+
timestamp: Date;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
// Single handler processes the command
|
|
1416
|
+
class CreateOrderHandler {
|
|
1417
|
+
async handle(command: CreateOrderCommand): Promise<void> {
|
|
1418
|
+
const order = Order.create(command);
|
|
1419
|
+
await this.repository.save(order);
|
|
1420
|
+
await this.eventBus.publish(new OrderCreatedEvent(order));
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
```
|
|
1424
|
+
|
|
1425
|
+
### Events
|
|
1426
|
+
Notification that something happened. Published to multiple subscribers.
|
|
1427
|
+
|
|
1428
|
+
```typescript
|
|
1429
|
+
interface OrderCreatedEvent {
|
|
1430
|
+
type: 'OrderCreated';
|
|
1431
|
+
orderId: string;
|
|
1432
|
+
customerId: string;
|
|
1433
|
+
totalAmount: number;
|
|
1434
|
+
occurredAt: Date;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// Multiple handlers can subscribe
|
|
1438
|
+
class InventoryService {
|
|
1439
|
+
@Subscribe('OrderCreated')
|
|
1440
|
+
async onOrderCreated(event: OrderCreatedEvent): Promise<void> {
|
|
1441
|
+
await this.reserveInventory(event.orderId);
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
class NotificationService {
|
|
1446
|
+
@Subscribe('OrderCreated')
|
|
1447
|
+
async onOrderCreated(event: OrderCreatedEvent): Promise<void> {
|
|
1448
|
+
await this.sendConfirmation(event.customerId);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
```
|
|
1452
|
+
|
|
1453
|
+
### Queries
|
|
1454
|
+
Request for data. Returns a response.
|
|
1455
|
+
|
|
1456
|
+
```typescript
|
|
1457
|
+
interface GetOrderQuery {
|
|
1458
|
+
type: 'GetOrder';
|
|
1459
|
+
orderId: string;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
class GetOrderHandler {
|
|
1463
|
+
async handle(query: GetOrderQuery): Promise<Order> {
|
|
1464
|
+
return this.repository.findById(query.orderId);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
```
|
|
1468
|
+
|
|
1469
|
+
## Message Bus Patterns
|
|
1470
|
+
|
|
1471
|
+
### In-Memory Bus
|
|
1472
|
+
|
|
1473
|
+
```typescript
|
|
1474
|
+
class EventBus {
|
|
1475
|
+
private handlers = new Map<string, Function[]>();
|
|
1476
|
+
|
|
1477
|
+
subscribe(eventType: string, handler: Function): void {
|
|
1478
|
+
const handlers = this.handlers.get(eventType) || [];
|
|
1479
|
+
handlers.push(handler);
|
|
1480
|
+
this.handlers.set(eventType, handlers);
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
async publish(event: Event): Promise<void> {
|
|
1484
|
+
const handlers = this.handlers.get(event.type) || [];
|
|
1485
|
+
await Promise.all(handlers.map(h => h(event)));
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
```
|
|
1489
|
+
|
|
1490
|
+
### Message Queue Integration
|
|
1491
|
+
|
|
1492
|
+
```typescript
|
|
1493
|
+
// RabbitMQ example
|
|
1494
|
+
class RabbitMQPublisher {
|
|
1495
|
+
async publish(event: Event): Promise<void> {
|
|
1496
|
+
const message = JSON.stringify({
|
|
1497
|
+
type: event.type,
|
|
1498
|
+
data: event,
|
|
1499
|
+
metadata: {
|
|
1500
|
+
correlationId: uuid(),
|
|
1501
|
+
timestamp: new Date().toISOString()
|
|
1502
|
+
}
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
await this.channel.publish(
|
|
1506
|
+
'events',
|
|
1507
|
+
event.type,
|
|
1508
|
+
Buffer.from(message),
|
|
1509
|
+
{ persistent: true }
|
|
1510
|
+
);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
class RabbitMQConsumer {
|
|
1515
|
+
async consume(queue: string, handler: EventHandler): Promise<void> {
|
|
1516
|
+
await this.channel.consume(queue, async (msg) => {
|
|
1517
|
+
if (!msg) return;
|
|
1518
|
+
|
|
1519
|
+
try {
|
|
1520
|
+
const event = JSON.parse(msg.content.toString());
|
|
1521
|
+
await handler.handle(event);
|
|
1522
|
+
this.channel.ack(msg);
|
|
1523
|
+
} catch (error) {
|
|
1524
|
+
this.channel.nack(msg, false, true); // Requeue
|
|
1525
|
+
}
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
```
|
|
1530
|
+
|
|
1531
|
+
## Delivery Guarantees
|
|
1532
|
+
|
|
1533
|
+
### At-Least-Once Delivery
|
|
1534
|
+
|
|
1535
|
+
```typescript
|
|
1536
|
+
// Producer: persist before publish
|
|
1537
|
+
async function publishWithRetry(event: Event): Promise<void> {
|
|
1538
|
+
// 1. Save to outbox
|
|
1539
|
+
await db.insert('outbox', {
|
|
1540
|
+
id: event.id,
|
|
1541
|
+
type: event.type,
|
|
1542
|
+
payload: JSON.stringify(event),
|
|
1543
|
+
status: 'pending'
|
|
1544
|
+
});
|
|
1545
|
+
|
|
1546
|
+
// 2. Publish (may fail)
|
|
1547
|
+
try {
|
|
1548
|
+
await messageBus.publish(event);
|
|
1549
|
+
await db.update('outbox', event.id, { status: 'sent' });
|
|
1550
|
+
} catch {
|
|
1551
|
+
// Retry worker will pick it up
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
// Consumer: idempotent handling
|
|
1556
|
+
async function handleIdempotent(event: Event): Promise<void> {
|
|
1557
|
+
const processed = await db.findOne('processed_events', event.id);
|
|
1558
|
+
if (processed) return; // Already handled
|
|
1559
|
+
|
|
1560
|
+
await handleEvent(event);
|
|
1561
|
+
await db.insert('processed_events', { id: event.id });
|
|
1562
|
+
}
|
|
1563
|
+
```
|
|
1564
|
+
|
|
1565
|
+
### Outbox Pattern
|
|
1566
|
+
|
|
1567
|
+
```typescript
|
|
1568
|
+
// Transaction includes outbox write
|
|
1569
|
+
async function createOrder(data: OrderData): Promise<Order> {
|
|
1570
|
+
return await db.transaction(async (tx) => {
|
|
1571
|
+
// 1. Business logic
|
|
1572
|
+
const order = Order.create(data);
|
|
1573
|
+
await tx.insert('orders', order);
|
|
1574
|
+
|
|
1575
|
+
// 2. Outbox entry (same transaction)
|
|
1576
|
+
await tx.insert('outbox', {
|
|
1577
|
+
id: uuid(),
|
|
1578
|
+
aggregateId: order.id,
|
|
1579
|
+
type: 'OrderCreated',
|
|
1580
|
+
payload: JSON.stringify(order)
|
|
1581
|
+
});
|
|
1582
|
+
|
|
1583
|
+
return order;
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
// Separate process polls and publishes
|
|
1588
|
+
async function processOutbox(): Promise<void> {
|
|
1589
|
+
const pending = await db.query(
|
|
1590
|
+
'SELECT * FROM outbox WHERE status = $1 ORDER BY created_at LIMIT 100',
|
|
1591
|
+
['pending']
|
|
1592
|
+
);
|
|
1593
|
+
|
|
1594
|
+
for (const entry of pending) {
|
|
1595
|
+
await messageBus.publish(JSON.parse(entry.payload));
|
|
1596
|
+
await db.update('outbox', entry.id, { status: 'sent' });
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
```
|
|
1600
|
+
|
|
1601
|
+
## Dead Letter Queues
|
|
1602
|
+
|
|
1603
|
+
```typescript
|
|
1604
|
+
class DeadLetterHandler {
|
|
1605
|
+
maxRetries = 3;
|
|
1606
|
+
|
|
1607
|
+
async handleFailure(message: Message, error: Error): Promise<void> {
|
|
1608
|
+
const retryCount = message.metadata.retryCount || 0;
|
|
1609
|
+
|
|
1610
|
+
if (retryCount < this.maxRetries) {
|
|
1611
|
+
// Retry with backoff
|
|
1612
|
+
await this.scheduleRetry(message, retryCount + 1);
|
|
1613
|
+
} else {
|
|
1614
|
+
// Move to DLQ
|
|
1615
|
+
await this.moveToDLQ(message, error);
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
async moveToDLQ(message: Message, error: Error): Promise<void> {
|
|
1620
|
+
await this.dlqChannel.publish('dead-letter', {
|
|
1621
|
+
originalMessage: message,
|
|
1622
|
+
error: error.message,
|
|
1623
|
+
failedAt: new Date()
|
|
1624
|
+
});
|
|
1625
|
+
|
|
1626
|
+
// Alert operations
|
|
1627
|
+
await this.alerting.notify('Message moved to DLQ', { message, error });
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
```
|
|
1631
|
+
|
|
1632
|
+
## Best Practices
|
|
1633
|
+
|
|
1634
|
+
- Use correlation IDs to trace message flows
|
|
1635
|
+
- Make consumers idempotent
|
|
1636
|
+
- Use dead letter queues for failed messages
|
|
1637
|
+
- Monitor queue depths and consumer lag
|
|
1638
|
+
- Design for eventual consistency
|
|
1639
|
+
- Version your message schemas
|
|
1640
|
+
- Include metadata (timestamp, correlationId, causationId)
|
|
1641
|
+
|
|
1642
|
+
|
|
1643
|
+
---
|
|
1644
|
+
|
|
1645
|
+
# Layered Architecture
|
|
1646
|
+
|
|
1647
|
+
## Layer Structure
|
|
1648
|
+
|
|
1649
|
+
```
|
|
1650
|
+
┌─────────────────────────────────────┐
|
|
1651
|
+
│ Presentation Layer │
|
|
1652
|
+
│ (Controllers, Views, APIs) │
|
|
1653
|
+
└───────────────┬─────────────────────┘
|
|
1654
|
+
│
|
|
1655
|
+
┌───────────────▼─────────────────────┐
|
|
1656
|
+
│ Domain Layer │
|
|
1657
|
+
│ (Business Logic, Services) │
|
|
1658
|
+
└───────────────┬─────────────────────┘
|
|
1659
|
+
│
|
|
1660
|
+
┌───────────────▼─────────────────────┐
|
|
1661
|
+
│ Data Access Layer │
|
|
1662
|
+
│ (Repositories, ORM, DAOs) │
|
|
1663
|
+
└─────────────────────────────────────┘
|
|
1664
|
+
```
|
|
1665
|
+
|
|
1666
|
+
## Presentation Layer
|
|
1667
|
+
|
|
1668
|
+
Handles user interaction and HTTP requests.
|
|
1669
|
+
|
|
1670
|
+
```typescript
|
|
1671
|
+
class OrderController {
|
|
1672
|
+
constructor(private orderService: OrderService) {}
|
|
1673
|
+
|
|
1674
|
+
async createOrder(req: Request, res: Response): Promise<void> {
|
|
1675
|
+
const dto = req.body as CreateOrderDTO;
|
|
1676
|
+
const result = await this.orderService.createOrder(dto);
|
|
1677
|
+
res.status(201).json(result);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
```
|
|
1681
|
+
|
|
1682
|
+
## Domain Layer
|
|
1683
|
+
|
|
1684
|
+
Contains business logic and rules.
|
|
1685
|
+
|
|
1686
|
+
```typescript
|
|
1687
|
+
class OrderService {
|
|
1688
|
+
constructor(
|
|
1689
|
+
private orderRepository: OrderRepository,
|
|
1690
|
+
private productRepository: ProductRepository
|
|
1691
|
+
) {}
|
|
1692
|
+
|
|
1693
|
+
async createOrder(dto: CreateOrderDTO): Promise<Order> {
|
|
1694
|
+
const products = await this.productRepository.findByIds(dto.productIds);
|
|
1695
|
+
|
|
1696
|
+
if (products.length !== dto.productIds.length) {
|
|
1697
|
+
throw new ProductNotFoundError();
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
const order = new Order(dto.customerId, products);
|
|
1701
|
+
order.calculateTotal();
|
|
1702
|
+
|
|
1703
|
+
await this.orderRepository.save(order);
|
|
1704
|
+
return order;
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
```
|
|
1708
|
+
|
|
1709
|
+
## Data Access Layer
|
|
1710
|
+
|
|
1711
|
+
Handles persistence operations.
|
|
1712
|
+
|
|
1713
|
+
```typescript
|
|
1714
|
+
class OrderRepository {
|
|
1715
|
+
constructor(private db: Database) {}
|
|
1716
|
+
|
|
1717
|
+
async save(order: Order): Promise<void> {
|
|
1718
|
+
await this.db.query(
|
|
1719
|
+
'INSERT INTO orders (id, customer_id, total) VALUES ($1, $2, $3)',
|
|
1720
|
+
[order.id, order.customerId, order.total]
|
|
1721
|
+
);
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
async findById(id: string): Promise<Order | null> {
|
|
1725
|
+
const row = await this.db.queryOne('SELECT * FROM orders WHERE id = $1', [id]);
|
|
1726
|
+
return row ? this.mapToOrder(row) : null;
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
```
|
|
1730
|
+
|
|
1731
|
+
## Layer Rules
|
|
1732
|
+
|
|
1733
|
+
1. Upper layers depend on lower layers
|
|
1734
|
+
2. Never skip layers
|
|
1735
|
+
3. Each layer exposes interfaces to the layer above
|
|
1736
|
+
4. Domain layer should not depend on data access implementation
|
|
1737
|
+
|
|
1738
|
+
## Best Practices
|
|
1739
|
+
|
|
1740
|
+
- Keep layers focused on their responsibility
|
|
1741
|
+
- Use DTOs to transfer data between layers
|
|
1742
|
+
- Define interfaces in domain layer, implement in data access
|
|
1743
|
+
- Avoid business logic in presentation or data access layers
|
|
1744
|
+
- Consider dependency inversion for testability
|
|
1745
|
+
|
|
1746
|
+
|
|
1747
|
+
---
|
|
1748
|
+
|
|
1749
|
+
# Serverless Architecture
|
|
1750
|
+
|
|
1751
|
+
## Key Principles
|
|
1752
|
+
|
|
1753
|
+
- **Stateless functions**: Each invocation is independent
|
|
1754
|
+
- **Event-driven**: Functions triggered by events
|
|
1755
|
+
- **Auto-scaling**: Platform handles scaling
|
|
1756
|
+
- **Pay-per-use**: Billed by execution
|
|
1757
|
+
|
|
1758
|
+
## Function Design
|
|
1759
|
+
|
|
1760
|
+
```typescript
|
|
1761
|
+
// Handler pattern
|
|
1762
|
+
export async function handler(
|
|
1763
|
+
event: APIGatewayEvent,
|
|
1764
|
+
context: Context
|
|
1765
|
+
): Promise<APIGatewayProxyResult> {
|
|
1766
|
+
try {
|
|
1767
|
+
const body = JSON.parse(event.body || '{}');
|
|
1768
|
+
const result = await processOrder(body);
|
|
1769
|
+
|
|
1770
|
+
return {
|
|
1771
|
+
statusCode: 200,
|
|
1772
|
+
body: JSON.stringify(result)
|
|
1773
|
+
};
|
|
1774
|
+
} catch (error) {
|
|
1775
|
+
return {
|
|
1776
|
+
statusCode: 500,
|
|
1777
|
+
body: JSON.stringify({ error: 'Internal error' })
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
```
|
|
1782
|
+
|
|
1783
|
+
## Cold Start Optimization
|
|
1784
|
+
|
|
1785
|
+
```typescript
|
|
1786
|
+
// Initialize outside handler (reused across invocations)
|
|
1787
|
+
const dbPool = createPool(process.env.DATABASE_URL);
|
|
1788
|
+
|
|
1789
|
+
export async function handler(event: Event): Promise<Response> {
|
|
1790
|
+
// Use cached connection
|
|
1791
|
+
const result = await dbPool.query('SELECT * FROM orders');
|
|
1792
|
+
return { statusCode: 200, body: JSON.stringify(result) };
|
|
1793
|
+
}
|
|
1794
|
+
```
|
|
1795
|
+
|
|
1796
|
+
## State Management
|
|
1797
|
+
|
|
1798
|
+
```typescript
|
|
1799
|
+
// Use external state stores
|
|
1800
|
+
class OrderService {
|
|
1801
|
+
constructor(
|
|
1802
|
+
private dynamodb: DynamoDB,
|
|
1803
|
+
private redis: Redis
|
|
1804
|
+
) {}
|
|
1805
|
+
|
|
1806
|
+
async getOrder(id: string): Promise<Order> {
|
|
1807
|
+
// Check cache first
|
|
1808
|
+
const cached = await this.redis.get(`order:${id}`);
|
|
1809
|
+
if (cached) return JSON.parse(cached);
|
|
1810
|
+
|
|
1811
|
+
// Fall back to database
|
|
1812
|
+
const result = await this.dynamodb.get({ Key: { id } });
|
|
1813
|
+
await this.redis.set(`order:${id}`, JSON.stringify(result));
|
|
1814
|
+
return result;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
```
|
|
1818
|
+
|
|
1819
|
+
## Best Practices
|
|
1820
|
+
|
|
1821
|
+
- Keep functions small and focused
|
|
1822
|
+
- Use environment variables for configuration
|
|
1823
|
+
- Minimize dependencies to reduce cold start time
|
|
1824
|
+
- Handle timeouts gracefully
|
|
1825
|
+
- Use async/await for all I/O operations
|
|
1826
|
+
- Implement idempotency for event handlers
|
|
1827
|
+
- Log structured data for observability
|
|
1828
|
+
- Set appropriate memory and timeout limits
|
|
1829
|
+
|
|
1830
|
+
|
|
1831
|
+
---
|
|
1832
|
+
|
|
1833
|
+
# Serverless Best Practices
|
|
1834
|
+
|
|
1835
|
+
## Function Design
|
|
1836
|
+
|
|
1837
|
+
### Single Responsibility
|
|
1838
|
+
|
|
1839
|
+
```typescript
|
|
1840
|
+
// ❌ Bad: Multiple responsibilities
|
|
1841
|
+
export const handler = async (event: APIGatewayEvent) => {
|
|
1842
|
+
if (event.path === '/users') {
|
|
1843
|
+
// Handle users
|
|
1844
|
+
} else if (event.path === '/orders') {
|
|
1845
|
+
// Handle orders
|
|
1846
|
+
} else if (event.path === '/products') {
|
|
1847
|
+
// Handle products
|
|
1848
|
+
}
|
|
1849
|
+
};
|
|
1850
|
+
|
|
1851
|
+
// ✅ Good: One function per responsibility
|
|
1852
|
+
// createUser.ts
|
|
1853
|
+
export const handler = async (event: APIGatewayEvent) => {
|
|
1854
|
+
const userData = JSON.parse(event.body);
|
|
1855
|
+
const user = await userService.create(userData);
|
|
1856
|
+
return { statusCode: 201, body: JSON.stringify(user) };
|
|
1857
|
+
};
|
|
1858
|
+
```
|
|
1859
|
+
|
|
1860
|
+
### Keep Functions Small
|
|
1861
|
+
|
|
1862
|
+
```typescript
|
|
1863
|
+
// ✅ Good: Small, focused function
|
|
1864
|
+
export const handler = async (event: SNSEvent) => {
|
|
1865
|
+
for (const record of event.Records) {
|
|
1866
|
+
const message = JSON.parse(record.Sns.Message);
|
|
1867
|
+
await processMessage(message);
|
|
1868
|
+
}
|
|
1869
|
+
};
|
|
1870
|
+
|
|
1871
|
+
// Extract business logic to separate module
|
|
1872
|
+
async function processMessage(message: OrderMessage): Promise<void> {
|
|
1873
|
+
const order = await orderService.process(message);
|
|
1874
|
+
await notificationService.sendConfirmation(order);
|
|
1875
|
+
}
|
|
1876
|
+
```
|
|
1877
|
+
|
|
1878
|
+
## Cold Start Optimization
|
|
1879
|
+
|
|
1880
|
+
### Minimize Dependencies
|
|
1881
|
+
|
|
1882
|
+
```typescript
|
|
1883
|
+
// ❌ Bad: Heavy imports at top level
|
|
1884
|
+
import * as AWS from 'aws-sdk';
|
|
1885
|
+
import moment from 'moment';
|
|
1886
|
+
import _ from 'lodash';
|
|
1887
|
+
|
|
1888
|
+
// ✅ Good: Import only what you need
|
|
1889
|
+
import { DynamoDB } from '@aws-sdk/client-dynamodb';
|
|
1890
|
+
|
|
1891
|
+
// ✅ Good: Lazy load optional dependencies
|
|
1892
|
+
let heavyLib: typeof import('heavy-lib') | undefined;
|
|
1893
|
+
|
|
1894
|
+
async function useHeavyFeature() {
|
|
1895
|
+
if (!heavyLib) {
|
|
1896
|
+
heavyLib = await import('heavy-lib');
|
|
1897
|
+
}
|
|
1898
|
+
return heavyLib.process();
|
|
1899
|
+
}
|
|
1900
|
+
```
|
|
1901
|
+
|
|
1902
|
+
### Initialize Outside Handler
|
|
1903
|
+
|
|
1904
|
+
```typescript
|
|
1905
|
+
// ✅ Good: Reuse connections across invocations
|
|
1906
|
+
import { DynamoDB } from '@aws-sdk/client-dynamodb';
|
|
1907
|
+
|
|
1908
|
+
// Created once, reused
|
|
1909
|
+
const dynamodb = new DynamoDB({});
|
|
1910
|
+
let cachedConnection: Connection | undefined;
|
|
1911
|
+
|
|
1912
|
+
export const handler = async (event: Event) => {
|
|
1913
|
+
// Reuse existing connection
|
|
1914
|
+
if (!cachedConnection) {
|
|
1915
|
+
cachedConnection = await createConnection();
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
return process(event, cachedConnection);
|
|
1919
|
+
};
|
|
1920
|
+
```
|
|
1921
|
+
|
|
1922
|
+
### Provisioned Concurrency
|
|
1923
|
+
|
|
1924
|
+
```yaml
|
|
1925
|
+
# serverless.yml
|
|
1926
|
+
functions:
|
|
1927
|
+
api:
|
|
1928
|
+
handler: handler.api
|
|
1929
|
+
provisionedConcurrency: 5 # Keep 5 instances warm
|
|
1930
|
+
```
|
|
1931
|
+
|
|
1932
|
+
## Error Handling
|
|
1933
|
+
|
|
1934
|
+
### Structured Error Responses
|
|
1935
|
+
|
|
1936
|
+
```typescript
|
|
1937
|
+
class LambdaError extends Error {
|
|
1938
|
+
constructor(
|
|
1939
|
+
message: string,
|
|
1940
|
+
public statusCode: number,
|
|
1941
|
+
public code: string
|
|
1942
|
+
) {
|
|
1943
|
+
super(message);
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
export const handler = async (event: APIGatewayEvent) => {
|
|
1948
|
+
try {
|
|
1949
|
+
const result = await processRequest(event);
|
|
1950
|
+
return {
|
|
1951
|
+
statusCode: 200,
|
|
1952
|
+
body: JSON.stringify(result)
|
|
1953
|
+
};
|
|
1954
|
+
} catch (error) {
|
|
1955
|
+
if (error instanceof LambdaError) {
|
|
1956
|
+
return {
|
|
1957
|
+
statusCode: error.statusCode,
|
|
1958
|
+
body: JSON.stringify({
|
|
1959
|
+
error: { code: error.code, message: error.message }
|
|
1960
|
+
})
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
console.error('Unexpected error:', error);
|
|
1965
|
+
return {
|
|
1966
|
+
statusCode: 500,
|
|
1967
|
+
body: JSON.stringify({
|
|
1968
|
+
error: { code: 'INTERNAL_ERROR', message: 'Internal server error' }
|
|
1969
|
+
})
|
|
1970
|
+
};
|
|
1971
|
+
}
|
|
1972
|
+
};
|
|
1973
|
+
```
|
|
1974
|
+
|
|
1975
|
+
### Retry and Dead Letter Queues
|
|
1976
|
+
|
|
1977
|
+
```yaml
|
|
1978
|
+
# CloudFormation
|
|
1979
|
+
Resources:
|
|
1980
|
+
MyFunction:
|
|
1981
|
+
Type: AWS::Lambda::Function
|
|
1982
|
+
Properties:
|
|
1983
|
+
DeadLetterConfig:
|
|
1984
|
+
TargetArn: !GetAtt DeadLetterQueue.Arn
|
|
1985
|
+
|
|
1986
|
+
DeadLetterQueue:
|
|
1987
|
+
Type: AWS::SQS::Queue
|
|
1988
|
+
Properties:
|
|
1989
|
+
QueueName: my-function-dlq
|
|
1990
|
+
```
|
|
1991
|
+
|
|
1992
|
+
## State Management
|
|
1993
|
+
|
|
1994
|
+
### Use External State Stores
|
|
1995
|
+
|
|
1996
|
+
```typescript
|
|
1997
|
+
// ❌ Bad: In-memory state (lost between invocations)
|
|
1998
|
+
let requestCount = 0;
|
|
1999
|
+
|
|
2000
|
+
export const handler = async () => {
|
|
2001
|
+
requestCount++; // Unreliable!
|
|
2002
|
+
};
|
|
2003
|
+
|
|
2004
|
+
// ✅ Good: External state store
|
|
2005
|
+
import { DynamoDB } from '@aws-sdk/client-dynamodb';
|
|
2006
|
+
|
|
2007
|
+
const dynamodb = new DynamoDB({});
|
|
2008
|
+
|
|
2009
|
+
export const handler = async (event: Event) => {
|
|
2010
|
+
// Atomic counter in DynamoDB
|
|
2011
|
+
await dynamodb.updateItem({
|
|
2012
|
+
TableName: 'Counters',
|
|
2013
|
+
Key: { id: { S: 'requests' } },
|
|
2014
|
+
UpdateExpression: 'ADD #count :inc',
|
|
2015
|
+
ExpressionAttributeNames: { '#count': 'count' },
|
|
2016
|
+
ExpressionAttributeValues: { ':inc': { N: '1' } }
|
|
2017
|
+
});
|
|
2018
|
+
};
|
|
2019
|
+
```
|
|
2020
|
+
|
|
2021
|
+
### Step Functions for Workflows
|
|
2022
|
+
|
|
2023
|
+
```yaml
|
|
2024
|
+
# Step Functions state machine
|
|
2025
|
+
StartAt: ValidateOrder
|
|
2026
|
+
States:
|
|
2027
|
+
ValidateOrder:
|
|
2028
|
+
Type: Task
|
|
2029
|
+
Resource: arn:aws:lambda:...:validateOrder
|
|
2030
|
+
Next: ProcessPayment
|
|
2031
|
+
|
|
2032
|
+
ProcessPayment:
|
|
2033
|
+
Type: Task
|
|
2034
|
+
Resource: arn:aws:lambda:...:processPayment
|
|
2035
|
+
Catch:
|
|
2036
|
+
- ErrorEquals: [PaymentFailed]
|
|
2037
|
+
Next: NotifyFailure
|
|
2038
|
+
Next: FulfillOrder
|
|
2039
|
+
|
|
2040
|
+
FulfillOrder:
|
|
2041
|
+
Type: Task
|
|
2042
|
+
Resource: arn:aws:lambda:...:fulfillOrder
|
|
2043
|
+
End: true
|
|
2044
|
+
|
|
2045
|
+
NotifyFailure:
|
|
2046
|
+
Type: Task
|
|
2047
|
+
Resource: arn:aws:lambda:...:notifyFailure
|
|
2048
|
+
End: true
|
|
2049
|
+
```
|
|
2050
|
+
|
|
2051
|
+
## Security
|
|
2052
|
+
|
|
2053
|
+
### Least Privilege IAM
|
|
2054
|
+
|
|
2055
|
+
```yaml
|
|
2056
|
+
# serverless.yml
|
|
2057
|
+
provider:
|
|
2058
|
+
iam:
|
|
2059
|
+
role:
|
|
2060
|
+
statements:
|
|
2061
|
+
# Only the permissions needed
|
|
2062
|
+
- Effect: Allow
|
|
2063
|
+
Action:
|
|
2064
|
+
- dynamodb:GetItem
|
|
2065
|
+
- dynamodb:PutItem
|
|
2066
|
+
Resource: arn:aws:dynamodb:*:*:table/Users
|
|
2067
|
+
|
|
2068
|
+
- Effect: Allow
|
|
2069
|
+
Action:
|
|
2070
|
+
- s3:GetObject
|
|
2071
|
+
Resource: arn:aws:s3:::my-bucket/*
|
|
2072
|
+
```
|
|
2073
|
+
|
|
2074
|
+
### Secrets Management
|
|
2075
|
+
|
|
2076
|
+
```typescript
|
|
2077
|
+
import { SecretsManager } from '@aws-sdk/client-secrets-manager';
|
|
2078
|
+
|
|
2079
|
+
const secretsManager = new SecretsManager({});
|
|
2080
|
+
let cachedSecret: string | undefined;
|
|
2081
|
+
|
|
2082
|
+
async function getSecret(): Promise<string> {
|
|
2083
|
+
if (!cachedSecret) {
|
|
2084
|
+
const response = await secretsManager.getSecretValue({
|
|
2085
|
+
SecretId: 'my-api-key'
|
|
2086
|
+
});
|
|
2087
|
+
cachedSecret = response.SecretString;
|
|
2088
|
+
}
|
|
2089
|
+
return cachedSecret!;
|
|
2090
|
+
}
|
|
2091
|
+
```
|
|
2092
|
+
|
|
2093
|
+
## Monitoring and Observability
|
|
2094
|
+
|
|
2095
|
+
### Structured Logging
|
|
2096
|
+
|
|
2097
|
+
```typescript
|
|
2098
|
+
import { Logger } from '@aws-lambda-powertools/logger';
|
|
2099
|
+
|
|
2100
|
+
const logger = new Logger({
|
|
2101
|
+
serviceName: 'order-service',
|
|
2102
|
+
logLevel: 'INFO'
|
|
2103
|
+
});
|
|
2104
|
+
|
|
2105
|
+
export const handler = async (event: Event, context: Context) => {
|
|
2106
|
+
logger.addContext(context);
|
|
2107
|
+
|
|
2108
|
+
logger.info('Processing order', {
|
|
2109
|
+
orderId: event.orderId,
|
|
2110
|
+
customerId: event.customerId
|
|
2111
|
+
});
|
|
2112
|
+
|
|
2113
|
+
try {
|
|
2114
|
+
const result = await processOrder(event);
|
|
2115
|
+
logger.info('Order processed', { orderId: event.orderId });
|
|
2116
|
+
return result;
|
|
2117
|
+
} catch (error) {
|
|
2118
|
+
logger.error('Order processing failed', { error, event });
|
|
2119
|
+
throw error;
|
|
2120
|
+
}
|
|
2121
|
+
};
|
|
2122
|
+
```
|
|
2123
|
+
|
|
2124
|
+
### Tracing
|
|
2125
|
+
|
|
2126
|
+
```typescript
|
|
2127
|
+
import { Tracer } from '@aws-lambda-powertools/tracer';
|
|
2128
|
+
|
|
2129
|
+
const tracer = new Tracer({ serviceName: 'order-service' });
|
|
2130
|
+
|
|
2131
|
+
export const handler = async (event: Event) => {
|
|
2132
|
+
const segment = tracer.getSegment();
|
|
2133
|
+
const subsegment = segment.addNewSubsegment('ProcessOrder');
|
|
2134
|
+
|
|
2135
|
+
try {
|
|
2136
|
+
const result = await processOrder(event);
|
|
2137
|
+
subsegment.close();
|
|
2138
|
+
return result;
|
|
2139
|
+
} catch (error) {
|
|
2140
|
+
subsegment.addError(error);
|
|
2141
|
+
subsegment.close();
|
|
2142
|
+
throw error;
|
|
2143
|
+
}
|
|
2144
|
+
};
|
|
2145
|
+
```
|
|
2146
|
+
|
|
2147
|
+
## Cost Optimization
|
|
2148
|
+
|
|
2149
|
+
- Set appropriate memory (more memory = faster CPU)
|
|
2150
|
+
- Use ARM architecture when possible (cheaper)
|
|
2151
|
+
- Batch operations to reduce invocations
|
|
2152
|
+
- Use reserved concurrency to limit costs
|
|
2153
|
+
- Monitor and alert on spending
|
|
2154
|
+
- Clean up unused functions and versions
|
|
2155
|
+
|
|
2156
|
+
|
|
2157
|
+
---
|
|
2158
|
+
|
|
2159
|
+
# Hexagonal Architecture (Ports & Adapters)
|
|
2160
|
+
|
|
2161
|
+
## Core Principle
|
|
2162
|
+
|
|
2163
|
+
The application core (domain logic) is isolated from external concerns through ports (interfaces) and adapters (implementations).
|
|
2164
|
+
|
|
2165
|
+
## Structure
|
|
2166
|
+
|
|
2167
|
+
```
|
|
2168
|
+
src/
|
|
2169
|
+
├── domain/ # Pure business logic, no external dependencies
|
|
2170
|
+
│ ├── models/ # Domain entities and value objects
|
|
2171
|
+
│ ├── services/ # Domain services
|
|
2172
|
+
│ └── ports/ # Interface definitions (driven & driving)
|
|
2173
|
+
├── application/ # Use cases, orchestration
|
|
2174
|
+
│ └── services/ # Application services
|
|
2175
|
+
├── adapters/
|
|
2176
|
+
│ ├── primary/ # Driving adapters (controllers, CLI, events)
|
|
2177
|
+
│ │ ├── http/
|
|
2178
|
+
│ │ ├── grpc/
|
|
2179
|
+
│ │ └── cli/
|
|
2180
|
+
│ └── secondary/ # Driven adapters (repositories, clients)
|
|
2181
|
+
│ ├── persistence/
|
|
2182
|
+
│ ├── messaging/
|
|
2183
|
+
│ └── external-apis/
|
|
2184
|
+
└── config/ # Dependency injection, configuration
|
|
2185
|
+
```
|
|
2186
|
+
|
|
2187
|
+
## Port Types
|
|
2188
|
+
|
|
2189
|
+
### Driving Ports (Primary)
|
|
2190
|
+
Interfaces that the application exposes to the outside world:
|
|
2191
|
+
|
|
2192
|
+
```typescript
|
|
2193
|
+
// domain/ports/driving/user-service.port.ts
|
|
2194
|
+
export interface UserServicePort {
|
|
2195
|
+
createUser(data: CreateUserDTO): Promise<User>;
|
|
2196
|
+
getUser(id: string): Promise<User | null>;
|
|
2197
|
+
updateUser(id: string, data: UpdateUserDTO): Promise<User>;
|
|
2198
|
+
}
|
|
2199
|
+
```
|
|
2200
|
+
|
|
2201
|
+
### Driven Ports (Secondary)
|
|
2202
|
+
Interfaces that the application needs from the outside world:
|
|
2203
|
+
|
|
2204
|
+
```typescript
|
|
2205
|
+
// domain/ports/driven/user-repository.port.ts
|
|
2206
|
+
export interface UserRepositoryPort {
|
|
2207
|
+
save(user: User): Promise<void>;
|
|
2208
|
+
findById(id: string): Promise<User | null>;
|
|
2209
|
+
findByEmail(email: string): Promise<User | null>;
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
// domain/ports/driven/email-sender.port.ts
|
|
2213
|
+
export interface EmailSenderPort {
|
|
2214
|
+
send(to: string, subject: string, body: string): Promise<void>;
|
|
2215
|
+
}
|
|
2216
|
+
```
|
|
2217
|
+
|
|
2218
|
+
## Adapter Implementation
|
|
2219
|
+
|
|
2220
|
+
### Primary Adapter (HTTP Controller)
|
|
2221
|
+
|
|
2222
|
+
```typescript
|
|
2223
|
+
// adapters/primary/http/user.controller.ts
|
|
2224
|
+
export class UserController {
|
|
2225
|
+
constructor(private userService: UserServicePort) {}
|
|
2226
|
+
|
|
2227
|
+
async create(req: Request, res: Response) {
|
|
2228
|
+
const user = await this.userService.createUser(req.body);
|
|
2229
|
+
res.status(201).json(user);
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
```
|
|
2233
|
+
|
|
2234
|
+
### Secondary Adapter (Repository)
|
|
2235
|
+
|
|
2236
|
+
```typescript
|
|
2237
|
+
// adapters/secondary/persistence/postgres-user.repository.ts
|
|
2238
|
+
export class PostgresUserRepository implements UserRepositoryPort {
|
|
2239
|
+
constructor(private db: DatabaseConnection) {}
|
|
2240
|
+
|
|
2241
|
+
async save(user: User): Promise<void> {
|
|
2242
|
+
await this.db.query('INSERT INTO users...', user);
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
async findById(id: string): Promise<User | null> {
|
|
2246
|
+
const row = await this.db.query('SELECT * FROM users WHERE id = $1', [id]);
|
|
2247
|
+
return row ? this.toDomain(row) : null;
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
```
|
|
2251
|
+
|
|
2252
|
+
## Dependency Rule
|
|
2253
|
+
|
|
2254
|
+
Dependencies always point inward:
|
|
2255
|
+
- Adapters depend on Ports
|
|
2256
|
+
- Application depends on Domain
|
|
2257
|
+
- Domain has no external dependencies
|
|
2258
|
+
|
|
2259
|
+
```
|
|
2260
|
+
[External World] → [Adapters] → [Ports] → [Domain]
|
|
2261
|
+
```
|
|
2262
|
+
|
|
2263
|
+
## Testing Benefits
|
|
2264
|
+
|
|
2265
|
+
```typescript
|
|
2266
|
+
// Test with mock adapters
|
|
2267
|
+
class InMemoryUserRepository implements UserRepositoryPort {
|
|
2268
|
+
private users = new Map<string, User>();
|
|
2269
|
+
|
|
2270
|
+
async save(user: User) { this.users.set(user.id, user); }
|
|
2271
|
+
async findById(id: string) { return this.users.get(id) || null; }
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
// Domain logic tested without infrastructure
|
|
2275
|
+
describe('UserService', () => {
|
|
2276
|
+
it('creates user', async () => {
|
|
2277
|
+
const repo = new InMemoryUserRepository();
|
|
2278
|
+
const service = new UserService(repo);
|
|
2279
|
+
const user = await service.createUser({ name: 'Test' });
|
|
2280
|
+
expect(user.name).toBe('Test');
|
|
2281
|
+
});
|
|
2282
|
+
});
|
|
2283
|
+
```
|
|
2284
|
+
|
|
2285
|
+
## When to Use
|
|
2286
|
+
|
|
2287
|
+
- Applications needing multiple entry points (HTTP, CLI, events)
|
|
2288
|
+
- Systems requiring easy infrastructure swapping
|
|
2289
|
+
- Projects prioritizing testability
|
|
2290
|
+
- Long-lived applications expecting technology changes
|
|
2291
|
+
|
|
2292
|
+
|
|
2293
|
+
---
|
|
2294
|
+
|
|
2295
|
+
# GUI Architecture Patterns
|
|
2296
|
+
|
|
2297
|
+
## MVC (Model-View-Controller)
|
|
2298
|
+
|
|
2299
|
+
```typescript
|
|
2300
|
+
// Model - data and business logic
|
|
2301
|
+
class UserModel {
|
|
2302
|
+
private users: User[] = [];
|
|
2303
|
+
|
|
2304
|
+
getUsers(): User[] { return this.users; }
|
|
2305
|
+
addUser(user: User): void { this.users.push(user); }
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
// View - presentation
|
|
2309
|
+
class UserView {
|
|
2310
|
+
render(users: User[]): void {
|
|
2311
|
+
console.log('Users:', users);
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
// Controller - handles input, coordinates
|
|
2316
|
+
class UserController {
|
|
2317
|
+
constructor(
|
|
2318
|
+
private model: UserModel,
|
|
2319
|
+
private view: UserView
|
|
2320
|
+
) {}
|
|
2321
|
+
|
|
2322
|
+
handleAddUser(userData: UserData): void {
|
|
2323
|
+
const user = new User(userData);
|
|
2324
|
+
this.model.addUser(user);
|
|
2325
|
+
this.view.render(this.model.getUsers());
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
```
|
|
2329
|
+
|
|
2330
|
+
## MVP (Model-View-Presenter)
|
|
2331
|
+
|
|
2332
|
+
```typescript
|
|
2333
|
+
// View interface - defines what presenter can call
|
|
2334
|
+
interface UserView {
|
|
2335
|
+
showUsers(users: User[]): void;
|
|
2336
|
+
showError(message: string): void;
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
// Presenter - all presentation logic
|
|
2340
|
+
class UserPresenter {
|
|
2341
|
+
constructor(
|
|
2342
|
+
private view: UserView,
|
|
2343
|
+
private model: UserModel
|
|
2344
|
+
) {}
|
|
2345
|
+
|
|
2346
|
+
loadUsers(): void {
|
|
2347
|
+
try {
|
|
2348
|
+
const users = this.model.getUsers();
|
|
2349
|
+
this.view.showUsers(users);
|
|
2350
|
+
} catch (error) {
|
|
2351
|
+
this.view.showError('Failed to load users');
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
// View implementation - passive, no logic
|
|
2357
|
+
class UserListView implements UserView {
|
|
2358
|
+
showUsers(users: User[]): void { /* render list */ }
|
|
2359
|
+
showError(message: string): void { /* show error */ }
|
|
2360
|
+
}
|
|
2361
|
+
```
|
|
2362
|
+
|
|
2363
|
+
## MVVM (Model-View-ViewModel)
|
|
2364
|
+
|
|
2365
|
+
```typescript
|
|
2366
|
+
// ViewModel - exposes observable state
|
|
2367
|
+
class UserViewModel {
|
|
2368
|
+
users = observable<User[]>([]);
|
|
2369
|
+
isLoading = observable(false);
|
|
2370
|
+
|
|
2371
|
+
async loadUsers(): Promise<void> {
|
|
2372
|
+
this.isLoading.set(true);
|
|
2373
|
+
const users = await this.userService.getUsers();
|
|
2374
|
+
this.users.set(users);
|
|
2375
|
+
this.isLoading.set(false);
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
// View binds to ViewModel
|
|
2380
|
+
const UserList = observer(({ viewModel }: { viewModel: UserViewModel }) => (
|
|
2381
|
+
<div>
|
|
2382
|
+
{viewModel.isLoading.get() ? (
|
|
2383
|
+
<Spinner />
|
|
2384
|
+
) : (
|
|
2385
|
+
viewModel.users.get().map(user => <UserItem key={user.id} user={user} />)
|
|
2386
|
+
)}
|
|
2387
|
+
</div>
|
|
2388
|
+
));
|
|
2389
|
+
```
|
|
2390
|
+
|
|
2391
|
+
## Component Architecture (React/Vue)
|
|
2392
|
+
|
|
2393
|
+
```typescript
|
|
2394
|
+
// Presentational component - no state, just props
|
|
2395
|
+
const UserCard = ({ user, onDelete }: UserCardProps) => (
|
|
2396
|
+
<div className="user-card">
|
|
2397
|
+
<h3>{user.name}</h3>
|
|
2398
|
+
<button onClick={() => onDelete(user.id)}>Delete</button>
|
|
2399
|
+
</div>
|
|
2400
|
+
);
|
|
2401
|
+
|
|
2402
|
+
// Container component - manages state
|
|
2403
|
+
const UserListContainer = () => {
|
|
2404
|
+
const [users, setUsers] = useState<User[]>([]);
|
|
2405
|
+
|
|
2406
|
+
useEffect(() => {
|
|
2407
|
+
userService.getUsers().then(setUsers);
|
|
2408
|
+
}, []);
|
|
2409
|
+
|
|
2410
|
+
const handleDelete = (id: string) => {
|
|
2411
|
+
userService.deleteUser(id).then(() => {
|
|
2412
|
+
setUsers(users.filter(u => u.id !== id));
|
|
2413
|
+
});
|
|
2414
|
+
};
|
|
2415
|
+
|
|
2416
|
+
return <UserList users={users} onDelete={handleDelete} />;
|
|
2417
|
+
};
|
|
2418
|
+
```
|
|
2419
|
+
|
|
2420
|
+
## Best Practices
|
|
2421
|
+
|
|
2422
|
+
- Separate UI logic from business logic
|
|
2423
|
+
- Keep views as simple as possible
|
|
2424
|
+
- Use unidirectional data flow when possible
|
|
2425
|
+
- Make components reusable and testable
|
|
2426
|
+
- Choose pattern based on framework and team familiarity
|
|
2427
|
+
|
|
2428
|
+
|
|
2429
|
+
---
|
|
2430
|
+
|
|
2431
|
+
# Feature Toggles
|
|
2432
|
+
|
|
2433
|
+
## Toggle Types
|
|
2434
|
+
|
|
2435
|
+
### Release Toggles
|
|
2436
|
+
Hide incomplete features in production.
|
|
2437
|
+
|
|
2438
|
+
```typescript
|
|
2439
|
+
if (featureFlags.isEnabled('new-checkout')) {
|
|
2440
|
+
return <NewCheckout />;
|
|
2441
|
+
}
|
|
2442
|
+
return <LegacyCheckout />;
|
|
2443
|
+
```
|
|
2444
|
+
|
|
2445
|
+
### Experiment Toggles
|
|
2446
|
+
A/B testing and gradual rollouts.
|
|
2447
|
+
|
|
2448
|
+
```typescript
|
|
2449
|
+
const variant = featureFlags.getVariant('pricing-experiment', userId);
|
|
2450
|
+
if (variant === 'new-pricing') {
|
|
2451
|
+
return calculateNewPricing(cart);
|
|
2452
|
+
}
|
|
2453
|
+
return calculateLegacyPricing(cart);
|
|
2454
|
+
```
|
|
2455
|
+
|
|
2456
|
+
### Ops Toggles
|
|
2457
|
+
Runtime operational control.
|
|
2458
|
+
|
|
2459
|
+
```typescript
|
|
2460
|
+
if (featureFlags.isEnabled('enable-caching')) {
|
|
2461
|
+
return cache.get(key) || fetchFromDatabase(key);
|
|
2462
|
+
}
|
|
2463
|
+
return fetchFromDatabase(key);
|
|
2464
|
+
```
|
|
2465
|
+
|
|
2466
|
+
## Implementation
|
|
2467
|
+
|
|
2468
|
+
```typescript
|
|
2469
|
+
interface FeatureFlags {
|
|
2470
|
+
isEnabled(flag: string, context?: Context): boolean;
|
|
2471
|
+
getVariant(flag: string, userId: string): string;
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
class FeatureFlagService implements FeatureFlags {
|
|
2475
|
+
constructor(private config: Map<string, FlagConfig>) {}
|
|
2476
|
+
|
|
2477
|
+
isEnabled(flag: string, context?: Context): boolean {
|
|
2478
|
+
const config = this.config.get(flag);
|
|
2479
|
+
if (!config) return false;
|
|
2480
|
+
|
|
2481
|
+
if (config.percentage) {
|
|
2482
|
+
return this.isInPercentage(context?.userId, config.percentage);
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
return config.enabled;
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
private isInPercentage(userId: string | undefined, percentage: number): boolean {
|
|
2489
|
+
if (!userId) return false;
|
|
2490
|
+
const hash = this.hashUserId(userId);
|
|
2491
|
+
return (hash % 100) < percentage;
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
```
|
|
2495
|
+
|
|
2496
|
+
## Best Practices
|
|
2497
|
+
|
|
2498
|
+
- Remove toggles after feature is stable
|
|
2499
|
+
- Use clear naming conventions
|
|
2500
|
+
- Log toggle decisions for debugging
|
|
2501
|
+
- Test both toggle states
|
|
2502
|
+
- Limit number of active toggles
|
|
2503
|
+
- Document toggle purpose and expiration
|
|
2504
|
+
|
|
2505
|
+
|
|
2506
|
+
---
|
|
2507
|
+
*Generated by aicgen*
|