@girardmedia/bootspring 3.3.2 → 3.4.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/assets/agents/accessibility-auditor.md +39 -0
- package/assets/agents/api-designer.md +40 -0
- package/assets/agents/auth-implementer.md +64 -0
- package/assets/agents/bug-hunter.md +42 -0
- package/assets/agents/bundle-analyzer.md +40 -0
- package/assets/agents/cache-optimizer.md +55 -0
- package/assets/agents/changelog-writer.md +55 -0
- package/assets/agents/ci-cd-builder.md +40 -0
- package/assets/agents/code-explainer.md +39 -0
- package/assets/agents/code-reviewer.md +39 -0
- package/assets/agents/cost-optimizer.md +57 -0
- package/assets/agents/cron-scheduler.md +51 -0
- package/assets/agents/data-seeder.md +56 -0
- package/assets/agents/database-architect.md +40 -0
- package/assets/agents/dependency-updater.md +40 -0
- package/assets/agents/deploy-checker.md +40 -0
- package/assets/agents/docker-optimizer.md +40 -0
- package/assets/agents/documentation-writer.md +40 -0
- package/assets/agents/email-builder.md +55 -0
- package/assets/agents/env-setup.md +40 -0
- package/assets/agents/error-handler.md +40 -0
- package/assets/agents/eslint-fixer.md +46 -0
- package/assets/agents/feature-flagger.md +69 -0
- package/assets/agents/git-detective.md +39 -0
- package/assets/agents/graphql-builder.md +60 -0
- package/assets/agents/incident-responder.md +59 -0
- package/assets/agents/log-analyzer.md +39 -0
- package/assets/agents/migration-planner.md +41 -0
- package/assets/agents/monorepo-navigator.md +39 -0
- package/assets/agents/nextjs-expert.md +57 -0
- package/assets/agents/notification-builder.md +56 -0
- package/assets/agents/onboarding-guide.md +39 -0
- package/assets/agents/performance-profiler.md +40 -0
- package/assets/agents/prisma-expert.md +57 -0
- package/assets/agents/rate-limiter.md +58 -0
- package/assets/agents/react-expert.md +58 -0
- package/assets/agents/refactorer.md +42 -0
- package/assets/agents/regex-builder.md +46 -0
- package/assets/agents/release-manager.md +40 -0
- package/assets/agents/s3-manager.md +58 -0
- package/assets/agents/schema-validator.md +40 -0
- package/assets/agents/search-builder.md +62 -0
- package/assets/agents/security-auditor.md +39 -0
- package/assets/agents/sitemap-generator.md +53 -0
- package/assets/agents/stripe-integrator.md +59 -0
- package/assets/agents/tailwind-expert.md +55 -0
- package/assets/agents/tech-debt-tracker.md +39 -0
- package/assets/agents/test-writer.md +42 -0
- package/assets/agents/type-fixer.md +45 -0
- package/assets/agents/webhook-builder.md +54 -0
- package/assets/rules/cpp.md +53 -0
- package/assets/rules/css.md +52 -0
- package/assets/rules/go.md +50 -0
- package/assets/rules/html.md +52 -0
- package/assets/rules/java.md +51 -0
- package/assets/rules/kotlin.md +50 -0
- package/assets/rules/php.md +51 -0
- package/assets/rules/python.md +51 -0
- package/assets/rules/ruby.md +51 -0
- package/assets/rules/rust.md +49 -0
- package/assets/rules/shell.md +52 -0
- package/assets/rules/sql.md +49 -0
- package/assets/rules/swift.md +50 -0
- package/assets/rules/typescript.md +52 -0
- package/assets/rules/yaml-json.md +51 -0
- package/assets/skills/accessibility.md +210 -0
- package/assets/skills/agent-patterns.md +387 -0
- package/assets/skills/ai-integration.md +263 -0
- package/assets/skills/animation-patterns.md +224 -0
- package/assets/skills/api-design.md +218 -0
- package/assets/skills/api-gateway.md +341 -0
- package/assets/skills/api-versioning.md +226 -0
- package/assets/skills/astro-patterns.md +233 -0
- package/assets/skills/auth-patterns.md +248 -0
- package/assets/skills/aws-patterns.md +171 -0
- package/assets/skills/background-jobs.md +162 -0
- package/assets/skills/browser-extensions.md +309 -0
- package/assets/skills/caching-patterns.md +253 -0
- package/assets/skills/ci-cd.md +251 -0
- package/assets/skills/cli-development.md +296 -0
- package/assets/skills/code-review.md +185 -0
- package/assets/skills/cron-patterns.md +327 -0
- package/assets/skills/data-fetching.md +231 -0
- package/assets/skills/database-migrations.md +346 -0
- package/assets/skills/database-patterns.md +219 -0
- package/assets/skills/debugging.md +281 -0
- package/assets/skills/design-system.md +289 -0
- package/assets/skills/django-patterns.md +182 -0
- package/assets/skills/docker-patterns.md +235 -0
- package/assets/skills/e2e-testing.md +287 -0
- package/assets/skills/edge-computing.md +268 -0
- package/assets/skills/electron-patterns.md +266 -0
- package/assets/skills/email-templates.md +206 -0
- package/assets/skills/error-handling.md +265 -0
- package/assets/skills/event-driven.md +232 -0
- package/assets/skills/express-patterns.md +239 -0
- package/assets/skills/fastapi-patterns.md +198 -0
- package/assets/skills/feature-flags.md +212 -0
- package/assets/skills/figma-to-code.md +298 -0
- package/assets/skills/file-upload.md +228 -0
- package/assets/skills/forms-patterns.md +264 -0
- package/assets/skills/gcp-patterns.md +189 -0
- package/assets/skills/git-workflow.md +187 -0
- package/assets/skills/golang-patterns.md +185 -0
- package/assets/skills/graphql-patterns.md +244 -0
- package/assets/skills/i18n-patterns.md +172 -0
- package/assets/skills/image-processing.md +350 -0
- package/assets/skills/java-springboot.md +226 -0
- package/assets/skills/kotlin-patterns.md +207 -0
- package/assets/skills/kubernetes-patterns.md +326 -0
- package/assets/skills/laravel-patterns.md +261 -0
- package/assets/skills/llm-fine-tuning.md +335 -0
- package/assets/skills/load-testing.md +303 -0
- package/assets/skills/logging-observability.md +228 -0
- package/assets/skills/markdown-processing.md +318 -0
- package/assets/skills/mcp-server-patterns.md +292 -0
- package/assets/skills/microservices.md +272 -0
- package/assets/skills/migration-patterns.md +239 -0
- package/assets/skills/mongodb-patterns.md +189 -0
- package/assets/skills/monorepo-patterns.md +287 -0
- package/assets/skills/nextjs-app-router.md +237 -0
- package/assets/skills/notification-patterns.md +348 -0
- package/assets/skills/oauth-patterns.md +246 -0
- package/assets/skills/payment-integration.md +222 -0
- package/assets/skills/pdf-generation.md +307 -0
- package/assets/skills/performance-optimization.md +277 -0
- package/assets/skills/php-patterns.md +210 -0
- package/assets/skills/prisma-patterns.md +241 -0
- package/assets/skills/prompt-engineering.md +193 -0
- package/assets/skills/pwa-patterns.md +247 -0
- package/assets/skills/python-patterns.md +158 -0
- package/assets/skills/python-testing.md +172 -0
- package/assets/skills/queue-patterns.md +295 -0
- package/assets/skills/rag-patterns.md +159 -0
- package/assets/skills/rate-limiting.md +319 -0
- package/assets/skills/react-components.md +201 -0
- package/assets/skills/react-native-patterns.md +299 -0
- package/assets/skills/real-time-patterns.md +181 -0
- package/assets/skills/redis-patterns.md +188 -0
- package/assets/skills/refactoring.md +218 -0
- package/assets/skills/regex-patterns.md +191 -0
- package/assets/skills/remix-patterns.md +262 -0
- package/assets/skills/responsive-design.md +199 -0
- package/assets/skills/ruby-rails-patterns.md +178 -0
- package/assets/skills/rust-patterns.md +211 -0
- package/assets/skills/search-patterns.md +227 -0
- package/assets/skills/security-hardening.md +237 -0
- package/assets/skills/seo-patterns.md +179 -0
- package/assets/skills/serverless-patterns.md +223 -0
- package/assets/skills/sql-optimization.md +154 -0
- package/assets/skills/state-management.md +254 -0
- package/assets/skills/storybook-patterns.md +330 -0
- package/assets/skills/svelte-patterns.md +258 -0
- package/assets/skills/swift-patterns.md +227 -0
- package/assets/skills/tailwind-patterns.md +272 -0
- package/assets/skills/tdd-workflow.md +199 -0
- package/assets/skills/terraform-patterns.md +270 -0
- package/assets/skills/testing-react.md +240 -0
- package/assets/skills/testing-vitest.md +232 -0
- package/assets/skills/typescript-strict.md +159 -0
- package/assets/skills/video-processing.md +340 -0
- package/assets/skills/vue-patterns.md +247 -0
- package/assets/skills/web-workers.md +327 -0
- package/assets/skills/webhooks-patterns.md +283 -0
- package/assets/skills/websocket-patterns.md +306 -0
- package/dist/cli/index.js +941 -958
- package/dist/core/index.d.ts +341 -11
- package/dist/core.js +58 -95
- package/dist/mcp/index.d.ts +33 -1
- package/dist/mcp-server.js +177 -255
- package/package.json +4 -1
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: microservices
|
|
3
|
+
description: Microservices patterns for service discovery, circuit breaker, saga, event sourcing, API gateway, and health checks.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Microservices Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
Apply these patterns when building distributed systems with multiple independently
|
|
11
|
+
deployable services. Use this skill for service-to-service communication, fault
|
|
12
|
+
tolerance, distributed transactions, event-driven architecture, and operational
|
|
13
|
+
observability. Do not adopt microservices until a monolith becomes a bottleneck.
|
|
14
|
+
|
|
15
|
+
## How It Works
|
|
16
|
+
|
|
17
|
+
### Service Discovery
|
|
18
|
+
|
|
19
|
+
Services register themselves on startup and deregister on shutdown. Other services
|
|
20
|
+
look up addresses from the registry instead of hardcoding URLs.
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
// Consul-based registration
|
|
24
|
+
class ServiceRegistry {
|
|
25
|
+
constructor(private consul: ConsulClient) {}
|
|
26
|
+
|
|
27
|
+
async register(service: ServiceDefinition): Promise<void> {
|
|
28
|
+
await this.consul.agent.service.register({
|
|
29
|
+
id: `${service.name}-${service.instance}`,
|
|
30
|
+
name: service.name,
|
|
31
|
+
address: service.host,
|
|
32
|
+
port: service.port,
|
|
33
|
+
check: {
|
|
34
|
+
http: `http://${service.host}:${service.port}/healthz`,
|
|
35
|
+
interval: '10s',
|
|
36
|
+
deregister_critical_service_after: '30s',
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async discover(serviceName: string): Promise<ServiceInstance[]> {
|
|
42
|
+
const result = await this.consul.health.service({ service: serviceName, passing: true })
|
|
43
|
+
return result.map(entry => ({
|
|
44
|
+
host: entry.Service.Address,
|
|
45
|
+
port: entry.Service.Port,
|
|
46
|
+
}))
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// DNS-based (Kubernetes): just use service names
|
|
51
|
+
// http://user-service.default.svc.cluster.local:3000
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Circuit Breaker
|
|
55
|
+
|
|
56
|
+
Prevent cascading failures by tracking error rates and short-circuiting calls to
|
|
57
|
+
unhealthy services. Three states: closed (normal), open (failing fast), half-open
|
|
58
|
+
(testing recovery).
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
class CircuitBreaker {
|
|
62
|
+
private failures = 0
|
|
63
|
+
private lastFailureTime = 0
|
|
64
|
+
private state: 'closed' | 'open' | 'half-open' = 'closed'
|
|
65
|
+
|
|
66
|
+
constructor(
|
|
67
|
+
private threshold: number = 5,
|
|
68
|
+
private resetTimeout: number = 30_000,
|
|
69
|
+
) {}
|
|
70
|
+
|
|
71
|
+
async call<T>(fn: () => Promise<T>): Promise<T> {
|
|
72
|
+
if (this.state === 'open') {
|
|
73
|
+
if (Date.now() - this.lastFailureTime > this.resetTimeout) {
|
|
74
|
+
this.state = 'half-open'
|
|
75
|
+
} else {
|
|
76
|
+
throw new CircuitOpenError('Circuit is open')
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const result = await fn()
|
|
82
|
+
this.onSuccess()
|
|
83
|
+
return result
|
|
84
|
+
} catch (error) {
|
|
85
|
+
this.onFailure()
|
|
86
|
+
throw error
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private onSuccess(): void {
|
|
91
|
+
this.failures = 0
|
|
92
|
+
this.state = 'closed'
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private onFailure(): void {
|
|
96
|
+
this.failures++
|
|
97
|
+
this.lastFailureTime = Date.now()
|
|
98
|
+
if (this.failures >= this.threshold) {
|
|
99
|
+
this.state = 'open'
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Usage
|
|
105
|
+
const userServiceBreaker = new CircuitBreaker(5, 30_000)
|
|
106
|
+
const user = await userServiceBreaker.call(() => userClient.getUser(id))
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Saga Pattern for Distributed Transactions
|
|
110
|
+
|
|
111
|
+
Coordinate multi-service operations as a sequence of local transactions with
|
|
112
|
+
compensating actions for rollback. Use orchestration (central coordinator) or
|
|
113
|
+
choreography (event-driven).
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
// Orchestration-based saga
|
|
117
|
+
class CreateOrderSaga {
|
|
118
|
+
private steps: SagaStep[] = [
|
|
119
|
+
{
|
|
120
|
+
name: 'reserve-inventory',
|
|
121
|
+
execute: (ctx) => inventoryService.reserve(ctx.items),
|
|
122
|
+
compensate: (ctx) => inventoryService.release(ctx.reservationId),
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: 'charge-payment',
|
|
126
|
+
execute: (ctx) => paymentService.charge(ctx.userId, ctx.total),
|
|
127
|
+
compensate: (ctx) => paymentService.refund(ctx.chargeId),
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: 'create-order',
|
|
131
|
+
execute: (ctx) => orderService.create(ctx),
|
|
132
|
+
compensate: (ctx) => orderService.cancel(ctx.orderId),
|
|
133
|
+
},
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
async execute(context: SagaContext): Promise<void> {
|
|
137
|
+
const completed: SagaStep[] = []
|
|
138
|
+
|
|
139
|
+
for (const step of this.steps) {
|
|
140
|
+
try {
|
|
141
|
+
const result = await step.execute(context)
|
|
142
|
+
Object.assign(context, result)
|
|
143
|
+
completed.push(step)
|
|
144
|
+
} catch (error) {
|
|
145
|
+
// Compensate in reverse order
|
|
146
|
+
for (const completedStep of completed.reverse()) {
|
|
147
|
+
try {
|
|
148
|
+
await completedStep.compensate(context)
|
|
149
|
+
} catch (compError) {
|
|
150
|
+
console.error(`Compensation failed for ${completedStep.name}:`, compError)
|
|
151
|
+
// Log for manual intervention
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
throw new SagaFailedError(step.name, error)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Event Sourcing
|
|
162
|
+
|
|
163
|
+
Store state changes as an immutable append-only log of events. Rebuild current
|
|
164
|
+
state by replaying events. Use snapshots to avoid replaying from the beginning.
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
interface DomainEvent {
|
|
168
|
+
eventId: string
|
|
169
|
+
aggregateId: string
|
|
170
|
+
type: string
|
|
171
|
+
data: Record<string, unknown>
|
|
172
|
+
timestamp: number
|
|
173
|
+
version: number
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
class EventStore {
|
|
177
|
+
async append(aggregateId: string, events: DomainEvent[], expectedVersion: number): Promise<void> {
|
|
178
|
+
// Optimistic concurrency: fail if version mismatch
|
|
179
|
+
const current = await this.getVersion(aggregateId)
|
|
180
|
+
if (current !== expectedVersion) {
|
|
181
|
+
throw new ConcurrencyError(`Expected version ${expectedVersion}, got ${current}`)
|
|
182
|
+
}
|
|
183
|
+
await this.db.insert('events', events)
|
|
184
|
+
await this.publishToStream(events)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async getEvents(aggregateId: string, afterVersion = 0): Promise<DomainEvent[]> {
|
|
188
|
+
return this.db.query(
|
|
189
|
+
'SELECT * FROM events WHERE aggregate_id = $1 AND version > $2 ORDER BY version',
|
|
190
|
+
[aggregateId, afterVersion]
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Rebuild state from events
|
|
196
|
+
function rebuildOrder(events: DomainEvent[]): Order {
|
|
197
|
+
let order = Order.empty()
|
|
198
|
+
for (const event of events) {
|
|
199
|
+
switch (event.type) {
|
|
200
|
+
case 'OrderCreated': order = order.applyCreated(event.data); break
|
|
201
|
+
case 'ItemAdded': order = order.applyItemAdded(event.data); break
|
|
202
|
+
case 'OrderShipped': order = order.applyShipped(event.data); break
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return order
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### API Gateway
|
|
210
|
+
|
|
211
|
+
Single entry point for external clients. Routes requests, handles auth, rate
|
|
212
|
+
limiting, and response aggregation. Keep it thin; no business logic.
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
// Gateway route config
|
|
216
|
+
const routes: GatewayRoute[] = [
|
|
217
|
+
{ path: '/api/users/**', upstream: 'user-service', auth: true },
|
|
218
|
+
{ path: '/api/orders/**', upstream: 'order-service', auth: true },
|
|
219
|
+
{ path: '/api/public/**', upstream: 'content-service', auth: false },
|
|
220
|
+
]
|
|
221
|
+
|
|
222
|
+
// Responsibilities: routing, auth, rate limiting, logging
|
|
223
|
+
// NOT responsibilities: business logic, data transformation, orchestration
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Health Checks
|
|
227
|
+
|
|
228
|
+
Every service exposes `/healthz` (liveness) and `/readyz` (readiness). Liveness
|
|
229
|
+
checks if the process is alive. Readiness checks if it can serve traffic.
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
app.get('/healthz', (req, res) => {
|
|
233
|
+
res.json({ status: 'ok' })
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
app.get('/readyz', async (req, res) => {
|
|
237
|
+
const checks = {
|
|
238
|
+
database: await checkDatabase(),
|
|
239
|
+
redis: await checkRedis(),
|
|
240
|
+
upstream: await checkUpstreamService(),
|
|
241
|
+
}
|
|
242
|
+
const healthy = Object.values(checks).every(c => c.status === 'ok')
|
|
243
|
+
res.status(healthy ? 200 : 503).json({ status: healthy ? 'ready' : 'not ready', checks })
|
|
244
|
+
})
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Examples
|
|
248
|
+
|
|
249
|
+
**Pattern: Idempotency key for safe retries**
|
|
250
|
+
```typescript
|
|
251
|
+
app.post('/api/payments', async (req, res) => {
|
|
252
|
+
const idempotencyKey = req.headers['idempotency-key']
|
|
253
|
+
const existing = await cache.get(`idem:${idempotencyKey}`)
|
|
254
|
+
if (existing) return res.json(JSON.parse(existing))
|
|
255
|
+
const result = await processPayment(req.body)
|
|
256
|
+
await cache.set(`idem:${idempotencyKey}`, JSON.stringify(result), 'EX', 86400)
|
|
257
|
+
res.json(result)
|
|
258
|
+
})
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Checklist
|
|
262
|
+
|
|
263
|
+
- [ ] Services register/deregister with service discovery on startup/shutdown
|
|
264
|
+
- [ ] Circuit breaker wraps all cross-service HTTP calls
|
|
265
|
+
- [ ] Sagas with compensating actions for multi-service transactions
|
|
266
|
+
- [ ] Idempotency keys on all mutating cross-service calls
|
|
267
|
+
- [ ] Event sourcing with optimistic concurrency for critical aggregates
|
|
268
|
+
- [ ] API gateway handles auth, routing, rate limiting (no business logic)
|
|
269
|
+
- [ ] `/healthz` (liveness) and `/readyz` (readiness) on every service
|
|
270
|
+
- [ ] Structured logging with correlation/trace IDs across all services
|
|
271
|
+
- [ ] Timeouts and retries with exponential backoff on all outbound calls
|
|
272
|
+
- [ ] Dead letter queues for failed event processing
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: migration-patterns
|
|
3
|
+
description: Data migration patterns for zero-downtime migrations, backfills, dual-write, and schema versioning.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Data Migration Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Apply when changing database schemas, moving data between systems, or evolving data formats in production. Zero-downtime migrations are essential for any service with an SLA. The expand-contract pattern prevents breaking running code. Schema versioning keeps teams aligned on changes.
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
### Zero-Downtime Schema Migration (Expand-Contract)
|
|
14
|
+
|
|
15
|
+
Never rename, drop, or change a column type in one step:
|
|
16
|
+
|
|
17
|
+
```sql
|
|
18
|
+
-- Step 1: EXPAND -- add new column alongside old
|
|
19
|
+
ALTER TABLE users ADD COLUMN display_name TEXT;
|
|
20
|
+
|
|
21
|
+
-- Step 2: BACKFILL -- copy data in batches (see next section)
|
|
22
|
+
-- Step 3: DEPLOY -- app writes to both columns
|
|
23
|
+
-- Step 4: SWITCH -- app reads from new column only
|
|
24
|
+
-- Step 5: CONTRACT -- drop old column after confirming no readers
|
|
25
|
+
ALTER TABLE users DROP COLUMN name;
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Each step is a separate deployment. Never combine expand and contract.
|
|
29
|
+
|
|
30
|
+
### Backfill with Batching
|
|
31
|
+
|
|
32
|
+
Process millions of rows without locking the table:
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
async function backfillDisplayName(batchSize = 1000): Promise<number> {
|
|
36
|
+
let totalUpdated = 0;
|
|
37
|
+
let lastId = "";
|
|
38
|
+
|
|
39
|
+
while (true) {
|
|
40
|
+
const result = await db.query(`
|
|
41
|
+
UPDATE users
|
|
42
|
+
SET display_name = name
|
|
43
|
+
WHERE id IN (
|
|
44
|
+
SELECT id FROM users
|
|
45
|
+
WHERE display_name IS NULL AND id > $1
|
|
46
|
+
ORDER BY id LIMIT $2
|
|
47
|
+
)
|
|
48
|
+
RETURNING id
|
|
49
|
+
`, [lastId, batchSize]);
|
|
50
|
+
|
|
51
|
+
if (result.rows.length === 0) break;
|
|
52
|
+
totalUpdated += result.rows.length;
|
|
53
|
+
lastId = result.rows[result.rows.length - 1].id;
|
|
54
|
+
console.log(`Backfilled ${totalUpdated} rows, last ID: ${lastId}`);
|
|
55
|
+
await new Promise((r) => setTimeout(r, 100)); // throttle
|
|
56
|
+
}
|
|
57
|
+
return totalUpdated;
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Dual-Write Pattern
|
|
62
|
+
|
|
63
|
+
Migrate between data stores without downtime:
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
class UserRepository {
|
|
67
|
+
constructor(
|
|
68
|
+
private oldDb: Database,
|
|
69
|
+
private newDb: Database,
|
|
70
|
+
private phase: "dual-write" | "dual-read" | "new-only"
|
|
71
|
+
) {}
|
|
72
|
+
|
|
73
|
+
async save(user: User): Promise<void> {
|
|
74
|
+
switch (this.phase) {
|
|
75
|
+
case "dual-write":
|
|
76
|
+
await this.oldDb.save(user);
|
|
77
|
+
await this.newDb.save(user).catch((err) =>
|
|
78
|
+
logger.error({ err, userId: user.id }, "New DB write failed")
|
|
79
|
+
);
|
|
80
|
+
break;
|
|
81
|
+
case "dual-read":
|
|
82
|
+
await Promise.all([this.newDb.save(user), this.oldDb.save(user)]);
|
|
83
|
+
break;
|
|
84
|
+
case "new-only":
|
|
85
|
+
await this.newDb.save(user);
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async findById(id: string): Promise<User | null> {
|
|
91
|
+
switch (this.phase) {
|
|
92
|
+
case "dual-write":
|
|
93
|
+
return this.oldDb.findById(id);
|
|
94
|
+
case "dual-read": {
|
|
95
|
+
const [newResult, oldResult] = await Promise.all([
|
|
96
|
+
this.newDb.findById(id),
|
|
97
|
+
this.oldDb.findById(id),
|
|
98
|
+
]);
|
|
99
|
+
if (JSON.stringify(newResult) !== JSON.stringify(oldResult)) {
|
|
100
|
+
logger.warn({ id }, "Data mismatch between old and new DB");
|
|
101
|
+
}
|
|
102
|
+
return newResult;
|
|
103
|
+
}
|
|
104
|
+
case "new-only":
|
|
105
|
+
return this.newDb.findById(id);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Schema Versioning with Migration Files
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
// migrations/001_create_users.ts
|
|
115
|
+
import { Knex } from "knex";
|
|
116
|
+
|
|
117
|
+
export async function up(knex: Knex): Promise<void> {
|
|
118
|
+
await knex.schema.createTable("users", (table) => {
|
|
119
|
+
table.uuid("id").primary().defaultTo(knex.fn.uuid());
|
|
120
|
+
table.string("email").notNullable().unique();
|
|
121
|
+
table.string("name").notNullable();
|
|
122
|
+
table.timestamp("created_at").defaultTo(knex.fn.now());
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function down(knex: Knex): Promise<void> {
|
|
127
|
+
await knex.schema.dropTable("users");
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
// migrations/002_add_display_name.ts
|
|
133
|
+
export async function up(knex: Knex): Promise<void> {
|
|
134
|
+
await knex.schema.alterTable("users", (table) => {
|
|
135
|
+
table.string("display_name"); // nullable first
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function down(knex: Knex): Promise<void> {
|
|
140
|
+
await knex.schema.alterTable("users", (table) => {
|
|
141
|
+
table.dropColumn("display_name");
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Data Validation After Migration
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
async function validateMigration(): Promise<{ isValid: boolean; details: string }> {
|
|
150
|
+
const [oldCount, newCount] = await Promise.all([
|
|
151
|
+
db.query("SELECT COUNT(*) FROM old_users"),
|
|
152
|
+
db.query("SELECT COUNT(*) FROM users"),
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
const mismatches = await db.query(`
|
|
156
|
+
SELECT o.id, o.email AS old_email, n.email AS new_email
|
|
157
|
+
FROM old_users o
|
|
158
|
+
LEFT JOIN users n ON o.id = n.id
|
|
159
|
+
WHERE o.email != n.email OR n.id IS NULL
|
|
160
|
+
LIMIT 100
|
|
161
|
+
`);
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
isValid: mismatches.rows.length === 0,
|
|
165
|
+
details: `Old: ${oldCount.rows[0].count}, New: ${newCount.rows[0].count}, Mismatches: ${mismatches.rows.length}`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Rollback Strategy with Step Tracking
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
interface MigrationStep {
|
|
174
|
+
name: string;
|
|
175
|
+
forward: () => Promise<void>;
|
|
176
|
+
rollback: () => Promise<void>;
|
|
177
|
+
validate: () => Promise<boolean>;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function executeMigration(steps: MigrationStep[]): Promise<void> {
|
|
181
|
+
const completed: MigrationStep[] = [];
|
|
182
|
+
|
|
183
|
+
for (const step of steps) {
|
|
184
|
+
try {
|
|
185
|
+
await step.forward();
|
|
186
|
+
const valid = await step.validate();
|
|
187
|
+
if (!valid) throw new Error(`Validation failed: ${step.name}`);
|
|
188
|
+
completed.push(step);
|
|
189
|
+
logger.info(`Completed: ${step.name}`);
|
|
190
|
+
} catch (err) {
|
|
191
|
+
logger.error({ err }, `Failed at ${step.name}, rolling back`);
|
|
192
|
+
for (const done of completed.reverse()) {
|
|
193
|
+
await done.rollback();
|
|
194
|
+
logger.info(`Rolled back: ${done.name}`);
|
|
195
|
+
}
|
|
196
|
+
throw err;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Dangerous Operations to Avoid
|
|
203
|
+
|
|
204
|
+
```sql
|
|
205
|
+
-- NEVER on large tables (locks table in PG < 11):
|
|
206
|
+
ALTER TABLE users ADD COLUMN status TEXT NOT NULL DEFAULT 'active';
|
|
207
|
+
|
|
208
|
+
-- SAFE alternative:
|
|
209
|
+
ALTER TABLE users ADD COLUMN status TEXT;
|
|
210
|
+
-- backfill, then:
|
|
211
|
+
ALTER TABLE users ALTER COLUMN status SET NOT NULL;
|
|
212
|
+
ALTER TABLE users ALTER COLUMN status SET DEFAULT 'active';
|
|
213
|
+
|
|
214
|
+
-- NEVER without CONCURRENTLY:
|
|
215
|
+
CREATE INDEX idx_users_email ON users (email);
|
|
216
|
+
-- SAFE:
|
|
217
|
+
CREATE INDEX CONCURRENTLY idx_users_email ON users (email);
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Examples
|
|
221
|
+
|
|
222
|
+
| Pattern | When | Result |
|
|
223
|
+
|---------|------|--------|
|
|
224
|
+
| Expand-contract | Column rename/type change | Zero downtime, reversible |
|
|
225
|
+
| Batched backfill | Millions of rows | No table locks, throttled |
|
|
226
|
+
| Dual-write | Database migration | Verify before cutover |
|
|
227
|
+
| Schema versioning | Team collaboration | Ordered, repeatable |
|
|
228
|
+
| Post-validation | Every migration | Catch data loss early |
|
|
229
|
+
| Rollback plan | Every migration | Recover from failures |
|
|
230
|
+
|
|
231
|
+
## Checklist
|
|
232
|
+
- [ ] Schema changes use expand-contract, never direct rename/drop
|
|
233
|
+
- [ ] Backfills process in batches with throttling
|
|
234
|
+
- [ ] Dual-write validates consistency before switching reads
|
|
235
|
+
- [ ] Every migration has a tested rollback procedure
|
|
236
|
+
- [ ] Migration scripts are idempotent (safe to run twice)
|
|
237
|
+
- [ ] Data validation runs after every migration step
|
|
238
|
+
- [ ] New columns are nullable until backfill completes
|
|
239
|
+
- [ ] `CREATE INDEX` always uses `CONCURRENTLY`
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mongodb-patterns
|
|
3
|
+
description: MongoDB patterns for schema design, aggregation pipeline, indexes, change streams, and transactions.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# MongoDB Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Use MongoDB when your data is document-shaped, schemas evolve frequently, or you need flexible nested structures. Apply these patterns for schema design decisions (embed vs. reference), aggregation pipelines for analytics, index tuning, multi-document transactions, and change streams for real-time event processing.
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
### Schema Design -- Embed vs. Reference
|
|
14
|
+
|
|
15
|
+
**Embed** when data is always read together and the embedded array stays small:
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
// Good embed: address always read with user, 1:few relationship
|
|
19
|
+
{
|
|
20
|
+
_id: ObjectId("..."),
|
|
21
|
+
name: "Alice",
|
|
22
|
+
email: "alice@example.com",
|
|
23
|
+
addresses: [
|
|
24
|
+
{ label: "home", street: "123 Main St", city: "Austin", zip: "78701" },
|
|
25
|
+
{ label: "work", street: "456 Tech Blvd", city: "Austin", zip: "78702" }
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Reference** when related data is large, grows unbounded, or queried independently:
|
|
31
|
+
|
|
32
|
+
```javascript
|
|
33
|
+
// Orders reference user by ID
|
|
34
|
+
{
|
|
35
|
+
_id: ObjectId("..."),
|
|
36
|
+
userId: ObjectId("..."),
|
|
37
|
+
items: [
|
|
38
|
+
{ sku: "WIDGET-1", qty: 3, price: 1299 },
|
|
39
|
+
{ sku: "GADGET-2", qty: 1, price: 4599 }
|
|
40
|
+
],
|
|
41
|
+
totalCents: 8496,
|
|
42
|
+
status: "shipped",
|
|
43
|
+
createdAt: ISODate("2026-05-01T10:00:00Z")
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
| Factor | Embed | Reference |
|
|
48
|
+
|--------|-------|-----------|
|
|
49
|
+
| Read together? | Always | Sometimes |
|
|
50
|
+
| Array growth | Bounded (< 100) | Unbounded |
|
|
51
|
+
| Update frequency | Low | High |
|
|
52
|
+
| Data size | < 16MB doc limit | Any size |
|
|
53
|
+
|
|
54
|
+
### Aggregation Pipeline
|
|
55
|
+
|
|
56
|
+
```javascript
|
|
57
|
+
// Monthly revenue by product category
|
|
58
|
+
db.orders.aggregate([
|
|
59
|
+
{ $match: { status: "paid", createdAt: { $gte: ISODate("2026-01-01") } } },
|
|
60
|
+
{ $unwind: "$items" },
|
|
61
|
+
{ $lookup: {
|
|
62
|
+
from: "products",
|
|
63
|
+
localField: "items.sku",
|
|
64
|
+
foreignField: "sku",
|
|
65
|
+
as: "product"
|
|
66
|
+
}},
|
|
67
|
+
{ $unwind: "$product" },
|
|
68
|
+
{ $group: {
|
|
69
|
+
_id: {
|
|
70
|
+
month: { $dateToString: { format: "%Y-%m", date: "$createdAt" } },
|
|
71
|
+
category: "$product.category"
|
|
72
|
+
},
|
|
73
|
+
revenue: { $sum: { $multiply: ["$items.qty", "$items.price"] } },
|
|
74
|
+
orderCount: { $sum: 1 }
|
|
75
|
+
}},
|
|
76
|
+
{ $sort: { "_id.month": 1, revenue: -1 } }
|
|
77
|
+
]);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Put `$match` first to leverage indexes. Use `$project` to drop unneeded fields before `$lookup`.
|
|
81
|
+
|
|
82
|
+
### Indexes
|
|
83
|
+
|
|
84
|
+
```javascript
|
|
85
|
+
// Compound index matching common query pattern
|
|
86
|
+
db.orders.createIndex({ userId: 1, createdAt: -1 });
|
|
87
|
+
|
|
88
|
+
// Text index for search
|
|
89
|
+
db.products.createIndex({ name: "text", description: "text" });
|
|
90
|
+
|
|
91
|
+
// TTL index for auto-expiry
|
|
92
|
+
db.sessions.createIndex({ lastAccess: 1 }, { expireAfterSeconds: 86400 });
|
|
93
|
+
|
|
94
|
+
// Partial index -- only index active documents
|
|
95
|
+
db.users.createIndex(
|
|
96
|
+
{ email: 1 },
|
|
97
|
+
{ unique: true, partialFilterExpression: { status: "active" } }
|
|
98
|
+
);
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Verify with `explain("executionStats")`:
|
|
102
|
+
|
|
103
|
+
```javascript
|
|
104
|
+
db.orders.find({ userId: id }).sort({ createdAt: -1 }).explain("executionStats");
|
|
105
|
+
// Look for: winningPlan.stage === "IXSCAN", totalDocsExamined close to nReturned
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Transactions -- Multi-Document Atomicity
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
const session = client.startSession();
|
|
112
|
+
try {
|
|
113
|
+
session.startTransaction();
|
|
114
|
+
await orders.insertOne({ userId, items, totalCents }, { session });
|
|
115
|
+
await inventory.updateMany(
|
|
116
|
+
{ sku: { $in: skus } },
|
|
117
|
+
{ $inc: { reserved: 1 } },
|
|
118
|
+
{ session }
|
|
119
|
+
);
|
|
120
|
+
await session.commitTransaction();
|
|
121
|
+
} catch (err) {
|
|
122
|
+
await session.abortTransaction();
|
|
123
|
+
throw err;
|
|
124
|
+
} finally {
|
|
125
|
+
session.endSession();
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Transactions require a replica set. Keep them short -- they hold locks.
|
|
130
|
+
|
|
131
|
+
### Change Streams -- Real-Time Events
|
|
132
|
+
|
|
133
|
+
```javascript
|
|
134
|
+
const pipeline = [
|
|
135
|
+
{ $match: { operationType: { $in: ["insert", "update"] } } }
|
|
136
|
+
];
|
|
137
|
+
const changeStream = db.collection("orders").watch(pipeline, {
|
|
138
|
+
fullDocument: "updateLookup",
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
changeStream.on("change", (event) => {
|
|
142
|
+
if (event.operationType === "insert") {
|
|
143
|
+
notifyWarehouse(event.fullDocument);
|
|
144
|
+
}
|
|
145
|
+
if (event.operationType === "update" && event.fullDocument.status === "shipped") {
|
|
146
|
+
sendTrackingEmail(event.fullDocument);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Store the resume token (`event._id`) to survive restarts.
|
|
152
|
+
|
|
153
|
+
### Schema Validation
|
|
154
|
+
|
|
155
|
+
```javascript
|
|
156
|
+
db.createCollection("users", {
|
|
157
|
+
validator: {
|
|
158
|
+
$jsonSchema: {
|
|
159
|
+
bsonType: "object",
|
|
160
|
+
required: ["email", "name"],
|
|
161
|
+
properties: {
|
|
162
|
+
email: { bsonType: "string", pattern: "^.+@.+\\..+$" },
|
|
163
|
+
name: { bsonType: "string", minLength: 1 },
|
|
164
|
+
status: { enum: ["active", "inactive", "suspended"] },
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Examples
|
|
172
|
+
|
|
173
|
+
| Pattern | Use Case | Key Benefit |
|
|
174
|
+
|---------|----------|-------------|
|
|
175
|
+
| Embedded docs | User + addresses | Single read, atomic write |
|
|
176
|
+
| Referenced docs | Orders + products | Independent scaling |
|
|
177
|
+
| Aggregation | Revenue dashboards | Server-side analytics |
|
|
178
|
+
| TTL index | Session cleanup | Automatic expiry |
|
|
179
|
+
| Change streams | Order notifications | Real-time, resumable |
|
|
180
|
+
|
|
181
|
+
## Checklist
|
|
182
|
+
- [ ] Schema decision (embed vs. reference) documented per collection
|
|
183
|
+
- [ ] No embedded arrays that grow unbounded
|
|
184
|
+
- [ ] Compound indexes match equality-sort-range (ESR) rule
|
|
185
|
+
- [ ] `explain()` confirms IXSCAN for all hot queries
|
|
186
|
+
- [ ] TTL indexes handle session/token expiry automatically
|
|
187
|
+
- [ ] Aggregation pipelines put `$match` first
|
|
188
|
+
- [ ] Multi-document writes use transactions
|
|
189
|
+
- [ ] Change streams store resume tokens for crash recovery
|