Roadmap · 26 modules · First principles

Backend Engineering — From First Principles

End-to-end backend curriculum: how a request travels the wire, hits HTTP, gets parsed, validated, routed, authorized, processed by business logic, queried against DB/cache/search, observed, and shipped. Each module: concepts → patterns → code → gotchas → interview lens.

MODULE 01 — FOUNDATIONS

Request Flow End-to-End

Browser → DNS → TCP → TLS → HTTP → LB → server → response. Every hop matters.

The full path

browser │ ├─ DNS lookup (recursive resolver → root → TLD → authoritative) │ ├─ TCP 3-way handshake (SYN → SYN-ACK → ACK) [1 RTT] ├─ TLS 1.3 handshake (ClientHello → cert → finished) [1 RTT] │ ├─ HTTP request bytes ──► public internet ──► ISP ──► transit ──► cloud edge │ │ │ ▼ │ CDN / Cloudflare / AWS edge │ │ │ ▼ │ Load Balancer (L7) │ │ │ ▼ │ Application server │ (routing → middleware │ → controller → service │ → DB / cache / queue) │ │ ◄────────────────── HTTP response (status, headers, body) ◄───────────┘

Hops & what each does

HopLayerJob
DNSAppResolve api.example.com → IP. Cached at OS, browser, resolver.
Firewall / NATL3/L4SNAT private → public IP. Drops disallowed traffic.
CDN edgeL7Serve static/cached. Terminate TLS close to user.
WAFL7OWASP rules: block SQLi/XSS patterns, geo blocks.
Load balancerL4 / L7Spread across servers. Health-check pools. Sticky or stateless.
API gatewayL7Auth, rate limit, transform, route to upstream services.
App serverL7Run your code: middleware chain → handler → response.

The response: structure

HTTP/2 200 OK
content-type: application/json; charset=utf-8
content-length: 87
cache-control: private, max-age=60
x-request-id: 7f3a-1c2b
date: Mon, 11 May 2026 09:14:22 GMT

{"id": 42, "name": "alice", "email": "a@x.com"}
MODULE 02 — PROTOCOL

HTTP Protocol

Message structure, headers, methods, CORS, status codes, caching, versions, TLS.

Raw message format

# request
POST /api/users HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer eyJ...
Content-Length: 41

{"email":"a@x.com","password":"hunter2"}

# response
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/users/42

{"id":42,"email":"a@x.com"}

Header families

FamilyExamplesPurpose
RequestHost, User-Agent, Accept, AuthorizationDescribe sender + intent
RepresentationalContent-Type, Content-Encoding, Content-Length, ETagDescribe body bytes
GeneralDate, Connection, Cache-Control, ViaApply both directions
SecurityStrict-Transport-Security, X-Frame-Options, Content-Security-Policy, X-Content-Type-OptionsBrowser hardening

Methods & semantics

MethodSafeIdempotentBodyUse
GETRead resource
HEADHeaders only (existence/size check)
OPTIONSCORS pre-flight, capability discovery
POSTCreate / non-idempotent action
PUTFull replace at known URI
PATCH✗*Partial update (*idempotent w/ JSON Merge Patch)
DELETERemove resource

CORS — Cross-Origin Resource Sharing

Browser enforces same-origin policy. Server opts other origins in via headers.

Simple request

Pre-flight

# browser sends first:
OPTIONS /api/users HTTP/1.1
Origin: https://app.x.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: authorization, content-type

# server replies:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.x.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: authorization, content-type
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400

Status codes — ones that matter

RangeCode · meaning
2xx200 OK · 201 Created · 202 Accepted (async) · 204 No Content · 206 Partial (range)
3xx301 Moved Permanent · 302 Found · 304 Not Modified (ETag hit) · 307/308 preserve method on redirect
4xx400 Bad Request · 401 Unauthorized (= unauthenticated) · 403 Forbidden · 404 Not Found · 409 Conflict · 422 Unprocessable · 429 Too Many Requests
5xx500 Internal Error · 502 Bad Gateway · 503 Service Unavailable · 504 Gateway Timeout

Caching: ETag vs max-age

# first response carries ETag
HTTP/1.1 200 OK
ETag: "v1-7f3a"
Cache-Control: max-age=60, must-revalidate

# subsequent request — conditional
GET /api/users/42
If-None-Match: "v1-7f3a"

# server unchanged → no body
HTTP/1.1 304 Not Modified
ETag: "v1-7f3a"

HTTP versions

VersionTransportMultiplexingHead-of-lineHeader compression
HTTP/1.1TCP, plaintextOne req/conn (pipelining broken)App-layerNone
HTTP/2TCP, binary framesStreams over 1 connTCP-level still blocksHPACK
HTTP/3QUIC over UDPIndependent streamsNone (per-stream loss)QPACK

Content negotiation & compression

Accept: application/json;q=0.9, application/xml;q=0.5
Accept-Encoding: gzip, br, zstd
Accept-Language: en-US, en;q=0.8

# server picks best match, replies:
Content-Type: application/json
Content-Encoding: br

TLS / HTTPS

TCP 3-way + TLS 1.3 handshake (RTTs)
MODULE 03 — DISPATCH

Routing

URL → handler. Method-aware. Versioned. Grouped. Fast.

Route components

GET /api/v1/users/:userId/posts?status=published&limit=20
     │   │   │     │           │
     │   │   │     │           └─ query params (filters, paging)
     │   │   │     └─ path param (resource id)
     │   │   └─ resource (collection)
     │   └─ version
     └─ namespace

Route types

