Glossary
Dev glossary (EN ↔ TH)
Developer concepts explained in both English and Thai — search a term you hit in the docs and read it in plain ไทย.
110 terms
Web & HTTP
ETag (Entity Tag)
An HTTP response header that acts as a fingerprint of a specific version of a resource, so a client can ask 'has this changed?' without re-downloading the whole body. On a later GET the client sends the saved value back in If-None-Match; if it still matches, the server replies 304 Not Modified with no body, saving bandwidth. The same mechanism powers optimistic concurrency on writes: send the ETag in If-Match on a PUT/PATCH and the server rejects the write with 412 Precondition Failed if someone else changed the resource first, preventing lost updates. The common gotcha is weak vs strong ETags (the W/ prefix means byte-for-byte equality is not guaranteed) and proxies, gzip, or load-balanced backends silently producing different ETags for the same content, so a cache that should hit ends up missing.
Content Negotiation
The mechanism where the client states its preferences via request headers (Accept for media type like JSON vs XML, Accept-Language for locale, Accept-Encoding for compression like gzip vs br) and the server picks a representation, returning the actual choice in Content-Type / Content-Encoding. The catch is caching: if a response varies by any of those headers you MUST send a Vary header (e.g. Vary: Accept-Encoding), otherwise a proxy or CDN can serve the wrong representation, like handing a gzip body to a client that did not ask for it.
GraphQL vs REST
Two API styles. REST exposes many resource URLs paired with HTTP verbs (GET/POST/PUT/DELETE), so each endpoint returns a fixed shape and you get HTTP caching and CDNs almost for free. GraphQL exposes a single endpoint where the client sends a query naming exactly the fields it wants, which kills the over-fetching and under-fetching (multiple round-trips) of rigid REST endpoints. The catch: that flexibility makes HTTP caching harder (most requests are a POST to one URL), lets clients write expensive queries by accident, and invites N+1 resolver problems on the server — so it's a trade-off, not a winner.
CORS (Cross-Origin Resource Sharing)
A browser security mechanism that lets a server explicitly opt in to letting JavaScript on a page from a *different* origin (scheme + host + port) read its responses. By default the same-origin policy lets the request go out but blocks the page from reading the response unless the server returns the matching `Access-Control-Allow-Origin` (and friends) headers. The key insight: CORS is enforced *by the browser*, not the server — so server-to-server calls, curl, and Postman are never blocked, which is why an API works in your terminal but fails in the browser console. For anything beyond a simple GET/POST (custom headers, `PUT`/`DELETE`, JSON bodies) the browser first fires a `preflight` `OPTIONS` request and only sends the real one if the server approves. The classic trap is thinking CORS adds security to your API — it doesn't; it only governs which web origins a browser will let read the response, and `Access-Control-Allow-Origin: *` can't be combined with credentialed (cookie) requests.
CSP (Content Security Policy)
An HTTP response header (Content-Security-Policy) that tells the browser which sources of scripts, styles, images, frames, and other resources a page is allowed to load — declared per-directive, e.g. script-src 'self' https://cdn.example.com. It's a key second line of defense against XSS: even if an attacker injects markup, the browser refuses to run scripts the policy didn't allow. The practical catch is that 'unsafe-inline' and 'unsafe-eval' quietly defeat most of that protection, and a broad allowlist (or a wildcard / a CDN that hosts attacker-controllable JSONP) is just as leaky — a strict policy uses a per-response nonce or hash to mark the exact inline scripts you meant to run. Lock down the baseline with default-src 'none' and grant only what each page needs, and roll out via Content-Security-Policy-Report-Only first so you catch what would break before it actually does.
Idempotent
An operation is idempotent if performing it once or many times with the same input leaves the server in the same end state — N identical calls have the same effect as one. In HTTP, GET, PUT, and DELETE are defined as idempotent while POST generally is not, which is what makes it safe for a client (or a load balancer, or a retry library) to re-send an idempotent request after a timeout without fearing duplicate side effects. Two things trip people up: idempotent is about end STATE, not the response — a second DELETE legitimately returns 404, and that's still idempotent; and idempotent is not the same as 'safe' (no side effects at all), since PUT and DELETE do mutate data. To make a non-idempotent POST (charge a card, create an order) retry-safe, attach a client-generated Idempotency-Key so the server can dedupe replays.
Rate Limiting
Capping how many requests a single caller (keyed by IP, API key, or user) may make in a time window, to absorb traffic spikes and protect a service from abuse, overload, and runaway costs. Common algorithms trade off smoothness against burst tolerance: a fixed window is simplest but lets a client double up by straddling the window boundary; a sliding window smooths that out; a token bucket deliberately allows short bursts (spending saved-up tokens) while still bounding the long-run rate; and a leaky bucket instead drains at a steady rate, shaping bursts into a smooth stream. When a caller exceeds the limit the server returns HTTP 429 with a Retry-After (or X-RateLimit-*) header. The practical gotcha is on both ends: a well-behaved client must read that header and back off with exponential backoff plus jitter — blindly retrying just turns one 429 into a retry storm. And across a fleet of servers your counter must live in shared state like Redis, or each instance enforces its own limit and the real ceiling is N times what you intended.
Webhook
A way for one service to push event notifications to another by sending an HTTP request (usually a POST with a JSON body) to a URL you registered ahead of time, instead of you polling its API for changes. It's reverse-direction: the provider calls you. Because that endpoint is publicly reachable, anyone could POST to it, so you must verify each delivery is genuine — typically by checking an HMAC signature header computed from the raw body and a shared secret (compare it before parsing, on the exact bytes received). Two more things trip people up: respond fast with a 2xx and do heavy work asynchronously, because senders enforce a short timeout and treat a slow or non-2xx response as failure; and make the handler idempotent, since that same failure makes them retry, so the identical event can arrive more than once (and sometimes out of order). Dedupe on the event ID.
Middleware
Code that sits in the request/response pipeline between the raw incoming request and your route handler, running as an ordered chain — logging, body parsing, CORS, authentication, rate limiting — where each layer can inspect or mutate the request, short-circuit with a response, or pass control to the next link (in Express you literally call `next()`; forget it and the request just hangs). The crucial insight is that ORDER is behavior, not style: auth must run before the handler it protects, body parsing before anything that reads the body, and error-handling middleware goes last so it can catch what the earlier ones throw. Because it runs on every matched request, any blocking or heavy work there taxes your whole app, so keep it lean. And know your runtime — if it runs on an edge runtime (an opt-in for Next.js middleware, now complemented by the Node-runtime `proxy.ts` in Next.js 16) you get a stripped-down environment with no Node APIs and tight limits, fit for routing, redirects, and header tweaks but not for database calls.
Cache-Control
An HTTP header telling every cache — the browser, a CDN, any proxy between — whether a response may be stored and for how long, replacing the vague Expires date with explicit rules. The core directive is max-age=N (fresh for N seconds); after that the entry is stale, not deleted, and the cache revalidates with the origin (usually via ETag) before reusing it. People mix these up: no-store means never write it to any cache (use for personal or secret data); no-cache does store it but forces revalidation on every use — it does NOT mean 'do not cache'. private limits storage to the user's own browser so a shared CDN won't keep it; public lets shared caches store even normally-uncacheable responses. Two power moves: immutable tells the browser the body never changes for that URL, so it skips revalidation even on reload — pair it with content-hashed filenames and a long max-age; stale-while-revalidate=N serves the stale copy instantly while refreshing in the background. The classic trap: shipping max-age=31536000 on an HTML page or un-hashed file, then being unable to push an urgent fix because every cache is pinned to the old copy.
WebSocket
A persistent, bidirectional TCP connection between browser and server that stays open after an initial HTTP handshake, so either side can push a message at any time without a new request — ideal for chat, live dashboards, and anything real-time. It opens as a normal HTTP GET carrying an Upgrade: websocket header; the server answers 101 Switching Protocols and from then on the socket speaks the WebSocket frame protocol (ws://, or wss:// over TLS), not HTTP. The big misconception is reaching for it by default. Being full-duplex and stateful means no HTTP caching, sticky-session and fan-out concerns behind a load balancer, and you owning the reconnect, heartbeat (ping/pong), and auth-on-reconnect logic, since the browser API cannot set custom headers on the handshake. If traffic only flows server-to-client (notifications, a live feed), prefer Server-Sent Events (SSE): it rides plain HTTP and auto-reconnects for free. Reserve WebSocket for genuinely two-way traffic.
CDN (Content Delivery Network)
A geographically distributed fleet of cache servers (edge nodes, or PoPs) that sit in front of your origin and serve copies of its content from the location closest to each user, cutting latency and offloading origin traffic. The first request for a path is a cache miss — the edge fetches from origin, stores the response, then serves it; later requests in that region are hits answered straight from the edge. Key insight: the CDN obeys your origin's caching signals — Cache-Control, s-maxage (shared-cache TTL), ETag, Vary — so cacheability is the origin's job, not a dashboard toggle. Classic traps: forgetting to purge after a deploy, so stale assets keep serving (this is why builds fingerprint filenames like app.4f3a.js, changing the URL itself); and serving personalized or auth-gated responses without Vary or private, so one user's logged-in page is cached for everyone. Don't edge-cache dynamic or per-user responses unless you do it deliberately, with a short TTL and a correct Vary key.
Security & auth
XSS (Cross-Site Scripting)
An attack where attacker-controlled input gets rendered as executable JavaScript in another user's browser, letting it steal cookies, hijack the session, or rewrite the page. It comes in three flavors: stored (payload saved server-side and served to everyone), reflected (echoed straight back from the request), and DOM-based (the unsafe write happens entirely in client-side JS, so the server never sees it). The fix is context-aware output escaping — encode for HTML, attribute, URL, or JS context at the point you render — backed by CSP as a second layer. The classic mistake is trying to blocklist 'bad' input instead of encoding on output, which is easy to bypass; sinks like innerHTML and dangerouslySetInnerHTML are the usual culprits.
SQL Injection (SQLi)
An attack where untrusted input is concatenated directly into a SQL query, so the attacker's text becomes part of the query logic instead of being treated as data. With a crafted value like ' OR '1'='1 they can bypass logins, dump entire tables, or modify and delete data. The fix is parameterized queries (prepared statements): you send the SQL and the values separately, so the database always treats input as data, never as code. Don't try to escape quotes or block keywords your way out — you can't do it reliably — and don't assume an ORM makes you immune, since its raw-query helpers reopen the same hole.
MFA (Multi-Factor Authentication)
An authentication scheme that requires two or more independent proofs of identity from different categories: something you know (a password), something you have (a TOTP code, passkey, or hardware security key), and something you are (a fingerprint or face). Because the factors are independent, a stolen or phished password alone is no longer enough to log in. Not all second factors are equal, though: TOTP codes (the 6-digit kind) can still be phished or relayed in real time, while WebAuthn/passkeys and hardware keys are bound to the site's origin and are effectively phishing-resistant — prefer them for anything that matters.
JWT (JSON Web Token)
A compact, URL-safe token that encodes claims (e.g. user id, roles, expiry) as a JSON payload and SIGNS it with a secret or private key, so the server can verify who issued it and that it wasn't tampered with — no database lookup per request. It has three Base64URL parts joined by dots: header, payload, signature. The biggest trap is assuming the payload is secret: it's only Base64-encoded, not encrypted, so anyone can decode and read it — never put passwords or PII inside. Two more gotchas: a valid token can't easily be revoked before it expires, so keep access-token lifetimes short and add refresh tokens; and always pin the expected algorithm server-side, since trusting the token's own `alg` header enables `alg: none` and RS256→HS256 confusion attacks.
OAuth Scope
A space-delimited string of permission labels (e.g. `read:user`, `repo`, `email`) that bounds what an OAuth access token is allowed to do. The app requests scopes at authorization time, the user sees and consents to them on the grant screen, and the resource server (API) is what actually enforces them on each call — the token itself doesn't restrict anything. Two things trip people up: scopes are coarse-grained, so a scope like `repo` covers ALL your repositories, not the one you care about — it's not a substitute for per-object authorization. And over-requesting hurts you twice: it spooks users at the consent screen, and it widens the blast radius if the token leaks. Follow least privilege: ask only for what you need, and use incremental/step-up authorization to request more scopes later when a feature actually requires them.
CSRF (Cross-Site Request Forgery)
An attack where a malicious site tricks a victim's browser into firing an authenticated request to a site the victim is already logged into — the browser auto-attaches the session cookie, so the request looks legitimate. The root cause is ambient authority: cookies ride along on cross-origin requests by default, so a hidden form or an `img` tag can POST to `bank.com/transfer` on the user's behalf. The standard fixes are a CSRF token (a secret value the server hands out and demands back on state-changing requests, which an attacker can't read or guess) and/or `SameSite=Lax/Strict` cookies, which tell the browser not to send the cookie on cross-site requests. The classic gotcha: GET requests that mutate state, and APIs that trust cookies but forget tokens — and note that CORS does NOT protect you, since the forged request is often a simple request the browser sends before any CORS check applies.
Same-Origin Policy (SOP)
A core browser security rule: JavaScript on a page may freely read responses only from the same origin — the exact same scheme, host, AND port (so `https://app.com`, `http://app.com`, `app.com:8080`, and `api.app.com` are all distinct origins). It exists to stop a page you visit from quietly reading data out of another site you're logged into. The key thing people miss is that SOP gates *reading the response*, not *sending the request*: the browser often still sends a cross-origin request (and attaches its cookies) and merely hides the result from your script — which is exactly why CSRF is a separate problem SOP doesn't solve. Don't confuse SOP with CORS either: SOP is the default lockdown, and CORS is the opt-in mechanism a server uses (via `Access-Control-*` response headers) to grant specific origins read access back.
Session vs Token (Auth)
Two ways to keep a user logged in after they authenticate. Session auth is stateful: the server stores the session (in memory, Redis, or a DB) and hands the browser only an opaque session id in a cookie, which it looks up on every request. Token auth (typically a JWT) is stateless: the token itself carries the identity and claims and is trusted because its signature verifies, so no server-side lookup is needed. The real trade-off isn't 'stateful vs stateless' for its own sake — it's revocability: sessions can be killed instantly by deleting one row, while a signed token stays valid until it expires, so 'log out everywhere' or banning a user needs a short token lifetime plus a blocklist or refresh-token rotation (which quietly reintroduces server state). Two gotchas people miss: cookie sessions ride along automatically so they need CSRF protection, whereas a Bearer token in an Authorization header dodges CSRF but is exposed to XSS if you stash it in JS-readable localStorage.
Refresh Token
A long-lived credential that an OAuth/OIDC client exchanges at the token endpoint for a fresh short-lived access token, so the user doesn't have to log in again. The split exists on purpose: access tokens are kept short (minutes) because they're usually self-contained (JWT) and validated without a callback to the auth server, which means the server can't easily revoke one before it expires — so the long-lived refresh token becomes the high-value secret that buys new ones. Best practice is rotation — each refresh issues a new refresh token and invalidates the old one, so if a stolen token is replayed the server detects reuse of a retired token and revokes the whole chain. The classic mistake is treating it like just another access token: never put it in `localStorage` (XSS-readable) or in a public SPA/mobile binary — store it server-side or in an `HttpOnly`, `Secure`, `SameSite` cookie, scope it narrowly, and always pair rotation with reuse detection.
SSRF (Server-Side Request Forgery)
An attack where the attacker tricks your server into making an HTTP request to a destination of their choosing, abusing the server's network position rather than the user's. The classic case is a feature that fetches a user-supplied URL (a webhook tester, a 'fetch image from URL' importer, a PDF renderer): the attacker points it at internal addresses the public internet can't reach, like 169.254.169.254 to read cloud metadata and steal IAM credentials, or localhost:6379 to talk to an unauthenticated Redis. The thing people miss is that a naive allowlist or blocklist on the raw string is trivially bypassed: DNS rebinding makes a hostname resolve to a safe IP at check time and an internal IP at fetch time, redirects send a 'safe' URL to an internal one, and odd notations like decimal IPs slip past string filters. Defend by resolving the host yourself, validating the actual resolved IP against a deny-by-default allowlist, re-checking after every redirect, and blocking link-local and private ranges at the network layer too.
Clickjacking (UI Redress)
A malicious page loads your real site in an invisible iframe and overlays a decoy UI, so the victim thinks they are clicking the attacker's button (a fake 'Play' or 'Win a prize') but the click lands on a hidden real button like 'delete account' or 'grant app access'. It works because Same-Origin Policy hides the framed page's pixels from the attacker's script, yet the browser still renders it and still attaches the victim's session cookie, so the click is a genuine authenticated action. CSRF tokens and SameSite cookies do NOT help: it is a real same-session click, not a forged cross-site request. The fix is to refuse being framed: send the CSP directive frame-ancestors 'none' (or list the origins allowed to embed you), backed by legacy X-Frame-Options: DENY or SAMEORIGIN for old browsers. Gotcha: X-Frame-Options cannot allowlist multiple origins and is superseded by frame-ancestors, so set both but treat the CSP one as authoritative.
Crypto & hashing
Symmetric vs Asymmetric Encryption
Two families of encryption. Symmetric uses one shared secret key to both encrypt and decrypt (e.g. AES) — fast and ideal for bulk data, but you have to get that key to the other side without anyone intercepting it. Asymmetric uses a public/private key pair (e.g. RSA, ECC): anyone can encrypt with your public key, only your private key decrypts, which solves key distribution but is far slower. The practical insight is that real systems combine them — TLS uses asymmetric crypto only to exchange a random symmetric session key, then encrypts the actual traffic with the fast symmetric cipher.
Digital Signature
A scheme where you sign data with your PRIVATE key and anyone can verify the signature with the matching PUBLIC key. It proves three things at once: the data wasn't altered (integrity), it really came from the key owner (authenticity), and they can't later deny signing it (non-repudiation). The key direction is the mirror image of public-key encryption: encryption uses the public key to lock and the private key to unlock, while signing uses the private key to sign and the public key to verify. The common gotcha is that a signature does NOT keep data secret, it only protects who-sent-it and whether-it-changed, so signing and encrypting are separate steps, and you often need both.
Key Derivation Function (KDF)
An algorithm such as PBKDF2, bcrypt, scrypt, or Argon2 that stretches a low-entropy password into a key or storable hash, using a per-user salt and a tunable work factor that makes each guess deliberately slow (and, for scrypt and Argon2, memory-hard so GPUs and ASICs lose their edge). The slowness is the whole point: it barely affects one legitimate login but cripples an attacker trying billions of guesses against a stolen database. The classic mistake is storing passwords with a plain fast hash like SHA-256 or MD5 — those are built to be fast, so a leaked table can be brute-forced at billions of hashes per second. Use a real password KDF (Argon2id is the modern default), let it generate the salt, and raise the cost parameters as hardware gets faster.
Hashing
A one-way function that maps input of any size to a fixed-length value (the hash or digest). It's deterministic — the same input always yields the same output — but designed so you can't reverse the digest back to the input, and a one-bit change to the input scrambles the whole output (the avalanche effect). Don't confuse it with encryption: there's no key and nothing to decrypt, so it's for integrity checks and fingerprints (dedup, content addressing, `ETag`s, signature inputs), not for hiding recoverable data. By the pigeonhole principle collisions must exist, so the security that matters is being collision- and preimage-resistant in practice; MD5 and SHA-1 are broken for that and unsafe for anything security-sensitive — reach for SHA-256/SHA-3/BLAKE3. And never hash passwords with a plain fast hash, even SHA-256: those are built for speed, which is exactly what an attacker wants, so use a slow password KDF (Argon2id) instead.
Encryption
A reversible transform that scrambles plaintext into ciphertext with a key, so only a holder of the right key can read it back — symmetric schemes (e.g. AES) use one shared key, asymmetric ones (e.g. RSA, ECC) use a public/private key pair, and in practice you usually combine them: asymmetric to exchange a random symmetric key, then fast symmetric encryption for the bulk data. The crucial thing people miss is that plain encryption only gives confidentiality, not integrity or authenticity — an attacker who can't read the ciphertext can still flip bits and tamper with it. So use authenticated encryption (AEAD like AES-GCM or ChaCha20-Poly1305), which encrypts and adds a tamper-detecting tag in one step; never use ECB mode (it leaks patterns) and never reuse a nonce/IV with the same key. And don't roll your own crypto — the real difficulty is key management and getting these details right, not the math.
Salt
A random, per-password value mixed into a password before it's hashed, then stored in the clear right next to the hash — it isn't secret, its job is to be unique. That uniqueness is what defeats precomputed attacks: with a distinct salt per user, an attacker can't reuse a rainbow table or a single cracked hash, and two people with the same password still get different stored hashes, so you can't tell from the table who shares a password. The classic mistakes are using no salt, or one global/hardcoded salt for everyone — both collapse back to the precomputed-attack problem. Equally important: a salt only adds uniqueness, not slowness, so it does not replace a real password KDF. In practice you almost never roll it yourself — use Argon2id (or bcrypt), let it generate a fresh random salt per hash, and store the salt and parameters that come bundled in its output string.
Nonce
A 'number used once' — an arbitrary value that must be unique for each operation under a given key or session, and never reused. Nonces defeat replay attacks (an attacker can't resend a captured request, because the old nonce is no longer accepted) and they keep stream/AEAD ciphers like AES-GCM and ChaCha20-Poly1305 secure, where the nonce (sometimes called an IV) makes the keystream unique per message. The critical gotcha is reuse: in AES-GCM, encrypting two messages with the same key+nonce pair leaks the XOR of the plaintexts AND lets an attacker forge the authentication tag — a catastrophic, key-compromising failure. Note that a nonce doesn't have to be random, just unique: a monotonic counter is a valid nonce, while a random one risks collisions if it's too short. Different from a CSP nonce, which is a fresh per-response random token tying inline scripts to a policy.
HMAC
Hash-based Message Authentication Code — a keyed construction that hashes a message together with a shared secret to produce a short tag. The receiver, who holds the same key, recomputes the tag and checks it matches, proving both that the message wasn't tampered with (integrity) and that it came from someone holding the key (authenticity). The reason HMAC exists rather than a naive `hash(key + message)` is that with older Merkle–Damgård hashes (MD5, SHA-1, SHA-256) that simple concatenation is vulnerable to length-extension attacks; HMAC's nested two-pass design (`H(key ⊕ opad ‖ H(key ⊕ ipad ‖ msg))`) closes that hole. Two gotchas in practice: compare tags with a constant-time function (`crypto.timingSafeEqual`, not `===`) so you don't leak the secret through timing, and use a high-entropy random key — HMAC authenticates, it does not slow down password guessing, so it's not a substitute for a password KDF.
Checksum
A short fixed-size value computed from a block of data so you can detect whether it changed: recompute it on the received bytes and compare against the published value. The catch is that 'checksum' covers two very different things. Error-detecting checksums like CRC32 or Adler-32 only catch accidental corruption (a flipped bit, a truncated download) and are trivial to forge on purpose. A strong cryptographic hash like SHA-256 resists deliberate forgery — but MD5 and SHA-1 are broken (practical collisions exist), so treat them as legacy fingerprints, not tamper-proofing. Even a strong hash only proves integrity if you got the digest over a trusted channel; otherwise an attacker swaps both the file and its checksum. To prove nobody deliberately tampered with the data, use a keyed MAC (HMAC) or a digital signature, not a bare checksum.
TLS (Transport Layer Security)
TLS (Transport Layer Security) encrypts a network connection so an eavesdropper cannot read it or tamper with the traffic undetected — giving confidentiality, integrity, and authentication of the server. It is the S in HTTPS and the successor to the old, broken SSL. A connection opens with a handshake: client and server agree on a cipher suite, the server proves its identity with a CA-signed certificate, and they derive a shared symmetric session key (modern TLS uses ephemeral ECDHE). That ephemeral exchange gives forward secrecy — stealing the server private key later still will not decrypt captured past sessions, since the session key never went on the wire. The gotcha devs miss: the certificate is what binds the key to a hostname, so never disable certificate verification to silence an error — that leaves you encrypted to an unknown party, exactly what TLS prevents. Prefer TLS 1.3 and let the platform pick the cipher suite.
Base64
An encoding that represents arbitrary binary data as ASCII text: it takes the bytes 6 bits at a time and maps each group to one of 64 printable characters (A-Z, a-z, 0-9, plus + and / ), padding the end with = signs. The whole point is transport — it lets you push raw bytes through channels that only tolerate text, like a JSON string, a URL, an email body, or an HTTP header. The number-one misconception is that Base64 is encryption or in any way secure: it is not. There is no key and anyone can decode it instantly, so it hides nothing — never use it to protect passwords or secrets. Two practical gotchas: it inflates size by about 33% (every 3 bytes become 4 characters), so big payloads like images embedded as a data: URI bloat the page; and the standard alphabet uses + and / which break inside URLs and filenames, so reach for the URL-safe variant (base64url, which swaps them for - and _ and usually drops the padding) in those contexts.
PKI (Public Key Infrastructure)
The whole system of certificate authorities (CAs), certificates, and trust rules that lets you trust a public key actually belongs to who it claims to. An X.509 certificate binds a public key to an identity (a domain, a person) and is signed by a CA's private key; your browser or OS ships a trust store of root CA certificates it trusts implicitly. Trust flows along a chain: a root CA signs intermediate CAs, intermediates sign leaf certificates, and a verifier walks that chain of trust up to a trusted root, checking each signature, the validity dates, and that the cert was issued for that name. The common gotchas: the server must send the full intermediate chain (a missing intermediate causes mysterious failures on some clients but not others, since they cache intermediates), trust ends the moment the root's private key is compromised, and a still-valid cert can be revoked early via CRL or OCSP — which clients often fail open on. Add too-permissive trusted roots and the whole model is only as strong as the least trustworthy CA in the store.
Async & concurrency
Pub/Sub (Publish/Subscribe)
A messaging pattern where producers publish events to a named topic without knowing who, if anyone, is listening, and any number of subscribers independently receive a copy. This decouples senders from receivers and gives you fan-out for free: add a new consumer without touching the producer. It differs from a point-to-point queue, where each message is delivered to exactly one worker; in pub/sub every subscriber gets every message. The gotcha: a slow or offline subscriber silently misses events unless the broker is configured for durable subscriptions, so think through your delivery guarantees and ordering up front.
At-Least-Once Delivery
A messaging guarantee where every message is delivered one OR MORE times: it is never lost, but the same message can arrive again (e.g. after a network hiccup, a redelivery, or a consumer that crashed before acking). The practical consequence is that your consumer MUST be idempotent: processing the same message twice has to produce the same end state. Contrast with at-most-once (fast, but may silently drop messages) and exactly-once (every message handled once and only once, which is expensive and usually faked at the application layer via dedup keys rather than truly guaranteed by the broker).
Race Condition
A bug where the correctness of the result depends on the unpredictable timing or interleaving of two or more concurrent operations touching shared state — so the same code can pass or fail depending on which one 'wins the race'. The classic shape is read-modify-write: two requests both read balance=100, each subtracts 10, and both write 90, so one deduction silently vanishes. What trips people up is the non-determinism: it passes every test on your laptop, then corrupts data in prod only under load, and a 'fix' that just adds a retry doesn't actually close the window. The real fix is to make the dangerous step atomic — a single DB UPDATE ... SET x = x - 10, a transaction with row locking, compare-and-swap, or a mutex. Any check-then-act on shared state (if not exists, then create) is a smell unless the check and the act happen as one indivisible operation.
Deadlock
A state where two or more threads (or processes, or transactions) are each blocked forever, each holding one resource while waiting for a lock that another in the cycle holds — so nobody can proceed and the work just hangs. It needs four conditions at once (mutual exclusion, hold-and-wait, no preemption, and a circular wait), and the easiest one to break in your own code is the circular wait: always acquire locks in a single, globally consistent order, so a cycle can never form. The classic trigger is two code paths grabbing the same two locks (A then B vs. B then A) and interleaving at just the wrong moment. Databases detect deadlocks and kill one transaction as the 'victim' (you get an error and must retry), but in-process thread deadlocks usually just freeze silently, so design lock ordering up front rather than hoping a timeout saves you.
Debounce
A technique that delays running a function until a burst of repeated events has stopped for a set quiet period (say 300ms), so only the LAST event in the burst triggers the work. Classic use is search-as-you-type: instead of hitting the API on every keystroke, you wait until the user pauses, then fire one request. Two things trip people up. First, the debounced function must keep a stable reference across renders — recreate it every render (a fresh closure in a React component) and the timer keeps resetting, so it never fires; memoize it. Second, cancel the pending timer on unmount, or a late call fires against a component that's gone. Contrast with throttle, which fires at a steady cadence during the burst instead of waiting for it to stop.
Throttle
A technique that caps how often a function runs to at most once per fixed interval, no matter how rapidly the triggering event fires — so a 100ms throttle on a scroll handler that fires 60+ times a second runs it roughly every 100ms instead. Use it for high-frequency continuous events where you want steady, regular updates: scroll, mousemove, drag, resize, or rate-limiting expensive API calls. The classic mix-up is throttle vs debounce: throttle runs at a steady cadence DURING the burst, while debounce waits for the burst to stop and runs once at the end — pick throttle when you need updates mid-stream, debounce for 'when they're done' (search-as-you-type). The other gotcha is the leading vs trailing edge: a trailing-only throttle can drop the very last event, so the final resting position is never rendered. Most real implementations (e.g. lodash `throttle`) fire on both edges by default for exactly this reason.
Backpressure
A flow-control mechanism for when a producer generates data faster than a consumer can handle it: instead of letting work pile up, the consumer signals back upstream to slow down, pause, or stop sending until it catches up. The three real responses are buffer (hold the excess, but bounded), drop (shed load you can't keep), or block/pause the producer — and that signal MUST propagate end-to-end through every stage of the pipeline, or the slowest stage just moves the bottleneck somewhere else. The classic mistake is treating an UNBOUNDED queue or buffer as 'handling' backpressure: it hides the problem until memory blows up or latency creeps into minutes. Built in to Node streams (`.pipe()` respects `highWaterMark`), Reactive Streams, and gRPC; with raw async loops or fire-and-forget message producers you have to enforce it yourself via bounded queues or `await`-ing the consumer.
Eventual Consistency
A consistency model for distributed/replicated datastores: right after a write, different replicas can return different values, but the system guarantees they all converge to the same value once writes stop propagating. You trade the immediate, everyone-sees-the-latest-write guarantee of strong consistency for higher availability and lower latency — the AP side of CAP. The classic trap is assuming a read issued right after a write will see it: it often hits a lagging replica and returns stale data, which breaks flows like 'save profile, then reload it.' Convergence also isn't free magic — concurrent writes to the same key need a conflict-resolution strategy (last-write-wins by timestamp, vector clocks, or CRDTs), and naive last-write-wins silently drops updates. When users must see their own change immediately, reach for read-your-writes / monotonic-read guarantees or route those reads to the primary.
Message Queue
A broker that sits between a producer and a consumer and holds messages in a durable buffer, so the sender can drop work and move on while a worker pulls it later at its own pace. In the point-to-point model each message is delivered to exactly one consumer, which gives you three things for free: decoupling (producer and consumer never need to be up at the same time), load leveling (a traffic spike fills the queue instead of crushing the worker), and durability (messages survive a consumer crash or restart because the broker persists them until acked). The gotcha is that a queue is a buffer, not a magic fix: under sustained overload it grows unbounded and just relocates the failure to memory or disk, so cap the depth and decide what happens when it is full. Because brokers like RabbitMQ, SQS, and Kafka almost always give at-least-once delivery, a redelivered message means your consumer MUST be idempotent, and a message that keeps failing needs a retry limit plus a dead-letter queue or it blocks the line forever. Reach for one to absorb spikes and run slow background jobs; do not reach for it when the caller actually needs a synchronous answer right now.
Optimistic Locking (Optimistic Concurrency)
A concurrency-control strategy that assumes write conflicts are rare, so it lets everyone read and edit freely and only checks for a clash at save time — no row is locked while a user is thinking. The usual implementation is a version column (or a timestamp, or an ETag): you read row version 7, and your UPDATE says SET ... , version = 8 WHERE id = 42 AND version = 7. If someone else already bumped it to 8, your WHERE matches zero rows, the write is rejected, and you re-read and retry. The gotcha is that the version check and the write must be one atomic statement (a compare-and-swap), not a separate SELECT then UPDATE, or the race you meant to prevent reopens. Use it on low-contention data with human think-time; switch to pessimistic locking (SELECT ... FOR UPDATE) only when conflicts get frequent enough that constant retries cost more than holding a lock.
Architecture
CAP Theorem (Consistency, Availability, Partition tolerance)
A rule about distributed datastores: you can't simultaneously guarantee all three of Consistency (every read sees the latest write), Availability (every request gets a non-error response), and Partition tolerance (the system keeps working when the network drops messages between nodes). Since network partitions will happen and you can't opt out of them, the real decision is what to do during a partition: refuse some requests to stay consistent (CP), or keep answering with possibly-stale data to stay available (AP). The popular 'pick 2 of 3' phrasing is misleading — it's really a C-vs-A trade-off that only bites during a partition, and a practical lens rather than a strict law.
Memoization
An optimization that caches the result of a function call keyed by its arguments, so a repeat call with the same inputs returns the stored value instead of recomputing it. It is only correct for pure functions — ones whose output depends solely on their inputs with no side effects — because a memoized impure function will keep handing back a stale first result. Two things trip people up in practice. First, the cache is unbounded by default: memoizing over high-cardinality inputs (user IDs, timestamps) is a slow memory leak, so anything hot needs a bounded cache with eviction like LRU. Second, the key is the hard part — object or array arguments must be turned into a stable key (serialize them, or rely on referential equality), or you get constant cache misses or, worse, a stale hit after the object was mutated. And note React's useMemo/useCallback are caching hints, not guarantees: React may drop them, so use them for performance, never for correctness.
Serialization
The process of converting an in-memory object or data structure into a flat, self-contained representation — JSON, Protobuf, MessagePack, or a raw byte stream — that can be written to disk or sent across a network, where pointers and object graphs mean nothing. Deserialization is the reverse: rebuilding a live object from those bytes. The format is a contract between two sides, so the classic failures are schema drift (the writer adds or renames a field the reader doesn't expect) and values that simply don't survive the trip — functions, open sockets, circular references, or a Date/Decimal that JSON flattens into a lossy string. Two deeper traps: native binary formats (Java/Python pickle, PHP unserialize) can execute attacker-controlled code, so never deserialize untrusted input that way; and floats, big integers, and timezones quietly lose precision. Prefer an explicit, versioned schema (Protobuf, Avro) over reflecting on whatever your language hands you.
Circuit Breaker
A resilience pattern that wraps calls to a downstream dependency in a state machine to stop hammering something that is already failing. It is normally `closed` (calls pass through); once errors or timeouts cross a threshold it trips to `open` and fails fast for a cooldown window, then moves to `half-open`, letting a few trial requests through to probe recovery before closing again or re-opening. The point is to fail fast: a dead dependency answered instantly beats threads and connection-pool slots piling up on doomed calls, which is how one slow service cascades into taking down its callers. The thing people miss is that a breaker protects the *caller*, not the broken service, so it only helps if you pair it with sensible timeouts (a breaker can't trip on a call that never returns) and a fallback for the open state. Tune the threshold too twitchy and it flaps open on normal blips; too loose and it never trips. Use a library (Resilience4j, Polly, Istio) rather than rolling the state machine yourself.
Idempotency Key
A unique, client-generated identifier attached to a request (often as an `Idempotency-Key` header) so the server can recognize a retry of the same logical operation and return the original result instead of performing it twice. It's what makes retrying a non-idempotent action like charging a card safe after a timeout, where you got no response but the charge may or may not have gone through. The server stores the key mapped to its outcome, so a duplicate replays the saved response rather than billing again. The classic mistakes: generating a fresh key on each retry (which defeats the whole point — the key must be created once and reused across attempts), and not handling two requests with the same key arriving concurrently before any result is stored, which needs a lock or a unique constraint so the operation runs exactly once. HTTP verbs that are idempotent by contract — GET, PUT, DELETE — usually don't need one; it's POST-style create/charge operations whose repeated effects aren't safe that do.
Load Balancing
Spreading incoming requests across multiple backend instances so no single one is overwhelmed, which raises throughput and availability and lets you scale horizontally. A load balancer (e.g. NGINX, HAProxy, or a cloud LB) sits in front and picks a target per request using a strategy like round-robin, least-connections, or consistent hashing (which keeps a given key on the same instance — useful for caches). The piece people forget is health checks: the LB must actively probe each instance and stop routing to ones that fail, or it will happily keep sending traffic to a dead box. The other classic trap is in-memory state — once a user's requests can land on any instance, anything kept in one instance's RAM (sessions, local caches, uploaded temp files) breaks. The right fix is to make instances stateless and push shared state to Redis or a database; sticky sessions are a fragile workaround, not a real solution.
Graceful Degradation
A design approach where, when a non-critical dependency fails or times out, the system keeps serving a reduced-but-usable experience instead of erroring out the whole request — e.g. hiding the recommendations rail (or showing a cached/default list) when the recommender is down, while checkout still works. The key move is separating core paths that must stay up from optional features that are allowed to disappear, then wrapping each optional call in a timeout, fallback value, and ideally a circuit breaker so one slow dependency can't drag the whole page down. The classic trap is that the degraded path is never exercised in normal traffic, so it silently rots: the fallback throws its own exception, the cache is empty, or the timeout is longer than the user's patience. Treat fallbacks as real code — test them, monitor when they fire, and fail fast rather than hanging.
API Gateway
A single entry point that sits in front of many backend services and routes each incoming request to the right one, so clients hit one address instead of a dozen. Beyond routing it centralizes the cross-cutting concerns you do not want duplicated in every service: TLS termination, authentication and token validation, rate limiting, request/response transformation, and observability. A common variant is the BFF (Backend For Frontend): one gateway per client type that aggregates several service calls into the single payload a mobile or web app needs. The gotcha is that a gateway is shared infrastructure and a single point of failure, so it must be scaled horizontally and kept thin: push business logic into it and you have rebuilt a monolith in the worst place. Keep auth, routing, and shaping there; keep domain rules in the services.
Feature Flag (Feature Toggle)
A boolean (or multi-variant) switch in config or a flag-management service that decides at runtime whether code runs, without shipping a new build. The same deployed binary can serve a feature off to everyone, on to an internal team, or on to 5 percent of traffic just by flipping the flag — which decouples deploy (code on the server) from release (users actually seeing it). It powers gradual rollouts and canaries, instant kill switches, and A/B tests by bucketing users on a stable hash of their id. The trap is flag debt: every flag is a live branch, so an evaluation that silently defaults wrong, two interacting flags, or a temporary flag left in for a year all become bugs. Treat short-lived rollout flags as cleanup tasks with an expiry, keep long-lived ops kill switches deliberately, and never gate auth or billing correctness on a flag.
Data & databases
ACID (Atomicity, Consistency, Isolation, Durability)
ACID is a set of four guarantees a database transaction provides so a group of reads and writes behaves as one reliable unit: Atomicity (all statements commit or none do, no half-applied transfer), Consistency (it moves the data from one valid state to another, honoring constraints — not the same as CAP's consistency), Isolation (concurrent transactions do not see each other's uncommitted changes), and Durability (after commit returns, the data survives a crash). The part people misread is Isolation: it is not all-or-nothing but a dial of levels (read committed, repeatable read, serializable), and weaker levels trade protection from dirty reads, non-repeatable reads, and phantom rows for throughput. The common mistake is assuming the default is serializable; most engines default to read committed, so you can still hit lost updates. Use a higher level or a row lock on money-moving paths, and remember ACID is a single-node story: shard or span services and you are back to eventual consistency and sagas.
Database Index
A separate, sorted data structure (usually a B-tree) that lets the database find rows by a column's value without scanning the whole table, the way a book index lets you jump to a page instead of reading every one. Without it, a query filtering on that column does a full table scan that gets linearly slower as the table grows. The common gotcha is that an index only helps when the query can use it: a composite index on (tenant_id, created_at) speeds up filters on tenant_id alone or both together, but not on created_at by itself, because B-trees are ordered left to right. Wrapping the column in a function, a leading-wildcard LIKE, or a type mismatch also disables it. Indexes are not free — every insert, update, and delete must maintain them, so they trade write speed and disk for read speed. Add them for the columns you actually filter, join, and sort on, consider a covering index that holds every column a hot query needs so it never touches the table, and drop indexes nothing uses.
Normalization (Database Normalization)
A schema-design discipline for relational tables that removes redundant data by splitting it into related tables linked by foreign keys, so every fact lives in exactly one place. The normal forms build on each other: 1NF means each column holds a single atomic value with no repeating groups or comma-lists; 2NF removes columns that depend on only part of a composite key; 3NF removes columns that depend on another non-key column instead of the key itself. The payoff is integrity — you update a customer's address in one row, not in every order they ever placed, so the data can't silently disagree with itself. The gotcha is that more tables means more JOINs, and on read-heavy paths those JOINs cost. So normalize first as the default, then denormalize deliberately and only where profiling proves it — duplicating a column or precomputing a total — and accept that you now own keeping the copies in sync.
Transaction Isolation (Isolation Levels)
A per-transaction setting controlling how much a running transaction can see uncommitted or concurrently-changing data from others — the dial that trades correctness for concurrency. The SQL standard names four levels by the read anomalies they forbid: READ UNCOMMITTED allows dirty reads (seeing another tx's uncommitted changes); READ COMMITTED shows only committed rows, but two reads in one tx can differ (a non-repeatable read); REPEATABLE READ keeps re-read rows stable, yet new matching rows can still appear (a phantom read); and SERIALIZABLE behaves as if transactions ran one at a time. The gotcha: these names are guarantees, not implementations, and defaults differ by engine — PostgreSQL defaults to READ COMMITTED, and its REPEATABLE READ is really snapshot isolation that also blocks phantoms, while MySQL InnoDB defaults to REPEATABLE READ. Never assume a level's behavior by name across databases; check the engine docs, and reach for SERIALIZABLE only when an invariant truly needs it, since it costs extra locking or serialization-failure retries.
N+1 Query Problem
A data-access anti-pattern where fetching a list of N records then triggers one extra query per record to load a related field, so you run 1 query for the parent rows plus N more for their children — N+1 round trips where 2 would do. It is the classic trap of lazy-loading in an ORM: looping over orders and reading order.customer.name inside the loop looks like plain field access, but each access quietly fires a separate SELECT. It rarely shows up in tests with 3 rows, then melts the database in production with 3,000, because the cost scales with the data, not the code. The fix is eager loading — pull the related rows up front in one go, via a JOIN or a single batched IN (...) query (Rails includes, Prisma include, JPA join fetch, a DataLoader). Watch query logs or an APM, not response shape: a page can look correct while firing hundreds of hidden queries. Eager-load only the relations you actually use, or you swap N+1 for over-fetching.
Connection Pooling
A technique where a fixed set of open database connections is kept alive and reused, handed out to requests on demand instead of opening a fresh connection per query and closing it afterward. Opening one is expensive — TCP handshake, TLS, auth, and a server-side backend (a whole process in Postgres) — so under load that per-request cost dominates, and since the database caps total connections (Postgres defaults to about 100) a flood of them can exhaust it. A pool bounds that: requests borrow a connection, run their query, and return it, so concurrency is capped by pool size, not by traffic. The classic gotcha is pool exhaustion — every connection is checked out and new requests block on a wait queue, so one slow query, a leaked connection, or a long-open transaction can stall the whole app and look like the database is down. Size the pool to what the database can handle, not to your request rate; in serverless or many-instance setups front it with an external pooler like PgBouncer, since each instance running its own pool multiplies straight into max-connection errors.
Replication (Primary–Replica)
Keeping continuously-updated copies of a dataset on more than one server, so a database survives a node dying and can spread read traffic across machines. The most common shape is primary–replica: all writes go to one primary, which streams its change log to one or more read replicas that you point read-only queries at. The catch is replication lag — a replica applies the log a moment behind the primary, so a read issued right after a write can land on a replica that has not caught up and return stale data, breaking flows like "save, then immediately reload." Failover (promoting a replica to primary when the old one dies) gives you high availability but is not free: an async replica can be promoted while still missing the last few committed writes, silently losing them. Use read replicas to scale reads, route read-your-writes traffic to the primary, and prefer synchronous or semi-sync replication when you cannot tolerate that data loss.
SQL vs NoSQL
Two database families that store and query data differently; the choice is about your access pattern, not which is newer. SQL (relational: Postgres, MySQL) keeps rows in tables with a fixed schema, splits related data across tables and rejoins it with JOINs, and gives you ACID transactions — where the C means enforcing your invariants and constraints (not CAP consistency), so several rows change as one all-or-nothing unit. That suits highly relational data where correctness matters, like money. NoSQL is an umbrella: document (MongoDB) stores whole JSON-like objects so reads need no joins, key-value (Redis, DynamoDB) is a hash map for fast lookups by key, and wide-column (Cassandra) suits append-heavy writes. The trap is the line that NoSQL means no schema — it still exists, you have just moved it from the database into the app code that reads the data (schema-on-read), so one careless write silently corrupts the shape. NoSQL scales out more easily but usually leans BASE — eventual consistency and weak or single-document transactions — so cross-document invariants become your job. Default to SQL; reach for NoSQL when one access pattern outgrows a relational table plus indexes, recalling an index speeds reads but slows writes and costs storage. Most real systems run both.
DevOps & deploy
CI/CD
An automated pipeline that takes code from a commit to a running system. CI (Continuous Integration) builds and tests every change as soon as it is pushed, so breakage shows up on each commit instead of during a painful big-bang merge — 'works on my machine' stops being a thing. CD has two meanings: Continuous Delivery stops at a button you press to release, while Continuous Deployment goes straight to production with no human gate. The mistake people make is treating CI as just 'the thing that runs tests'. A real pipeline produces one immutable artifact — a container image or versioned bundle — built once and promoted unchanged through staging to prod; rebuild it per environment and you ship something you never tested. Keep it fast and deterministic, because flaky tests train people to retry and ignore red, and never deploy without a path back: pair Deployment with health checks and auto-rollback, or you just ship bugs to users faster.
Containerization
A way to package an application with its runtime, libraries, and config into one isolated, portable unit (a container) that runs the same on a laptop, in CI, or in production. The common misconception is that a container is a lightweight VM — it is not. It is just a normal Linux process whose view of the system is fenced off by kernel features: namespaces hide other processes, the filesystem, and the network, while cgroups cap its CPU and memory. There is no guest OS, so it starts in milliseconds and shares the host kernel. The image is built in stacked, content-addressed layers (the OCI image format) from a Dockerfile, and Docker is just one runtime that runs them. Layer caching is what people get wrong: put slow, stable steps like installing dependencies before the line that copies your fast-changing source, or every edit rebuilds the world. And containers isolate but do not secure by default — they share the kernel, so never treat one as a hard boundary for untrusted code, and never bake secrets into an image, since every layer is recoverable.
Container Orchestration
A control plane that runs and manages containerized workloads across a cluster of machines, so you declare a desired state — run 3 replicas of this image on this port — and it works to keep reality matching that, instead of SSH-ing into boxes to start containers by hand. Kubernetes (k8s) is the de-facto standard: you submit manifests, a scheduler places pods (the smallest unit — one or more containers that share a network and storage) onto nodes with spare capacity, and controllers continuously reconcile, giving you self-healing (a crashed container is restarted, and a dead node’s pods are rescheduled elsewhere) plus rolling updates, service discovery, and autoscaling. The thing people underestimate is the operational weight: it is a distributed system you now have to run, and most early outages are self-inflicted — missing resource limits so one pod starves a node, liveness probes that restart healthy pods, or keeping state in a pod whose local disk is lost the moment it is rescheduled. Reach for it when you genuinely have many services to schedule and scale; for a single container a managed runner (Cloud Run, ECS/Fargate, a PaaS) is usually the saner call.
Infrastructure as Code (IaC)
Infrastructure as Code (IaC) is the practice of defining servers, networks, and databases in version-controlled text files that a tool provisions automatically, instead of clicking through a cloud console by hand. The dominant style is declarative: you describe the desired end state and a tool like Terraform or Pulumi computes the diff against reality, then applies only what changed. That is what makes a re-run idempotent — applying the same config twice leaves the same state instead of creating duplicates. The gotcha is drift: someone hotfixes a resource by hand in the console, the live world stops matching the code, and the next apply either reverts their fix or fails on a surprise. Treat the code as the single source of truth, never change infra by hand, keep the state file in a locked remote backend (not a laptop), and review every plan/diff before apply the way you review a pull request — a careless change can drop a prod database.
Observability
How well you can understand what a running system is doing from the outside, from the data it emits — so you can answer new questions about a production problem without shipping code to add print statements. It rests on three pillars: logs (timestamped records of discrete events), metrics (cheap numeric time-series like p99 latency, good for dashboards and alerts), and traces (one request's path as it hops across services, showing where the time went). Do not treat it as a synonym for monitoring: monitoring watches the known failure modes you predicted; observability is for debugging the unknown-unknowns you did not. The trap is data without correlation — a log, a metric spike, and a slow trace in three tools that you cannot tie back to one request. So propagate a trace/request id everywhere, stamp it onto structured logs (not string blobs), and adopt OpenTelemetry to keep instrumentation vendor-neutral. And watch cardinality: a label like a user id on a metric quietly explodes storage cost.
Blue-Green Deployment
A release strategy that runs two identical production environments — blue (live) and green (idle) — and ships a new version by deploying to the idle one, smoke-testing it in isolation, then flipping the load balancer or router so all traffic lands there in a single switch. Because the cutover is instant and atomic, users see no downtime, and rollback is just pointing traffic back at the still-running old environment instead of redeploying. The catch is the shared state behind those "two environments": the database is usually common to both, so migrations must be backward-compatible — expand then contract, so a new column is nullable and additive while the old version still runs, and you only drop or rename it after that version is gone. You also pay for double the infrastructure during the switch, and in-flight requests and warmed caches do not transfer, so drain connections first. Reach for a canary instead when you want to expose the new version to a slice of real traffic and watch metrics before committing.
Canary Release
A progressive-delivery technique where a new version is rolled out to a small slice of real production traffic first — say 1 then 5 then 25 percent — while you watch its error rate, latency, and key business metrics, and only promote it to everyone once it proves healthy (otherwise you roll back the canary, not the whole fleet). The name comes from the canary in a coal mine: the small cohort takes the risk so the majority never sees a bad build. It differs from a feature flag, which gates a feature for chosen users, and from blue-green, which flips 100 percent of traffic at once — a canary shifts a *percentage* and is steered by a load balancer, service mesh, or ingress weights. The trap people hit is trusting time instead of signals: a fixed wait with nobody actually comparing canary-vs-baseline metrics is not a canary, it is a delayed full rollout. Make promotion automatic and metric-gated (Argo Rollouts, Flagger, Spinnaker), pin the canary to its own dashboards, and remember sticky sessions or a shared database migration can leak canary behavior into the baseline and hide the problem.
Horizontal vs Vertical Scaling
Two opposite ways to add capacity. Vertical scaling (scale up) means giving one machine more resources — more CPU, RAM, or a bigger instance type — so it does more work. Horizontal scaling (scale out) means running more instances of the same service behind a load balancer and splitting traffic across them. Scaling up is simplest, with no code change, but hits a ceiling: there is a biggest instance money can buy, and that one box is still a single point of failure. Scaling out has no hard ceiling and adds redundancy, but only works if your instances are stateless — anything kept in one instance's RAM (sessions, local cache, in-progress uploads) breaks the moment a request lands on a different replica. The usual mistake is grabbing a bigger box because it is easy, when the real fix is pushing shared state to Redis or a database so you can add replicas freely. Scale out the stateless app tier; the database tier is the genuinely hard part.
Performance
Lazy Loading
A technique that defers loading a resource — an image, a component, a route bundle, a database relation — until the moment it is actually needed, instead of paying for everything up front. On the web the goal is a smaller initial payload and faster first paint: below-the-fold images get loading=lazy, heavy components are code-split behind a dynamic import so their chunk only downloads on demand, and an IntersectionObserver fires the load as an element scrolls into view. The catch is that deferral is not free — it trades startup cost for a later stall, so lazy-loading something the user needs immediately, like the hero image, just adds a blank flash and hurts LCP. Reserve space with explicit width and height or an aspect ratio (and show a placeholder while loading) so deferred content does not shift layout and tank CLS when it lands. And note the backend failure mode: lazy-loading an ORM relation inside a loop is exactly how you get an N+1 query. Lazy-load what is off-screen, secondary, or expensive; eager-load what is critical and certain.
Code-Splitting
A build-time optimization that splits your JavaScript bundle into multiple smaller chunks loaded on demand, instead of shipping one giant file the browser must download and parse before anything runs. The trigger is almost always a dynamic import() — the bundler sees import("./Chart") and carves what it pulls in into a separate chunk fetched only when that code path executes, typically per route or behind an interaction, shrinking the initial bundle and speeding up first load. Two things trip people up. First, splitting too finely backfires: dozens of tiny chunks mean dozens of round trips, and a chunk on the critical path just trades parse time for network latency. Second, a dynamically imported module is a network request that can fail or lag, so you need a loading state and an error boundary or a flaky connection leaves users staring at nothing. Split along boundaries users rarely cross on first paint (admin panels, modals, heavy libs like charting or an editor), not the hero component every visitor sees, and never split something so small the request overhead dwarfs the bytes saved.
Tree-Shaking
A build optimization where the bundler statically analyzes which exports are actually imported and drops the unreachable ones from the final bundle, so a util library you pull one function from does not ship its other fifty. It works only on ES modules (import/export), because their static structure lets the bundler trace the dependency graph at build time — CommonJS require is dynamic, so it defeats it. The classic trap is the sideEffects field in package.json: code with side effects (a polyfill, a CSS import, a global registration) must not be pruned, so a package wrongly flagged sideEffects: false has those imports vanish silently, while one too conservative shakes nothing. Re-exporting a whole module, barrel index files, and reaching exports dynamically by string all keep dead code alive. Trust the bundle analyzer, not intuition: import named members, mark a package side-effect-free only when it truly is, and verify what shipped.
Core Web Vitals (LCP, INP, CLS)
Google's three real-user metrics for a page's loading, interactivity, and visual stability — scored from field data (actual visitors), with the lab only a debugger. LCP (Largest Contentful Paint) is how long until the biggest element — usually the hero image or headline — finishes rendering, a proxy for perceived load. INP (Interaction to Next Paint) replaced FID in 2024 and reports a near-worst input-to-paint latency across the visit, catching a janky main thread, not just the first click. CLS (Cumulative Layout Shift) scores how much content jumps as things load late. The trap is tuning on a fast dev laptop: the verdict is the 75th percentile of real devices — often mid-range phones on slow networks — so a green Lighthouse score can still fail there. Trust the field (CrUX, web-vitals, Search Console). Fix CLS by reserving space (width/height on images, no late injection); fix INP by breaking up long tasks and trimming hydration.
Profiling
Measuring where a running program actually spends its time or memory, so you optimize on evidence instead of guessing. A profiler watches real execution and reports per-function cost. A sampling profiler periodically interrupts the program and records the call stack — cheap and production-safe; an instrumenting profiler counts every call exactly but adds overhead heavy enough to distort the numbers you read. The classic output is a flame graph, where a box width is the time spent in a function plus its children, so the widest bar is your bottleneck. The trap is optimizing by intuition: the slow code is rarely where you assumed, and a function that looks expensive may just be waiting on I/O, not burning CPU — so confirm CPU-bound versus I/O-bound first. Profile under a realistic workload (ten rows on a dev box hide the hot path that melts production at ten million), fix the widest bar, then re-profile. Stop once the rest is noise: shaving one percent of runtime buys nothing.
Caching Strategies (Cache-Aside, Read-Through, Write-Through, Write-Back)
The patterns that decide how your code and your cache (Redis, a CDN, an in-process map) stay in sync with the system of record, usually a database. Cache-aside (lazy loading) is the default: the app checks the cache, and on a miss reads the DB, writes the value back, then returns it — the cache is dumb and the app owns the logic. Read-through and write-through push that into the cache layer: reads populate the cache automatically, and write-through writes to cache and DB together, staying consistent at the cost of slower writes. Write-back (write-behind) writes to cache first and flushes to the DB later — fast, but a crash before the flush loses data, so it suits counters and metrics, not orders. The hard part is never the read path — it is invalidation: a stale entry served after the source changed. Prefer expiring entries with a TTL plus invalidating on write over updating the cache in lockstep, and watch for the thundering herd (cache stampede) — when a hot key expires, a thousand requests miss at once and stampede the DB; mitigate with a short lock or stale-while-revalidate.
Virtualization (Windowing)
A rendering technique for very long lists or grids that only mounts the rows currently visible in the viewport (plus a small overscan buffer) instead of putting all 10,000 items in the DOM at once — also called windowing or virtual scrolling. As you scroll it recycles a handful of DOM nodes, swapping their content and repositioning them, so the cost stays flat no matter how long the data is. The catch is the browser no longer knows the full scroll height, so the library fakes it with a tall spacer and absolute positioning — so rows whose height it cannot measure (variable, wrapping, or image content) get jumpy scrollbars and misplaced items unless you feed it accurate sizes. It also breaks anything assuming off-screen rows exist: Ctrl+F, anchor links, and tab order skip un-rendered rows, and screen readers need ARIA wiring. Reach for it only past a few hundred rows — below that the bookkeeping costs more than the DOM it replaces.
Prefetching (Resource Hints)
Fetching a resource the user has not asked for yet — a page, a script, a font — before it is needed, so it is already cached the instant they navigate — a network round-trip becomes an instant read. The browser exposes this through resource hints in the head or sent as Link headers, each a different priority: dns-prefetch resolves a domain's DNS early; preconnect goes further and opens the TCP and TLS connection ahead of time; prefetch pulls a whole resource at the lowest priority for a likely next navigation. preload is the odd one out — NOT speculative; it flags a resource the current page definitely needs (its as attribute sets the real priority) so the browser does not find it late. The misconception is that more hints are always faster. Every prefetch competes for bandwidth and can steal it from what the page needs right now, and a preload the page never uses is pure waste the console warns about. Use them surgically: preconnect the two or three origins you know you will hit, preload the LCP image or a critical font, and prefetch only the one next route your analytics says users actually take — then measure, because guessing wrong makes things slower, not faster.
Testing
Test Pyramid
A heuristic for balancing an automated test suite by speed and scope: lots of fast, isolated unit tests at the base, fewer integration tests checking how pieces fit together in the middle, and only a thin layer of slow end-to-end tests driving the whole system through the UI at the top. The shape matters because cost runs the opposite way — an E2E test is slow, flaky, and expensive to write and debug, while a unit test runs in milliseconds and points straight at the broken line — so you want most of your confidence coming from the cheap layer. The classic failure is the inverted pyramid or 'ice-cream cone': a fat E2E layer over almost no unit tests, giving a suite that takes an hour, fails randomly on timing, and tells you something broke but not where. Do not read the layers as fixed percentages, though; treat it as a reminder to push each test to the lowest layer that can still catch the bug, and lean on integration tests when the real risk lives in the wiring between components, not inside them.
Test Double
A test double is any stand-in you swap in for a real dependency so the code under test runs without its slow, flaky, or external collaborator (a database, an HTTP API, the clock). 'Double' is the umbrella term, and its five sub-types differ by what they do: a dummy is filler passed only to fill a parameter list but never actually used; a stub returns canned answers to feed your code a fixed input; a fake is a working but lightweight implementation, like an in-memory repository; a spy records how it was called so you can assert afterward; and a mock is pre-programmed with expectations and fails the test itself if expected calls do not happen. The common mistake is reaching for a mock when a stub would do: asserting a method was called couples the test to the implementation, so a harmless refactor turns the suite red though behavior is unchanged. Prefer stubs and fakes that verify outcomes over mocks that verify interactions, and never double a type you do not own — wrap it behind your own interface and double that.
Code Coverage
A metric that reports how much of your code actually ran while the tests executed, usually as a percentage. The flavors measure different things: line coverage counts which lines ran, while branch coverage checks whether both the true and false sides of each conditional were taken — and branch is the stricter number, because a test can run a line without ever exercising its else path. The big trap is treating coverage as a measure of correctness. It only proves a line was reached, not that you asserted anything meaningful about it; a test with zero assertions still lights the file green, so a high number can sit on tests that verify nothing. Chasing a hard 100 percent backfires too: people write trivial tests for getters, assert on mocks instead of real behavior, and add ignore comments to game the gate. Treat it as a flashlight, not a scoreboard — read the report for branches you forgot, hold a sane floor (say 70 to 80 percent) on code that matters, and judge the assertions, not the number.
TDD (Test-Driven Development)
A workflow where you write a failing test before the production code, following a tight red-green-refactor loop: write one small test that fails (red), write the minimum code to make it pass (green), then clean up the design while the test keeps you safe (refactor). The point is not testing for its own sake but design pressure — letting a test you have to call drive you toward small, decoupled units with sane interfaces, plus a regression suite as a free by-product. The common misconception is that TDD means writing all the tests up front; it is actually one tiny cycle at a time, and the 'refactor' step is the one people skip, which is exactly where the value is. The gotcha is testing implementation detail instead of behavior — assert on observable outputs, not private internals, or every refactor breaks tests and TDD feels like a tax. Reach for it on logic with clear inputs and outputs (parsers, business rules, bug fixes where the failing test reproduces the bug first); do not force it on exploratory spikes or thin glue where you do not yet know the shape of the answer.
Flaky Test
A test that passes or fails on the same code with no change, because its result depends on something non-deterministic (timing, test ordering, shared state, a real network call, the clock, or randomness) instead of the behavior it claims to verify. Usual culprits are async races (asserting before a promise settles), tests that leak state from a previous one, hardcoded sleeps that are sometimes too short, or hitting a live external service. The real harm is what it does to trust: once a suite cries wolf, people re-run CI until it goes green and stop reading failures, so a genuine regression sails through. The wrong fix is wrapping it in a retry, which only hides the bug and rewards flakiness. Instead reproduce it (loop the test, randomize order, add load), then kill the cause: await real conditions, reset state between tests, fake the clock and randomness, and mock the network. Quarantine a known-flaky test so it stops blocking merges, but track it as a bug, never a permanent skip.
Snapshot Testing
A test that serializes a component or value to a stored reference file (the snapshot, or golden file) on its first run, then on every later run compares the new output against that saved baseline and fails if they differ. It is most associated with UI regression testing: render a component, serialize its markup, and any unintended change to the output trips the test. The trap is that a snapshot asserts only that output did not change, not that it is correct — a wrong snapshot gets committed as the baseline just as happily as a right one, so a green run only means nothing changed. Worse, when a real change makes dozens of snapshots fail, the reflex is to run update (jest -u) and blindly accept all of them, baking in regressions. Use snapshots for stable, serializable output, review every diff like real code, and keep them small and inline; for anything with real logic, prefer explicit assertions over one giant opaque snapshot.
Property-Based Testing
A testing style where, instead of writing example cases by hand, you state a property that must hold for every valid input — a rule like 'reverse(reverse(list)) equals list' or 'the output is always sorted' — and a framework (fast-check, QuickCheck, Hypothesis) generates hundreds of random inputs trying to break it. When it finds a failure it shrinks: it pares the input down to the smallest reproducible counterexample, so instead of a 4000-item array you get list = 0, 0 and a one-line repro. The shift people miss is that you stop asserting on a known output and start asserting on an invariant, and finding good invariants (round-trips, idempotence, an oracle to compare against) is the real skill. The trap is non-reproducible runs: log and pin the failing seed, or a red build vanishes on the next CI run. Use it for pure logic with clear rules — parsers, serializers, data structures; it complements example tests rather than replacing them.
Regression Test
A test that re-runs existing, previously-passing behavior to catch a regression — a defect where a change silently breaks something that used to work, often far from the code you touched. The name is the point: you are guarding against backsliding, not testing the new feature. The common misconception is that 'regression test' is a separate manual pass before release; in practice it is your accumulated test suite run automatically in CI on every change, and its real job is to encode each fixed bug as a permanent test so it can never quietly return. The trap is letting the suite rot: flaky or slow tests train people to ignore red, and coverage that never grows leaves new fixes unguarded. So when you fix a bug, first write a test that reproduces it and fails, then fix the code so it passes — that failing test is your regression test, proving the fix and locking it in. Keep the suite fast and deterministic, or people stop trusting it.
Networking
DNS (Domain Name System)
DNS (Domain Name System) is the internet phone book that translates a human name like example.com into the IP address a machine actually connects to, since computers route on numbers, not names. You send one recursive query to a resolver (your ISP's, or a public one like 8.8.8.8) and it does the rest, walking the hierarchy with iterative queries — root to the .com servers to the domain's authoritative server — to find who answers, then returning it over UDP port 53 (falling back to TCP for big replies). Records are typed: A points a name at an IPv4 address, AAAA at IPv6, CNAME aliases one name to another, MX directs mail, TXT carries verification strings. The gotcha that burns everyone is TTL and propagation: each record carries a TTL telling resolvers how long to cache it, so after you change a record the old value keeps serving worldwide until those caches expire. Lower the TTL before a migration, and never trust a change is live just because your own machine sees it.
TCP vs UDP
The two main transport protocols on top of IP, deciding how packets get delivered. TCP is connection-oriented and reliable: a three-way handshake opens the connection, every byte is numbered, arrivals are acknowledged, losses are retransmitted, and the app gets a clean, in-order stream — at the cost of round-trip latency and head-of-line blocking, where one lost packet stalls everything behind it. UDP is connectionless and fire-and-forget: it sends datagrams with almost no overhead and zero guarantees — packets can be lost, duplicated, or reordered, with no congestion control. The common trap is assuming UDP is just faster, then re-implementing ordering and retransmission yourself, usually worse than TCP already does. Pick TCP when correctness matters (HTTP, databases, file transfer) and UDP when timeliness beats completeness and you tolerate loss (live video, VoIP, games, DNS) — which is why QUIC rebuilds reliability on UDP to dodge TCP head-of-line blocking.
HTTP/1.1 vs HTTP/2 vs HTTP/3
HTTP is the request/response protocol the web runs on, and its three live versions differ mainly in how bytes move underneath, not in the methods, headers, and status codes you write. HTTP/1.1 sends one request at a time per TCP connection, so a slow response stalls everything queued behind it (head-of-line blocking) and browsers open several connections to compensate. HTTP/2 keeps the same semantics but adds multiplexing: many requests share one connection as interleaved streams, plus header compression. The catch is it still rides on TCP, so a single lost packet stalls every stream until retransmission. HTTP/3 fixes that by running over QUIC on UDP, where streams are independent, so a lost packet blocks only its own stream, and it folds the TLS handshake into connection setup for faster starts. You do not pick a version in code; client and server negotiate the highest both support (ALPN, or an Alt-Svc hint for HTTP/3). What matters is enabling HTTP/2 or HTTP/3 at your edge or load balancer and dropping old HTTP/1.1 tricks like domain sharding, which backfire on the newer versions.
Latency vs Bandwidth
Two limits on a network link that beginners constantly conflate: bandwidth is capacity (how many bits per second the pipe carries, e.g. 100 Mbps), while latency is delay (how long one packet takes one way; round trip time or RTT is there-and-back, what ping reports in milliseconds). The key insight is that they are independent — a fat transcontinental link can have huge bandwidth yet brutal RTT, because latency is floored by physical distance and the speed of light plus per-hop queuing. Throughput, the rate you actually observe, is bounded by both, and for TCP it is also capped by RTT: one connection cannot send more than its window of unacknowledged data per round trip, so high latency starves throughput even on a wide pipe (the classic long fat network). The common mistake is buying more bandwidth to fix a slow app when the real cost is latency — many small serial round trips, a chatty API, a far region. You cannot beat the speed of light, so the wins are cutting round trips (batch, pipeline, keep connections alive), moving data closer (CDN, edge, read replicas), and overlapping transfers rather than adding megabits.
Forward vs Reverse Proxy
Both are intermediaries that relay traffic, but they sit on opposite ends and serve opposite owners. A forward proxy sits in front of clients and acts on their behalf: the browser or app is configured to send requests through it, and the proxy reaches out to any server on the internet — used for egress control, corporate filtering, caching outbound fetches, or hiding the client's IP. A reverse proxy sits in front of your servers and acts on their behalf: clients think they are talking straight to your site, but NGINX, HAProxy, or a cloud gateway terminates the connection and forwards it to one of several backends — used for load balancing, TLS termination, caching, and routing by path or host. The mental shortcut: a forward proxy hides the client from the server, a reverse proxy hides the servers from the client. The gotcha is that once a reverse proxy is in the path, your app no longer sees the real client IP or scheme directly — it must trust the X-Forwarded-For and X-Forwarded-Proto headers the proxy sets, and you must configure the app to honor them only from the trusted proxy, or a caller can spoof them. Pick by direction: outbound and client-side is forward, inbound and server-side is reverse.
Subnetting (CIDR)
Subnetting splits one IP address block into smaller networks by choosing how many leading bits form the shared network prefix; the rest are host bits. CIDR writes that split as an address, a slash, and a prefix length — like 192.168.10.0/24, where /24 means the first 24 bits are the network and the last 8 (256 addresses) are host space. The gotcha is the math: in IPv4 the first address in a block is the network address and the last is the broadcast address, so a /24 holds 256 addresses but only 254 usable hosts, and each step (/25, /26) halves the range. People also confuse the prefix with the subnet mask, but they are identical: /24 is just 255.255.255.0 in dotted form. To use it right, size each subnet to the prefix that fits its host count — VLSM lets you mix sizes, so a point-to-point link can be a /30 — keep blocks non-overlapping, and remember that a smaller number after the slash means a bigger network, not a smaller one.
NAT (Network Address Translation)
NAT (Network Address Translation) lets many devices on a private network share one public IP address by rewriting the source address and port of outgoing packets, then reversing the swap on the replies. Private ranges like 10.x and 192.168.x are not routable on the public internet by design, so NAT is the workaround that lets those hosts reach the outside. Your home router does exactly this: it keeps a translation table mapping each internal host and port to a unique external port, so a returning reply reaches the right private machine. The gotcha: NAT is not a firewall and not real security, it just happens to drop unsolicited inbound traffic because no table entry matches it. It also breaks any protocol that assumes a host is directly reachable, so inbound connections need explicit port forwarding (a static table entry) and peer-to-peer apps lean on STUN or relays to punch through. When a service is unreachable from outside, check whether a NAT sits in the path before blaming the app.
OSI Model (Open Systems Interconnection)
A seven-layer reference model that splits network communication into layered duties — physical, data link, network, transport, session, presentation, application (L1 to L7) — each talking only to the layer above and below. Going out, data is encapsulated: every layer wraps the one above in its own header (plus a trailer at layer 2), turning a segment into a packet into a frame into bits; the receiver reverses it. Keep this straight: OSI is a teaching and debugging map, not what the internet runs — real stacks use the leaner TCP/IP model, which folds OSI layers 5 to 7 into one application layer. Devs mostly live at two: layer 3 is IP addresses and routing between networks, layer 4 is TCP and UDP ports and reliability. The payoff is localizing faults fast and reading labels right: an L4 load balancer sees only IP and port, an L7 one routes on URL path or host header, so do not expect path-based routing or TLS termination from an L4 box.
Mobile & cross-platform
Responsive Design (Responsive Web Design)
A layout approach where one codebase adapts to any viewport — phone, tablet, laptop, ultrawide — using fluid grids, relative units (percent, rem, fr, vw), flexible images, and CSS media queries that re-flow content at breakpoints. The mainstream practice is mobile-first: write base styles for small screens, then layer min-width media queries to enhance for larger ones. The classic gotcha is forgetting the viewport meta tag, so mobile browsers render at a fake 980px width and your queries never fire. Another trap is chasing devices with pixel-perfect breakpoints (320, 768, 1024), which ages badly — set breakpoints where the content actually breaks, not by device. Better still, reach for intrinsic layout first: Flexbox, CSS Grid with auto-fit and minmax, and clamp for fluid type handle most resizing with no media queries at all, so reserve queries for true structural shifts. The payoff is one site, one URL usable everywhere, not a brittle desktop layout bolted onto a separate m-dot.
Progressive Web App (PWA)
A Progressive Web App (PWA) is an ordinary website that, by shipping a web app manifest and registering a service worker, can install to the home screen and behave like a native app — running offline, getting its own icon, and receiving push notifications — with no app store involved. The manifest is just JSON describing name, icons, and display mode; the real engine is the service worker, a script that runs apart from the page as a programmable network proxy, so it can cache assets and serve them offline. The common misconception is that a manifest alone makes an app installable: it does not. Browsers also require HTTPS, a valid manifest with name, icons, and a standalone display mode, and traditionally a registered service worker — and that worker only controls pages within its own scope. Do it right and you get one codebase that is linkable, indexable, and updatable on deploy yet feels installed; just remember iOS only added web push in 16.4, only for apps on the Home Screen, with no automatic install prompt, so verify on real Safari, not just Chrome on Android.
Deep Linking
A URL that opens a specific screen or state inside an app — say a product page or a chat thread — instead of launching it to the home screen. Two flavors get conflated: a custom scheme like myapp colon slash slash product slash 42 is easy but unverified, so any app can register the same scheme and hijack it, and it just errors out in a browser if the app is not installed. The robust kind is a real https link the OS recognizes as yours — Universal Links on iOS, App Links on Android — working only because you host an association file on your domain over https (apple-app-site-association, or assetlinks.json with your app signing fingerprint on Android) proving you own both site and app. The payoff is one link that opens the native app when installed and falls back to the website when not, with no app-chooser prompt. The classic gotcha is deferred deep-linking: a fresh install has no app to catch the tap, so the target is lost across the store trip unless you stash and replay it on launch.
Push Notification
A message your server sends to one specific device through a platform push service — APNs for Apple, FCM for Android and modern web — that the OS can display even when your app is backgrounded or closed, because the OS, not your app, holds the live connection. It is opt-in: you request the user's permission, and on success you get a device token (on web, a subscription tied to a service worker) identifying that one install on that one device; you send to the token, never to a user or phone number. The classic mistake is treating that token as stable — it rotates and is invalidated on reinstall or revoked permission, and a send to a dead token returns a failure you must act on by pruning it, or your delivery stats quietly rot. Two more: the payload is small and not guaranteed in order or even delivered, so push is a signal to wake up and fetch, not a transport for real data; and you never call APNs or FCM from the client — only your server, holding the provider credentials, does.
Native vs Hybrid (mobile app architecture)
The choice between fully native apps — Swift/SwiftUI on iOS, Kotlin/Jetpack Compose on Android, compiled against each platform SDK — versus a cross-platform or hybrid stack like React Native, Flutter, or a web app wrapped in a WebView shell that shares one codebase across both platforms. The trade is real: native gives the best performance, instant access to brand-new OS APIs, and pixel-perfect fidelity, but you carry two codebases plus per-platform expertise; cross-platform collapses most of that into one, at the cost of a bridge or rendering layer between you and the metal. The common misconception is that hybrid means slow — Flutter renders its own pixels through its engine (Impeller today, Skia historically) and React Native's New Architecture swaps the old serialized bridge for direct JSI calls, so for typical CRUD and content apps the gap is invisible. Choose native for heavy animation, AR, low-latency audio, or day-one OS features; choose cross-platform when shared logic, faster shipping, and one team outweigh squeezing the last frame. Pick by where your hardest constraint lives, not by hype.
Mobile Viewport (viewport meta, DPR, safe-area)
How a mobile browser decides what area of the page to lay out and how to scale it onto a small, high-density screen. Without the viewport meta tag set to width=device-width, initial-scale=1, the browser assumes a desktop-width canvas (around 980px) and shrinks the whole page to fit, so your responsive CSS never triggers and users pinch-zoom everything. The first gotcha is device pixel ratio (DPR): CSS px are layout units, not hardware pixels, so on a DPR-3 phone one CSS px maps to three device pixels per axis — that is why a 1x raster image looks blurry and you ship 2x or 3x assets or SVG. The second is the notch and home indicator: content can slide under them unless you opt in with viewport-fit=cover and pad with the env safe-area-inset values. Do it right by setting the meta tag once, using min-height 100dvh instead of 100vh so mobile address-bar collapse does not clip your layout, and serving DPR-aware images plus safe-area padding rather than hardcoding pixel offsets.
Touch Target
A touch target is the on-screen region that actually responds to a tap on a touchscreen — not the visible icon or label, but the full hit area a finger can land on. Platform guidance puts the comfortable minimum around 44 by 44 points on Apple and 48 by 48 density-independent pixels on Android, roughly a 7 to 9 millimetre square at typical densities. The gotcha is that designers size the visible glyph, not the hittable region: a 24px icon looks fine but is a misery to tap, and two targets pressed flat together cause mis-taps even when each is large enough, so about 8px of spacing between them matters as much as size. Do it right by growing the interactive element, not the art — pad the button, expand the label clickable area, or add hit-slop or invisible touch padding around small controls so the tap zone exceeds the visual one. This is an accessibility requirement, not polish: WCAG 2.2 target-size, plus tremor, motor impairment, or one-handed thumb reach, all depend on it.
Offline-First
An architecture where the app treats the local store — IndexedDB, SQLite, or a cache — as the source of truth, reading and writing locally first and syncing to the server in the background, reconciling changes when the connection returns. The user never waits on the network for the common path, so the app stays instant and usable on a plane, a subway, or flaky mobile data. The part people underestimate is conflict resolution: once two devices (or a device and the server) mutate the same record while offline, a naive last-write-wins silently drops edits and counters drift. You need an explicit strategy — last-write-wins by timestamp or vector clock, per-field merges, or CRDTs for truly collaborative data — plus stable client-generated IDs (a UUID, not a server sequence) so offline-created rows do not collide on sync. Do it right by queuing mutations as an idempotent, replayable outbox, making sync retriable, and surfacing pending or conflicted state in the UI instead of pretending everything saved. The payoff is an app that feels instant and survives zero bars, which is why it underpins most serious PWAs.
AI & machine learning
Large Language Model (LLM)
A large language model (LLM) is a transformer neural network with billions of parameters, trained on massive text corpora to do one mechanical thing: given the preceding context, predict a probability distribution over the next token, sample from it, and repeat. Fluent answers, translation, and code all emerge from that single objective at scale, plus instruction tuning and RLHF — there is no fact database inside. That is the gotcha: an LLM does not look anything up, it generates the statistically likely continuation, so it confidently invents citations and APIs (hallucination) and only knows the world up to its training cutoff. The context window is a budget measured in tokens, not characters — prompt plus reply must fit, and a token is roughly a word piece. To use one well, ground it in your own data with RAG (paste retrieved facts into the prompt) instead of trusting recall; reserve fine-tuning, which actually changes weights, for format or style, not for adding fresh knowledge.
Embedding
An embedding is a fixed-length vector of numbers — often hundreds or thousands of dimensions — that a model produces to encode the MEANING of a piece of text (or an image, audio, or code) so that semantically similar items land close together in that vector space while unrelated ones sit far apart. You compare two embeddings with a distance metric, usually cosine similarity, and that closeness is the whole point: it lets you search by meaning instead of by exact keywords. The gotcha is that an embedding is not fine-tuning and not RAG — it changes no model weights and stores nothing by itself; it is just the numeric output you keep in a vector database and query later. Two more traps: vectors are only comparable if the SAME model and version produced them, so mixing models gives garbage scores, and similarity is not truth — neighbors are topically related, not verified facts. Reach for embeddings for semantic search, deduplication, clustering, or the retrieval step of a RAG pipeline.
Vector Database
A datastore built to index high-dimensional embeddings — the numeric vectors a model emits for text, images, or audio — so you can run fast nearest-neighbor search: given a query vector, return the k items whose vectors sit closest by cosine or dot-product similarity, i.e. the most semantically related ones. The speed comes from an approximate index like HNSW or IVF, and that is the catch: ANN trades a little recall for huge speed, so it can miss a true nearest neighbor, and the index must be rebuilt or tuned as data grows. Keep the roles straight too — the database only stores and searches vectors; some other model produces the embeddings, and it does not learn or fine-tune anything by being queried. Reach for one when you are doing semantic search, deduplication, or the retrieval half of RAG over more rows than a brute-force scan can handle; for a few thousand vectors a plain array and a cosine loop, or pgvector inside Postgres, is simpler than standing up a dedicated service.
Retrieval-Augmented Generation (RAG)
Retrieval-Augmented Generation (RAG) is a pattern where, at query time, you fetch the documents most relevant to a question — usually by embedding the query into a vector and running a nearest-neighbour search over a vector store of your own content — then paste those snippets into the LLM prompt so the answer is grounded in your data, not just the patterns the model learned in training. The key point: RAG never touches the model's weights. It is retrieval plus prompting, which is why people confuse it with fine-tuning — but fine-tuning re-trains weights to change behaviour, while RAG injects fresh context at inference time, so it is cheaper to keep current. The catch is RAG is only as good as your retrieval: bad chunks come back and the model answers from them confidently, and everything you stuff in competes for a fixed context window measured in tokens, not characters. So chunk sensibly, cite sources, and reach for RAG when facts are private or change often and retraining is overkill.
Prompt Engineering
A practical discipline of structuring everything you hand an LLM in a single request — a system role that sets behaviour, few-shot examples that demonstrate the pattern, the relevant context, an explicit output format, and sometimes a step-by-step or chain-of-thought nudge — so the model predicts the tokens you actually want. Remember the model is not looking facts up; it is continuing your text one token at a time, so a vague or contradictory prompt yields confident, plausible-sounding nonsense. Prompting changes no weights, so it teaches the model nothing new: fine-tuning updates weights to bake in a behaviour, RAG injects fresh facts at query time by retrieving them, and prompting does neither. Get it right by being explicit: assign a role, show two or three worked examples, state the exact shape you want (JSON keys, a fixed list, a max length), give it room to reason before the final answer, and tell it what to do when unsure instead of letting it guess. The payoff is reliable, machine-parseable output you can wire into code without brittle regex cleanup, plus far fewer silent format drifts across calls.
Fine-tuning
Continuing to train a pre-trained base model on your own labeled examples so it absorbs a style, behavior, or output format you cannot reliably get from prompting alone — and unlike prompting or RAG, it actually changes the model weights. The misconception is treating it as a way to teach the model new facts: it is much better at shaping HOW the model responds (tone, JSON structure, a domain dialect, refusal patterns) than at reliably storing fresh knowledge, which RAG handles better by retrieving documents into the context window at request time. It is also not embedding — embeddings turn text into vectors for search, while fine-tuning adjusts the network that predicts tokens. Reach for it only after prompting and RAG fall short, because you need hundreds to thousands of clean, consistent examples, the cost and lock-in are real, and every base-model upgrade means retraining. Done right, the payoff is concrete: shorter prompts, lower latency, and steadier formatting on a narrow task.
Hallucination
Hallucination is when a large language model emits fluent, confident text that is factually wrong or wholly invented — fake citations, non-existent API methods, plausible-but-untrue dates — because it predicts the next likely token, it does not look anything up. The dangerous part is the tone: the same statistical machine that gets things right phrases a fabrication with the same authority, so confidence is no signal of correctness, and the smoother the prose the easier a wrong answer is to trust. Treat every claim as unverified until grounded: pin facts with retrieval (RAG) so the model answers from documents you supply in context, not from memorized weights, demand and actually open inline source links, lower temperature on factual tasks to cut randomness, and run code and citations through a real compiler or lookup before you ship. Note that fine-tuning teaches style and format but does not reliably install new facts, so it rarely cures hallucination — grounding and verification do.
Context Window
The maximum number of tokens a model can consider in a single call, counting the prompt AND the generated output together — the system prompt, your instructions, any retrieved documents, the chat history, and every token the model produces in reply all draw from the same budget. The common trap is treating it as a character count or as permanent memory: it is measured in tokens (a token is roughly three-quarters of a word in English, and many other scripts, Thai included, tend to consume more tokens per word), and anything that overflows is truncated or silently dropped, so a long conversation can lose its own earlier turns and a stuffed prompt can crowd out room for the answer. Bigger is not free either — quality often sags for facts buried in the middle of a huge window, and you pay per token. Do it right by budgeting deliberately: trim or summarize history, retrieve only the passages you actually need (RAG instead of pasting whole files), and reserve enough headroom for the completion so the model is not cut off mid-sentence.