@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.
Files changed (147) hide show
  1. package/README.md +59 -4
  2. package/dist/commands/consoleCommand.d.ts +2 -0
  3. package/dist/commands/consoleCommand.d.ts.map +1 -0
  4. package/dist/commands/consoleCommand.js +129 -0
  5. package/dist/commands/consoleCommand.js.map +1 -0
  6. package/dist/commands/projectInit.d.ts.map +1 -1
  7. package/dist/commands/projectInit.js +40 -53
  8. package/dist/commands/projectInit.js.map +1 -1
  9. package/dist/commands/setup.d.ts.map +1 -1
  10. package/dist/commands/setup.js +14 -2
  11. package/dist/commands/setup.js.map +1 -1
  12. package/dist/commands/skillCommand.d.ts +2 -0
  13. package/dist/commands/skillCommand.d.ts.map +1 -0
  14. package/dist/commands/skillCommand.js +235 -0
  15. package/dist/commands/skillCommand.js.map +1 -0
  16. package/dist/console-assets/assets/index-Bhd2f9AP.js +125 -0
  17. package/dist/console-assets/assets/index-bsAN2a12.css +1 -0
  18. package/dist/console-assets/index.html +16 -0
  19. package/dist/console-server/index.d.ts +29 -0
  20. package/dist/console-server/index.d.ts.map +1 -0
  21. package/dist/console-server/index.js +145 -0
  22. package/dist/console-server/index.js.map +1 -0
  23. package/dist/console-server/lib/lockFile.d.ts +17 -0
  24. package/dist/console-server/lib/lockFile.d.ts.map +1 -0
  25. package/dist/console-server/lib/lockFile.js +61 -0
  26. package/dist/console-server/lib/lockFile.js.map +1 -0
  27. package/dist/console-server/lib/portPicker.d.ts +3 -0
  28. package/dist/console-server/lib/portPicker.d.ts.map +1 -0
  29. package/dist/console-server/lib/portPicker.js +25 -0
  30. package/dist/console-server/lib/portPicker.js.map +1 -0
  31. package/dist/console-server/routes/projects.d.ts +11 -0
  32. package/dist/console-server/routes/projects.d.ts.map +1 -0
  33. package/dist/console-server/routes/projects.js +149 -0
  34. package/dist/console-server/routes/projects.js.map +1 -0
  35. package/dist/console-server/routes/system.d.ts +7 -0
  36. package/dist/console-server/routes/system.d.ts.map +1 -0
  37. package/dist/console-server/routes/system.js +19 -0
  38. package/dist/console-server/routes/system.js.map +1 -0
  39. package/dist/console-server/sse/eventBus.d.ts +25 -0
  40. package/dist/console-server/sse/eventBus.d.ts.map +1 -0
  41. package/dist/console-server/sse/eventBus.js +32 -0
  42. package/dist/console-server/sse/eventBus.js.map +1 -0
  43. package/dist/console-server/watchers/cardWatcher.d.ts +9 -0
  44. package/dist/console-server/watchers/cardWatcher.d.ts.map +1 -0
  45. package/dist/console-server/watchers/cardWatcher.js +42 -0
  46. package/dist/console-server/watchers/cardWatcher.js.map +1 -0
  47. package/dist/core/skillStore.d.ts +46 -0
  48. package/dist/core/skillStore.d.ts.map +1 -0
  49. package/dist/core/skillStore.js +210 -0
  50. package/dist/core/skillStore.js.map +1 -0
  51. package/dist/core/skillStore.test.d.ts +2 -0
  52. package/dist/core/skillStore.test.d.ts.map +1 -0
  53. package/dist/core/skillStore.test.js +203 -0
  54. package/dist/core/skillStore.test.js.map +1 -0
  55. package/dist/main.js +27 -17
  56. package/dist/main.js.map +1 -1
  57. package/package.json +8 -2
  58. package/skills/architecture-decision-records/SKILL.md +207 -0
  59. package/skills/backend/SKILL.md +62 -0
  60. package/skills/backend/references/api-design.md +168 -0
  61. package/skills/backend/references/caching.md +181 -0
  62. package/skills/backend/references/data-access.md +173 -0
  63. package/skills/backend/references/layering.md +181 -0
  64. package/skills/backend/references/observability.md +190 -0
  65. package/skills/backend/references/resilience.md +201 -0
  66. package/skills/backend/references/security.md +186 -0
  67. package/skills/backend-architect/SKILL.md +119 -0
  68. package/skills/code-reviewer/SKILL.md +143 -0
  69. package/skills/coding-standards/SKILL.md +60 -0
  70. package/skills/coding-standards/references/clean-code.md +258 -0
  71. package/skills/coding-standards/references/code-review.md +192 -0
  72. package/skills/coding-standards/references/commits-and-prs.md +226 -0
  73. package/skills/coding-standards/references/error-strategy.md +193 -0
  74. package/skills/coding-standards/references/naming.md +185 -0
  75. package/skills/coding-standards/references/tdd.md +171 -0
  76. package/skills/database/SKILL.md +53 -0
  77. package/skills/database/references/indexing.md +190 -0
  78. package/skills/database/references/migrations.md +199 -0
  79. package/skills/database/references/nosql.md +185 -0
  80. package/skills/database/references/queries.md +295 -0
  81. package/skills/database/references/scaling.md +203 -0
  82. package/skills/database/references/schema.md +191 -0
  83. package/skills/database-optimizer/SKILL.md +168 -0
  84. package/skills/debugging-workflow/SKILL.md +244 -0
  85. package/skills/devops/SKILL.md +55 -0
  86. package/skills/devops/references/ci-cd.md +204 -0
  87. package/skills/devops/references/containers.md +272 -0
  88. package/skills/devops/references/deploy.md +201 -0
  89. package/skills/devops/references/iac.md +252 -0
  90. package/skills/devops/references/observability.md +228 -0
  91. package/skills/devops/references/secrets.md +178 -0
  92. package/skills/devops-automator/SKILL.md +164 -0
  93. package/skills/frontend/SKILL.md +52 -0
  94. package/skills/frontend/references/accessibility.md +222 -0
  95. package/skills/frontend/references/components.md +206 -0
  96. package/skills/frontend/references/performance.md +219 -0
  97. package/skills/frontend/references/routing.md +209 -0
  98. package/skills/frontend/references/state.md +190 -0
  99. package/skills/frontend/references/testing.md +216 -0
  100. package/skills/frontend-developer/SKILL.md +115 -0
  101. package/skills/git-workflow/SKILL.md +355 -0
  102. package/skills/golang/SKILL.md +49 -0
  103. package/skills/golang/references/concurrency.md +284 -0
  104. package/skills/golang/references/errors.md +241 -0
  105. package/skills/golang/references/idioms.md +285 -0
  106. package/skills/golang/references/testing.md +238 -0
  107. package/skills/java/SKILL.md +50 -0
  108. package/skills/java/references/concurrency.md +194 -0
  109. package/skills/java/references/idioms.md +283 -0
  110. package/skills/java/references/testing.md +228 -0
  111. package/skills/kotlin/SKILL.md +47 -0
  112. package/skills/kotlin/references/coroutines.md +240 -0
  113. package/skills/kotlin/references/idioms.md +268 -0
  114. package/skills/kotlin/references/testing.md +219 -0
  115. package/skills/mobile/SKILL.md +50 -0
  116. package/skills/mobile/references/architecture.md +204 -0
  117. package/skills/mobile/references/navigation.md +158 -0
  118. package/skills/mobile/references/performance.md +152 -0
  119. package/skills/mobile/references/platform.md +166 -0
  120. package/skills/mobile/references/state-and-data.md +174 -0
  121. package/skills/python/SKILL.md +51 -0
  122. package/skills/python/THIRD_PARTY.md +14 -0
  123. package/skills/python/references/async.md +218 -0
  124. package/skills/python/references/error-handling.md +254 -0
  125. package/skills/python/references/idioms.md +279 -0
  126. package/skills/python/references/packaging.md +233 -0
  127. package/skills/python/references/testing.md +269 -0
  128. package/skills/python/references/typing.md +292 -0
  129. package/skills/qa-tester/SKILL.md +186 -0
  130. package/skills/rust/SKILL.md +50 -0
  131. package/skills/rust/references/async.md +224 -0
  132. package/skills/rust/references/errors.md +240 -0
  133. package/skills/rust/references/ownership.md +263 -0
  134. package/skills/rust/references/testing.md +274 -0
  135. package/skills/rust/references/traits.md +250 -0
  136. package/skills/security-engineer/SKILL.md +157 -0
  137. package/skills/swift/SKILL.md +48 -0
  138. package/skills/swift/references/concurrency.md +280 -0
  139. package/skills/swift/references/idioms.md +334 -0
  140. package/skills/swift/references/testing.md +229 -0
  141. package/skills/typescript/SKILL.md +51 -0
  142. package/skills/typescript/references/async.md +241 -0
  143. package/skills/typescript/references/errors.md +208 -0
  144. package/skills/typescript/references/idioms.md +246 -0
  145. package/skills/typescript/references/testing.md +225 -0
  146. package/skills/typescript/references/tooling.md +208 -0
  147. 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.