TypeExampleNotes
Static/healthO(1) hash lookup possible.
Dynamic/users/:idParam capture. Most frameworks use radix/trie.
Nested / hierarchical/orgs/:org/teams/:team/membersAuthorization often cascades.
Catchall / wildcard/files/*pathGreedy — last priority.
Regex/users/{id:\d+}Type-narrowed. Powerful, slower.

API versioning strategies

StrategyExamplePros / Cons
URI/v1/users+ Visible, cache-friendly. − Many URLs.
HeaderAPI-Version: 2+ Clean URL. − Hidden in tooling.
Query?v=2+ Trivial. − Breaks caching on shared keys.
Media typeAccept: application/vnd.x.v2+json+ RESTful. − Hardest to test.

Deprecation pattern

HTTP/1.1 200 OK
Deprecation: true
Sunset: Wed, 01 Jan 2027 00:00:00 GMT
Link: <https://api.x.com/v2/users>; rel="successor-version"
Warning: 299 - "v1 deprecated; migrate to v2 by 2027-01-01"

Route grouping

# pseudo-framework
group("/api/v1", middleware=[logger, requestId]) {
  group("/auth", middleware=[rateLimit(5, "1m")]) {
    POST("/login",   loginHandler)
    POST("/refresh", refreshHandler)
  }
  group("/admin", middleware=[requireAuth, requireRole("admin")]) {
    GET("/users",         listUsers)
    DELETE("/users/:id",  deleteUser)
  }
}
MODULE 04 — DATA ON THE WIRE

Serialization & Deserialization

Native ↔ wire bytes. Pick format by audience + perf budget.

Text vs binary

JSONXMLProtobufMessagePackAvro
Readable
SchemaoptionalXSDrequirednonerequired
Sizebaseline1.5–2×0.2–0.5×0.5×0.3×
Parse speedbaselineslow10–20× faster10×
Useweb APIslegacy/SOAPgRPC, internalcache, RPCKafka, big data

JSON deep-dive

{
  "string": "hello",
  "int":    42,
  "float":  3.14,
  "bool":   true,
  "null":   null,
  "array":  [1, 2, 3],
  "nested": { "k": "v" },
  "date":   "2026-05-11T09:14:22Z"   // ISO-8601 with offset
}

Native mapping

JSONPythonGoJS/TS
objectdictstruct / map[string]anyobject
arraylist[]TArray
numberint/floatfloat64 (or typed)number
nullNonenil / zero / pointernull

Edge cases

Schema validation (JSON Schema)

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "required": ["email", "age"],
  "additionalProperties": false,
  "properties": {
    "email": { "type": "string", "format": "email", "maxLength": 254 },
    "age":   { "type": "integer", "minimum": 0, "maximum": 150 },
    "tags":  { "type": "array", "items": { "type": "string" }, "maxItems": 20 }
  }
}
MODULE 05 — TRUST

Authentication, Authorization, Security

Who you are, what you can do, how attackers try to get past it.

Authentication mechanisms

MechanismStateHowUse
Basic authstatelessAuthorization: Basic base64(user:pass)Internal/dev. HTTPS mandatory.
API keystatelessLong random string per clientServer-to-server, partner APIs.
Session cookiestateful (server)Random ID → server lookupClassic web apps.
JWT (bearer)statelessSigned claims in tokenSPAs, mobile, microservices.
OAuth 2.0delegatedAuthorization code → access tokenThird-party app access.
OIDCOAuth + identityOAuth + id_token JWTSSO / "Login with Google".
MFA+factorTOTP, WebAuthn, SMS (weak)High-value accounts.

OAuth 2.0 + PKCE

The authorization code grant with PKCE (Proof Key for Code Exchange, RFC 7636) is the modern default. The client picks a random code_verifier, derives code_challenge = BASE64URL(SHA-256(code_verifier)) with code_challenge_method=S256, and binds the two halves of the flow together.

# 1. client generates a high-entropy secret
code_verifier  = base64url(random 32 bytes)          # 43–128 chars
code_challenge = base64url(sha256(code_verifier))    # S256

# 2. redirect user to /authorize (sends the CHALLENGE only)
GET /authorize?response_type=code
   &client_id=spa
   &redirect_uri=https://app.x.com/cb
   &scope=read:users
   &state=xyz&code_challenge=E9Mq...&code_challenge_method=S256

# 3. user authenticates → redirected back with a one-time code
GET https://app.x.com/cb?code=AUTH_CODE&state=xyz

# 4. client exchanges code at /token (sends the VERIFIER)
POST /token
grant_type=authorization_code&code=AUTH_CODE
   &redirect_uri=https://app.x.com/cb
   &client_id=spa&code_verifier=dBjftJ...
# server recomputes sha256(code_verifier), compares to stored challenge → issues tokens

JWT anatomy

header.payload.signature

# header
{ "alg": "RS256", "typ": "JWT", "kid": "key-2026-q2" }

# payload (claims)
{
  "sub": "user_42",
  "iss": "https://auth.x.com",
  "aud": "api.x.com",
  "exp": 1715420000,
  "iat": 1715416400,
  "scope": "read:users write:posts"
}

# signature = sign(base64(header) + "." + base64(payload), private_key)

JWT pitfalls

Password storage

# NEVER: plaintext, MD5, SHA-1, SHA-256 plain
# CORRECT: slow KDF with per-user salt
hash = argon2id(password, salt, m=64MB, t=3, p=1)
# alternatives: bcrypt (cost ≥ 12), scrypt, PBKDF2 (≥ 600k iter)

Authorization models

ModelDecision inputExample
RBAC role-based(user, role) → permsadmin, editor, viewer.
ABAC attribute-based(user.attrs, resource.attrs, env) → allow?"engineer in same dept can read".
ReBAC relationship-basedgraph: user → owns → docGoogle Docs sharing. Zanzibar.

OWASP-style attacks & defenses

AttackMechanismDefense
SQL injectionUntrusted input concatenated into SQLParameterized queries / prepared statements. Never string-interpolate.
NoSQL injectionObject-shaped input replaces operatorsSchema validate. Reject objects where strings expected.
XSSUntrusted HTML renderedContext-aware escaping. CSP header. HttpOnly cookies.
CSRFBrowser auto-sends cookiesSameSite=Lax/Strict, CSRF tokens, double-submit, Origin check.
MITMNetwork attacker reads trafficTLS everywhere, HSTS, cert pinning for mobile.
Insecure deserializationNative binary parsers on untrusted inputJSON only for untrusted; signed payloads for internal.
SSRFServer fetches attacker URLAllow-list URLs. Block link-local + metadata IPs (169.254.169.254).
IDORPredictable IDs without authz checkCheck ownership server-side every request. UUIDs help defense-in-depth.

Secure design principles

Attack prevention practices

MODULE 06 — INPUT HYGIENE

Validation, Transformation, Normalization

Fail fast on bad input. Normalize before processing. Sanitize before storing.

Three validation types

TypeWhat it checksExamples
TypeRight shapeString not array; integer not string.
SyntacticRight formatEmail regex, UUID, ISO date, phone.
SemanticRight meaningAge 0–150; endDate > startDate; SKU exists in catalog.

Client vs server

Transform & normalize

email   = email.strip().lower()
phone   = re.sub(r'\D', '', phone)        # digits only
country = country.upper()                  # "us" → "US"
name    = ' '.join(name.split())           # collapse spaces
slug    = slugify(title)                   # "Hello World!" → "hello-world"

Sanitization (escape, don't trust)

# HTML
clean_html = bleach.clean(user_html, tags=['p','b','i'], strip=True)
# SQL — never string-format
cur.execute("SELECT * FROM users WHERE id = %s", (user_id,))
# Shell — avoid; if must, use shlex.quote

Complex rules

Error aggregation

HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json

{
  "type": "https://x.com/errors/validation",
  "title": "Validation failed",
  "status": 422,
  "errors": [
    { "field": "email", "code": "invalid_format", "message": "not a valid email" },
    { "field": "age",   "code": "out_of_range",   "message": "must be 0–150" }
  ]
}
MODULE 07 — PIPELINE

Middleware

Cross-cutting logic in chain. Order matters more than content.

What middleware does

Canonical ordering

request ──► recovery (panic/exception catcher) ──► requestId / traceId ──► access log start ──► CORS ──► security headers (HSTS, X-Content-Type-Options, CSP) ──► body parser (json, urlencoded, multipart) ──► compression negotiation ──► rate limiter ──► authentication ──► authorization ──► validation ──► route ──► handler ──► response ◄── log finish (status, duration) ◄── error handler (if thrown)

Common middlewares

TypeExamples
Securityhelmet (sets headers), CSRF, CORS
ParsingJSON, urlencoded, multipart (file upload)
AuthJWT verify, session lookup, API-key check
Rate limittoken bucket per IP/user/route
Loggingaccess log, request-id propagation
Compressiongzip/br based on Accept-Encoding
Errorcentralized handler — maps exceptions to status codes

Rate-limiting algorithms

AlgorithmHowTrade-off
Fixed windowINCR a per-window counter (e.g. requests in current minute)Cheap, O(1) memory. Allows 2× burst at window boundary.
Sliding window logStore timestamp of every request; count those within the trailing windowExact, but memory grows with request volume.
Sliding window counterWeighted blend of current + previous fixed-window countsGood approximation; smooths the boundary burst at O(1) memory.
Leaky bucketQueue drains at a fixed rate; overflow is droppedSmooths output to a constant rate; no bursts.
Token bucketTokens refill at rate r, cap b; each request spends oneAllows bursts up to b, then throttles to r. Most common.
Token bucket: burst, drain, refill

Keep middleware lightweight

MODULE 08 — STATE

Request Context

Per-request scratch space that flows with the call — without leaking across requests.

What lives in context

Patterns

LanguagePattern
Gocontext.Context as first arg. ctx.WithValue, ctx.Done(), ctx.Deadline.
NodeAsyncLocalStorage (avoids passing through every layer).
Pythoncontextvars.ContextVar (async-safe).
JavaThreadLocal (blocking) / Context with reactive frameworks.

Request ID propagation

# inbound middleware
const reqId = req.headers['x-request-id'] ?? randomUUID()
res.setHeader('x-request-id', reqId)
ctx.set('requestId', reqId)
logger.child({ reqId })

# outbound HTTP calls
fetch(url, { headers: { 'x-request-id': ctx.get('requestId') }})

Timeouts & cancellation

# Go
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
row := db.QueryRowContext(ctx, "SELECT ...")    # aborts on timeout

# Node
const ctrl = new AbortController()
setTimeout(() => ctrl.abort(), 2000)
await fetch(url, { signal: ctrl.signal })
MODULE 09 — STRUCTURE

MVC, Controllers, REST APIs

Separation of concerns inside request path.

Layered responsibility

LayerOwnsDoesn't touch
Handler / ControllerParse req, validate, call service, shape responseSQL, business rules
Service (business logic)Use cases: placeOrder, cancelSubscriptionHTTP, DB driver specifics
Repository / DAOPersistence, queries, ORM callsBusiness rules, HTTP
ModelEntity definition, invariantsI/O

CRUD ↔ HTTP mapping

POST   /users           → create
GET    /users           → list (paginated)
GET    /users/:id       → read
PUT    /users/:id       → full replace
PATCH  /users/:id       → partial update
DELETE /users/:id       → remove
POST   /users/:id/reset → action (non-CRUD verb)

Standard list response

{
  "data": [ { "id": 1, "name": "alice" }, ... ],
  "meta": {
    "page":  2,
    "limit": 20,
    "total": 137,
    "hasMore": true
  },
  "links": {
    "self": "/users?page=2&limit=20",
    "next": "/users?page=3&limit=20",
    "prev": "/users?page=1&limit=20"
  }
}

Pagination styles

StyleProsCons
Offset (?page=N)Simple, jump to pageSlow on big tables; inconsistent with writes
Cursor (?after=cursor)Stable, fast, infinite-scroll friendlyNo "jump to page N"
Keyset (WHERE id > ?)Same as cursor; index-friendlyRequires sortable monotonic key

Search / sort / filter

GET /products?q=phone&category=electronics&minPrice=100&sort=-price,name&page=2

# parsed:
{
  q:          "phone",
  filters:    { category: "electronics", price: { gte: 100 } },
  sort:       [{ field: "price", dir: "desc" }, { field: "name", dir: "asc" }],
  page:       2
}

REST principles

MODULE 10 — PERSISTENCE

Databases

Storage shape, consistency, indexing, query plans, ORMs.

Relational vs non-relational

Relational (Postgres, MySQL)Document (Mongo)Key-value (Redis, DynamoDB)Wide-column (Cassandra)
Schemafixedflexiblenonerow-flexible
Joinsstrongweak (lookup/aggregate)nonenone
Txnfull ACIDper-doc, multi-doc limitedper-keyper-row
Scalevertical + read replicasshard by keyhorizontalhorizontal
Usetransactional, complex queriesnested objects, agile schemacache, hot keystime-series, massive write

ACID

Isolation levels vs anomalies

AnomalyRead UncommittedRead CommittedRepeatable ReadSerializable
Dirty readallowedpreventedpreventedprevented
Non-repeatable readallowedallowedpreventedprevented
Phantom readallowedallowedallowed*prevented
Write skewallowedallowedallowedprevented

*ANSI permits phantoms at Repeatable Read; Postgres RR is snapshot isolation and happens to block phantoms but still allows write skew.

CAP theorem

Under network Partition, must choose: Consistency (reject reads) or Availability (serve possibly-stale). Real systems pick on partition — most of the time partitions are rare and you have both.

Indexing — rules

Query optimization workflow

EXPLAIN ANALYZE
SELECT u.name, COUNT(o.id)
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE u.country = 'US' AND o.created_at > now() - interval '30 day'
GROUP BY u.name;

# look for:
#   Seq Scan on big table         → missing index
#   high "rows removed by filter" → predicate not pushed to index
#   Sort spilled to disk          → work_mem too low
#   Nested Loop on big rowcounts  → expected Hash/Merge join

Connection pooling

Constraints & transactions

BEGIN;
-- lock both rows in a fixed order (ascending id) to avoid deadlock:
  SELECT balance FROM accounts WHERE id IN ($1, $2) ORDER BY id FOR UPDATE;
  UPDATE accounts SET balance = balance - 100 WHERE id = $1;
  UPDATE accounts SET balance = balance + 100 WHERE id = $2;
  INSERT INTO transfers (from_id, to_id, amount) VALUES ($1, $2, 100);
COMMIT;

-- table constraints catch invariants:
CHECK (balance >= 0)
UNIQUE (email)
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE

ORMs & migrations

MODULE 11 — DOMAIN

Business Logic Layer

Where rules live. Independent of HTTP and DB drivers.

Three-layer architecture

┌──────────────────────────────────┐ │ Presentation │ routes, controllers, DTOs, │ (HTTP / gRPC / CLI) │ validation, serialization └────────────┬─────────────────────┘ │ calls ┌────────────▼─────────────────────┐ │ Business Logic │ use-case services, domain models, │ (pure, framework-agnostic) │ rules, invariants, orchestration └────────────┬─────────────────────┘ │ uses ports ┌────────────▼─────────────────────┐ │ Data Access │ repositories, ORM, SQL, cache │ (Postgres / Redis / S3 / 3rd p.) │ adapters, external clients └──────────────────────────────────┘

Why split

SOLID applied

PrincipleHow it shows up
Single responsibilityOne service = one use case. RegisterUserService.handle().
Open/closedNew auth provider = new adapter implementing AuthPort; existing code unchanged.
LiskovAny UserRepository impl must honor contract (same shapes/errors).
Interface segregationReadOnlyUserRepo vs full repo for handlers that only read.
Dependency inversionService depends on EmailSenderPort (interface), not SendgridClient (concrete).

Error propagation pattern

# BLL throws domain errors
class DomainError(Exception): ...
class NotFound(DomainError): ...
class Forbidden(DomainError): ...
class Conflict(DomainError): ...

# presentation layer maps to HTTP
{
  NotFound:  404,
  Forbidden: 403,
  Conflict:  409,
  Validation: 422,
  DomainError: 500,
}[type(e)]
MODULE 12 — SPEED

Caching

Trade staleness for latency. Choose layer, strategy, eviction with intent.

Caching layers

LayerLatencyScopeExample
CPU L1/L2/L3nsPer-core
App in-memoryµsPer-processLRU map, Caffeine
Distributed0.5–2 msClusterRedis, Memcached
CDN edge1–30 msRegionCloudflare, CloudFront
Browser0 msPer-clientHTTP cache

Strategies

StrategyReadWriteUse
Cache-aside (lazy)app checks cache → on miss reads DB → fills cacheapp writes DB → invalidates cacheDefault. Most common.
Read-throughcache lib reads DB on misssameCleaner code, library-dependent.
Write-throughapp writes cache → cache writes DB synchronouslyConsistent cache, slower writes.
Write-behindapp writes cache; DB written asyncFast writes; risk on crash.

Eviction policies

Invalidation patterns

# by key
cache.delete(f"user:{id}")

# by tag (Redis-stack, Varnish)
cache.delete_by_tag(f"user:{id}")

# fan-out (pub/sub)
pubsub.publish("cache:invalidate", {"keys": [f"user:{id}"]})

# versioned key — never need to delete
cache.set(f"user:{id}:v{version}", data)
# bump version on write → old key TTLs out naturally

Use-case recipes

MODULE 13 — ASYNC WORK

Queues, Background Jobs, Emails

Don't make user wait. Hand off to workers.

What belongs off request path

Architecture

┌─────────┐ enqueue ┌────────┐ pull ┌────────┐ │Producer │ ─────────────► │ Broker │ ───────────►│Consumer│ │ (API) │ │ (Redis,│ │(worker)│ └─────────┘ │ SQS, │◄─── ack ────└────┬───┘ │ Kafka) │ │ └────────┘ ▼ side effects: DB, email, S3, API

Broker comparison

Redis (BullMQ, Sidekiq)RabbitMQSQSKafka
Modellist/streamAMQP exchangesdistributed queuepartitioned log
OrderFIFO per queueFIFO per queueFIFO queue typeFIFO per partition
DurabilityRDB/AOFpersistent queuesmulti-AZreplicated log
Replayfull
Best forweb appscomplex routingAWS-native, simpleevent-sourcing, analytics

Job semantics

Transactional outbox

"Update the DB and publish an event atomically" has no safe answer with a naive dual write (write DB, then call the broker): if the commit succeeds but the publish fails — or vice versa — the two stores diverge and there is no rollback across them.

-- write the event in the SAME transaction as the state change
BEGIN;
  INSERT INTO orders (id, status) VALUES ($1, 'paid');
  INSERT INTO outbox (id, topic, payload, published)
    VALUES ($2, 'order.paid', $3, false);
COMMIT;
-- a separate relay (poll the outbox, or Debezium CDC tailing the WAL)
-- reads unpublished rows, publishes to the broker, then marks them done

Chaining & concurrency

# BullMQ-style flow
const flow = new FlowProducer()
await flow.add({
  name: 'order-complete',
  queueName: 'orders',
  children: [
    { name: 'charge-card',     queueName: 'payments' },
    { name: 'send-receipt',    queueName: 'email'    },
    { name: 'update-warehouse',queueName: 'inventory'},
  ],
})
// parent runs only after all children succeed

Transactional email anatomy

Subject:    Your order #4582 is confirmed
Preheader:  Track shipping below • Need help? Reply to this email.
Body:
  Hi Alice, thanks for your order...
  [ Track shipment ]    ← single CTA
Footer:     Unsubscribe • Address (CAN-SPAM)

Scheduling

MODULE 14 — SEARCH

Elasticsearch

Inverted index for full-text + analytics, plus vector / hybrid search for semantic + RAG retrieval at scale.

Internals

Use cases

Query patterns

POST /products/_search
{
  "query": {
    "bool": {
      "must":   [{ "match": { "title": "wireless headphones" } }],
      "filter": [
        { "term":  { "category": "audio" } },
        { "range": { "price": { "lte": 200 } } }
      ],
      "should": [
        { "match": { "brand": "sony" } }   // boost
      ]
    }
  },
  "aggs": {
    "by_brand": { "terms": { "field": "brand.keyword" } },
    "price_p":  { "percentiles": { "field": "price" } }
  },
  "size": 20,
  "from": 0
}

Field mapping rules

NeedMapping
Full-text search"type": "text" with analyzer
Exact match / sort / aggregate"type": "keyword"
Range queriesinteger, date, double
Both above (common)text with fields.keyword multi-field
Geogeo_point for lat/lon
Semantic / kNN vectordense_vector (HNSW) or semantic_text

Vector & hybrid search

POST /products/_search
{
  "retriever": {
    "rrf": {                              // fuse lexical + vector
      "retrievers": [
        { "standard": { "query": { "match": { "title": "wireless headphones" } } } },
        { "knn": { "field": "title_vector", "query_vector": [/* embedding */],
                   "k": 50, "num_candidates": 200 } }
      ]
    }
  },
  "size": 20
}

