@coralai/sps-cli 0.42.0 → 0.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -4
- package/dist/commands/consoleCommand.d.ts +2 -0
- package/dist/commands/consoleCommand.d.ts.map +1 -0
- package/dist/commands/consoleCommand.js +129 -0
- package/dist/commands/consoleCommand.js.map +1 -0
- package/dist/commands/projectInit.d.ts.map +1 -1
- package/dist/commands/projectInit.js +40 -53
- package/dist/commands/projectInit.js.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +14 -2
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/skillCommand.d.ts +2 -0
- package/dist/commands/skillCommand.d.ts.map +1 -0
- package/dist/commands/skillCommand.js +235 -0
- package/dist/commands/skillCommand.js.map +1 -0
- package/dist/console-assets/assets/index-Bhd2f9AP.js +125 -0
- package/dist/console-assets/assets/index-bsAN2a12.css +1 -0
- package/dist/console-assets/index.html +16 -0
- package/dist/console-server/index.d.ts +29 -0
- package/dist/console-server/index.d.ts.map +1 -0
- package/dist/console-server/index.js +145 -0
- package/dist/console-server/index.js.map +1 -0
- package/dist/console-server/lib/lockFile.d.ts +17 -0
- package/dist/console-server/lib/lockFile.d.ts.map +1 -0
- package/dist/console-server/lib/lockFile.js +61 -0
- package/dist/console-server/lib/lockFile.js.map +1 -0
- package/dist/console-server/lib/portPicker.d.ts +3 -0
- package/dist/console-server/lib/portPicker.d.ts.map +1 -0
- package/dist/console-server/lib/portPicker.js +25 -0
- package/dist/console-server/lib/portPicker.js.map +1 -0
- package/dist/console-server/routes/projects.d.ts +11 -0
- package/dist/console-server/routes/projects.d.ts.map +1 -0
- package/dist/console-server/routes/projects.js +149 -0
- package/dist/console-server/routes/projects.js.map +1 -0
- package/dist/console-server/routes/system.d.ts +7 -0
- package/dist/console-server/routes/system.d.ts.map +1 -0
- package/dist/console-server/routes/system.js +19 -0
- package/dist/console-server/routes/system.js.map +1 -0
- package/dist/console-server/sse/eventBus.d.ts +25 -0
- package/dist/console-server/sse/eventBus.d.ts.map +1 -0
- package/dist/console-server/sse/eventBus.js +32 -0
- package/dist/console-server/sse/eventBus.js.map +1 -0
- package/dist/console-server/watchers/cardWatcher.d.ts +9 -0
- package/dist/console-server/watchers/cardWatcher.d.ts.map +1 -0
- package/dist/console-server/watchers/cardWatcher.js +42 -0
- package/dist/console-server/watchers/cardWatcher.js.map +1 -0
- package/dist/core/skillStore.d.ts +46 -0
- package/dist/core/skillStore.d.ts.map +1 -0
- package/dist/core/skillStore.js +210 -0
- package/dist/core/skillStore.js.map +1 -0
- package/dist/core/skillStore.test.d.ts +2 -0
- package/dist/core/skillStore.test.d.ts.map +1 -0
- package/dist/core/skillStore.test.js +203 -0
- package/dist/core/skillStore.test.js.map +1 -0
- package/dist/main.js +27 -17
- package/dist/main.js.map +1 -1
- package/package.json +8 -2
- package/skills/architecture-decision-records/SKILL.md +207 -0
- package/skills/backend/SKILL.md +62 -0
- package/skills/backend/references/api-design.md +168 -0
- package/skills/backend/references/caching.md +181 -0
- package/skills/backend/references/data-access.md +173 -0
- package/skills/backend/references/layering.md +181 -0
- package/skills/backend/references/observability.md +190 -0
- package/skills/backend/references/resilience.md +201 -0
- package/skills/backend/references/security.md +186 -0
- package/skills/backend-architect/SKILL.md +119 -0
- package/skills/code-reviewer/SKILL.md +143 -0
- package/skills/coding-standards/SKILL.md +60 -0
- package/skills/coding-standards/references/clean-code.md +258 -0
- package/skills/coding-standards/references/code-review.md +192 -0
- package/skills/coding-standards/references/commits-and-prs.md +226 -0
- package/skills/coding-standards/references/error-strategy.md +193 -0
- package/skills/coding-standards/references/naming.md +185 -0
- package/skills/coding-standards/references/tdd.md +171 -0
- package/skills/database/SKILL.md +53 -0
- package/skills/database/references/indexing.md +190 -0
- package/skills/database/references/migrations.md +199 -0
- package/skills/database/references/nosql.md +185 -0
- package/skills/database/references/queries.md +295 -0
- package/skills/database/references/scaling.md +203 -0
- package/skills/database/references/schema.md +191 -0
- package/skills/database-optimizer/SKILL.md +168 -0
- package/skills/debugging-workflow/SKILL.md +244 -0
- package/skills/devops/SKILL.md +55 -0
- package/skills/devops/references/ci-cd.md +204 -0
- package/skills/devops/references/containers.md +272 -0
- package/skills/devops/references/deploy.md +201 -0
- package/skills/devops/references/iac.md +252 -0
- package/skills/devops/references/observability.md +228 -0
- package/skills/devops/references/secrets.md +178 -0
- package/skills/devops-automator/SKILL.md +164 -0
- package/skills/frontend/SKILL.md +52 -0
- package/skills/frontend/references/accessibility.md +222 -0
- package/skills/frontend/references/components.md +206 -0
- package/skills/frontend/references/performance.md +219 -0
- package/skills/frontend/references/routing.md +209 -0
- package/skills/frontend/references/state.md +190 -0
- package/skills/frontend/references/testing.md +216 -0
- package/skills/frontend-developer/SKILL.md +115 -0
- package/skills/git-workflow/SKILL.md +355 -0
- package/skills/golang/SKILL.md +49 -0
- package/skills/golang/references/concurrency.md +284 -0
- package/skills/golang/references/errors.md +241 -0
- package/skills/golang/references/idioms.md +285 -0
- package/skills/golang/references/testing.md +238 -0
- package/skills/java/SKILL.md +50 -0
- package/skills/java/references/concurrency.md +194 -0
- package/skills/java/references/idioms.md +283 -0
- package/skills/java/references/testing.md +228 -0
- package/skills/kotlin/SKILL.md +47 -0
- package/skills/kotlin/references/coroutines.md +240 -0
- package/skills/kotlin/references/idioms.md +268 -0
- package/skills/kotlin/references/testing.md +219 -0
- package/skills/mobile/SKILL.md +50 -0
- package/skills/mobile/references/architecture.md +204 -0
- package/skills/mobile/references/navigation.md +158 -0
- package/skills/mobile/references/performance.md +152 -0
- package/skills/mobile/references/platform.md +166 -0
- package/skills/mobile/references/state-and-data.md +174 -0
- package/skills/python/SKILL.md +51 -0
- package/skills/python/THIRD_PARTY.md +14 -0
- package/skills/python/references/async.md +218 -0
- package/skills/python/references/error-handling.md +254 -0
- package/skills/python/references/idioms.md +279 -0
- package/skills/python/references/packaging.md +233 -0
- package/skills/python/references/testing.md +269 -0
- package/skills/python/references/typing.md +292 -0
- package/skills/qa-tester/SKILL.md +186 -0
- package/skills/rust/SKILL.md +50 -0
- package/skills/rust/references/async.md +224 -0
- package/skills/rust/references/errors.md +240 -0
- package/skills/rust/references/ownership.md +263 -0
- package/skills/rust/references/testing.md +274 -0
- package/skills/rust/references/traits.md +250 -0
- package/skills/security-engineer/SKILL.md +157 -0
- package/skills/swift/SKILL.md +48 -0
- package/skills/swift/references/concurrency.md +280 -0
- package/skills/swift/references/idioms.md +334 -0
- package/skills/swift/references/testing.md +229 -0
- package/skills/typescript/SKILL.md +51 -0
- package/skills/typescript/references/async.md +241 -0
- package/skills/typescript/references/errors.md +208 -0
- package/skills/typescript/references/idioms.md +246 -0
- package/skills/typescript/references/testing.md +225 -0
- package/skills/typescript/references/tooling.md +208 -0
- package/skills/typescript/references/types.md +259 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Caching
|
|
2
|
+
|
|
3
|
+
Rules, strategies, pitfalls. Cache-aside covers 90% of cases.
|
|
4
|
+
|
|
5
|
+
## Cache-aside (lazy loading)
|
|
6
|
+
|
|
7
|
+
Application checks cache first; on miss, loads from source and populates cache.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
get(id):
|
|
11
|
+
v = cache.get(key(id))
|
|
12
|
+
if v is not None:
|
|
13
|
+
return v # hit
|
|
14
|
+
v = source.load(id) # miss
|
|
15
|
+
if v is not None:
|
|
16
|
+
cache.set(key(id), v, ttl=5min)
|
|
17
|
+
return v
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Pros: simple; stale data only appears on cached keys; source is authoritative.
|
|
21
|
+
Cons: first reader after expiry pays full latency; risk of **cache stampede** when many readers miss together.
|
|
22
|
+
|
|
23
|
+
## Write-through
|
|
24
|
+
|
|
25
|
+
Application writes to cache AND source atomically (usually: write source first, then cache).
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
save(entity):
|
|
29
|
+
source.save(entity)
|
|
30
|
+
cache.set(key(entity.id), entity, ttl=5min)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Pros: cache is always fresh after a write.
|
|
34
|
+
Cons: writes are slower; if cache write fails, you have stale data (decide: rollback, or fire-and-forget with expiry).
|
|
35
|
+
|
|
36
|
+
## Write-behind (deferred)
|
|
37
|
+
|
|
38
|
+
Application writes to cache; a background job flushes to source later. Rare — only for very high write volume and tolerance for delayed durability. Almost always the wrong choice; you're trading data loss risk for write throughput.
|
|
39
|
+
|
|
40
|
+
## What to cache (and what NOT to)
|
|
41
|
+
|
|
42
|
+
**Cache-friendly**:
|
|
43
|
+
- Read-heavy, changes rarely (config, product catalog, user profile)
|
|
44
|
+
- Expensive to compute (rendered HTML, aggregations, vector search)
|
|
45
|
+
- Idempotent reads
|
|
46
|
+
|
|
47
|
+
**Avoid caching**:
|
|
48
|
+
- Per-user personalized data with high cardinality (cache hit rate too low)
|
|
49
|
+
- Rapidly changing data (reconciliation cost > cache benefit)
|
|
50
|
+
- Anything where staleness is a correctness bug (balances, seat availability)
|
|
51
|
+
|
|
52
|
+
## TTL strategy
|
|
53
|
+
|
|
54
|
+
Every cache entry must expire. No TTL = memory leak.
|
|
55
|
+
|
|
56
|
+
| Data type | Starting TTL |
|
|
57
|
+
|---|---|
|
|
58
|
+
| Static config | 1–24 h |
|
|
59
|
+
| User profile | 5–60 min |
|
|
60
|
+
| Hot aggregation | 10 s – 5 min |
|
|
61
|
+
| Computed render | minutes |
|
|
62
|
+
| Feature flag eval | 30–60 s |
|
|
63
|
+
|
|
64
|
+
Add a small random jitter (±10%) so entries don't all expire at the same instant → stampede.
|
|
65
|
+
|
|
66
|
+
## Invalidation
|
|
67
|
+
|
|
68
|
+
The second hardest problem in computing. Three approaches:
|
|
69
|
+
|
|
70
|
+
1. **TTL only** — simple; tolerate staleness up to TTL. Default choice.
|
|
71
|
+
2. **Explicit invalidation** — on write, delete the cache key. Works if your mutation paths are countable.
|
|
72
|
+
```
|
|
73
|
+
save(user):
|
|
74
|
+
db.update(user)
|
|
75
|
+
cache.delete(key(user.id))
|
|
76
|
+
```
|
|
77
|
+
3. **Event-driven** — publish `UserUpdated`; subscribers invalidate their caches. Needed when many services cache the same entity.
|
|
78
|
+
|
|
79
|
+
Don't try to *update* the cache on write in complex systems — delete instead and let the next read repopulate. Updates race; deletes don't.
|
|
80
|
+
|
|
81
|
+
## Cache key design
|
|
82
|
+
|
|
83
|
+
Stable, explicit, version-prefixed.
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
# ✅
|
|
87
|
+
user:v2:{user_id}
|
|
88
|
+
product:v1:{sku}:detail
|
|
89
|
+
list:orders:v1:user={uid}:status=paid:cursor={c}
|
|
90
|
+
|
|
91
|
+
# ❌
|
|
92
|
+
u_123 # ambiguous across services
|
|
93
|
+
users:123:details # no version
|
|
94
|
+
${JSON.stringify(query)} # fragile; order-dependent
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Version prefix lets you deploy a new format without stampeding the old one; old keys simply age out.
|
|
98
|
+
|
|
99
|
+
## Stampede protection
|
|
100
|
+
|
|
101
|
+
When a hot key expires, many requests miss at once and pile onto the source. Two fixes:
|
|
102
|
+
|
|
103
|
+
### Single-flight / coalescing
|
|
104
|
+
|
|
105
|
+
In-process: at most one loader per key; concurrent callers wait for the same result.
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
load(key):
|
|
109
|
+
with singleFlight(key):
|
|
110
|
+
return source.load(key)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Probabilistic early expiration (XFetch)
|
|
114
|
+
|
|
115
|
+
Before the TTL, some fraction of readers voluntarily refresh.
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
get(key):
|
|
119
|
+
v, ttl_remaining = cache.get_with_ttl(key)
|
|
120
|
+
if v is None or should_refresh_early(ttl_remaining):
|
|
121
|
+
v = source.load(key)
|
|
122
|
+
cache.set(key, v, ttl=5min)
|
|
123
|
+
return v
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Negative caching
|
|
127
|
+
|
|
128
|
+
Cache misses are expensive if they happen constantly (e.g., 404 lookups). Cache the absence too, with a short TTL.
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
get(id):
|
|
132
|
+
v = cache.get(key(id))
|
|
133
|
+
if v is MISSING_SENTINEL:
|
|
134
|
+
return None # known-not-found
|
|
135
|
+
if v is not None:
|
|
136
|
+
return v
|
|
137
|
+
v = source.load(id)
|
|
138
|
+
cache.set(key(id), v if v else MISSING_SENTINEL, ttl=30s)
|
|
139
|
+
return v
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Short TTL — don't cache `None` for hours; the item may just have been created.
|
|
143
|
+
|
|
144
|
+
## HTTP-level caching
|
|
145
|
+
|
|
146
|
+
For public GET endpoints, let the HTTP layer cache. Free, correctly implemented, respected by CDNs.
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
Cache-Control: public, max-age=300, s-maxage=600, stale-while-revalidate=60
|
|
150
|
+
ETag: "abc123"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
- `max-age`: browser/client
|
|
154
|
+
- `s-maxage`: shared caches (CDN)
|
|
155
|
+
- `stale-while-revalidate`: serve stale while refreshing in the background
|
|
156
|
+
- `ETag` + `If-None-Match`: 304 responses save bandwidth
|
|
157
|
+
|
|
158
|
+
## Local (in-process) cache vs distributed
|
|
159
|
+
|
|
160
|
+
| | Local (in-process) | Distributed (Redis, Memcached) |
|
|
161
|
+
|---|---|---|
|
|
162
|
+
| Latency | Nanoseconds | ~1 ms |
|
|
163
|
+
| Consistency across instances | No — each pod has its own | Yes |
|
|
164
|
+
| Size | Limited to process memory | Limited to cluster |
|
|
165
|
+
| Eviction | LRU, LFU | LRU, LFU, TTL |
|
|
166
|
+
| Cost | Free | Infra + ops |
|
|
167
|
+
| Invalidation | Hard across pods | One call |
|
|
168
|
+
|
|
169
|
+
Use local for small hot data; distributed for shared state. Don't mix carelessly — a per-pod cache that's supposed to be consistent will drift.
|
|
170
|
+
|
|
171
|
+
## Anti-patterns
|
|
172
|
+
|
|
173
|
+
| Anti-pattern | Why bad |
|
|
174
|
+
|---|---|
|
|
175
|
+
| No TTL anywhere | Memory leak; stale data forever |
|
|
176
|
+
| Caching mutable objects by reference | Next reader mutates the cached copy |
|
|
177
|
+
| Caching per-user data with high cardinality | Low hit rate; wastes memory |
|
|
178
|
+
| Cache key includes a timestamp that changes every request | Every request is a miss |
|
|
179
|
+
| Serializing cache writes into the request path without timeout | Cache outage → requests hang |
|
|
180
|
+
| Reading cache without a fallback path | Cache is a dependency; treat it as optional |
|
|
181
|
+
| Storing secrets in shared cache | Secret sprawl across cluster |
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Data Access
|
|
2
|
+
|
|
3
|
+
Transactions, queries, migrations, connection pooling. Language-neutral patterns.
|
|
4
|
+
|
|
5
|
+
## N+1 queries — the universal killer
|
|
6
|
+
|
|
7
|
+
The single most common backend performance bug.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
# ❌ N+1
|
|
11
|
+
orders = orderRepo.findAll() # 1 query
|
|
12
|
+
for order in orders:
|
|
13
|
+
order.user = userRepo.find(order.userId) # N queries
|
|
14
|
+
|
|
15
|
+
# ✅ Batch fetch
|
|
16
|
+
orders = orderRepo.findAll()
|
|
17
|
+
userIds = unique(o.userId for o in orders)
|
|
18
|
+
users = userRepo.findByIds(userIds) # 1 query
|
|
19
|
+
userMap = { u.id: u for u in users }
|
|
20
|
+
for o in orders:
|
|
21
|
+
o.user = userMap[o.userId]
|
|
22
|
+
|
|
23
|
+
# ✅ Join (if the ORM supports eager loading)
|
|
24
|
+
orders = orderRepo.findAll(include=['user'])
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Detect early: log every query in test mode; assert query count on hot paths.
|
|
28
|
+
|
|
29
|
+
## Select only what you need
|
|
30
|
+
|
|
31
|
+
Wide `SELECT *` costs bandwidth, memory, and breaks when the schema changes.
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
# ❌
|
|
35
|
+
SELECT * FROM users WHERE active = true
|
|
36
|
+
|
|
37
|
+
# ✅
|
|
38
|
+
SELECT id, email, name FROM users WHERE active = true
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Indexes
|
|
42
|
+
|
|
43
|
+
An index is a write-time tax for a read-time refund. Worth it on columns used in WHERE, JOIN, ORDER BY of hot queries.
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
# Common first indexes
|
|
47
|
+
CREATE INDEX idx_orders_user_id ON orders(user_id);
|
|
48
|
+
CREATE INDEX idx_orders_status ON orders(status) WHERE status = 'pending'; -- partial
|
|
49
|
+
CREATE INDEX idx_users_email_lower ON users (LOWER(email)); -- expression
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Rules:
|
|
53
|
+
- Read the query plan. Don't guess.
|
|
54
|
+
- Composite index order matters: `(user_id, created_at)` helps `WHERE user_id = ? ORDER BY created_at`, not the reverse.
|
|
55
|
+
- Every index slows writes. More indexes ≠ faster system.
|
|
56
|
+
|
|
57
|
+
## Transactions
|
|
58
|
+
|
|
59
|
+
One business operation = one transaction. Cross the boundary at the use case, not inside a repository.
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
unitOfWork.begin()
|
|
63
|
+
try:
|
|
64
|
+
order = orderRepo.save(newOrder)
|
|
65
|
+
inventoryRepo.decrement(order.items)
|
|
66
|
+
eventBus.publish(OrderPlaced(order.id))
|
|
67
|
+
unitOfWork.commit()
|
|
68
|
+
except:
|
|
69
|
+
unitOfWork.rollback()
|
|
70
|
+
raise
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Isolation levels:
|
|
74
|
+
- **READ COMMITTED**: default on most DBs, fine for most workloads
|
|
75
|
+
- **REPEATABLE READ**: if you read the same row twice within a transaction and want consistency
|
|
76
|
+
- **SERIALIZABLE**: correctness over throughput; expect retries
|
|
77
|
+
|
|
78
|
+
Keep transactions short. Long-running transactions hold locks and block everyone.
|
|
79
|
+
|
|
80
|
+
## Connection pooling
|
|
81
|
+
|
|
82
|
+
Every real backend uses a pool, not per-request connections. DBs limit max connections (Postgres default ~100); without pooling, a traffic spike exhausts the DB.
|
|
83
|
+
|
|
84
|
+
| Pool param | Starting value | Notes |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| min idle | 2–5 | Warm connections for low traffic |
|
|
87
|
+
| max size | (DB max ÷ replicas) − safety margin | e.g., 100 ÷ 4 = 25 per instance, then leave room |
|
|
88
|
+
| connection timeout | 2–5 s | Fail fast if pool is saturated |
|
|
89
|
+
| idle timeout | 30 s – 5 min | Recycle stale connections |
|
|
90
|
+
| max lifetime | 30 min | Force re-resolve DNS, rotate creds |
|
|
91
|
+
|
|
92
|
+
Serverless + traditional DB: use a pooler (PgBouncer, RDS Proxy) — each cold lambda can't open its own pool.
|
|
93
|
+
|
|
94
|
+
## Read replicas
|
|
95
|
+
|
|
96
|
+
Route reads to replicas, writes to primary. Beware of replication lag:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
user.save(newEmail) # primary
|
|
100
|
+
user = user.reload() # replica — may still show old email
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Common fix: stick to primary for N seconds after a write, or read-your-writes from primary only.
|
|
104
|
+
|
|
105
|
+
## Migrations
|
|
106
|
+
|
|
107
|
+
Every schema change is a migration file, checked in, applied in CI/CD, reversible where possible.
|
|
108
|
+
|
|
109
|
+
Rules:
|
|
110
|
+
- **Never edit a merged migration.** Write a new one.
|
|
111
|
+
- **Additive first, destructive later.** Add the new column → backfill → switch code → drop the old column (separate deploys).
|
|
112
|
+
- **Index creation on a hot table**: use `CREATE INDEX CONCURRENTLY` (Postgres) so you don't lock the table.
|
|
113
|
+
- **Default values**: adding a `NOT NULL` column with a default on a big table can rewrite the whole table. In Postgres 11+, adding `DEFAULT` is metadata-only; in older DBs, do `ADD NULLABLE → backfill → SET NOT NULL`.
|
|
114
|
+
|
|
115
|
+
## Soft deletes
|
|
116
|
+
|
|
117
|
+
Don't add `deleted_at` everywhere by default. It creates a silent contract that every query must filter. Use it when:
|
|
118
|
+
- You genuinely need to recover records, and
|
|
119
|
+
- You accept the cognitive tax on every query.
|
|
120
|
+
|
|
121
|
+
Prefer hard deletes + an `audit_log` / `events` table if you only need history.
|
|
122
|
+
|
|
123
|
+
## Bulk operations
|
|
124
|
+
|
|
125
|
+
One round-trip per row kills throughput. Use batch APIs.
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
# ❌
|
|
129
|
+
for row in 10_000_rows:
|
|
130
|
+
db.insert(row)
|
|
131
|
+
|
|
132
|
+
# ✅
|
|
133
|
+
db.bulk_insert(10_000_rows) # one statement
|
|
134
|
+
# or
|
|
135
|
+
db.copy_from(csv_buffer) # Postgres COPY, fastest
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
On upserts, use the DB's native construct (`INSERT ... ON CONFLICT`, `MERGE`, `INSERT ... ON DUPLICATE KEY UPDATE`), not read-then-update in app code.
|
|
139
|
+
|
|
140
|
+
## Pagination queries
|
|
141
|
+
|
|
142
|
+
Offset pagination gets slow on large tables because the DB still walks the skipped rows.
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
# ❌ Slow on page 10 000
|
|
146
|
+
SELECT * FROM events ORDER BY id LIMIT 50 OFFSET 500000
|
|
147
|
+
|
|
148
|
+
# ✅ Keyset / cursor
|
|
149
|
+
SELECT * FROM events WHERE id > :last_id ORDER BY id LIMIT 50
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Keyset pagination is O(log n); offset is O(offset + limit).
|
|
153
|
+
|
|
154
|
+
## NoSQL quick notes
|
|
155
|
+
|
|
156
|
+
- **Key-value (Redis, DynamoDB)**: design the key; scan queries are evil.
|
|
157
|
+
- **Document (Mongo)**: embed what you always read together; reference what you sometimes read separately.
|
|
158
|
+
- **Wide column (Cassandra, Bigtable)**: query patterns decide the schema, not the other way around.
|
|
159
|
+
- **Graph (Neo4j)**: use when the traversal depth would be painful in SQL.
|
|
160
|
+
|
|
161
|
+
Rule: pick the store that matches the access pattern. Don't use Mongo because "it's flexible"; flexibility defers modeling pain, it doesn't erase it.
|
|
162
|
+
|
|
163
|
+
## Anti-patterns
|
|
164
|
+
|
|
165
|
+
| Anti-pattern | Why bad | Fix |
|
|
166
|
+
|---|---|---|
|
|
167
|
+
| Queries in a loop | N+1; one slow endpoint tanks the DB | Batch / join / cache |
|
|
168
|
+
| No timeout on DB calls | A single slow query hangs threads / pool | Set statement timeout |
|
|
169
|
+
| `SELECT *` in hot code | Brittle, wasteful | List columns |
|
|
170
|
+
| Business logic in stored procedures "for speed" | Hard to test, version, review | Keep logic in code; use SQL for set operations |
|
|
171
|
+
| Multiple orthogonal indexes on the same table | Slow writes, bloated storage | Review `pg_stat_user_indexes`; drop unused |
|
|
172
|
+
| Editing an applied migration | Divergent envs | New migration |
|
|
173
|
+
| Schema changes without a rollback plan | Stuck deploys | Reversible migrations or documented forward-only fix |
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Layering
|
|
2
|
+
|
|
3
|
+
Split the code so business rules don't depend on the framework, the database, or the network. Hexagonal / clean architecture in practical form.
|
|
4
|
+
|
|
5
|
+
## The four layers
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
┌──────────────────────────────────────────────┐
|
|
9
|
+
│ Delivery (HTTP handler, CLI, gRPC, worker) │ — framework-aware
|
|
10
|
+
├──────────────────────────────────────────────┤
|
|
11
|
+
│ Application (use cases, orchestration) │ — framework-ignorant
|
|
12
|
+
├──────────────────────────────────────────────┤
|
|
13
|
+
│ Domain (entities, value objects, rules) │ — pure
|
|
14
|
+
├──────────────────────────────────────────────┤
|
|
15
|
+
│ Infrastructure (DB, cache, HTTP clients) │ — implements domain ports
|
|
16
|
+
└──────────────────────────────────────────────┘
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Dependency direction: **only inward**. Delivery → Application → Domain. Infrastructure implements interfaces owned by the inner layers.
|
|
20
|
+
|
|
21
|
+
If your domain imports an HTTP framework, a DB driver, or a cache client, the layering is broken.
|
|
22
|
+
|
|
23
|
+
## Minimal layer roles
|
|
24
|
+
|
|
25
|
+
| Layer | Contains | Does NOT contain |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| Delivery | Request parsing, auth check, calls a use case, maps result to response | Business rules, DB queries |
|
|
28
|
+
| Application | Use case orchestration, transaction boundaries, calls repositories and services | SQL, HTTP, JSON parsing |
|
|
29
|
+
| Domain | Entities, value objects, invariants, domain events | I/O, frameworks |
|
|
30
|
+
| Infrastructure | Repository impls, HTTP client impls, message queue impls | Business decisions |
|
|
31
|
+
|
|
32
|
+
## Ports and adapters
|
|
33
|
+
|
|
34
|
+
The domain declares a **port** (interface). Infrastructure provides an **adapter** (implementation).
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
Domain declares (port):
|
|
38
|
+
interface UserRepository
|
|
39
|
+
findById(id) -> User | null
|
|
40
|
+
save(user) -> void
|
|
41
|
+
|
|
42
|
+
Infrastructure provides (adapter):
|
|
43
|
+
PostgresUserRepository implements UserRepository
|
|
44
|
+
InMemoryUserRepository implements UserRepository (for tests)
|
|
45
|
+
RedisUserRepository implements UserRepository (cache-aside)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Rule: the adapter file imports the port. The port file never imports any adapter.
|
|
49
|
+
|
|
50
|
+
## Use case pattern
|
|
51
|
+
|
|
52
|
+
A use case is one method, one transaction boundary, one business intent.
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
class CreateOrder:
|
|
56
|
+
deps: OrderRepository, UserRepository, PaymentGateway, EventBus
|
|
57
|
+
|
|
58
|
+
execute(cmd: CreateOrderCommand) -> OrderId:
|
|
59
|
+
user = userRepository.findById(cmd.userId)
|
|
60
|
+
if not user: raise UserNotFound
|
|
61
|
+
if not user.canOrder(): raise UserCannotOrder
|
|
62
|
+
|
|
63
|
+
order = Order.create(user, cmd.items) # domain rules
|
|
64
|
+
paymentRepository.authorize(order) # infra
|
|
65
|
+
orderRepository.save(order) # infra
|
|
66
|
+
eventBus.publish(OrderCreated(order.id)) # infra
|
|
67
|
+
|
|
68
|
+
return order.id
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Delivery turns an HTTP request into `CreateOrderCommand`, calls `execute`, turns the result into a response. That's it.
|
|
72
|
+
|
|
73
|
+
## Repository pattern
|
|
74
|
+
|
|
75
|
+
Collect the DB operations for one aggregate behind one interface.
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
interface OrderRepository:
|
|
79
|
+
findById(id) -> Order | null
|
|
80
|
+
findByUser(uid) -> list[Order]
|
|
81
|
+
save(order) -> void
|
|
82
|
+
delete(id) -> void
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Rules:
|
|
86
|
+
- Repositories return **domain objects**, not DB rows.
|
|
87
|
+
- Queries that cross aggregates (reporting, analytics) do NOT belong in a repository; put them in a dedicated `Queries` / `ReadModel` interface.
|
|
88
|
+
- Avoid growing `findByXAndYAndZ` explosions — those signal you need a query object or a read model.
|
|
89
|
+
|
|
90
|
+
## Service vs domain vs use case
|
|
91
|
+
|
|
92
|
+
People confuse these. Rough guide:
|
|
93
|
+
|
|
94
|
+
| Name | Lives in | Contains |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| Entity / Aggregate | Domain | State + invariants + rules that depend ONLY on that state |
|
|
97
|
+
| Domain Service | Domain | Rules that span multiple aggregates but are still pure |
|
|
98
|
+
| Use Case / Application Service | Application | Orchestration: load, decide, persist, publish |
|
|
99
|
+
| Gateway / Client | Infrastructure | Talks to the outside world (HTTP, DB, queue) |
|
|
100
|
+
|
|
101
|
+
If you have a `FooService` that does both business rules and DB calls, split it.
|
|
102
|
+
|
|
103
|
+
## Dependency injection, without magic
|
|
104
|
+
|
|
105
|
+
Pass dependencies in as constructor args. Don't pull them from globals.
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
# Good
|
|
109
|
+
CreateOrder(orderRepo, userRepo, paymentGateway, eventBus)
|
|
110
|
+
|
|
111
|
+
# Bad
|
|
112
|
+
class CreateOrder:
|
|
113
|
+
def execute():
|
|
114
|
+
order_repo = Container.get("OrderRepository") # hidden dep
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Any framework DI container that ends up manipulating constructor signatures reflectively becomes impossible to reason about. Prefer explicit wiring in a composition root.
|
|
118
|
+
|
|
119
|
+
## Composition root
|
|
120
|
+
|
|
121
|
+
One file where everything is wired up.
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
# main / bootstrap
|
|
125
|
+
db = Postgres(config.url)
|
|
126
|
+
cache = Redis(config.redis_url)
|
|
127
|
+
eventBus = Kafka(config.brokers)
|
|
128
|
+
|
|
129
|
+
userRepo = PostgresUserRepository(db)
|
|
130
|
+
orderRepo = CachedOrderRepository(
|
|
131
|
+
PostgresOrderRepository(db),
|
|
132
|
+
cache,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
createOrder = CreateOrder(orderRepo, userRepo, PaymentStripe(config.key), eventBus)
|
|
136
|
+
|
|
137
|
+
app.register("POST /orders", lambda req: http_create_order(req, createOrder))
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
All layering choices become visible in this one file.
|
|
141
|
+
|
|
142
|
+
## Transaction boundary
|
|
143
|
+
|
|
144
|
+
The use case decides where the transaction starts and ends, not the repository.
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
class TransferMoney:
|
|
148
|
+
execute(cmd):
|
|
149
|
+
with unitOfWork.begin():
|
|
150
|
+
src = accountRepo.findById(cmd.fromId)
|
|
151
|
+
dst = accountRepo.findById(cmd.toId)
|
|
152
|
+
src.withdraw(cmd.amount)
|
|
153
|
+
dst.deposit(cmd.amount)
|
|
154
|
+
accountRepo.save(src)
|
|
155
|
+
accountRepo.save(dst)
|
|
156
|
+
# commit happens here; rollback on exception
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
One transaction per use case, not per repository call. If a use case needs multiple transactions, it's probably two use cases.
|
|
160
|
+
|
|
161
|
+
## Anti-patterns
|
|
162
|
+
|
|
163
|
+
| Anti-pattern | Why bad | Fix |
|
|
164
|
+
|---|---|---|
|
|
165
|
+
| Framework objects (HTTP request/response) inside the domain | Couples domain to HTTP | Parse at delivery, pass plain command |
|
|
166
|
+
| Repository returns a DB row | Leaks schema upward | Map to domain object at the edge |
|
|
167
|
+
| Controller calls the DB directly | Skips domain rules | Every write goes through a use case |
|
|
168
|
+
| ORM entities ARE the domain entities | Can't change storage without rewriting rules | Separate persistence model from domain model |
|
|
169
|
+
| Static "Service" class with 40 unrelated methods | No cohesion; everything imports everything | One use case per class |
|
|
170
|
+
| Domain event published before persistence succeeds | Consumers act on data that doesn't exist | Publish after commit, or use transactional outbox |
|
|
171
|
+
|
|
172
|
+
## Don't over-engineer
|
|
173
|
+
|
|
174
|
+
A 500-line CRUD service doesn't need four layers, a DI container, and a port-adapter diagram. Start simple:
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
# Acceptable for small services
|
|
178
|
+
handler -> repository -> db
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Introduce the extra seams **when you feel the pain**: when tests get hard, when the DB needs replacing, when rules start repeating across endpoints. Layering is a response to complexity, not a prerequisite for it.
|