codecall 1.3.0 → 1.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/README.md +22 -0
- package/package.json +1 -1
- package/plugins/codecall/.claude-plugin/plugin.json +1 -1
- package/plugins/codecall/.codex-plugin/plugin.json +1 -1
- package/plugins/codecall/agents/codecall-planner.md +6 -1
- package/plugins/codecall/skills/codecall/SKILL.md +20 -3
- package/plugins/codecall/skills/codecall/references/backend-map/api.md +113 -0
- package/plugins/codecall/skills/codecall/references/backend-map/architecture.md +65 -0
- package/plugins/codecall/skills/codecall/references/backend-map/async.md +81 -0
- package/plugins/codecall/skills/codecall/references/backend-map/auth.md +97 -0
- package/plugins/codecall/skills/codecall/references/backend-map/caching.md +65 -0
- package/plugins/codecall/skills/codecall/references/backend-map/data.md +144 -0
- package/plugins/codecall/skills/codecall/references/backend-map/http.md +97 -0
- package/plugins/codecall/skills/codecall/references/backend-map/index.md +165 -0
- package/plugins/codecall/skills/codecall/references/backend-map/observability.md +65 -0
- package/plugins/codecall/skills/codecall/references/backend-map/ops.md +129 -0
- package/plugins/codecall/skills/codecall/references/backend-map/quality.md +81 -0
- package/plugins/codecall/skills/codecall/references/backend-map/realtime.md +65 -0
- package/plugins/codecall/skills/codecall/references/backend-map/reliability.md +65 -0
- package/plugins/codecall/skills/codecall/references/backend-map/scaling.md +81 -0
- package/plugins/codecall/skills/codecall/references/backend-map/security.md +97 -0
- package/plugins/codecall/skills/codecall/references/backend-map/storage.md +81 -0
- package/plugins/codecall/skills/codecall/references/receipt-template.html +7 -3
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Databases
|
|
2
|
+
|
|
3
|
+
## schema-design-constraints
|
|
4
|
+
|
|
5
|
+
- **Name:** Schema design and constraints
|
|
6
|
+
- **Requires:** none
|
|
7
|
+
- **Signals:** `NOT NULL`, `UNIQUE`, `REFERENCES`, `CHECK (`, `ON DELETE CASCADE`, `unique=True`
|
|
8
|
+
- **Idea:** Types and constraints encode rules your data must always obey, so the database rejects bad rows even when a code path forgets to validate.
|
|
9
|
+
- **Misconceptions:**
|
|
10
|
+
- App validation makes constraints redundant; scripts, races and other services bypass it, and only the database sees every write.
|
|
11
|
+
- Foreign keys are just slow; they prevent orphaned rows, and their cost is usually an index on the referencing column.
|
|
12
|
+
- **Checks:**
|
|
13
|
+
- Which rule in your change lives only in application code, and what bad row could a concurrent request or script insert?
|
|
14
|
+
- What should happen to child rows when the parent is deleted, and does your `ON DELETE` choice match?
|
|
15
|
+
- **Go deeper:**
|
|
16
|
+
- [Sriniously · Backend from first principles #12 · 1:01:37](https://www.youtube.com/watch?v=F7Vwp2Xo5Do&t=3697s)
|
|
17
|
+
- [PostgreSQL docs: Constraints](https://www.postgresql.org/docs/current/ddl-constraints.html)
|
|
18
|
+
|
|
19
|
+
## database-migrations
|
|
20
|
+
|
|
21
|
+
- **Name:** Database migrations
|
|
22
|
+
- **Requires:** `schema-design-constraints`
|
|
23
|
+
- **Signals:** `migrations/`, `ALTER TABLE`, `knex migrate`, `alembic upgrade`, `manage.py migrate`, `CREATE INDEX CONCURRENTLY`
|
|
24
|
+
- **Idea:** Versioned, ordered scripts change the schema the same way in every environment, and must stay compatible with the code version still running during deploys.
|
|
25
|
+
- **Misconceptions:**
|
|
26
|
+
- Migrations are instant; adding a constraint or rewriting a big table can block writes for minutes.
|
|
27
|
+
- Renaming or dropping a column in one step is fine; old instances still query it mid-deploy, so expand first and contract later.
|
|
28
|
+
- **Checks:**
|
|
29
|
+
- On a large production table, what locks does your migration take, and which requests wait behind it?
|
|
30
|
+
- If this deploy rolls back after your migration ran, can the previous code still use the new schema?
|
|
31
|
+
- **Go deeper:**
|
|
32
|
+
- [Sriniously · Backend from first principles #12 · 53:33](https://www.youtube.com/watch?v=F7Vwp2Xo5Do&t=3213s)
|
|
33
|
+
- [Martin Fowler: Evolutionary Database Design](https://martinfowler.com/articles/evodb.html)
|
|
34
|
+
|
|
35
|
+
## database-indexes
|
|
36
|
+
|
|
37
|
+
- **Name:** Database indexes
|
|
38
|
+
- **Requires:** `schema-design-constraints`
|
|
39
|
+
- **Signals:** `CREATE INDEX`, `EXPLAIN ANALYZE`, `Seq Scan`, `@@index([`, `db_index=True`, composite `(user_id, created_at)`
|
|
40
|
+
- **Idea:** An index is a sorted lookup structure that finds matching rows without scanning the table, trading write cost and storage for fast reads.
|
|
41
|
+
- **Misconceptions:**
|
|
42
|
+
- More indexes are always better; every insert and update must maintain each one, and unused indexes only cost writes.
|
|
43
|
+
- A composite index helps any query on its columns; it serves filters on its leading columns, so column order matters.
|
|
44
|
+
- **Checks:**
|
|
45
|
+
- Does `EXPLAIN` show an index scan for your main query, and which column order serves both its filter and `ORDER BY`?
|
|
46
|
+
- Which new index in your code slows a write-heavy table, and is the read it speeds up worth it?
|
|
47
|
+
- **Go deeper:**
|
|
48
|
+
- [Sriniously · Backend from first principles #21.1 · 53:55](https://www.youtube.com/watch?v=z7kt_p44rjs&t=3235s)
|
|
49
|
+
- [PostgreSQL docs: Indexes](https://www.postgresql.org/docs/current/indexes.html)
|
|
50
|
+
|
|
51
|
+
## n-plus-one-queries
|
|
52
|
+
|
|
53
|
+
- **Name:** The N+1 query problem
|
|
54
|
+
- **Requires:** none
|
|
55
|
+
- **Signals:** `await` inside a `for` loop, `select_related(`, `prefetch_related(`, `.includes(`, `DataLoader`
|
|
56
|
+
- **Idea:** Loading a list then querying once per item turns one page into N+1 round trips, so fetch related data in a join or one batched query.
|
|
57
|
+
- **Misconceptions:**
|
|
58
|
+
- Each query is fast, so it is fine; round-trip latency multiplies with N and grows silently as data grows.
|
|
59
|
+
- The ORM handles it; lazy-loaded relations accessed in templates or serializers are exactly what triggers it.
|
|
60
|
+
- **Checks:**
|
|
61
|
+
- If your endpoint returns 100 items, how many queries does your code send, and where does each extra one start?
|
|
62
|
+
- Would a join or one `WHERE id IN (...)` batch suit your code better, given how many related rows each item has?
|
|
63
|
+
- **Go deeper:**
|
|
64
|
+
- [Sriniously · Backend from first principles #21.1 · 42:55](https://www.youtube.com/watch?v=z7kt_p44rjs&t=2575s)
|
|
65
|
+
- [Microsoft Learn: Efficient Querying in EF Core](https://learn.microsoft.com/en-us/ef/core/performance/efficient-querying)
|
|
66
|
+
|
|
67
|
+
## connection-pooling
|
|
68
|
+
|
|
69
|
+
- **Name:** Connection pooling
|
|
70
|
+
- **Requires:** none
|
|
71
|
+
- **Signals:** `new Pool(`, `pool.connect()`, `SetMaxOpenConns`, `pool_size=`, `maximumPoolSize`, `PgBouncer`
|
|
72
|
+
- **Idea:** Opening a database connection is slow and servers allow few of them, so a pool keeps a bounded set open and lends them to requests.
|
|
73
|
+
- **Misconceptions:**
|
|
74
|
+
- A bigger pool means more throughput; past the database's capacity, extra connections add contention, and replicas multiply the total.
|
|
75
|
+
- Connections return themselves; a client not released on an error path leaks until the pool is empty.
|
|
76
|
+
- **Checks:**
|
|
77
|
+
- Given your pool size and maximum instance count, how many connections could hit the database at peak, versus its `max_connections`?
|
|
78
|
+
- On the error path in your handler, what guarantees the connection or transaction is released back to the pool?
|
|
79
|
+
- **Go deeper:**
|
|
80
|
+
- [Sriniously · Backend from first principles #21.1 · 1:07:20](https://www.youtube.com/watch?v=z7kt_p44rjs&t=4040s)
|
|
81
|
+
- [Go docs: Managing connections](https://go.dev/doc/database/manage-connections)
|
|
82
|
+
|
|
83
|
+
## transactions-isolation
|
|
84
|
+
|
|
85
|
+
- **Name:** Transactions and isolation levels
|
|
86
|
+
- **Requires:** `schema-design-constraints`
|
|
87
|
+
- **Signals:** `BEGIN` / `COMMIT`, `SERIALIZABLE`, `SELECT ... FOR UPDATE`, `@Transactional`, `transaction.atomic`
|
|
88
|
+
- **Idea:** A transaction makes statements succeed or fail together, and its isolation level decides which concurrency anomalies, like lost updates, your code can still see.
|
|
89
|
+
- **Misconceptions:**
|
|
90
|
+
- A transaction alone prevents races; at the default Read Committed, read-then-write logic still loses updates without row locks or stronger isolation.
|
|
91
|
+
- Serializable needs no code changes; it aborts conflicting transactions with serialization errors your code must retry.
|
|
92
|
+
- **Checks:**
|
|
93
|
+
- If two requests run your read-then-update code at the same moment, what isolation level or lock stops one from overwriting the other?
|
|
94
|
+
- Does your transaction stay open across a network call, and how long could it hold locks?
|
|
95
|
+
- **Go deeper:**
|
|
96
|
+
- [PostgreSQL docs: Transaction Isolation](https://www.postgresql.org/docs/current/transaction-iso.html)
|
|
97
|
+
|
|
98
|
+
## optimistic-concurrency
|
|
99
|
+
|
|
100
|
+
- **Name:** Optimistic concurrency and conditional writes
|
|
101
|
+
- **Requires:** `transactions-isolation`
|
|
102
|
+
- **Signals:** `version` column, `WHERE id = $1 AND version = $2`, `If-Match`, `@Version`, `ConditionExpression`, `rowCount === 0`
|
|
103
|
+
- **Idea:** Instead of locking, a write succeeds only if the row still matches the version you read, so concurrent edits are detected rather than silently overwritten.
|
|
104
|
+
- **Misconceptions:**
|
|
105
|
+
- The update succeeded because no error was thrown; zero affected rows is the conflict signal, and code must check it.
|
|
106
|
+
- Optimistic always beats locking; under heavy contention most writes fail and retry, so pessimistic locks can be cheaper.
|
|
107
|
+
- **Checks:**
|
|
108
|
+
- When your update affects zero rows because the version changed, what does your code return, and is the user's edit lost?
|
|
109
|
+
- How often do two writers touch the same row in your feature, and does that favor version checks or `SELECT ... FOR UPDATE`?
|
|
110
|
+
- **Go deeper:**
|
|
111
|
+
- [Sriniously · Backend from first principles #23 · 1:03:09](https://www.youtube.com/watch?v=Ie0TjKI9cDI&t=3789s)
|
|
112
|
+
- [Martin Fowler: Optimistic Offline Lock](https://martinfowler.com/eaaCatalog/optimisticOfflineLock.html)
|
|
113
|
+
|
|
114
|
+
## read-replicas-sharding
|
|
115
|
+
|
|
116
|
+
- **Name:** Read replicas and sharding
|
|
117
|
+
- **Requires:** `database-indexes`
|
|
118
|
+
- **Signals:** `DATABASE_REPLICA_URL`, `readReplicas(`, `using('replica')`, `shard_key`, `tenant_id` routing, `replication lag`
|
|
119
|
+
- **Idea:** Replicas copy the primary to spread reads, while sharding splits data across databases by key to spread writes, each adding consistency and routing costs.
|
|
120
|
+
- **Misconceptions:**
|
|
121
|
+
- A replica read right after a write returns that write; asynchronous replication lags, so read-your-writes needs the primary.
|
|
122
|
+
- Sharding is the first fix for a slow database; indexes, caching and replicas come first, since cross-shard queries are hard.
|
|
123
|
+
- **Checks:**
|
|
124
|
+
- After your code writes a row, which following read could hit a lagging replica, and what would the user see?
|
|
125
|
+
- If your table were sharded by the key your code uses, which query would need to touch every shard?
|
|
126
|
+
- **Go deeper:**
|
|
127
|
+
- [Sriniously · Backend from first principles #21.2 · 27:47](https://www.youtube.com/watch?v=sOhAopEwjH4&t=1667s)
|
|
128
|
+
- [Microsoft Learn: Sharding pattern](https://learn.microsoft.com/en-us/azure/architecture/patterns/sharding)
|
|
129
|
+
|
|
130
|
+
## full-text-search
|
|
131
|
+
|
|
132
|
+
- **Name:** Full-text search and inverted indexes
|
|
133
|
+
- **Requires:** `database-indexes`
|
|
134
|
+
- **Signals:** `to_tsvector(`, `@@ to_tsquery`, `USING GIN`, `ILIKE '%`, `_search` (Elasticsearch), `pg_trgm`
|
|
135
|
+
- **Idea:** An inverted index maps each normalized word to the documents containing it, giving ranked, stemmed search that `LIKE` scans cannot match at scale.
|
|
136
|
+
- **Misconceptions:**
|
|
137
|
+
- `ILIKE '%term%'` with a B-tree index is fast; a leading wildcard cannot use it, so every row is scanned.
|
|
138
|
+
- The search engine is the source of truth; it is an eventually consistent copy needing reindexing on change.
|
|
139
|
+
- **Checks:**
|
|
140
|
+
- On a million rows, does your search query use an index that supports word matching and ranking?
|
|
141
|
+
- When a record changes, how does your code update the search index, and how stale can results get?
|
|
142
|
+
- **Go deeper:**
|
|
143
|
+
- [Sriniously · Backend from first principles #15 · 8:48](https://www.youtube.com/watch?v=7_sovzAhRSM&t=528s)
|
|
144
|
+
- [PostgreSQL docs: Full Text Search Introduction](https://www.postgresql.org/docs/current/textsearch-intro.html)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# HTTP
|
|
2
|
+
|
|
3
|
+
## http-messages
|
|
4
|
+
|
|
5
|
+
- **Name:** HTTP request and response anatomy
|
|
6
|
+
- **Requires:** none
|
|
7
|
+
- **Signals:** `req.headers`, `Content-Type`, `res.status(`, `request.body`, `http.ResponseWriter`, `w.Header().Set(`
|
|
8
|
+
- **Idea:** Every HTTP exchange is a request (method, path, headers, optional body) answered by a response (status, headers, optional body), and handlers only work when they read and write each part deliberately.
|
|
9
|
+
- **Misconceptions:**
|
|
10
|
+
- The body explains itself; servers parse it according to the `Content-Type` header, so a missing or wrong header gives you an empty or garbled body.
|
|
11
|
+
- Header names are case-sensitive; HTTP treats them case-insensitively, and HTTP/2 and HTTP/3 send them lowercased on the wire.
|
|
12
|
+
- **Checks:**
|
|
13
|
+
- If a client sends this endpoint form-encoded data instead of JSON, what does your handler receive, and where would you see the mismatch?
|
|
14
|
+
- Which headers does your handler set on the response, and what would a client do differently if one were missing?
|
|
15
|
+
- **Go deeper:**
|
|
16
|
+
- [Sriniously · Backend from first principles #5 · 7:29](https://www.youtube.com/watch?v=a3C1DMswClQ&t=449s)
|
|
17
|
+
- [MDN: HTTP messages](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Messages)
|
|
18
|
+
|
|
19
|
+
## http-methods-idempotency
|
|
20
|
+
|
|
21
|
+
- **Name:** HTTP methods and idempotency
|
|
22
|
+
- **Requires:** `http-messages`
|
|
23
|
+
- **Signals:** `app.get(`, `router.post(`, `@PutMapping`, `@app.delete`, `PATCH`, `http.MethodPost`
|
|
24
|
+
- **Idea:** The method declares intent: GET reads safely, PUT and DELETE are idempotent so repeating them leaves the same state, and POST and PATCH may not be, which decides what clients and proxies can safely retry.
|
|
25
|
+
- **Misconceptions:**
|
|
26
|
+
- Idempotent means the response is identical every time; it means the server state ends the same, so a second DELETE may return 404 and still be idempotent.
|
|
27
|
+
- A GET that changes data is fine if the UI only calls it on click; crawlers, prefetchers and retrying proxies will call it too.
|
|
28
|
+
- **Checks:**
|
|
29
|
+
- If a network timeout makes the client resend this request, what does your handler do to the data the second time?
|
|
30
|
+
- Your code updates a record with this method; would a client sending only changed fields expect PUT or PATCH semantics, and which does it actually implement?
|
|
31
|
+
- **Go deeper:**
|
|
32
|
+
- [Sriniously · Backend from first principles #5 · 18:03](https://www.youtube.com/watch?v=a3C1DMswClQ&t=1083s)
|
|
33
|
+
- [RFC 9110: HTTP Semantics, Idempotent Methods](https://www.rfc-editor.org/rfc/rfc9110#section-9.2.2)
|
|
34
|
+
|
|
35
|
+
## http-status-codes
|
|
36
|
+
|
|
37
|
+
- **Name:** Status codes as a contract
|
|
38
|
+
- **Requires:** `http-messages`
|
|
39
|
+
- **Signals:** `res.status(404)`, `HttpStatus.CREATED`, `status.HTTP_422_UNPROCESSABLE_ENTITY`, `http.StatusConflict`, `raise HTTPException(`
|
|
40
|
+
- **Idea:** The status code is the machine-readable outcome clients, retries, caches and monitoring act on, so choosing between 2xx, 4xx and 5xx correctly matters more than the message text.
|
|
41
|
+
- **Misconceptions:**
|
|
42
|
+
- Returning 200 with `{"error": ...}` is harmless; clients, caches and alerting treat it as success and never react to the failure.
|
|
43
|
+
- 401 and 403 are interchangeable; 401 means not authenticated, 403 means authenticated but not allowed.
|
|
44
|
+
- **Checks:**
|
|
45
|
+
- When validation fails versus when the database is down, which status does your handler return for each, and would a retrying client behave correctly?
|
|
46
|
+
- If your handler throws an unexpected error, does the client get a 500 with a safe message, or a 200 or a leaked stack trace?
|
|
47
|
+
- **Go deeper:**
|
|
48
|
+
- [Sriniously · Backend from first principles #5 · 38:51](https://www.youtube.com/watch?v=a3C1DMswClQ&t=2331s)
|
|
49
|
+
- [MDN: HTTP response status codes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status)
|
|
50
|
+
|
|
51
|
+
## cors
|
|
52
|
+
|
|
53
|
+
- **Name:** CORS and preflight requests
|
|
54
|
+
- **Requires:** `http-messages`
|
|
55
|
+
- **Signals:** `Access-Control-Allow-Origin`, `cors()`, `CORSMiddleware`, `@CrossOrigin`, `OPTIONS` preflight
|
|
56
|
+
- **Idea:** Browsers block scripts from reading cross-origin responses unless the server opts in with CORS headers, and non-simple requests first send an OPTIONS preflight the server must answer.
|
|
57
|
+
- **Misconceptions:**
|
|
58
|
+
- CORS protects your API from attackers; it only restricts browsers, so curl, scripts and other servers call your endpoints regardless.
|
|
59
|
+
- `Access-Control-Allow-Origin: *` works with cookies; browsers reject a wildcard on credentialed requests, so the exact origin must be echoed back.
|
|
60
|
+
- **Checks:**
|
|
61
|
+
- Your frontend sends JSON with an `Authorization` header; which preflight response headers must your server return, and what breaks if OPTIONS hits auth middleware first?
|
|
62
|
+
- If your code reflects any request `Origin` while allowing credentials, what could a malicious site do with a logged-in user's browser?
|
|
63
|
+
- **Go deeper:**
|
|
64
|
+
- [Sriniously · Backend from first principles #5 · 20:07](https://www.youtube.com/watch?v=a3C1DMswClQ&t=1207s)
|
|
65
|
+
- [MDN: Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS)
|
|
66
|
+
|
|
67
|
+
## http-caching-headers
|
|
68
|
+
|
|
69
|
+
- **Name:** HTTP caching headers
|
|
70
|
+
- **Requires:** `http-messages`
|
|
71
|
+
- **Signals:** `Cache-Control`, `ETag`, `If-None-Match`, `304 Not Modified`, `Last-Modified`, `Vary`
|
|
72
|
+
- **Idea:** Response headers tell browsers and CDNs whether and how long they may reuse a response, and validators like ETag let them revalidate cheaply instead of downloading it again.
|
|
73
|
+
- **Misconceptions:**
|
|
74
|
+
- `no-cache` means never store; it means store but revalidate before each use, while `no-store` forbids storing.
|
|
75
|
+
- Per-user responses can share a CDN cache safely; without `private` or a correct `Vary`, one user's data can be served to another.
|
|
76
|
+
- **Checks:**
|
|
77
|
+
- Your endpoint returns user-specific data; which `Cache-Control` value does your code send, and could a shared cache ever serve it to someone else?
|
|
78
|
+
- After you deploy a change to this response, how long might clients keep seeing the old version, and what in your headers decides that?
|
|
79
|
+
- **Go deeper:**
|
|
80
|
+
- [Sriniously · Backend from first principles #5 · 55:20](https://www.youtube.com/watch?v=a3C1DMswClQ&t=3320s)
|
|
81
|
+
- [MDN: HTTP caching](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Caching)
|
|
82
|
+
|
|
83
|
+
## tls-https
|
|
84
|
+
|
|
85
|
+
- **Name:** TLS and HTTPS
|
|
86
|
+
- **Requires:** `http-messages`
|
|
87
|
+
- **Signals:** `Strict-Transport-Security`, `https.createServer(`, `ListenAndServeTLS`, `ssl_certificate`, `X-Forwarded-Proto`, `rejectUnauthorized`
|
|
88
|
+
- **Idea:** TLS encrypts and authenticates the connection using certificates, so tokens, cookies and data cannot be read or altered in transit and clients know they reached the real server.
|
|
89
|
+
- **Misconceptions:**
|
|
90
|
+
- Turning off certificate verification in a client is fine for internal calls; it removes the authentication and lets anyone in the path impersonate the server.
|
|
91
|
+
- HTTPS at the load balancer means the app sees HTTPS; behind a TLS-terminating proxy the app must trust `X-Forwarded-Proto` to know.
|
|
92
|
+
- **Checks:**
|
|
93
|
+
- Your app sits behind a proxy that terminates TLS; how does your code decide to set `Secure` cookies or build https redirect URLs?
|
|
94
|
+
- Does any outbound call in your code disable certificate verification, and what would an attacker on that network path gain?
|
|
95
|
+
- **Go deeper:**
|
|
96
|
+
- [Sriniously · Backend from first principles #5 · 1:15:18](https://www.youtube.com/watch?v=a3C1DMswClQ&t=4518s)
|
|
97
|
+
- [OWASP Cheat Sheet: Transport Layer Security](https://cheatsheetseries.owasp.org/cheatsheets/Transport_Layer_Security_Cheat_Sheet.html)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Backend concept map: index
|
|
2
|
+
|
|
3
|
+
Generated by `scripts/backend-map.mjs`; edit the cluster files, not this one.
|
|
4
|
+
|
|
5
|
+
Use only when the change touches server-side code. Match concepts by what the
|
|
6
|
+
change actually does; signals are hints, not proof. Pick at most four, never
|
|
7
|
+
force a match, and fall back to free-form concepts when nothing fits. Then read
|
|
8
|
+
only the cluster files that hold the chosen ids.
|
|
9
|
+
|
|
10
|
+
## api.md: API design
|
|
11
|
+
|
|
12
|
+
| id | requires | signals |
|
|
13
|
+
| --- | --- | --- |
|
|
14
|
+
| routing-params | http-messages | `/users/:id`, `req.params`, `req.query`, `r.PathValue(` |
|
|
15
|
+
| api-versioning | routing-params | `/api/v1/`, `/v2/`, `Deprecation`, `Sunset` |
|
|
16
|
+
| serialization-json | http-messages | `JSON.stringify(`, `json.Marshal(`, `express.json()`, `@JsonProperty` |
|
|
17
|
+
| input-validation | serialization-json | `z.object(`, `pydantic.BaseModel`, `@Valid`, `Joi.object(` |
|
|
18
|
+
| rest-resource-design | routing-params, http-methods-idempotency | `GET /orders/{id}`, `POST /orders`, `/users/{id}/orders`, `Location` |
|
|
19
|
+
| pagination-filtering | rest-resource-design | `LIMIT`, `OFFSET`, `?cursor=`, `starting_after` |
|
|
20
|
+
| openapi-contract | rest-resource-design | `openapi.yaml`, `openapi: 3.1`, `swagger-ui`, `@ApiOperation` |
|
|
21
|
+
|
|
22
|
+
## architecture.md: Request flow and layering
|
|
23
|
+
|
|
24
|
+
| id | requires | signals |
|
|
25
|
+
| --- | --- | --- |
|
|
26
|
+
| middleware-pipeline | http-messages | `app.use(`, `next()`, `MIDDLEWARE = [`, `func(next http.Handler)` |
|
|
27
|
+
| request-context | middleware-pipeline | `context.Context`, `r.Context()`, `res.locals`, `req.user` |
|
|
28
|
+
| handler-service-repository | routing-params | `controllers/`, `services/`, `repositories/`, `@Service` |
|
|
29
|
+
| dependency-injection | handler-service-repository | `constructor(private readonly`, `Depends(`, `@Autowired`, `@Injectable()` |
|
|
30
|
+
|
|
31
|
+
## async.md: Background work and webhooks
|
|
32
|
+
|
|
33
|
+
| id | requires | signals |
|
|
34
|
+
| --- | --- | --- |
|
|
35
|
+
| background-jobs | - | `BullMQ`, `@shared_task`, `perform_later`, `@Async` |
|
|
36
|
+
| idempotency-keys | http-methods-idempotency | `Idempotency-Key`, `ON CONFLICT DO NOTHING`, `processed_events` |
|
|
37
|
+
| transactional-outbox | transactions-isolation, background-jobs | `outbox_events`, `INSERT INTO outbox`, `FOR UPDATE SKIP LOCKED`, `published_at IS NULL` |
|
|
38
|
+
| webhook-receiver | idempotency-keys | `/webhooks/stripe`, `express.raw(`, `@csrf_exempt`, `switch (event.type)` |
|
|
39
|
+
| webhook-signatures | webhook-receiver | `Stripe-Signature`, `X-Hub-Signature-256`, `crypto.timingSafeEqual`, `hmac.compare_digest` |
|
|
40
|
+
|
|
41
|
+
## auth.md: Authentication and authorization
|
|
42
|
+
|
|
43
|
+
| id | requires | signals |
|
|
44
|
+
| --- | --- | --- |
|
|
45
|
+
| session-auth | http-messages | `express-session`, `req.session`, `SESSION_ENGINE`, `connect-redis` |
|
|
46
|
+
| cookie-security | session-auth | `HttpOnly`, `Secure`, `SameSite=Lax`, `__Host-` |
|
|
47
|
+
| jwt-auth | http-messages | `Authorization: Bearer`, `jwt.verify(`, `jsonwebtoken`, `PyJWT` |
|
|
48
|
+
| oauth2-oidc | jwt-auth | `/oauth/authorize`, `code_challenge`, `redirect_uri`, `id_token` |
|
|
49
|
+
| password-hashing | - | `argon2id`, `bcrypt.hash(`, `scrypt`, `PBKDF2` |
|
|
50
|
+
| rbac-authorization | session-auth | `@PreAuthorize`, `hasRole(`, `requireRole(`, `user.role ===` |
|
|
51
|
+
|
|
52
|
+
## caching.md: Caching
|
|
53
|
+
|
|
54
|
+
| id | requires | signals |
|
|
55
|
+
| --- | --- | --- |
|
|
56
|
+
| cache-aside | - | `redis.get(`, `redis.set(`, `cache.get_or_set(`, `@Cacheable` |
|
|
57
|
+
| cache-invalidation | cache-aside | `redis.del(`, `cache.delete(`, `@CacheEvict`, `revalidateTag(` |
|
|
58
|
+
| cache-eviction-ttl | cache-aside | `EX 300`, `EXPIRE`, `maxmemory-policy`, `allkeys-lru` |
|
|
59
|
+
| cdn-edge-caching | http-caching-headers | `Cache-Control: public, s-maxage=`, `Surrogate-Key`, `CDN-Cache-Control`, `Vary` |
|
|
60
|
+
|
|
61
|
+
## data.md: Databases
|
|
62
|
+
|
|
63
|
+
| id | requires | signals |
|
|
64
|
+
| --- | --- | --- |
|
|
65
|
+
| schema-design-constraints | - | `NOT NULL`, `UNIQUE`, `REFERENCES`, `CHECK (` |
|
|
66
|
+
| database-migrations | schema-design-constraints | `migrations/`, `ALTER TABLE`, `knex migrate`, `alembic upgrade` |
|
|
67
|
+
| database-indexes | schema-design-constraints | `CREATE INDEX`, `EXPLAIN ANALYZE`, `Seq Scan`, `@@index([` |
|
|
68
|
+
| n-plus-one-queries | - | `await`, `for`, `select_related(`, `prefetch_related(` |
|
|
69
|
+
| connection-pooling | - | `new Pool(`, `pool.connect()`, `SetMaxOpenConns`, `pool_size=` |
|
|
70
|
+
| transactions-isolation | schema-design-constraints | `BEGIN`, `COMMIT`, `SERIALIZABLE`, `SELECT ... FOR UPDATE` |
|
|
71
|
+
| optimistic-concurrency | transactions-isolation | `version`, `WHERE id = $1 AND version = $2`, `If-Match`, `@Version` |
|
|
72
|
+
| read-replicas-sharding | database-indexes | `DATABASE_REPLICA_URL`, `readReplicas(`, `using('replica')`, `shard_key` |
|
|
73
|
+
| full-text-search | database-indexes | `to_tsvector(`, `@@ to_tsquery`, `USING GIN`, `ILIKE '%` |
|
|
74
|
+
|
|
75
|
+
## http.md: HTTP
|
|
76
|
+
|
|
77
|
+
| id | requires | signals |
|
|
78
|
+
| --- | --- | --- |
|
|
79
|
+
| http-messages | - | `req.headers`, `Content-Type`, `res.status(`, `request.body` |
|
|
80
|
+
| http-methods-idempotency | http-messages | `app.get(`, `router.post(`, `@PutMapping`, `@app.delete` |
|
|
81
|
+
| http-status-codes | http-messages | `res.status(404)`, `HttpStatus.CREATED`, `status.HTTP_422_UNPROCESSABLE_ENTITY`, `http.StatusConflict` |
|
|
82
|
+
| cors | http-messages | `Access-Control-Allow-Origin`, `cors()`, `CORSMiddleware`, `@CrossOrigin` |
|
|
83
|
+
| http-caching-headers | http-messages | `Cache-Control`, `ETag`, `If-None-Match`, `304 Not Modified` |
|
|
84
|
+
| tls-https | http-messages | `Strict-Transport-Security`, `https.createServer(`, `ListenAndServeTLS`, `ssl_certificate` |
|
|
85
|
+
|
|
86
|
+
## observability.md: Observability
|
|
87
|
+
|
|
88
|
+
| id | requires | signals |
|
|
89
|
+
| --- | --- | --- |
|
|
90
|
+
| structured-logging | - | `pino`, `structlog`, `log/slog`, `logger.info({` |
|
|
91
|
+
| metrics-monitoring | - | `prom-client`, `prometheus_client`, `/metrics`, `http_requests_total` |
|
|
92
|
+
| distributed-tracing | request-context | `traceparent`, `@opentelemetry/api`, `tracer.start_as_current_span`, `otelhttp.NewHandler` |
|
|
93
|
+
| latency-percentiles | metrics-monitoring | `histogram_quantile(0.99`, `http_request_duration_seconds`, `_bucket{le=`, `p99` |
|
|
94
|
+
|
|
95
|
+
## ops.md: Configuration and delivery
|
|
96
|
+
|
|
97
|
+
| id | requires | signals |
|
|
98
|
+
| --- | --- | --- |
|
|
99
|
+
| config-management | - | `process.env`, `os.environ`, `os.Getenv(`, `BaseSettings` |
|
|
100
|
+
| secrets-management | config-management | `.env.example`, `aws secretsmanager`, `vault kv get`, `secretKeyRef` |
|
|
101
|
+
| disposable-processes | stateless-services, graceful-shutdown | `SIGTERM`, `terminationGracePeriodSeconds`, `process.on("SIGTERM"`, `signal.NotifyContext` |
|
|
102
|
+
| ci-pipeline | - | `.github/workflows/`, `.gitlab-ci.yml`, `on: pull_request`, `actions/cache` |
|
|
103
|
+
| container-images | - | `Dockerfile`, `FROM node:22-alpine AS build`, `COPY package*.json ./`, `.dockerignore` |
|
|
104
|
+
| deployment-strategies | load-balancing | `strategy: RollingUpdate`, `maxUnavailable`, `maxSurge`, `canary` |
|
|
105
|
+
| infrastructure-as-code | - | `resource "aws_`, `terraform plan`, `.tfstate`, `pulumi up` |
|
|
106
|
+
| slos-incidents | metrics-monitoring | `slo:`, `error_budget`, `histogram_quantile(`, `burn rate` |
|
|
107
|
+
|
|
108
|
+
## quality.md: Testing
|
|
109
|
+
|
|
110
|
+
| id | requires | signals |
|
|
111
|
+
| --- | --- | --- |
|
|
112
|
+
| test-pyramid | - | `describe(`, `pytest`, `func TestX(t *testing.T)`, `@SpringBootTest` |
|
|
113
|
+
| test-doubles | dependency-injection | `jest.mock(`, `vi.fn()`, `unittest.mock.patch`, `@MockBean` |
|
|
114
|
+
| integration-tests-real-deps | test-pyramid | `testcontainers`, `PostgreSqlContainer`, `docker-compose.test.yml`, `services: postgres` |
|
|
115
|
+
| flaky-tests | test-pyramid | `jest.useFakeTimers`, `freezegun`, `setTimeout(`, `Date.now()` |
|
|
116
|
+
| code-coverage | test-pyramid | `--coverage`, `coverageThreshold`, `pytest-cov`, `go test -cover` |
|
|
117
|
+
|
|
118
|
+
## realtime.md: Real-time
|
|
119
|
+
|
|
120
|
+
| id | requires | signals |
|
|
121
|
+
| --- | --- | --- |
|
|
122
|
+
| polling-long-polling | http-messages | `setInterval(`, `refetchInterval`, `?since=`, `WaitTimeSeconds` |
|
|
123
|
+
| server-sent-events | polling-long-polling | `text/event-stream`, `EventSource`, `Last-Event-ID`, `StreamingResponse` |
|
|
124
|
+
| websockets | polling-long-polling | `new WebSocket(`, `Upgrade: websocket`, `socket.io`, `ws.on('message'` |
|
|
125
|
+
| pub-sub-fanout | websockets, stateless-services | `redis.publish(`, `@socket.io/redis-adapter`, `CHANNEL_LAYERS`, `pg_notify(` |
|
|
126
|
+
|
|
127
|
+
## reliability.md: Errors and reliability
|
|
128
|
+
|
|
129
|
+
| id | requires | signals |
|
|
130
|
+
| --- | --- | --- |
|
|
131
|
+
| error-handling-global | middleware-pipeline | `app.use((err, req, res, next)`, `setErrorHandler`, `@ControllerAdvice`, `@app.exception_handler` |
|
|
132
|
+
| timeouts-external-calls | - | `AbortSignal.timeout(`, `requests.get(url, timeout=`, `context.WithTimeout`, `http.Client{Timeout:` |
|
|
133
|
+
| retries-backoff-jitter | timeouts-external-calls, idempotency-keys | `Retry-After`, `p-retry`, `tenacity`, `@Retryable` |
|
|
134
|
+
| graceful-shutdown | - | `SIGTERM`, `server.close(`, `srv.Shutdown(ctx)`, `terminationGracePeriodSeconds` |
|
|
135
|
+
|
|
136
|
+
## scaling.md: Scaling and concurrency
|
|
137
|
+
|
|
138
|
+
| id | requires | signals |
|
|
139
|
+
| --- | --- | --- |
|
|
140
|
+
| stateless-services | - | `MemoryStore`, `connect-redis`, `SESSION_ENGINE`, `sticky sessions` |
|
|
141
|
+
| load-balancing | stateless-services | `upstream backend {`, `proxy_pass`, `least_conn`, `X-Forwarded-For` |
|
|
142
|
+
| io-vs-cpu-bound | - | `worker_threads`, `ProcessPoolExecutor`, `asyncio.to_thread(`, `bcrypt.hashSync` |
|
|
143
|
+
| event-loop-async | io-vs-cpu-bound | `async def`, `Promise.all(`, `asyncio.gather(`, `setImmediate` |
|
|
144
|
+
| race-conditions | - | `SELECT ... FOR UPDATE`, `pg_advisory_xact_lock`, `sync.Mutex`, `SET stock = stock - 1` |
|
|
145
|
+
|
|
146
|
+
## security.md: Security
|
|
147
|
+
|
|
148
|
+
| id | requires | signals |
|
|
149
|
+
| --- | --- | --- |
|
|
150
|
+
| sql-injection | - | `WHERE id = $1`, `cursor.execute(sql, params)`, `PreparedStatement`, `$queryRawUnsafe` |
|
|
151
|
+
| object-level-authorization | rbac-authorization | `req.params.id`, `findById(id)`, `WHERE id = $1 AND owner_id = $2`, `get_object_or_404(` |
|
|
152
|
+
| rate-limiting | http-status-codes | `429 Too Many Requests`, `Retry-After`, `express-rate-limit`, `golang.org/x/time/rate` |
|
|
153
|
+
| xss | - | `innerHTML`, `dangerouslySetInnerHTML`, `v-html`, `mark_safe(` |
|
|
154
|
+
| csrf | cookie-security | `SameSite=Lax`, `X-CSRF-Token`, `@csrf_exempt`, `csrf().disable()` |
|
|
155
|
+
| ssrf | - | `fetch(req.body.url)`, `requests.get(url)`, `http.Get(userURL)`, `169.254.169.254` |
|
|
156
|
+
|
|
157
|
+
## storage.md: Files and object storage
|
|
158
|
+
|
|
159
|
+
| id | requires | signals |
|
|
160
|
+
| --- | --- | --- |
|
|
161
|
+
| object-storage | stateless-services | `PutObjectCommand`, `boto3.client("s3")`, `@google-cloud/storage`, `S3_BUCKET` |
|
|
162
|
+
| presigned-urls | object-storage | `getSignedUrl`, `generate_presigned_url`, `PresignPutObject`, `X-Amz-Signature` |
|
|
163
|
+
| streaming-uploads | - | `stream.pipeline`, `req.pipe(`, `busboy`, `multer.memoryStorage` |
|
|
164
|
+
| multipart-upload | object-storage | `CreateMultipartUpload`, `UploadPart`, `CompleteMultipartUpload`, `AbortIncompleteMultipartUpload` |
|
|
165
|
+
| range-requests | http-messages | `Range: bytes=`, `Accept-Ranges: bytes`, `206 Partial Content`, `Content-Range` |
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Observability
|
|
2
|
+
|
|
3
|
+
## structured-logging
|
|
4
|
+
|
|
5
|
+
- **Name:** Structured logging and log levels
|
|
6
|
+
- **Requires:** none
|
|
7
|
+
- **Signals:** `pino`, `structlog`, `log/slog`, `logger.info({`, `LOG_LEVEL`, `logstash-logback-encoder`
|
|
8
|
+
- **Idea:** Logs emitted as key-value JSON with a level, timestamp and request id can be filtered and queried in production, unlike free-text lines that someone must grep and guess at.
|
|
9
|
+
- **Misconceptions:**
|
|
10
|
+
- More logging is always safer; logging every request body at info level costs money, buries the signal and often leaks passwords, tokens or personal data.
|
|
11
|
+
- Interpolating values into the message string is structured logging; put variables in separate fields so queries can filter on `user_id` or `order_id`.
|
|
12
|
+
- **Checks:**
|
|
13
|
+
- If this request fails in production, which fields in your log lines would let you find every line from that one request?
|
|
14
|
+
- Which values logged by your code could contain secrets or personal data, and where are they redacted?
|
|
15
|
+
- **Go deeper:**
|
|
16
|
+
- [Sriniously · Backend from first principles #18 · 18:12](https://www.youtube.com/watch?v=5PEuwgLOQQM&t=1092s)
|
|
17
|
+
- [OpenTelemetry: Logs](https://opentelemetry.io/docs/concepts/signals/logs/)
|
|
18
|
+
|
|
19
|
+
## metrics-monitoring
|
|
20
|
+
|
|
21
|
+
- **Name:** Metrics and monitoring
|
|
22
|
+
- **Requires:** none
|
|
23
|
+
- **Signals:** `prom-client`, `prometheus_client`, `/metrics`, `http_requests_total`, `Micrometer`, `histogram_quantile(`
|
|
24
|
+
- **Idea:** Metrics are cheap numeric time series such as request rate, error rate, latency and saturation that you aggregate, graph and alert on, so you learn a service is unhealthy before users report it.
|
|
25
|
+
- **Misconceptions:**
|
|
26
|
+
- Adding user id or full URL as a metric label is harmless; each unique value creates a new series, and high cardinality can overwhelm the metrics backend.
|
|
27
|
+
- Alerting on CPU and memory covers you; alert on symptoms users feel, such as error rate and latency, and use resource metrics for diagnosis.
|
|
28
|
+
- **Checks:**
|
|
29
|
+
- Which metric would move first if the code you just changed started failing, and is there an alert on it?
|
|
30
|
+
- What labels does your code attach to this metric, and how many distinct values could each one take?
|
|
31
|
+
- **Go deeper:**
|
|
32
|
+
- [Sriniously · Backend from first principles #18 · 3:57](https://www.youtube.com/watch?v=5PEuwgLOQQM&t=237s)
|
|
33
|
+
- [Google SRE book: Monitoring distributed systems](https://sre.google/sre-book/monitoring-distributed-systems/)
|
|
34
|
+
|
|
35
|
+
## distributed-tracing
|
|
36
|
+
|
|
37
|
+
- **Name:** Distributed tracing
|
|
38
|
+
- **Requires:** `request-context`
|
|
39
|
+
- **Signals:** `traceparent`, `@opentelemetry/api`, `tracer.start_as_current_span`, `otelhttp.NewHandler`, `OTEL_EXPORTER_OTLP_ENDPOINT`, `span.setAttribute(`
|
|
40
|
+
- **Idea:** Tracing gives each request a trace id that travels across services and queues, recording timed spans for every hop so you can see where a slow or failing request spent its time.
|
|
41
|
+
- **Misconceptions:**
|
|
42
|
+
- Installing the tracing SDK traces everything; context is lost across queues, hand-rolled HTTP clients and thread pools unless `traceparent` is propagated explicitly.
|
|
43
|
+
- Tracing every request in production is free; most systems sample, so a missing trace for one request does not mean the request never happened.
|
|
44
|
+
- **Checks:**
|
|
45
|
+
- When your code enqueues a job or calls another service here, how does the current trace context get passed along?
|
|
46
|
+
- If this endpoint becomes slow, which spans in your code would show whether the time goes to the database, an external API or your own logic?
|
|
47
|
+
- **Go deeper:**
|
|
48
|
+
- [Sriniously · Backend from first principles #18 · 28:10](https://www.youtube.com/watch?v=5PEuwgLOQQM&t=1690s)
|
|
49
|
+
- [OpenTelemetry: Traces](https://opentelemetry.io/docs/concepts/signals/traces/)
|
|
50
|
+
|
|
51
|
+
## latency-percentiles
|
|
52
|
+
|
|
53
|
+
- **Name:** Latency percentiles
|
|
54
|
+
- **Requires:** `metrics-monitoring`
|
|
55
|
+
- **Signals:** `histogram_quantile(0.99`, `http_request_duration_seconds`, `_bucket{le=`, `p99`, `buckets:`
|
|
56
|
+
- **Idea:** Percentiles like p50, p95 and p99 show how slow requests are for typical users and for the unlucky tail, which averages hide even though busy users hit that tail constantly.
|
|
57
|
+
- **Misconceptions:**
|
|
58
|
+
- Average latency is a fine health signal; a few very slow requests barely move the mean while a real slice of users waits seconds.
|
|
59
|
+
- You can average p99 values across instances; percentiles do not combine that way, so compute them from merged histogram buckets instead.
|
|
60
|
+
- **Checks:**
|
|
61
|
+
- When your code fans out to several services per request, how does the slowest call's tail latency shape the latency of the whole request?
|
|
62
|
+
- Do the histogram buckets in your code have enough resolution around your latency target to tell 300 ms from 900 ms?
|
|
63
|
+
- **Go deeper:**
|
|
64
|
+
- [Sriniously · Backend from first principles #21.1 · 8:00](https://www.youtube.com/watch?v=z7kt_p44rjs&t=480s)
|
|
65
|
+
- [Prometheus: Histograms and summaries](https://prometheus.io/docs/practices/histograms/)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Configuration and delivery
|
|
2
|
+
|
|
3
|
+
## config-management
|
|
4
|
+
|
|
5
|
+
- **Name:** Configuration from the environment
|
|
6
|
+
- **Requires:** none
|
|
7
|
+
- **Signals:** `process.env`, `os.environ`, `os.Getenv(`, `BaseSettings`, `dotenv`, `@ConfigurationProperties`
|
|
8
|
+
- **Idea:** Values that differ between deploys, like database URLs, ports and feature flags, are read from the environment at startup, so one build runs unchanged in dev, staging and production.
|
|
9
|
+
- **Misconceptions:**
|
|
10
|
+
- Reading `process.env.X` wherever it is needed is fine; parse and validate once at startup so a missing value fails the boot, not a request later.
|
|
11
|
+
- Config and code constants are the same; values that never change between deploys belong in code, not an ever-growing list of variables.
|
|
12
|
+
- **Checks:**
|
|
13
|
+
- If a required variable is missing in production, does your code fail at startup or on the first request that needs it?
|
|
14
|
+
- Which value hardcoded in your code would have to change between staging and production, and how would you move it to config?
|
|
15
|
+
- **Go deeper:**
|
|
16
|
+
- [Sriniously · Backend from first principles #17 · 26:41](https://www.youtube.com/watch?v=GR9NtirPXyc&t=1601s)
|
|
17
|
+
- [The Twelve-Factor App: Config](https://12factor.net/config)
|
|
18
|
+
|
|
19
|
+
## secrets-management
|
|
20
|
+
|
|
21
|
+
- **Name:** Secrets management
|
|
22
|
+
- **Requires:** `config-management`
|
|
23
|
+
- **Signals:** `.env.example`, `aws secretsmanager`, `vault kv get`, `secretKeyRef`, `${{ secrets.`, `SecretStr`
|
|
24
|
+
- **Idea:** Credentials are kept out of code and images, injected at runtime from a secret store with access control, auditing and rotation, so a leaked repo or log does not leak them.
|
|
25
|
+
- **Misconceptions:**
|
|
26
|
+
- Deleting a committed secret in a later commit fixes the leak; it stays in history and forks, so it must be rotated.
|
|
27
|
+
- Environment variables are secure storage; they leak through crash dumps, debug endpoints, child processes and logs that print the config.
|
|
28
|
+
- **Checks:**
|
|
29
|
+
- If the API key in your code leaked today, how would you rotate it, and would your service need a redeploy to pick it up?
|
|
30
|
+
- Where could your code accidentally print a secret, such as an error message, request log or config dump, and how would you redact it?
|
|
31
|
+
- **Go deeper:**
|
|
32
|
+
- [Sriniously · Backend from first principles #17 · 31:03](https://www.youtube.com/watch?v=GR9NtirPXyc&t=1863s)
|
|
33
|
+
- [OWASP Cheat Sheet: Secrets Management](https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html)
|
|
34
|
+
|
|
35
|
+
## disposable-processes
|
|
36
|
+
|
|
37
|
+
- **Name:** Disposable, stateless processes (12-factor)
|
|
38
|
+
- **Requires:** `stateless-services`, `graceful-shutdown`
|
|
39
|
+
- **Signals:** `SIGTERM`, `terminationGracePeriodSeconds`, `process.on("SIGTERM"`, `signal.NotifyContext`, `replicas:`, `readinessProbe`
|
|
40
|
+
- **Idea:** Each process starts fast, keeps no state it cannot lose and shuts down cleanly on a signal, so the platform can kill, restart or scale it at any moment safely.
|
|
41
|
+
- **Misconceptions:**
|
|
42
|
+
- A process only stops on deploys; autoscalers, node drains and crashes kill it anytime, so in-memory sessions and queues get lost.
|
|
43
|
+
- Shutting down means exiting immediately; in-flight requests and jobs should finish or be handed back before the process exits.
|
|
44
|
+
- **Checks:**
|
|
45
|
+
- If the platform sends your process SIGTERM mid-request, what happens to that request and to any job your code is running?
|
|
46
|
+
- What state does your code keep in memory or on local disk that would vanish if this instance were replaced right now?
|
|
47
|
+
- **Go deeper:**
|
|
48
|
+
- [Sriniously · Backend from first principles #27 · 50:16](https://www.youtube.com/watch?v=2_fcv3-XfN4&t=3016s)
|
|
49
|
+
- [The Twelve-Factor App: Disposability](https://12factor.net/disposability)
|
|
50
|
+
|
|
51
|
+
## ci-pipeline
|
|
52
|
+
|
|
53
|
+
- **Name:** Continuous integration pipelines
|
|
54
|
+
- **Requires:** none
|
|
55
|
+
- **Signals:** `.github/workflows/`, `.gitlab-ci.yml`, `on: pull_request`, `actions/cache`, `Jenkinsfile`, `npm ci`
|
|
56
|
+
- **Idea:** Every push is built and tested automatically in a clean environment, so integration problems surface within minutes on the change that caused them instead of at release time.
|
|
57
|
+
- **Misconceptions:**
|
|
58
|
+
- Passing locally means CI will pass; CI starts clean, so missing lockfile entries, env vars and machine-specific state show up there.
|
|
59
|
+
- A red build can wait; once main stays broken, every later failure hides behind it and people stop trusting the pipeline.
|
|
60
|
+
- **Checks:**
|
|
61
|
+
- Which checks run on your change before merge, and which bug class in your code would still reach main untested?
|
|
62
|
+
- If your pipeline takes 25 minutes, which steps would you cache, parallelize or move later, and what feedback would that delay?
|
|
63
|
+
- **Go deeper:**
|
|
64
|
+
- [Sriniously · Backend from first principles #30 · 15:50](https://www.youtube.com/watch?v=_E8X5RIJfQ4&t=950s)
|
|
65
|
+
- [martinfowler.com: Continuous Integration](https://martinfowler.com/articles/continuousIntegration.html)
|
|
66
|
+
|
|
67
|
+
## container-images
|
|
68
|
+
|
|
69
|
+
- **Name:** Containers and image layers
|
|
70
|
+
- **Requires:** none
|
|
71
|
+
- **Signals:** `Dockerfile`, `FROM node:22-alpine AS build`, `COPY package*.json ./`, `.dockerignore`, `USER node`, `docker build`
|
|
72
|
+
- **Idea:** An image is a stack of cached, read-only layers built from Dockerfile steps, so ordering steps from least to most often changed keeps rebuilds fast and images small.
|
|
73
|
+
- **Misconceptions:**
|
|
74
|
+
- Deleting a file in a later layer removes it from the image; it still ships in the earlier layer, including any secret copied in.
|
|
75
|
+
- Copying the whole project before installing dependencies is harmless; any source change invalidates the cache and reinstalls everything.
|
|
76
|
+
- **Checks:**
|
|
77
|
+
- In your Dockerfile, which line invalidates the cache on every code change, and how would reordering steps speed up rebuilds?
|
|
78
|
+
- What ends up in your final image that production does not need, such as compilers, tests or `.git`, and how would a multi-stage build drop it?
|
|
79
|
+
- **Go deeper:**
|
|
80
|
+
- [Sriniously · Backend from first principles #30 · 38:18](https://www.youtube.com/watch?v=_E8X5RIJfQ4&t=2298s)
|
|
81
|
+
- [Docker docs: Understanding the image layers](https://docs.docker.com/get-started/docker-concepts/building-images/understanding-image-layers/)
|
|
82
|
+
|
|
83
|
+
## deployment-strategies
|
|
84
|
+
|
|
85
|
+
- **Name:** Rolling, blue-green and canary deploys
|
|
86
|
+
- **Requires:** `load-balancing`
|
|
87
|
+
- **Signals:** `strategy: RollingUpdate`, `maxUnavailable`, `maxSurge`, `canary`, `blue-green`, `trafficRouting`
|
|
88
|
+
- **Idea:** Rolling, blue-green and canary deploys shift traffic to a new version gradually or switchably, so a bad release reaches few users and can be rolled back without downtime.
|
|
89
|
+
- **Misconceptions:**
|
|
90
|
+
- Blue-green makes rollback free; if the new version already migrated the schema or wrote new data, switching back can break the old version.
|
|
91
|
+
- During a rolling deploy only one version runs; old and new serve traffic together, so APIs and schema must work with both.
|
|
92
|
+
- **Checks:**
|
|
93
|
+
- While your deploy is half rolled out, which requests could hit the old version after the new one wrote data, and would that break?
|
|
94
|
+
- What metric would tell you a canary of your change is bad, and how quickly could your setup roll it back?
|
|
95
|
+
- **Go deeper:**
|
|
96
|
+
- [Sriniously · Backend from first principles #30 · 1:16:26](https://www.youtube.com/watch?v=_E8X5RIJfQ4&t=4586s)
|
|
97
|
+
- [Google SRE Workbook: Canarying Releases](https://sre.google/workbook/canarying-releases/)
|
|
98
|
+
|
|
99
|
+
## infrastructure-as-code
|
|
100
|
+
|
|
101
|
+
- **Name:** Infrastructure as code
|
|
102
|
+
- **Requires:** none
|
|
103
|
+
- **Signals:** `resource "aws_`, `terraform plan`, `.tfstate`, `pulumi up`, `AWSTemplateFormatVersion`, `cdk deploy`
|
|
104
|
+
- **Idea:** Servers, networks, databases and permissions are declared in versioned files that a tool applies, so environments are reproducible, reviewed like code and not hand-configured in a console.
|
|
105
|
+
- **Misconceptions:**
|
|
106
|
+
- Once infrastructure is in code, console tweaks are harmless; manual changes create drift that the next apply silently reverts or fights.
|
|
107
|
+
- The state file is a disposable artifact; losing or corrupting it makes the tool forget what it owns, and it often holds secrets.
|
|
108
|
+
- **Checks:**
|
|
109
|
+
- Before your change applies, what in the plan output would warn you that a resource will be destroyed and recreated instead of updated?
|
|
110
|
+
- Where does your project keep its state file, who can read it, and what stops two people applying at once?
|
|
111
|
+
- **Go deeper:**
|
|
112
|
+
- [Sriniously · Backend from first principles #30 · 1:25:18](https://www.youtube.com/watch?v=_E8X5RIJfQ4&t=5118s)
|
|
113
|
+
- [martinfowler.com: Infrastructure As Code](https://martinfowler.com/bliki/InfrastructureAsCode.html)
|
|
114
|
+
|
|
115
|
+
## slos-incidents
|
|
116
|
+
|
|
117
|
+
- **Name:** SLOs, error budgets and incidents
|
|
118
|
+
- **Requires:** `metrics-monitoring`
|
|
119
|
+
- **Signals:** `slo:`, `error_budget`, `histogram_quantile(`, `burn rate`, `availability: 99.9`, `postmortem`
|
|
120
|
+
- **Idea:** An SLO sets a target for a user-visible indicator like success rate or latency, and the error budget left over decides when to ship faster or stop and fix reliability.
|
|
121
|
+
- **Misconceptions:**
|
|
122
|
+
- The goal is 100% reliability; it is unattainable and costly, and users cannot tell 99.99% from 100% behind their own networks.
|
|
123
|
+
- Alert on every CPU spike or error; page on symptoms that burn the error budget, not causes users never notice.
|
|
124
|
+
- **Checks:**
|
|
125
|
+
- Which user-facing indicator would you measure for the endpoint in your code, and what latency or success threshold would count as good?
|
|
126
|
+
- If your change caused errors for 5% of requests for ten minutes, how much of a 99.9% monthly budget would that burn?
|
|
127
|
+
- **Go deeper:**
|
|
128
|
+
- [Sriniously · Backend from first principles #30 · 1:33:01](https://www.youtube.com/watch?v=_E8X5RIJfQ4&t=5581s)
|
|
129
|
+
- [Google SRE Book: Service Level Objectives](https://sre.google/sre-book/service-level-objectives/)
|