Tuning

MODULE 15 — FAILURES

Error Handling

Errors are first-class output. Plan them.

Error categories

TypeWhenStrategy
SyntaxCompile / parse timeLint + CI catch.
Runtime — transientNetwork blip, DB lockedRetry with backoff + circuit breaker.
Runtime — permanentBad input, missing recordFail fast, return 4xx.
Logical / businessInsufficient funds, conflictDomain error → 4xx with code.
SystemOut of memory, disk fullCrash + restart + alert.

Strategies

Circuit breaker state machine

failures ≥ threshold ┌────────┐ ─────────────────────────────► ┌────────┐ │ CLOSED │ │ OPEN │ ◄──┐ │ (pass) │ │ (fail │ │ └────────┘ │ fast) │ │ probe fails ▲ └───┬────┘ │ (re-open) │ probe succeeds │ │ │ (success ≥ threshold) cool-down │ │ │ elapses ▼ │ │ ┌───────────┐ │ └──────────────────────────────── │ HALF-OPEN │ ───┘ │ (1 probe) │ └───────────┘
Circuit breaker state machine

Custom error types

class AppError(Exception):
    code:    str
    status:  int
    message: str
    cause:   Exception | None = None

class NotFound(AppError):
    status = 404
    code   = "NOT_FOUND"

