codecall 1.3.0 → 1.5.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/ARCHITECTURE.md +3 -1
- package/README.md +56 -1
- 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 +46 -7
- 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/drill-receipt-template.html +78 -0
- package/plugins/codecall/skills/codecall/references/drill.md +116 -0
- package/plugins/codecall/skills/codecall/references/receipt-template.html +9 -3
- package/plugins/codecall/skills/codecall/references/trace.md +42 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Caching
|
|
2
|
+
|
|
3
|
+
## cache-aside
|
|
4
|
+
|
|
5
|
+
- **Name:** Cache-aside pattern
|
|
6
|
+
- **Requires:** none
|
|
7
|
+
- **Signals:** `redis.get(` then `redis.set(`, `cache.get_or_set(`, `@Cacheable`, `SETEX`, `if cached == nil`
|
|
8
|
+
- **Idea:** The application checks the cache first, and on a miss reads the database and stores the result, so repeated reads skip the slow store while the database stays the source of truth.
|
|
9
|
+
- **Misconceptions:**
|
|
10
|
+
- A cache miss is harmless; when a hot key expires, many requests miss together and stampede the database unless one request refills it.
|
|
11
|
+
- The cache is always available; if it is down or slow, your code should fall back to the database rather than fail the request.
|
|
12
|
+
- **Checks:**
|
|
13
|
+
- If the cached key for this endpoint expires during peak traffic, how many requests in your code hit the database at once to rebuild it?
|
|
14
|
+
- What does your handler do when the cache call times out, and does that turn a cache outage into a full outage?
|
|
15
|
+
- **Go deeper:**
|
|
16
|
+
- [Sriniously · Backend from first principles #21.1 · 1:17:35](https://www.youtube.com/watch?v=z7kt_p44rjs&t=4655s)
|
|
17
|
+
- [Microsoft Learn: Cache-Aside pattern](https://learn.microsoft.com/en-us/azure/architecture/patterns/cache-aside)
|
|
18
|
+
|
|
19
|
+
## cache-invalidation
|
|
20
|
+
|
|
21
|
+
- **Name:** Cache invalidation
|
|
22
|
+
- **Requires:** `cache-aside`
|
|
23
|
+
- **Signals:** `redis.del(`, `cache.delete(`, `@CacheEvict`, `revalidateTag(`, `user:${id}` cache keys, `PUBLISH` invalidation event
|
|
24
|
+
- **Idea:** When the underlying data changes, cached copies must be deleted or updated, otherwise readers keep getting the old value until it happens to expire.
|
|
25
|
+
- **Misconceptions:**
|
|
26
|
+
- Deleting the key before the database write is safe; a concurrent reader can refill the old value in between, so update first, then delete.
|
|
27
|
+
- Invalidating one key is enough; lists, counts and aggregates that include the changed row are cached under other keys too.
|
|
28
|
+
- **Checks:**
|
|
29
|
+
- After your code updates this record, which cached keys still contain the old value, including lists or summaries that embed it?
|
|
30
|
+
- If the database write succeeds but your cache delete fails, how long does stale data survive, and what bounds it?
|
|
31
|
+
- **Go deeper:**
|
|
32
|
+
- [Sriniously · Backend from first principles #21.1 · 1:17:35](https://www.youtube.com/watch?v=z7kt_p44rjs&t=4655s)
|
|
33
|
+
- [Microsoft Learn: Caching guidance](https://learn.microsoft.com/en-us/azure/architecture/best-practices/caching)
|
|
34
|
+
|
|
35
|
+
## cache-eviction-ttl
|
|
36
|
+
|
|
37
|
+
- **Name:** Eviction policies and TTLs
|
|
38
|
+
- **Requires:** `cache-aside`
|
|
39
|
+
- **Signals:** `EX 300`, `EXPIRE`, `maxmemory-policy`, `allkeys-lru`, `ttl=`, `lru-cache`
|
|
40
|
+
- **Idea:** A TTL bounds how stale an entry can get, while an eviction policy decides what to drop when memory fills, together keeping the cache fresh and within its size.
|
|
41
|
+
- **Misconceptions:**
|
|
42
|
+
- A longer TTL is always better; it raises the hit rate but also how long users see outdated data after a change.
|
|
43
|
+
- Unbounded in-process caches are fine; without a size limit they grow until the process runs out of memory.
|
|
44
|
+
- **Checks:**
|
|
45
|
+
- How stale can the data your code caches safely be, and does the TTL you chose match that rather than a round number?
|
|
46
|
+
- If many keys in your code share one TTL and were written together, what happens to the database when they all expire at once?
|
|
47
|
+
- **Go deeper:**
|
|
48
|
+
- [Sriniously · Backend from first principles #13 · 42:48](https://www.youtube.com/watch?v=estH64OkwxU&t=2568s)
|
|
49
|
+
- [Redis docs: Key eviction](https://redis.io/docs/latest/develop/reference/eviction/)
|
|
50
|
+
|
|
51
|
+
## cdn-edge-caching
|
|
52
|
+
|
|
53
|
+
- **Name:** CDNs and edge caching
|
|
54
|
+
- **Requires:** `http-caching-headers`
|
|
55
|
+
- **Signals:** `Cache-Control: public, s-maxage=`, `Surrogate-Key`, `CDN-Cache-Control`, `Vary`, `CF-Cache-Status`, purge API call
|
|
56
|
+
- **Idea:** A CDN keeps copies of responses at servers near users, cutting latency and origin load, with your cache headers deciding what is shared and for how long.
|
|
57
|
+
- **Misconceptions:**
|
|
58
|
+
- Only static files belong on a CDN; public API responses can be edge-cached too with short `s-maxage` and stale-while-revalidate.
|
|
59
|
+
- Marking a response `public` is harmless; if it is personalized and the cache key ignores the cookie, the CDN serves one user's data to others.
|
|
60
|
+
- **Checks:**
|
|
61
|
+
- Does any response your code marks cacheable depend on the cookie, auth header or language, and how does the CDN key keep users apart?
|
|
62
|
+
- After your code updates content, how does the edge copy get refreshed: by TTL expiry, a purge call or a versioned URL?
|
|
63
|
+
- **Go deeper:**
|
|
64
|
+
- [Sriniously · Backend from first principles #21.2 · 51:22](https://www.youtube.com/watch?v=sOhAopEwjH4&t=3082s)
|
|
65
|
+
- [Cloudflare docs: Origin Cache Control](https://developers.cloudflare.com/cache/concepts/cache-control/)
|
|
@@ -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/)
|