Backend Engineering

Understand backend APIs, data correctness, authentication, caching, distributed reliability, and mobile service boundaries.

~28 min reading
Suggest content

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

TEXT
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

ChoiceInteraction modelStrengthsCosts / fit
HTTPRequest/response over TCP (typically TLS)Ubiquitous; intermediaries and cachingPer-request overhead and retry semantics must be designed
RESTResource-oriented HTTP conventionsCacheable, debuggable, broad toolingMultiple round trips or endpoint-specific aggregation may be needed
GraphQLClient selects a typed response shapeUseful for varied clients and nested readsQuery cost limits, authorization at field level, caching complexity
gRPCProtobuf RPC, often HTTP/2Strong contracts, compact payloads, streamingMobile gateway/proxy support and browser compatibility considerations
WebSocketLong-lived bidirectional connectionInteractive realtime collaboration/chatConnection lifecycle, fan-out, reconnection and backpressure
Server-Sent EventsServer-to-client event stream over HTTPSimple one-way updates and reconnection modelClient-to-server commands still need another request path
BFFBackend-for-Frontend adapterTailors aggregation/auth contracts to app needsAnother 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

PatternShape / dependency sketchBenefitLimitation / operational costScaling and testing implicationsAppropriate starting point
MonolithClient → app process → DBSimple local calls, transactions, deploymentBoundaries can erode; one release unitScale whole process first; integration tests are straightforward but regression scope can growSmall domain/team, fast-changing product
Modular monolith`app → [module Amodule B] → DB`Clear domain seams without distributed callsRequires dependency rules and ownership disciplineScale whole process initially; add module/API and dependency-rule testsMost products before independent scaling/deployment is proven
MicroservicesClient → gateway → svc A/B → owned storesIndependent ownership/scaling and failure containmentNetwork, observability, data consistency, DevOps, and on-call complexityScale hot services independently; requires contract, integration, resilience, and deployment testsTeams/domains with real independent deployment or scaling needs
Serverlessevent → function → managed serviceLow idle operations and event-triggered scalingCold starts, runtime/provider constraints, distributed tracingPlatform scales invocations; test retries, concurrency, cold-start budgets, and provider integrationBursty event workloads and managed integrations
Event-drivenproducer → queue → consumerLoose temporal coupling and fan-outDuplicate/out-of-order events; schema evolution and operationsScale consumers by lag; test idempotency, replay, poison messages, and event contractsAsynchronous workflows and integration boundaries
CQRScommand → write model → event → read modelOptimized read/write pathsEventual consistency and duplicated model complexityScale read projections separately; test projection lag and read-after-write behaviorRead/write shapes diverge measurably
Event sourcingcommand → append-only events → projectionsAudit/replay and historical state reconstructionEvent schema evolution, replay cost, privacy deletion complexityReplay and projection tests become core; storage and rebuild cost grow with historyAudit-heavy domains where history is a business capability
Hexagonal / Cleanadapter → port → domain ← port ← adapterReplaceable transport and persistenceMore design surface; layering can become ceremonyUnit-test domain policy and contract-test adapters; scaling is deployment-dependentStable business rules with changing integrations
Domain-driven designbounded context A ↔ contract ↔ context BAligns software model with business ownershipRequires domain collaboration; not just aggregate classesScale teams around real boundaries; test invariants and context integration contractsComplex 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

TEXT
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 provider

The 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

ModelStrengthMain tradeoff
Relational SQL (PostgreSQL, MySQL)Constraints, joins, transactions, flexible queriesSchema evolution, index/write cost, connection management
Document NoSQL (e.g. MongoDB)Aggregate-shaped documents and flexible fieldsDuplication, cross-document consistency, query/index discipline
Key/value or in-memory cache (Redis)Low-latency lookup, counters, leases/queues with careEviction, 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.

SQL
-- 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

ConceptInterview-ready pointMobile consequence
Vertical / horizontal scalingBigger node vs more nodes; horizontal scaling needs statelessness or shared coordinationAPI latency and rate limits affect perceived app health
CAPDuring a network partition, a distributed store cannot guarantee both strong consistency and availability for all operations; CAP is not a simple permanent three-way menuChoose product behavior for stale reads, failed writes, and retry
Eventual consistencyReplicas/projections converge after updates propagateUI can show pending, stale, or confirmed state explicitly
Queue / Kafka / RabbitMQDurable async handoff; delivery commonly requires idempotent consumersA user command may be accepted before a downstream effect finishes
Circuit breakerStop repeated calls to a failing dependency, then probe recoveryFail fast to cached/degraded UX rather than wait on cascading timeouts
Retry + exponential backoff + jitterRetry transient failures within a deadline; spread retries to avoid thundering herdsRetry only safe/idempotent operations and respect cancellation
IdempotencyRepeating the same logical command has one effectRe-submit payment/order with same key after timeout/unknown outcome
Rate limiting / backpressureBound demand and communicate overloadApply server hints, queue locally only where product semantics allow
High availability / fault toleranceRedundancy plus tested detection and recoveryPlan for offline mode, stale cache, and partial feature degradation
Service discoveryResolve current service instances rather than hard-code themNormally server-side; mobile clients use stable public endpoints
Distributed tracingPropagate trace context across service hopsCorrelate 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)

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

TEXT
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 authorization

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

TYPESCRIPT
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 familyReview question for a mobile API
Object-level authorizationCan a caller swap an account/order ID and read or mutate someone else's record?
AuthenticationCan token replay, weak recovery, or refresh handling impersonate a user?
Object-property / function authorizationCan mass assignment alter protected fields or invoke an admin-only function?
Resource consumption / sensitive business flowCan repeated requests consume costly resources or abuse purchases/OTP/refunds?
SSRF / unsafe API consumptionCan user-controlled URLs or untrusted upstream responses reach internal systems or bypass validation?
Misconfiguration / inventoryAre 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.

TYPESCRIPT
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));
  },
);
YAML
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: JWT

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

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

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

TYPESCRIPT
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" });
});
TYPESCRIPT
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.”

References