class RateLimited(AppError):
    status = 429
    code   = "RATE_LIMITED"

Global handler

@app.errorhandler(Exception)
def handle(e):
    request_id = g.get("request_id")
    if isinstance(e, AppError):
        log.warn({"code": e.code, "rid": request_id})
        return jsonify(error=e.code, message=e.message), e.status
    log.error({"rid": request_id}, exc_info=True)
    return jsonify(error="INTERNAL", message="something went wrong"), 500

User-facing error response

{
  "error":     "INSUFFICIENT_FUNDS",
  "message":   "Balance $20.00 below $50.00 needed.",
  "requestId": "7f3a-1c2b",
  "docsUrl":   "https://x.com/docs/errors#insufficient_funds"
}

Monitoring & alerting

MODULE 16 — CONFIG

Config Management

Separate config from code. Environment-aware. Secrets isolated.

Config types

TypeExamplesWhere
Staticretry counts, page size, timeoutsYAML / JSON in repo
Environment-specificDB URL, Redis host, log levelenv vars / per-env file
SensitiveAPI keys, signing secrets, DB credssecret manager (Vault, AWS SM, GCP SM, sops)
Dynamicfeature flags, kill switchesLaunchDarkly, Unleash, Flagsmith, ConfigCat

Precedence (12-factor)

defaults (in code)
  ↓ overridden by
config file (config.yaml)
  ↓ overridden by
env vars (DATABASE_URL=...)
  ↓ overridden by
command-line flags (--port 8080)

.env workflow

# .env (NEVER commit)
DATABASE_URL=postgres://app:secret@localhost/app
JWT_SECRET=hunter2

# .env.example (commit this)
DATABASE_URL=
JWT_SECRET=

# loading
load_dotenv()
db_url = os.environ["DATABASE_URL"]    # crash fast on missing
log_level = os.environ.get("LOG_LEVEL", "info")

Feature flags

if flags.enabled("new-checkout", user_id=user.id):
    return new_checkout_flow(order)
return legacy_checkout_flow(order)

# rollout patterns:
#   percentage:    10% of users
#   targeting:     users in cohort "beta"
#   kill switch:   instantly disable broken feature without redeploy

Secret rotation

MODULE 17 — OBSERVABILITY

Logging, Monitoring, Tracing

Three pillars: logs (events), metrics (aggregates), traces (causal chains).

Logs

Levels

LevelUseAlert?
DEBUGDev troubleshootingNo
INFOLifecycle: startup, shutdown, important business eventsNo
WARNRecoverable, degradedTrend
ERRORFailed request, exceptionYes if rate spikes
FATALProcess can't continuePage

Structured logging

# DON'T
log.info(f"user {uid} placed order {oid} for ${amt}")

# DO  — JSON keys are queryable
log.info("order_placed", extra={
  "user_id":   uid,
  "order_id":  oid,
  "amount":    amt,
  "request_id": rid,
  "trace_id":  tid,
})

What NOT to log

Rotation & retention

Metrics

TypeUseExample
CounterMonotonic counthttp_requests_total{route="/users",code="200"}
GaugePoint-in-time valuedb_pool_in_use, queue_depth
HistogramDistributionhttp_duration_seconds_bucket
SummaryPre-computed quantilesp50 / p95 / p99 latency

RED / USE / Four Golden Signals

Tracing

trace_id: 4f3a... (one per request, propagated across services)
  ├─ span A: api-gateway      (1.2 ms)
  ├─ span B: auth-service     (3.0 ms)
  └─ span C: user-service     (15.4 ms)
       ├─ span D: postgres    (8.1 ms)
       └─ span E: redis       (0.3 ms)
MODULE 18 — SHUTDOWN

Graceful Shutdown

Stop without dropping in-flight work.

Signals

SignalNumberCatchableSender
SIGTERM15k8s/systemd normal stop
SIGINT2Ctrl-C
SIGHUP1Reload config (convention)
SIGKILL9Force kill — no chance to clean up

Shutdown sequence

  1. Mark unhealthy — readiness probe returns 503. LB stops sending new traffic.
  2. Drain — keep serving in-flight requests; reject new ones (or 503).
  3. Wait grace period — typically 10–30s.
  4. Close external resources — DB pool drain, flush log buffers, close file handles, ack pending queue messages.
  5. Exit 0.

Pattern (Go)

srv := &http.Server{Addr: ":8080", Handler: mux}
go srv.ListenAndServe()

sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT)
<-sigCh

ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
defer cancel()
srv.Shutdown(ctx)        // stop accepting, finish in-flight
db.Close()
log.Sync()
os.Exit(0)

Kubernetes specifics

MODULE 19 — PERFORMANCE

Scaling, Performance, Concurrency

Find bottleneck → fix smallest → measure → repeat.

Find bottleneck

DB optimization

App-level

Concurrency vs parallelism

ConcurrencyParallelism
WhatMultiple tasks interleaved on one coreMultiple tasks on multiple cores
Wins onI/O-bound (DB, HTTP, file)CPU-bound (encoding, math)
Primitivesasync/await, goroutines, threadsprocess pools, worker threads
Pythonasyncio, aiohttpmultiprocessing (GIL blocks CPU threads)
Nodeevent loop (default)worker_threads, cluster
GogoroutinesGOMAXPROCS = #CPUs

Scaling axes

MODULE 20 — INTEGRATIONS

Advanced Integrations

Big files, real-time, push patterns.

Object storage (S3 et al.)

# pre-signed PUT
url = s3.generate_presigned_url(
  "put_object",
  Params={"Bucket": "uploads", "Key": f"users/{uid}/{uuid}.jpg",
          "ContentType": "image/jpeg"},
  ExpiresIn=900,
)
# client then: PUT url -H "Content-Type: image/jpeg" --data-binary @file

Real-time

WebSocketsServer-Sent Events (SSE)Long polling
DirectionBidirectionalServer → clientClient polls; server holds
TransportWS upgrade over TCPHTTP keep-alive + text/event-streamHTTP
ReconnectApp-levelBuilt-in (Last-Event-ID)Per-poll
UseChat, games, collabNotifications, dashboardsLegacy/fallback

Pub/Sub architecture

Webhooks (server-initiated)

PollingWebhook
InitiatorConsumerProducer
LatencyIntervalNear-instant
CostWasted pollsPay per event
ReliabilityEasy (idempotent reads)Hard (retries, DLQ, signatures)

Outbound webhook checklist

# signing
sig = hmac.new(secret, body, sha256).hexdigest()
headers = {
  "X-Webhook-Signature": f"sha256={sig}",
  "X-Webhook-Timestamp": ts,
  "X-Webhook-Id": event_id,
}

