Backend
What experienced backend engineers forget before an interview, grouped by subtopic. For PostgreSQL internals see postgresql.md; for Redis see redis.md; for queues, sagas, and retries see distributed.md; for caching and rate limiting see system.md; for HTTP versions see networking.md; for auth and vulnerabilities see security.md.
API design
Status codes people confuse
| Codes |
Meaning |
| 401 vs 403 |
missing or invalid credentials (re-auth may help) vs refused — credentials, if sent, are insufficient; others might succeed (or 404 to hide that the resource exists) |
| 409 vs 422 |
conflicts with current state vs well-formed but fails validation |
| 202 / 204 |
accepted for async processing / success with no body |
| 412 / 429 |
precondition (If-Match) failed / rate limited |
Method semantics
| Method |
Safe |
Idempotent |
| GET, HEAD |
yes |
yes |
| PUT, DELETE |
no |
yes |
| POST |
no |
no |
| PATCH |
no |
not guaranteed — depends on the patch format |
- JSON Merge Patch (RFC 7396): send a partial object;
null deletes a field — so it can't set a value to null or edit one array element.
- JSON Patch (RFC 6902): a list of operations (
add, remove, replace, test) on JSON Pointer paths; test makes the patch conditional.
REST vs gRPC vs GraphQL
|
REST |
gRPC |
GraphQL |
| Contract |
OpenAPI (optional) |
Protobuf, strict |
schema, strict |
| Transport |
any HTTP version |
HTTP/2 only |
usually HTTP POST |
| Strength |
HTTP caching, simplicity |
streaming, speed, codegen |
client picks fields, one round trip |
| Weakness |
over/under-fetching |
browsers need gRPC-Web |
hard to cache, N+1, query-cost limits |
- Keyset:
WHERE (created_at, id) > (:c, :id) ORDER BY ... LIMIT n — fast at any depth and unaffected by inserts; needs a unique, immutable sort key, or updated rows skip or repeat.
- Expose the position as an opaque cursor; a cursor is just a token and can also encode an offset.
- Offset: simple and jumps to page N, but slows with depth and skips or repeats rows as data changes.
Versioning
- URI (
/v1/) is explicit and cacheable; a header (Accept: ...;version=2) keeps URLs clean but is harder to discover.
- Whatever the scheme: additive changes only within a version; removing or renaming a field is a breaking change.
Error responses
- One error shape everywhere — RFC 9457 problem details (
type, title, status, detail).
- Never return stack traces or SQL; log details server-side and return a request ID.
Idempotency and concurrency
Idempotency keys
- The client sends a unique
Idempotency-Key per logical operation; the server stores the key with the first result and returns it on every retry.
- Handle the in-flight case: a retry arriving while the first attempt is running must wait or get 409, not execute twice.
- Scope keys per client and expire them after a TTL (e.g. 24 h).
Transactional outbox
- Write the business row and an outbox row in the same DB transaction; a relay (poller or CDC) publishes outbox rows and marks them sent — the transactional outbox.
- Fixes the dual-write problem without 2PC, which most brokers don't support. Delivery is at-least-once (see distributed.md).
Inbox deduplication
- Consumers record processed message IDs in the same transaction as their effect and skip repeats — the receiving side of the outbox (idempotent consumer).
- Expire entries only after the broker's maximum redelivery window.
Service calls
Timeouts and deadline propagation
- Every outbound call gets a timeout, shorter than the caller's own.
- Propagate the deadline so downstream services stop work once the original caller has given up — gRPC sends it on the wire, but each server must pass it on to its outbound calls and honor cancellation.
- Retries, backoff, and circuit breakers: see distributed.md.
Schema evolution
- Backward compatible: new readers read old data; forward compatible: old readers read new data — rolling deploys and replayed events need both.
- Protobuf: never reuse or renumber a field tag (mark removed ones
reserved); adding fields is safe; proto3 has no required; unknown fields survive a binary round trip, but not conversion to JSON.
- Avro with a schema registry checks BACKWARD, FORWARD, or FULL compatibility on every schema change.
- JSON consumers must ignore unknown fields (tolerant reader); rename a field by adding the new one, writing both, moving readers over, then removing the old.
Webhooks
- Sign each payload (HMAC over timestamp + body) and have receivers verify it with a constant-time compare; reject stale timestamps to stop replays.
- Deliver at least once with backoff, so receivers dedupe by event ID; receivers should ack fast (2xx) and process asynchronously.
- Fetching a user-supplied webhook URL is an SSRF risk — see security.md.
Async work
Long-running operations
- Return 202 Accepted with a
Location to a status resource (/operations/123); the client polls it or gets a webhook when done.
- The status resource carries state (
pending, running, succeeded, failed) and the result or error (asynchronous request-reply).
Background jobs
- At-least-once delivery means jobs must be idempotent; persistently failing jobs go to a dead-letter queue with alerting.
- Give every job a timeout; long jobs checkpoint progress so a retry resumes instead of restarting.
Scheduled jobs
- Guard against overlapping runs, where one run lasts longer than the interval.
- With several replicas, each fires the same cron — use a lock (DB row, advisory lock, lease) or a single scheduler.
Data access
N+1 queries
- Loading a list, then one query per item. Fix with a
JOIN, a batched WHERE id IN (...), or a DataLoader that batches lookups per tick (GraphQL resolvers).
- Spot it with per-request query counts in logs or APM.
Connection pool sizing
- Size the connection pool from the database's capacity, not the app's thread count — many app instances × a large pool overwhelm the database.
- A good starting point is a small pool per instance (~2× DB cores total across instances, HikariCP's formula), then measure wait time; server-side pooling in postgresql.md.
Transaction boundaries
- Never make a network call inside an open DB transaction — it holds locks and a connection for the whole round trip.
- For "commit, then notify another service", use the outbox instead of calling inside the transaction.
Multi-tenancy models
- Shared tables with a
tenant_id: cheapest, but every query must filter — enforce it with PostgreSQL row-level security (the table owner bypasses it unless FORCE ROW LEVEL SECURITY).
- Schema per tenant: more isolation, but migrations and connection pools multiply with the tenant count.
- Database per tenant: strongest isolation, per-tenant restore and data residency, the most operational work.
- Move noisy neighbors (the largest tenants) to their own shard or database, and rate-limit per tenant.
Time handling
- Store and send instants in UTC (RFC 3339 with an offset,
2026-03-08T09:30:00Z); in PostgreSQL use timestamptz.
- For future local-time events (meetings, schedules), store the local time plus an IANA zone (
Europe/Berlin), not an offset — DST rules and offsets change.
- A DST switch skips or repeats a local hour, so a job scheduled at 02:30 local time runs never or twice; schedule jobs in UTC.
File uploads
Service lifecycle
Graceful shutdown
- On
SIGTERM: fail readiness → stop accepting new connections → finish in-flight requests within a deadline → close pools and flush telemetry → exit.
- Workers stop pulling jobs and finish or checkpoint the current one. Kubernetes timing: see devops.md.
Health endpoints
- Separate endpoints: liveness checks only that the process can make progress; readiness checks what serving needs (warm-up done, required dependencies) and fails while draining.
- Keep them cheap and on an internal port; probe configuration and pitfalls: see devops.md.
Backend security
Mass assignment
- Binding the request body straight onto a model lets clients set
isAdmin: true (mass assignment) — allowlist bindable fields or use separate DTOs.
- Serialize responses through explicit DTOs too, so a new sensitive column doesn't leak.
- Validate type, format, and range at the boundary before business logic (400); business-rule failures come later (409/422).
- Cap sizes — body, array lengths, string lengths, nesting depth — to prevent resource exhaustion.
Secrets in logs
- Request logging middleware can capture tokens, auth headers, and PII — redact centrally in the logging layer, not per call site (OWASP logging).
- Tokens in URL query strings end up in proxy and CDN logs — send them in headers.
Testing
Integration tests with real dependencies
- Testcontainers runs real dependencies (Postgres, Kafka) in tests, catching SQL and serialization bugs that mocks hide.
- A mock only checks your assumptions about a dependency; test the real one at least at the adapter boundary.
Consumer-driven contract tests
- Consumer-driven contract tests (Pact): each consumer records the requests and fields it relies on, and the provider's CI verifies them.
- Catches breaking API changes without deploying both services together.