Skip to content

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

PATCH formats

  • 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

Pagination

  • 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).

Optimistic concurrency with ETags

  • GET returns an ETag; the client sends PUT with If-Match: <etag>, and the server answers 412 if the resource changed — no lost updates between clients.
  • If-Match needs a strong ETag (not W/); If-None-Match: * makes a PUT create-only.

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.

Input validation

  • 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.