# verifying — constant-time compare!
expected = "sha256=" + hmac.new(secret, body, sha256).hexdigest()
hmac.compare_digest(expected, headers["X-Webhook-Signature"])
MODULE 21 — CONTRACT

OpenAPI Standards

Spec-first APIs: describe → generate clients/servers/docs/tests.

Why API-first

Evolution

Document anatomy

openapi: 3.1.0
info:
  title:   Orders API
  version: 1.4.0
servers:
  - url: https://api.x.com/v1

paths:
  /orders/{id}:
    get:
      operationId: getOrder
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Order' }
        '404': { $ref: '#/components/responses/NotFound' }
      security:
        - bearerAuth: []

components:
  schemas:
    Order:
      type: object
      required: [id, status, total]
      properties:
        id:     { type: string, format: uuid }
        status: { type: string, enum: [pending, paid, shipped] }
        total:  { type: number, minimum: 0 }
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

Best practices

MODULE 22 — DELIVERY

Testing, Code Quality, DevOps

Ship safely. Verify automatically. Operate continuously.

Test types & pyramid

TypeScopeSpeedQuantity
UnitPure function / classmsMany (base of pyramid)
IntegrationModule + real DB / queuesecSome
ContractService boundary (consumer-driven)secPer-pair
End-to-endFull user flow through UIminFew (top of pyramid)
Load / stressSystem under trafficmin–hrPre-release
UATReal users on stagingdaysPer release
SecuritySAST, DAST, dep scan, pentestContinuous + pre-release

TDD cycle

  1. Red — write failing test for desired behavior.
  2. Green — minimum code to pass.
  3. Refactor — clean up; tests still pass.

CI/CD pipeline shape

push / PR ─► lint ─► unit ─► build ─► integration ─► sec-scan ─► sign image ─► deploy to staging ─► smoke tests ─► (manual approval?) ─► deploy to prod ─► rollout watch (auto-rollback on SLO breach)

Code quality

12-Factor App

  1. Codebase: one app, one repo, many deploys.
  2. Dependencies: explicit, isolated (lockfile).
  3. Config: in environment, never in code.
  4. Backing services: treat as attached resources (DB, queue swap by URL).
  5. Build → Release → Run: strict separation.
  6. Processes: stateless, share-nothing.
  7. Port binding: app exports HTTP itself.
  8. Concurrency: scale via process model.
  9. Disposability: fast startup, graceful shutdown.
  10. Dev/prod parity: same OS, services, data shape.
  11. Logs: stream to stdout; let platform aggregate.
  12. Admin processes: one-off scripts run in same env.

DevOps stack

LayerTools
IaCTerraform, Pulumi, CDK, CloudFormation
ContainersDocker / OCI, BuildKit, buildpacks
OrchestrationKubernetes, ECS, Nomad
CI/CDGitHub Actions, GitLab CI, ArgoCD, Flux
SecretsVault, AWS SM, sops, sealed-secrets
ObservabilityPrometheus, Grafana, Loki, Jaeger, Datadog

Deployment strategies

StrategyHowRollbackRisk
RecreateStop v1, start v2Stop v2, start v1Downtime — small apps only.
RollingReplace pods batch-by-batchRoll back same wayMixed versions during rollout.
Blue/GreenStand up v2 fully; flip LBFlip LB back to v12× capacity briefly.
Canaryv2 to 1%/5%/25%/100%Halt rollout, drain canaryNeed automated metrics gate.
Feature flagDeploy dark; toggle on for cohortToggle off — no redeployFlag-debt if not cleaned up.

Horizontal vs vertical scaling

MODULE 23 — INTERFACES

API Styles

How clients and services talk. Pick the contract style that fits the call pattern.

Every style trades along the same axes: coupling (loose REST vs tight binary stubs), payload shape (resource document vs client-shaped query vs compact binary), streaming need (one-shot vs long-lived push), browser reachability (can a raw fetch hit it, or do you need a proxy), and schema/codegen (hand-written vs generated from an IDL). Map the call pattern to those axes first; the name follows.

The landscape

StyleTransportPayloadSchema/contractStreamingBrowser-nativeBest for
RESTHTTP/1.1+JSON (text)OpenAPI (optional)NoYesPublic resource CRUD
GraphQLHTTP (1 endpoint)JSON, client-shapedSDL (typed)SubscriptionsYesAggregating many resources, varied clients
gRPCHTTP/2Protobuf (binary).proto (strict)All 4 modesNo (needs gRPC-Web)Internal service-to-service
SOAPHTTP/SMTPXML envelopeWSDL+XSD (strict)NoAwkwardEnterprise/legacy, formal contracts
WebSocketTCP (via HTTP upgrade)Any framesNone (DIY)Full-duplexYesBidirectional realtime (chat, games)
SSEHTTP/1.1+text/event-streamNoneServer→clientYesLive feeds, token streaming
WebhooksHTTP (callback)JSONPer-providerEvent pushN/A (server)Async event notification
tRPCHTTPJSONTS types (inferred)SubscriptionsYesTS monorepo, full-stack types

REST — the default for public surfaces

Resource-oriented: nouns as URLs, HTTP verbs as actions, status codes as outcomes. Depth lives in MVC and REST APIs — here just the contract essentials.

Richardson levelMeaning
0Single endpoint, RPC-over-HTTP (one URL, POST everything)
1Resources — distinct URLs per noun
2HTTP verbs + status codes used correctly (most "REST" stops here)
3HATEOAS — responses embed links to next actions

GraphQL — one endpoint, client-shaped queries

Single POST /graphql. The client sends a query naming exactly the fields it wants; the server walks resolvers to fill them. Kills over-fetching (REST returns the whole object) and under-fetching (REST needs N round-trips to stitch related data). Three operation types: query (read), mutation (write), subscription (live push over WS).

# schema (SDL)
type User { id: ID!, name: String!, posts: [Post!]! }
type Post { id: ID!, title: String! }

type Query { user(id: ID!): User }

# client query — ask for exactly these fields
query {
  user(id: "42") {
    name
    posts { title }
  }
}

gRPC — binary RPC over HTTP/2

Define the service in a .proto IDL, run protoc to generate typed client + server stubs in any language. Wire format is Protocol Buffers (compact binary), multiplexed over HTTP/2 — so many calls share one connection and the four streaming modes come free.

syntax = "proto3";
service UserService {
  rpc GetUser (UserRequest) returns (User);              // unary
  rpc ListUsers (Query) returns (stream User);           // server-streaming
  rpc Upload (stream Chunk) returns (UploadAck);         // client-streaming
  rpc Chat (stream Msg) returns (stream Msg);            // bidirectional
}
message UserRequest { string id = 1; }
message User { string id = 1; string name = 2; }
Call typeShapeUse
Unary1 req → 1 respNormal RPC
Server-streaming1 req → N respFeeds, large result sets
Client-streamingN req → 1 respUploads, telemetry batch
BidirectionalN req ↔ N respChat, live sync

Realtime: long-polling vs SSE vs WebSocket

MechanismDirectionProtocolReconnectUse-case
Long-pollingServer→client (faked)HTTP request held openManual re-requestLegacy fallback, low-frequency events
SSEServer→client onlyHTTP text/event-streamAuto (Last-Event-ID)Notifications, LLM token streaming, live dashboards
WebSocketFull-duplexTCP after HTTP upgradeManual (DIY heartbeat)Chat, multiplayer, collaborative editing

Webhooks — server-to-server event push

Inverted REST: the provider POSTs to a URL you registered when an event fires, instead of you polling. Delivery is at-least-once — providers retry on non-2xx with backoff, so the same event can arrive twice. Verify the signature because the URL is public and anyone could forge a payload.

# HMAC verify — constant-time compare
expected = hmac.new(secret, raw_body, sha256).hexdigest()
if not hmac.compare_digest(expected, header_sig):
    return 401
process_idempotent(event_id)   # dedupe: ignore if event_id already seen

SOAP & tRPC — the bookends

Choosing — decision guide

call pattern │ ├─ public, third-party clients? ── resource CRUD ─► REST (+OpenAPI) │ └ many shapes/clients ─► GraphQL │ ├─ internal microservice ↔ microservice? ─► gRPC (HTTP/2, protobuf) │ ├─ realtime? ── server→client only ─► SSE │ └ bidirectional ────────► WebSocket │ └─ async "tell me when X happens" ─► Webhooks → queue
MODULE 24 — FRAMEWORK

FastAPI

Python async API framework. Type hints become validation, docs, and serialization for free.

FastAPI sits on two pillars: Starlette (the ASGI toolkit handling routing, requests, middleware, WebSockets) and Pydantic (the validation/serialization engine). You annotate function signatures with type hints; FastAPI reads them at startup to validate inbound data, serialize outbound data, and emit an OpenAPI schema. One source of truth — the signature — drives all three. Async-first, but plays nicely with sync code too.

