Backend Engineering and Distributed Systems
This sheet is the canonical home for backend, API, data, security, and distributed-systems concepts used by mobile teams.
Mobile-to-backend request lifecycle
iOS UI → feature/use case → API client → DNS → TLS connection → edge/WAF
→ load balancer/API gateway → authenticated service → domain logic
→ database/cache/message broker → response/stream → client decode
→ state update → telemetry (without secrets or unnecessary PII)The request crosses trust and failure boundaries. The server authenticates every request, authorizes the requested resource, validates input, applies timeouts and rate limits, performs a bounded unit of work, and returns a stable contract. The app handles offline state, cancellation, duplicate submission, old server/client versions, and user-visible recovery. A successful TCP/TLS connection does not imply an authorized or successful business operation.
Transport and API styles
| Choice | Interaction model | Strengths | Costs / fit |
|---|---|---|---|
| HTTP | Request/response over TCP (typically TLS) | Ubiquitous; intermediaries and caching | Per-request overhead and retry semantics must be designed |
| REST | Resource-oriented HTTP conventions | Cacheable, debuggable, broad tooling | Multiple round trips or endpoint-specific aggregation may be needed |
| GraphQL | Client selects a typed response shape | Useful for varied clients and nested reads | Query cost limits, authorization at field level, caching complexity |
| gRPC | Protobuf RPC, often HTTP/2 | Strong contracts, compact payloads, streaming | Mobile gateway/proxy support and browser compatibility considerations |
| WebSocket | Long-lived bidirectional connection | Interactive realtime collaboration/chat | Connection lifecycle, fan-out, reconnection and backpressure |
| Server-Sent Events | Server-to-client event stream over HTTP | Simple one-way updates and reconnection model | Client-to-server commands still need another request path |
| BFF | Backend-for-Frontend adapter | Tailors aggregation/auth contracts to app needs | Another service and deployment surface; avoid duplicating domain policy |
TCP provides reliable ordered byte-stream transport; UDP is datagram-oriented and may lose/reorder packets (higher-level protocols can add reliability). DNS maps names to addresses and is itself cached; TLS authenticates endpoints and protects transport confidentiality/integrity when configured correctly. Reverse proxies and load balancers distribute or mediate traffic; gateways commonly centralize routing, auth enforcement, quotas, and observability but should not become a dumping ground for business logic.
Backend architecture patterns
| Pattern | Shape / dependency sketch | Benefit | Limitation / operational cost | Scaling and testing implications | Appropriate starting point | |
|---|---|---|---|---|---|---|
| Monolith | Client → app process → DB | Simple local calls, transactions, deployment | Boundaries can erode; one release unit | Scale whole process first; integration tests are straightforward but regression scope can grow | Small domain/team, fast-changing product | |
| Modular monolith | `app → [module A | module B] → DB` | Clear domain seams without distributed calls | Requires dependency rules and ownership discipline | Scale whole process initially; add module/API and dependency-rule tests | Most products before independent scaling/deployment is proven |
| Microservices | Client → gateway → svc A/B → owned stores | Independent ownership/scaling and failure containment | Network, observability, data consistency, DevOps, and on-call complexity | Scale hot services independently; requires contract, integration, resilience, and deployment tests | Teams/domains with real independent deployment or scaling needs | |
| Serverless | event → function → managed service | Low idle operations and event-triggered scaling | Cold starts, runtime/provider constraints, distributed tracing | Platform scales invocations; test retries, concurrency, cold-start budgets, and provider integration | Bursty event workloads and managed integrations | |
| Event-driven | producer → queue → consumer | Loose temporal coupling and fan-out | Duplicate/out-of-order events; schema evolution and operations | Scale consumers by lag; test idempotency, replay, poison messages, and event contracts | Asynchronous workflows and integration boundaries | |
| CQRS | command → write model → event → read model | Optimized read/write paths | Eventual consistency and duplicated model complexity | Scale read projections separately; test projection lag and read-after-write behavior | Read/write shapes diverge measurably | |
| Event sourcing | command → append-only events → projections | Audit/replay and historical state reconstruction | Event schema evolution, replay cost, privacy deletion complexity | Replay and projection tests become core; storage and rebuild cost grow with history | Audit-heavy domains where history is a business capability | |
| Hexagonal / Clean | adapter → port → domain ← port ← adapter | Replaceable transport and persistence | More design surface; layering can become ceremony | Unit-test domain policy and contract-test adapters; scaling is deployment-dependent | Stable business rules with changing integrations | |
| Domain-driven design | bounded context A ↔ contract ↔ context B | Aligns software model with business ownership | Requires domain collaboration; not just aggregate classes | Scale teams around real boundaries; test invariants and context integration contracts | Complex domain and multiple teams |
Tradeoff rule: split a modular monolith only when an independently deployable boundary pays for network, data, on-call, and coordination costs. Database-per-service improves ownership but removes easy cross-service transactions; teams then need explicit consistency and compensation strategies.
Service boundary example
Mobile App ──HTTPS──> API Gateway / BFF
├── Accounts service ──> PostgreSQL
├── Payments service ──> PostgreSQL + outbox
├── Notification worker <── message broker
└── Identity provider (OIDC)
Outbox event → broker → consumer (idempotent) → notification providerThe diagram separates synchronous reads/commands from asynchronous side effects. It does not imply a particular vendor or that every product needs a broker.
Databases and data correctness
| Model | Strength | Main tradeoff |
|---|---|---|
| Relational SQL (PostgreSQL, MySQL) | Constraints, joins, transactions, flexible queries | Schema evolution, index/write cost, connection management |
| Document NoSQL (e.g. MongoDB) | Aggregate-shaped documents and flexible fields | Duplication, cross-document consistency, query/index discipline |
| Key/value or in-memory cache (Redis) | Low-latency lookup, counters, leases/queues with care | Eviction, stale data, durability/consistency semantics, memory cost |
ACID describes transaction properties: atomicity, consistency with declared invariants, isolation between concurrent transactions, durability after commit. Isolation levels trade off anomalies and contention. Optimistic locking detects concurrent changes at write time with a version; pessimistic locking reserves a resource earlier and can block. Replication improves read scale/availability but can introduce replica lag. Sharding partitions data and adds routing, rebalancing, and cross-shard query costs. Connection pools cap concurrent database sessions; unbounded connection creation can take down the database before app CPU is saturated.
Indexes speed selected reads at storage and write-amplification cost. Confirm with query plans and representative data; a low-selectivity or oversized index can hurt more than help. Migrations need a backward-compatible expand → migrate → contract rollout while old app and service versions still exist.
-- Example only: idempotent command record and optimistic version check.
BEGIN;
INSERT INTO payment_requests (request_id, account_id, amount_cents, status)
VALUES (:idempotency_key, :account_id, :amount, 'accepted')
ON CONFLICT (request_id) DO NOTHING;
UPDATE accounts
SET balance_cents = balance_cents - :amount,
version = version + 1
WHERE account_id = :account_id
AND version = :expected_version
AND balance_cents >= :amount;
-- Application checks affected row counts; otherwise rollback/report conflict.
COMMIT;Production payment logic needs currency/rounding rules, authorization, ledger invariants, audit, fraud controls, and a carefully designed transaction boundary. This abbreviated SQL is not a banking implementation.
Distributed systems reliability
| Concept | Interview-ready point | Mobile consequence |
|---|---|---|
| Vertical / horizontal scaling | Bigger node vs more nodes; horizontal scaling needs statelessness or shared coordination | API latency and rate limits affect perceived app health |
| CAP | During a network partition, a distributed store cannot guarantee both strong consistency and availability for all operations; CAP is not a simple permanent three-way menu | Choose product behavior for stale reads, failed writes, and retry |
| Eventual consistency | Replicas/projections converge after updates propagate | UI can show pending, stale, or confirmed state explicitly |
| Queue / Kafka / RabbitMQ | Durable async handoff; delivery commonly requires idempotent consumers | A user command may be accepted before a downstream effect finishes |
| Circuit breaker | Stop repeated calls to a failing dependency, then probe recovery | Fail fast to cached/degraded UX rather than wait on cascading timeouts |
| Retry + exponential backoff + jitter | Retry transient failures within a deadline; spread retries to avoid thundering herds | Retry only safe/idempotent operations and respect cancellation |
| Idempotency | Repeating the same logical command has one effect | Re-submit payment/order with same key after timeout/unknown outcome |
| Rate limiting / backpressure | Bound demand and communicate overload | Apply server hints, queue locally only where product semantics allow |
| High availability / fault tolerance | Redundancy plus tested detection and recovery | Plan for offline mode, stale cache, and partial feature degradation |
| Service discovery | Resolve current service instances rather than hard-code them | Normally server-side; mobile clients use stable public endpoints |
| Distributed tracing | Propagate trace context across service hops | Correlate app request IDs without recording credentials or message content |
Distributed transactions span separate systems or service-owned stores. Two-phase commit can coordinate a narrow set of transactional participants but couples availability and operations. A saga splits work into local transactions and explicit compensating actions; compensation is not time travel, and some effects cannot be perfectly undone. Prefer an outbox and idempotent consumers for common event publication, and make intermediate/pending states visible when the business process is asynchronous.
A timeout means “outcome unknown,” not necessarily “operation failed.” For writes, use an idempotency key and query status or retry with the same key. Distinguish connection, transport, server, validation, and domain errors; do not blindly retry a 4xx or every 5xx.
Retry helper sketch (TypeScript pseudocode)
type RetryPolicy = { attempts: number; baseMs: number; maxMs: number };
async function retryTransient<T>(
operation: (attempt: number) => Promise<T>,
isRetryable: (error: unknown) => boolean,
policy: RetryPolicy,
): Promise<T> {
for (let attempt = 1; ; attempt += 1) {
try {
return await operation(attempt);
} catch (error) {
if (attempt >= policy.attempts || !isRetryable(error)) throw error;
const cap = Math.min(policy.maxMs, policy.baseMs * 2 ** (attempt - 1));
const delay = Math.random() * cap; // full jitter
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}Add an overall deadline, cancellation, server Retry-After handling, metrics, and an idempotency contract. The helper alone is not a safe retry policy.
Authentication and authorization for mobile APIs
Authentication asks who the principal is; authorization asks whether that principal may perform this action on this resource. OAuth 2.0 is an authorization framework; OpenID Connect adds an identity layer. For native apps, use the authorization-code flow with PKCE through the system browser where applicable; do not embed a client secret in the app. Use short-lived access tokens, carefully rotated/revocable refresh credentials, and server-side audience/issuer/scope validation. A JWT is a signed token format, not encryption and not proof that a request is authorized for a particular record.
The API must enforce object-level authorization on every resource access. RBAC assigns permissions to roles; ABAC evaluates attributes such as tenant, ownership, risk, and environment. Session/token revocation, refresh replay detection, device compromise, account recovery, and clock skew are product and threat-model concerns. Store mobile credentials using platform-protected storage; Keychain protection does not eliminate phishing, compromised devices, screenshots, or malicious in-process code.
iOS app → system authorization user-agent → identity provider
← short-lived authorization code + state/nonce
app + PKCE verifier → token endpoint → access token (+ refresh token policy)
app → API with bearer access token → API validates → resource authorizationTLS protects the channel; mTLS is useful for managed service-to-service identities but is usually not an appropriate general consumer-app secret because a distributed client cannot keep a shared secret. CSRF is primarily a browser-cookie concern; XSS is a web rendering concern; SQL injection/SSRF apply to server-side input and fetch boundaries. Protect secrets in a managed secret store, redact logs, rotate credentials, and restrict outbound network access.
Practical Node.js API shape
The following is framework-neutral TypeScript pseudocode to show middleware order. Names such as verifyToken, authorizeAccountAccess, and db are application-owned, not built-in Fastify/NestJS APIs.
type Principal = { subject: string; scopes: ReadonlySet<string> };
async function getAccountRoute(request: Request, reply: Reply) {
const token = readBearerToken(request.headers.authorization);
const principal: Principal = await verifyToken(token); // issuer/audience/expiry
requireScope(principal, "accounts:read");
const accountId = parseAccountId(request.params.accountId);
await authorizeAccountAccess(principal.subject, accountId); // object-level check
const account = await accountService.get(accountId);
request.log.info({ traceId: request.id }, "account_read"); // no token/PII
return reply.code(200).send(accountResponseSchema.parse(account));
}Separate pure unit tests for domain policy, integration tests against real HTTP/database boundaries, contract tests for OpenAPI/API compatibility, and a small number of end-to-end flows. Use structured logs with stable event names; traces link services; metrics track latency, error rate, saturation, queue lag, and budget. Docker can make development dependencies reproducible, but containerization does not itself secure a deployment.
OWASP API Security Top 10 — practical review map
The OWASP API Security project currently publishes its 2023 Top 10 edition. Treat the list as an awareness checklist, not a complete threat model:
| Risk family | Review question for a mobile API |
|---|---|
| Object-level authorization | Can a caller swap an account/order ID and read or mutate someone else's record? |
| Authentication | Can token replay, weak recovery, or refresh handling impersonate a user? |
| Object-property / function authorization | Can mass assignment alter protected fields or invoke an admin-only function? |
| Resource consumption / sensitive business flow | Can repeated requests consume costly resources or abuse purchases/OTP/refunds? |
| SSRF / unsafe API consumption | Can user-controlled URLs or untrusted upstream responses reach internal systems or bypass validation? |
| Misconfiguration / inventory | Are debug endpoints, old app/API versions, or permissive CORS/config exposed? |
Test authorization at endpoint and property level; add quotas and business-abuse controls; inventory old API versions; and validate third-party response data. See the OWASP API Security Top 10 2023 for the full list and methodology.
Fastify route shape and OpenAPI contract
This is a Fastify-style sketch: authenticate and accountService are application-owned, and the request type augmentation is omitted. Keep transport schema validation, authentication, resource authorization, domain work, and response shaping distinct. In NestJS the same boundary is normally expressed with guards, pipes, services, and controllers; framework syntax is not the security policy.
import Fastify from "fastify";
const app = Fastify({ logger: true });
app.get<{ Params: { accountId: string } }>(
"/v1/accounts/:accountId",
{ preHandler: authenticate },
async (request, reply) => {
const accountId = parseAccountId(request.params.accountId);
await authorizeAccountRead(request.user.subject, accountId);
const account = await accountService.get(accountId);
return reply.code(200).send(toAccountResponse(account));
},
);openapi: 3.1.0
info:
title: Accounts API
version: 1.0.0
paths:
/v1/accounts/{accountId}:
get:
operationId: getAccount
security:
- bearerAuth: []
parameters:
- name: accountId
in: path
required: true
schema:
type: string
responses:
"200":
description: Authorized account summary
"401":
description: Missing or invalid authentication
"403":
description: Authenticated principal cannot access this account
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWTThe contract advertises behavior but does not implement validation or authorization. Generate compatibility tests or verify the runtime implementation against this contract.
PostgreSQL and Redis in local development
PostgreSQL is the durable source of truth in this sample. Redis is a disposable cache/rate-limit primitive only when the product has an explicit consistency contract. Cache keys need tenant and resource scope; authorize before serving a cache hit, and define TTL/invalidation. A cache failure should not accidentally grant access or silently make critical writes succeed.
async function accountSummary(tenantId: string, accountId: string, principalId: string) {
await authorizeAccountRead(principalId, tenantId, accountId);
const key = `account-summary:v1:${tenantId}:${accountId}`;
const cached = await redis.get(key);
if (cached !== null) return parseAccountSummary(JSON.parse(cached));
const summary = await accountRepository.summary(tenantId, accountId);
await redis.set(key, JSON.stringify(summary), { EX: 30 }); // illustrative short TTL
return summary;
}Redis client option syntax varies by client library. This is an architecture sketch, not drop-in code. The database access must use parameterized queries and enforce tenant/resource conditions even after the application check.
services:
api:
build: .
environment:
DATABASE_URL: ${DATABASE_URL:?set a local development connection}
REDIS_URL: redis://cache:6379
depends_on:
database:
condition: service_healthy
cache:
condition: service_started
database:
image: postgres:17
environment:
POSTGRES_USER: study_app
POSTGRES_PASSWORD: local-development-only
POSTGRES_DB: study
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U study_app -d study"]
interval: 5s
timeout: 3s
retries: 10
cache:
image: redis:7
volumes:
postgres-data:This compose file is a local learning example, not a production image/version or secret-management recommendation. Pin approved image digests in real deployment; do not reuse the sample password outside a disposable local environment.
Unit and HTTP integration test examples
Unit tests exercise domain rules with deterministic inputs and no network/database. Integration tests exercise HTTP middleware, database constraints, and serialization against an isolated test environment. OpenAPI contract and compatibility tests catch client/server drift; UI end-to-end tests should remain few and focus on critical user flows.
import assert from "node:assert/strict";
import test from "node:test";
test("payment rejects an amount above the available balance", () => {
const decision = decidePayment({ amountCents: 1500, availableCents: 1000 });
assert.deepEqual(decision, { accepted: false, reason: "insufficient_funds" });
});test("account endpoint rejects a principal without resource access", async () => {
const app = buildTestApp({ accountAccess: "deny" });
await app.ready();
const response = await app.inject({
method: "GET",
url: "/v1/accounts/account-123",
headers: { authorization: "Bearer test-token" },
});
assert.equal(response.statusCode, 403);
await app.close();
});buildTestApp, accountAccess, and test-token are test fixture names, not a real credential or built-in Fastify API. The test should additionally prove that another tenant's record cannot be obtained by changing the path ID.
Quick Recap — Backend interview hit points
🧠 Say: “I start with consistency, availability, latency, and failure semantics per operation. I keep one deployable boundary until independent ownership or scale justifies distribution, then add idempotency, observability, versioning, and rollback with the new network boundary.”
🟥 Wrong: “We retry every failed payment request three times.”
🟩 Better: “A timeout leaves the outcome uncertain. The client reuses an idempotency key, the service records a durable result, and the UI checks status before treating a second submission as new work.”