Minimal app + run

# main.py
from fastapi import FastAPI

app = FastAPI(title="Orders API", version="1.0.0")

@app.get("/health")
async def health() -> dict[str, str]:
    return {"status": "ok"}

# dev:  uvicorn main:app --reload --port 8000
# prod: gunicorn main:app -k uvicorn.workers.UvicornWorker -w 9

Parameters: path, query, body

FastAPI infers a parameter's source from its type and signature position: a name that matches a {placeholder} in the path is a path param; a Pydantic model is the JSON body; everything else with a scalar type is a query param.

from fastapi import FastAPI, Path, Query
from pydantic import BaseModel

class ItemIn(BaseModel):
    name: str
    price: float
    tags: list[str] = []

@app.put("/items/{item_id}")
async def upsert(
    item_id: int = Path(gt=0),                    # path: /items/42
    q: str | None = Query(default=None, max_length=50),  # query: ?q=...
    verbose: bool = False,                        # optional query, defaults False
    body: ItemIn = ...,                           # JSON request body
):
    return {"id": item_id, "q": q, "item": body}

Pydantic v2 models

Models declare the shape of data and the rules it must satisfy. Field carries constraints; custom field_validators handle cross-cutting logic; response_model reshapes and redacts output so internal fields (like password hashes) never leak. See Validation for the deeper Pydantic treatment.

from pydantic import BaseModel, Field, EmailStr, field_validator

class UserIn(BaseModel):
    name: str = Field(min_length=1, max_length=80)
    age: int = Field(gt=0, lt=130)
    email: EmailStr
    password: str = Field(min_length=8)

    @field_validator("name")
    @classmethod
    def no_blank(cls, v: str) -> str:
        if not v.strip():
            raise ValueError("name cannot be whitespace")
        return v.strip()

class UserOut(BaseModel):          # note: no password field
    id: int
    name: str
    email: EmailStr

@app.post("/users", response_model=UserOut, status_code=201)
async def create_user(user: UserIn) -> UserOut:
    row = await db.insert_user(user)   # returns object incl. password hash
    return row                          # serialized through UserOut -> hash dropped

Dependency injection

Depends() lets a route declare what it needs; FastAPI resolves and injects it, caching the result per request. Dependencies can have their own dependencies (sub-deps). A yield-based dep runs setup, hands over the value, then runs teardown after the response — perfect for DB sessions that must always close.

from typing import Annotated
from fastapi import Depends

async def get_db():
    session = SessionLocal()
    try:
        yield session            # handed to the route
    finally:
        await session.close()    # always runs, even on exception

def pagination(skip: int = 0, limit: int = Query(default=20, le=100)):
    return {"skip": skip, "limit": limit}

DB = Annotated[AsyncSession, Depends(get_db)]
Page = Annotated[dict, Depends(pagination)]

@app.get("/items")
async def list_items(db: DB, page: Page):
    return await db.fetch_items(**page)

Async and concurrency

Declare a route async def when you await non-blocking I/O (async DB driver, httpx, async Redis). Declare it plain def when your libraries are synchronous — FastAPI runs plain def routes in a threadpool, so blocking calls don't stall the loop. The cardinal sin: synchronous blocking work inside an async def, which freezes the single event-loop thread for every other request. See Scaling and Concurrency.

Your I/ODeclare asRuns on
async libs (httpx, asyncpg)async def + awaitEvent loop
sync libs (requests, psycopg2)plain defThreadpool
CPU-bound workplain def → offloadProcess pool / queue

Authentication: OAuth2 + JWT

OAuth2PasswordBearer extracts the bearer token from the Authorization header and feeds it to a get_current_user dependency that verifies the JWT and loads the user. Compose scope/role checks as further dependencies. See Auth and Security.

from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
import jwt

oauth2 = OAuth2PasswordBearer(tokenUrl="token")

async def get_current_user(token: Annotated[str, Depends(oauth2)], db: DB):
    cred_exc = HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid token",
                             headers={"WWW-Authenticate": "Bearer"})
    try:
        payload = jwt.decode(token, SECRET, algorithms=["HS256"])
    except jwt.PyJWTError:
        raise cred_exc
    user = await db.get_user(payload["sub"])
    if user is None:
        raise cred_exc
    return user

def require_role(role: str):
    def checker(user = Depends(get_current_user)):
        if role not in user.roles:
            raise HTTPException(status.HTTP_403_FORBIDDEN, "Forbidden")
        return user
    return checker

@app.get("/admin")
async def admin_panel(user = Depends(require_role("admin"))):
    return {"hello": user.name}

Middleware, CORS, exception handlers

Middleware wraps every request/response; exception handlers map raised errors to clean HTTP responses. Register CORS via CORSMiddleware so browsers permit cross-origin calls. See Middleware and HTTP.

from fastapi import Request
from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://app.example.com"],
    allow_methods=["*"], allow_headers=["*"], allow_credentials=True,
)

class OutOfStock(Exception):
    def __init__(self, sku: str): self.sku = sku

@app.exception_handler(OutOfStock)
async def oos_handler(request: Request, exc: OutOfStock):
    return JSONResponse(status_code=409, content={"sku": exc.sku, "error": "out_of_stock"})

Background tasks vs a real queue

BackgroundTasks runs after the response is sent — but in the same process, with no retries or durability. Fine for a welcome email. For heavy, durable, or retryable work, push to a real broker. See Queues.

from fastapi import BackgroundTasks

@app.post("/signup")
async def signup(user: UserIn, tasks: BackgroundTasks):
    saved = await db.create(user)
    tasks.add_task(send_welcome_email, saved.email)  # light, fire-and-forget
    return {"id": saved.id}
NeedUse
Light, non-critical, sub-secondBackgroundTasks
Heavy / retryable / durable / scheduledCelery, arq, RQ + Redis/RabbitMQ

Auto docs

Testing

Use TestClient (sync, Starlette/requests-based) for quick tests, or httpx.AsyncClient with ASGITransport to exercise async paths. app.dependency_overrides swaps real deps (DB, auth) for fakes — no monkeypatching internals.

from fastapi.testclient import TestClient
from main import app, get_db

def fake_db():
    yield FakeSession()

app.dependency_overrides[get_db] = fake_db
client = TestClient(app)

def test_health():
    r = client.get("/health")
    assert r.status_code == 200 and r.json() == {"status": "ok"}

def test_create_user():
    r = client.post("/users", json={
        "name": "Ada", "age": 36, "email": "ada@x.io", "password": "hunter2!!"})
    assert r.status_code == 201
    assert "password" not in r.json()   # redacted by response_model

Deploy

Production runs multiple worker processes behind a reverse proxy. Either bare uvicorn --workers N, or gunicorn as a process manager using the uvicorn worker class for graceful restarts and supervision. Put nginx in front for TLS termination, static files, and buffering. Use the lifespan context to open/close pools on startup/shutdown.

from contextlib import asynccontextmanager

@asynccontextmanager
async def lifespan(app: FastAPI):
    app.state.pool = await create_pool()   # startup
    yield
    await app.state.pool.close()           # shutdown

app = FastAPI(lifespan=lifespan)

# gunicorn main:app -k uvicorn.workers.UvicornWorker -w 9 \
#   --bind 0.0.0.0:8000 --timeout 60
MODULE 25 — IN-MEMORY

Redis Deep-Dive

Single-threaded in-memory store. Know the data type and the command for each pattern.

Redis runs commands on a single thread via an event loop, so every command is atomic by nature — no locks, no races between commands. Data lives in RAM, hence sub-millisecond reads. Durability is optional (snapshot or append-log). It is more than a cache: it is a data-structure server — strings, hashes, lists, sets, sorted sets, streams — manipulated in place with rich commands. This deepens Caching with concrete mechanics.

Data types — pick the right one

TypeWhen to use
StringCached blob/JSON, counters (INCR), flags, bytes up to 512 MB
HashObject with fields — user profile, partial updates without re-serializing
ListQueue/stack, recent-activity feed, BRPOP work queue
SetUniqueness, membership, tags, set algebra (intersect followers)
Sorted Set (ZSet)Leaderboards, priority queue, time-ordered indexes, sliding-window rate limit
BitmapDaily active users, feature flags per id — 1 bit/user, cheap analytics
HyperLogLogApprox unique counts (cardinality) in ~12 KB, ~0.81% error
StreamAppend-only log, event sourcing, durable queues with consumer groups
GeoRadius/nearest queries (GEOADD/GEOSEARCH) — backed by a ZSet

Core command cheat-sheet

# strings + counters
SET user:1 "alice"
GET user:1
SETEX session:1 3600 "tok"      # value + 3600s TTL in one op
TTL session:1                    # seconds left (-1 no TTL, -2 gone)
INCR page:views                 # atomic +1, returns new value

# hash (object)
HSET user:1 name alice age 30
HGETALL user:1

# list as work queue
LPUSH jobs "j1"                 # push left
BRPOP jobs 5                    # block up to 5s, pop right (FIFO)

# set membership
SADD online 42
SISMEMBER online 42            # 1 / 0

# sorted set (leaderboard / time index)
ZADD board 100 alice 250 bob
ZRANGE board 0 -1 WITHSCORES    # asc
ZRANGEBYSCORE board 100 200     # score window

# atomic create-if-absent w/ expiry (locks, idempotency)
SET key val NX EX 30

Cache-aside in Redis

The dominant pattern: app reads cache, on miss loads the DB, writes back with a TTL. See Caching for the general model.

def get_user(uid):
    key = f"user:{uid}"
    cached = r.get(key)
    if cached is not None:
        return None if cached == b"\x00" else json.loads(cached)  # negative cache
    row = db.query(uid)                       # miss -> source of truth
    if row is None:
        r.set(key, b"\x00", ex=30)            # cache the miss, short TTL
        return None
    ttl = 300 + random.randint(0, 60)         # jitter -> avoid synchronized expiry
    r.set(key, json.dumps(row), ex=ttl)
    return row

Rate limiting

Three escalating designs. Fixed-window is cheapest but allows 2x burst at the boundary; sliding-window log is exact but stores every hit; token bucket smooths bursts.

AlgorithmMechanismTrade-off
Fixed windowINCR + EXPIREO(1), tiny; boundary burst
Sliding logZSet of timestamps + ZREMRANGEBYSCOREExact; O(N) memory/key
Token bucketHash {tokens, ts} refilled in LuaSmooth bursts; more logic
# fixed-window: 100 req / 60s per user (atomic via single thread)
key = f"rl:{uid}:{epoch // 60}"
n = r.incr(key)
if n == 1:
    r.expire(key, 60)          # set TTL only on first hit of window
allowed = n <= 100

# sliding-window log (sketch)
# ZADD rl:{uid} now now ; ZREMRANGEBYSCORE rl:{uid} 0 (now-60000) ; ZCARD rl:{uid}

Distributed lock

Acquire with SET NX PX so only one client wins and the lock auto-expires if the holder dies. Release with a Lua compare-and-delete so you never delete a lock that already expired and was re-acquired by someone else.

# acquire: unique token, 30s safety TTL
SET lock:order:1 <uuid> NX PX 30000

# release: delete ONLY if value is still mine (atomic CAS)
EVAL "if redis.call('get', KEYS[1]) == ARGV[1]
        then return redis.call('del', KEYS[1]) else return 0 end"
     1 lock:order:1 <uuid>

Pub/Sub vs Streams

Pub/SubStreams
DeliveryFire-and-forgetAt-least-once w/ ack
PersistenceNone — offline subs miss itPersisted log, capped by length
Consumer groupsNo — every sub gets allYes — work split across consumers
ReplayImpossibleRead history by id (XRANGE)
XADD events * type signup uid 42        # append, * = auto id
XGROUP CREATE events g1 0               # consumer group from start
XREADGROUP GROUP g1 c1 COUNT 10 STREAMS events >   # new msgs for this consumer
XACK events g1 1689-0                   # ack -> remove from pending

Prefer Streams over Pub/Sub when you need durability, replay, or balanced consumers. Reach for Kafka/queues when you need long retention, high partition throughput, or multi-day reprocessing — Redis keeps the log in RAM.

Atomicity and transactions

WATCH balance:1
val = GET balance:1
MULTI
SET balance:1 (val - 10)
EXEC            # nil if balance:1 changed since WATCH -> retry

Persistence: RDB vs AOF

RDB (snapshot)AOF (append-only)
DurabilityLose seconds–minutesUp to ~1s (everysec) or per-write
Restart speedFast — load compact dumpSlower — replay log
File sizeSmall, compressedLarger; rewrite/compaction needed
CostFork at snapshot (CoW spike)Continuous write amplification

Production default: enable both. The hybrid mode writes an RDB preamble inside the AOF (aof-use-rdb-preamble yes) — fast load of the RDB base plus the AOF tail for the last seconds.

Eviction and memory

maxmemory 4gb
maxmemory-policy allkeys-lru     # evict least-recently-used across all keys

Scaling

Sentinel quorum (3) | monitors / fails over +------v------+ async +-----------+ | PRIMARY | ------------> | REPLICA | <- read traffic +------+------+ +-----------+ | CRC16(key) % 16384 slots 0-5460 5461-10922 10923-16383 [shard A] [shard B] [shard C] (Cluster)
MODULE 26 — PLUMBING

Intermediary Systems

The boxes between client and service. Each adds a concern: routing, buffering, resilience.

Intermediaries exist to keep services dumb and focused. Push cross-cutting concerns — TLS, auth, rate limiting, retries, load spreading, async buffering — out of application code and into shared infrastructure that scales independently and absorbs load before it reaches origin. The cost: more hops, more latency, more moving parts to operate. Add each box only when a real concern demands it.

The request chain

Client │ HTTPS ▼ ┌─────────┐ edge cache, static assets, geo POPs │ CDN │ └────┬────┘ ▼ ┌──────────────┐ L4/L7 spread, TLS terminate, health checks │ Load Balancer│ └──────┬───────┘ ▼ ┌──────────────┐ auth, rate limit, route, aggregate │ API Gateway │ └──────┬───────┘ ▼ ┌──────────────────────────────┐ │ ┌────────┐ ┌───────────┐ │ sidecar = mesh data plane │ │ Envoy │───▶│ Service │ │ (mTLS, retry, timeout) │ │sidecar │ └───────────┘ │ │ └────────┘ │ └──────────────┬────────────────┘ ▼ publish ┌───────────────┐ │ Message Broker│ queue / log └───────┬───────┘ ▼ consume ┌─────────┐ │ Workers │ async, decoupled └─────────┘

Reverse proxy & load balancer

A reverse proxy fronts your services: it terminates TLS, picks a healthy backend, and shields origin topology from clients. Load balancing is the spreading policy on top. L4 routes by IP:port (fast, opaque, protocol-agnostic); L7 parses HTTP so it can route by path/header/cookie, do sticky sessions, and retry idempotent requests.

DimensionL4 (TCP/UDP)L7 (HTTP)
SeesIP, port, flagsMethod, path, headers, body
RoutingConnection-levelPath/host/header-based
CostLowest latencyParse + buffer overhead
ToolsHAProxy, NLB, IPVSnginx, Envoy, ALB
AlgorithmPicksUse when
Round-robinNext in rotationUniform, stateless backends
Least-connFewest active connsVariable request duration
Consistent-hashHash(key) → ring slotCache affinity, sticky shards
WeightedBy capacity weightHeterogeneous instance sizes
# nginx: L7 upstream, least-conn, passive health checks (max_fails/fail_timeout)
upstream api {
    least_conn;
    server 10.0.1.10:8080 max_fails=3 fail_timeout=10s;
    server 10.0.1.11:8080 max_fails=3 fail_timeout=10s;
}
server {
    listen 443 ssl;
    ssl_certificate     /etc/ssl/api.crt;   # TLS termination here
    ssl_certificate_key /etc/ssl/api.key;
    location / { proxy_pass http://api; }
}

API Gateway

A gateway is an L7 proxy that owns the public API contract. A load balancer answers "which instance?"; a gateway answers "should this caller even reach the system, and in what shape?" It centralizes auth, quotas, and routing so every service doesn't reimplement them.

ConcernLoad balancerAPI gateway
Primary jobSpread trafficMediate the API
Auth / API keysNoYes
Rate limitingRarePer-key / per-route
AggregationNoFan-out + compose
TransformNoReq/resp rewrite
ExamplesNLB, HAProxyKong, AWS API GW, Apigee

BFF — Backend-for-Frontend

One gateway per client type rather than one universal API. A web BFF returns rich, denormalized payloads; a mobile BFF trims fields and pre-aggregates to spare radio and battery. Each frontend team owns its BFF, so shaping changes don't fight over a shared contract.

Web app ──▶ Web BFF ──┐ ├─▶ user-svc, cart-svc, price-svc Mobile ──▶ Mobile BFF─┘ (each BFF aggregates differently)

Message broker

Brokers decouple producers from consumers in time: the producer fires and forgets, the broker buffers, workers drain at their own pace — absorbing spikes and surviving consumer downtime. Two models dominate. A queue deletes a message once acked (work distribution). A log is an append-only, replayable stream where consumers track their own offset.

PropertyQueue (RabbitMQ, SQS)Log (Kafka, Kinesis)
ModelMessage deleted on ackAppend-only, offset-based
OrderingPer-queue, weak under redeliveryStrict within partition
ReplayNo — gone once consumedYes — rewind offset
ConsumersCompete for messagesGroups, each at own offset
RetentionUntil ackedTime/size (days, TB)
Fan-outExchange copiesMany groups, one log

Service mesh

A mesh moves service-to-service concerns out of app code into a sidecar proxy (Envoy) deployed next to every pod. Apps speak plaintext to localhost; the sidecar handles mTLS, retries, timeouts, circuit breaking, and emits uniform metrics/traces. The data plane is the fleet of sidecars carrying traffic; the control plane (Istiod, Linkerd) pushes config and certs to them.

control plane (Istiod) ── config + certs ──┐ │ ▼ ▼ ┌──────────────┐ ┌──────────────┐ mTLS, retry │ Pod B │ │ Pod A │◀═════════════════▶│ ┌────┐ ┌───┐ │ │ ┌───┐ ┌────┐ │ data plane │ │app │ │ENV│ │ │ │app│ │ENV │ │ │ └────┘ └───┘ │ │ └───┘ └────┘ │ └──────────────┘ └──────────────┘

Resilience at the boundary

# retry with capped exponential backoff + full jitter
import random, time
def call_with_retry(fn, attempts=5, base=0.1, cap=10):
    for i in range(attempts):
        try:
            return fn()
        except Transient as e:
            if i == attempts - 1:
                raise
            sleep = random.uniform(0, min(cap, base * 2 ** i))  # full jitter
            time.sleep(sleep)

When to add which

AddWhenDon't bother if
Load balancer>1 instance of anythingNever — you always need it
API gatewayMany public APIs, shared auth/quotasOne internal service
BFFDivergent web/mobile shaping needsSingle client type
Message brokerAsync decoupling, spike absorption, fan-outSync request/response suffices
Service meshDozens of services, mTLS + uniform policy3 services — YAGNI, use libraries
MODULE 27 — ADVANCED

Distributed Data & Advanced Concurrency

Sharding, replication topologies, distributed transactions, and the concurrency/observability depth interviewers push on.

Partitioning schemes

Partitioning (sharding) splits one dataset across N nodes so storage and throughput scale horizontally. The whole game is picking a partition key and a mapping function that spreads load evenly, keeps related rows co-located, and survives adding/removing nodes. Get the key wrong and you get hotspots — one shard melts while the rest idle.

SchemeMappingWinsLoses
RangeKey falls in [lo, hi) per shardEfficient range scans, ordered iterationHotspots on sequential keys (timestamps, auto-inc IDs)
Hashshard = hash(key) % NUniform spread, no hot rangesKills range queries; % N reshuffles almost everything on resize
Consistent hashKey & nodes on a ring; walk clockwiseAdd/remove node moves only ~1/N of keysRing skew without virtual nodes
DirectoryLookup table key→shardArbitrary placement, easy rebalanceLookup service is a hop + SPOF; must be HA

Resharding without downtime

The reason hash(key) % N is a trap: bump N from 4 to 5 and roughly (N-1)/N of keys remap — a near-total data shuffle mid-flight. Consistent hashing and directory schemes exist precisely to make rebalancing incremental. Production resharding is a careful choreography, not a flip of a config value.

Phase 1 DUAL-WRITE writes → old shard AND new shard (reads still old) Phase 2 BACKFILL bulk-copy historical rows old → new, checksum verify Phase 3 DUAL-READ/VERIFY read both, compare, log mismatches (shadow) Phase 4 CUTOVER flip reads → new shard for the moved key range Phase 5 DECOMMISSION stop dual-write, drop old copies

Replication topologies

Replication keeps copies of each partition on multiple nodes for durability, read scaling, and failover. The topology decides who accepts writes — the single hardest question, because it dictates your conflict and consistency story.

TopologyWritesConflict handlingUse when
Single-leaderOne leader, replicas followNone — total order at leaderDefault RDBMS; read-heavy, strong-ish reads
Multi-leaderSeveral leaders accept writesMust resolve (LWW, CRDTs, app merge)Multi-region write locality, offline clients
LeaderlessAny replica; client/coordinator fans outQuorum + read-repair + hinted handoffDynamo-style (Cassandra, Riak); high availability

Quorum reads & writes (R+W>N)

Leaderless systems tune consistency per operation with three knobs: N replicas per key, W nodes that must ack a write, R nodes that must respond to a read. When W + R > N the read and write sets are guaranteed to overlap on at least one node — so a read sees the latest acked write. That overlap is the entire mechanism behind "strong-ish" quorum consistency.

N = 3   (replicas per key)

W=2, R=2 → W+R = 4 > 3  ✓ overlap → reads see latest write, tolerate 1 down node
W=3, R=1 → fast reads, write blocks if ANY replica down (no write availability)
W=1, R=1 → W+R = 2 ≤ 3  ✗ no overlap → fast but may read stale (eventual only)

Distributed transactions: 2PC

When one logical operation must atomically touch multiple databases/services, local ACID isn't enough. Two-phase commit coordinates atomic commit across participants via a coordinator: everyone promises before anyone commits.

Coordinator Participants (A, B, C) │── PREPARE ─────────────▶ each: do work, write to durable log, │ lock rows, reply YES (vote) or NO │◀── votes ──────────────── │ │ all YES → COMMIT ──────▶ release, make durable, ack │ any NO → ABORT ───────▶ rollback, release locks

Sagas, idempotency & the outbox

A saga replaces one distributed ACID transaction with a sequence of local transactions, each with a compensating action that semantically undoes it (refund, not rollback). You trade atomicity for availability: the system passes through inconsistent intermediate states but converges. Two coordination styles:

StyleControl flowProsCons
OrchestrationCentral orchestrator calls each step, drives compensationExplicit flow, easy to reason about & monitorOrchestrator is a dependency & can bloat into a God service
ChoreographyEach service reacts to events, emits the nextLoose coupling, no central bottleneckFlow is implicit & emergent — hard to debug cyclic event chains
# Order saga (orchestration): forward steps + compensations
1 reserve_inventory   ⇄  release_inventory
2 charge_payment      ⇄  refund_payment
3 create_shipment     ⇄  cancel_shipment
# step 3 fails → run comps 2,1 in reverse. Comps MUST be idempotent
# & retryable, since the compensation itself can fail and be retried.

Concurrency models & backpressure

Two ways a server handles many in-flight requests. Thread-per-request (classic Java/Tomcat, synchronous) gives each request a blocking thread — simple to write, but each thread costs ~1 MB stack and context switches, so ~thousands cap you out and blocking I/O idles CPU. Async / event-loop (Node, Netty, Python asyncio, Go's goroutines as a hybrid) multiplexes thousands of connections on a few threads by never blocking — I/O yields control — winning huge concurrency for I/O-bound work.

DimensionThread-per-requestAsync / event-loop
Memory / conn~1 MB stack each~KB per task/promise
CeilingThousands (thread limit)10k–100k+ conns
Best forCPU-bound, simple codeI/O-bound fan-out, high concurrency
DangerThread-pool starvationOne blocking call stalls the whole loop

Capacity: Little's Law & load testing

You cannot size pools, threads, or fleets by guessing. Little's Law is the one equation that ties throughput, latency, and concurrency together for any stable system: L = λ × W — average concurrent requests in the system = arrival rate × average time each spends in it. It holds regardless of distribution, so it's your back-of-envelope for every capacity question.

L = λ × W
  λ = arrival rate (req/s),  W = time-in-system (s),  L = concurrent in flight

# How big must the DB connection pool be?
λ = 500 req/s,  W = 20 ms per query = 0.020 s
L = 500 × 0.020 = 10 concurrent  → pool of ~10 (add headroom → 15)

# What if the DB slows to 200 ms?
L = 500 × 0.200 = 100 concurrent  → a pool of 10 now queues 90 → exhaustion
# same request rate, 10× latency ⇒ 10× concurrency demand

Observability: SLOs, error budgets & alerting

Monitoring answers "is it up?"; observability answers "why is it behaving like this?" from the outside, via the three pillars — metrics (cheap aggregates), logs (discrete events), traces (one request across services). The discipline that makes it actionable is defining what "good" means numerically.

FrameworkForSignals
REDRequest-driven servicesRate, Errors, Duration
USEResources (CPU, disk, pool)Utilization, Saturation, Errors
Four Golden SignalsGoogle SRE defaultLatency, Traffic, Errors, Saturation