Data Management and API Design Questions
Designing how applications structure, expose, and consume data through APIs and backend systems. Covers API design principles for data endpoints, pagination, filtering and sorting, idempotency, versioning, rate limiting, schema design, normalization versus denormalization, consistency models, caching strategies, client side fetching patterns, and error handling. Includes considerations for data integrity across integrations, transactional boundaries, master data management, and how API decisions affect downstream consumers and performance.
MediumTechnical
36 practiced
Implement an in-memory idempotency key store in Python with methods put(key, response, ttl_seconds) and get(key). It must be thread-safe, support TTL-based eviction (lazy or periodic), and return the previous response if the same key is seen. Provide considerations for concurrency and extension to multi-process deployments.
Sample Answer
Approach: use a threadsafe dict mapping key -> (response, expires_at). Provide lazy eviction on get/put and an optional background janitor thread that periodically purges expired entries. Use a single Lock for correctness; for higher throughput use sharded locks or read-write locks. For multi-process, replace in-memory store with a central store (Redis, DynamoDB) that supports SETNX/EX/PERSIST patterns and atomic operations.Key points:- Thread-safety: single lock ensures atomic check-and-set for put; prevents race where two threads both think key absent and both write. For higher concurrency, use sharded locks or concurrent maps.- TTL handling: lazy eviction on access avoids work when inactive; janitor reduces memory accumulation.- Return semantics: put returns existing response if present and not expired — ensures idempotency.- Complexity: get/put average O(1). Janitor runs O(n) occasionally.- Edge cases: clock skew (use monotonic if comparisons are relative), large responses (store references or serialize), serialization for cross-process.- Multi-process extension: use Redis with SET key value NX EX ttl to atomically set only if absent and expire; to return previous value, read-before-set may race — better use Lua script in Redis to GET and SET atomically or keep a two-key pattern (lock + result). Alternatively use durable stores (DynamoDB conditional writes) for stronger guarantees.
python
import threading
import time
from typing import Any, Optional
class IdempotencyStore:
def __init__(self, janitor_interval: Optional[float] = 60.0):
self._store = {} # key -> (response, expires_at)
self._lock = threading.Lock()
self._stop = threading.Event()
self._janitor_interval = janitor_interval
if janitor_interval:
self._janitor = threading.Thread(target=self._run_janitor, daemon=True)
self._janitor.start()
def _now(self) -> float:
return time.time()
def _is_expired(self, expires_at: float) -> bool:
return expires_at is not None and expires_at <= self._now()
def put(self, key: str, response: Any, ttl_seconds: Optional[float] = None) -> Optional[Any]:
"""
Store response for key with TTL. If key already present and not expired,
return previous response and do NOT overwrite. If expired or absent, store and return None.
"""
expires_at = None if ttl_seconds is None else self._now() + ttl_seconds
with self._lock:
item = self._store.get(key)
if item:
prev_resp, prev_exp = item
if not self._is_expired(prev_exp):
return prev_resp # idempotent: return previous response
# expired: fall through to overwrite
# store new entry
self._store[key] = (response, expires_at)
return None
def get(self, key: str) -> Optional[Any]:
"""Return stored response or None if absent/expired."""
with self._lock:
item = self._store.get(key)
if not item:
return None
resp, expires_at = item
if self._is_expired(expires_at):
# lazy eviction
del self._store[key]
return None
return resp
def _run_janitor(self):
while not self._stop.wait(self._janitor_interval):
now = self._now()
with self._lock:
keys_to_del = [k for k, (_, ex) in self._store.items() if ex is not None and ex <= now]
for k in keys_to_del:
del self._store[k]
def close(self):
self._stop.set()
if hasattr(self, "_janitor"):
self._janitor.join(timeout=1)EasyTechnical
36 practiced
Describe the trade-offs between normalization and denormalization when designing a database schema for API-backed services. For a read-heavy analytics endpoint that aggregates user actions, which approach would you choose and why? Discuss update complexity, storage cost, and query performance.
Sample Answer
Normalization reduces redundancy by splitting data into related tables (3NF+), which makes writes and updates simple and consistent (single source of truth), minimizes storage, and enforces integrity via foreign keys. However, normalized schemas require joins for read queries, increasing query cost and latency—problematic for complex aggregations.Denormalization duplicates or pre-aggregates data into read-optimized shapes (wide tables, materialized views, or summaries). This improves query performance dramatically for read-heavy analytics because fewer joins and pre-computed aggregates reduce latency. Trade-offs: higher storage cost due to redundancy, and greater update complexity—writes must update multiple places or refresh materialized views, which can require batching, upserts, or eventual consistency.For a read-heavy analytics endpoint that aggregates user actions, I’d choose denormalization: use event logs (append-only), plus periodic materialized views or pre-aggregated counters updated via streaming (Kafka + consumer) or scheduled jobs. This gives low-latency reads and scalable query performance. Mitigations for the downsides: keep raw normalized store for authority, use idempotent update pipelines, TTL/lifecycle policies to control storage, and accept eventual consistency where appropriate.
MediumSystem Design
42 practiced
Design a webhook delivery system that guarantees at-least-once delivery: include exponential backoff retry policies, a dead-letter queue for failing endpoints, HMAC signing of payloads, metrics on delivery success, and ways to avoid retry storms when downstream is overloaded.
Sample Answer
Requirements:- Functional: deliver webhook POSTs to subscriber endpoints, at-least-once delivery, HMAC-signed payloads.- Non-functional: durable retries with exponential backoff, dead-letter queue (DLQ) after N attempts, observability (success/failure metrics), avoid retry storms and cascading failure.High-level architecture:Publisher → Event Store (durable log) → Dispatcher Service → Delivery Workers → Subscriber Endpoint ↘ DLQ (after max attempts) ↘ Backoff Scheduler / Rate LimiterMetrics/Monitoring & Admin UI.Core components:1. Event Store: append-only durable queue (Kafka/Rabbit/Redis Stream). Stores event payload, subscriber ID, attempts, next_retry_at, secret.2. Dispatcher Service: reads ready-to-deliver events (next_retry_at <= now) and enqueues to Delivery Workers.3. Delivery Worker: executes HTTP POST with HMAC-SHA256 signature header (using subscriber secret), waits for response, records latency/status. On 2xx → ack and emit success metric. On transient error (5xx, connection timeout) → increment attempts, compute next_retry_at via exponential backoff + jitter, write back. On permanent error (4xx like 410/404) or attempts > N → move to DLQ.4. Backoff Scheduler: enforces exponential backoff: next = now + base * 2^(attempts-1) +/- jitter (e.g., ±10%). Persist next_retry_at in Event Store.5. DLQ: separate durable store for manual inspection & replay.6. Rate Limiter / Circuit Breaker: global & per-subscriber token bucket and circuit breaker that opens if error rate or latency exceeds thresholds; while open, suspend retries for that subscriber for cooldown window and emit metrics.7. Bulkhead & Concurrency Controls: per-subscriber concurrency limits to avoid overwhelming a single endpoint.8. Metrics: counters for deliveries (success/fail/retries), histograms for latency, queue lengths, DLQ size, per-subscriber error rates. Alerting on abnormal DLQ growth or high error rates.9. Admin Tools: retry, purge, inspect DLQ, rotate secrets.Delivery signing (example):- Header: X-Signature: sha256=hex(HMAC_SHA256(secret, timestamp + '.' + payload))- Include timestamp and signature; worker rejects if subscriber's verification fails or timestamp out of window.Avoiding retry storms:- Use exponential backoff with randomized jitter.- Circuit breakers per subscriber to stop hammering unhealthy endpoints.- Leaky-bucket/global rate limiting to smooth bursts.- Staggered retry windows by adding small random offsets to next_retry_at.- Backpressure: if Dispatcher sees consumer lag or high downstream latency, slow consumption rate.Data flow summary (ASCII):Event Store --> Dispatcher --(ready)--> Delivery Worker --(HTTP + HMAC)--> Subscriber |--(retry updates)--> Event Store |--(max attempts)--> DLQTrade-offs:- At-least-once implies idempotency needed at consumers; system could include idempotency keys in headers.- Using Kafka gives high throughput and offset control; simpler DB-backed queue easier for small scale.- Aggressive retries shorten latency but risk overload; circuit breakers and throttling mitigate.Edge cases:- Secret rotation: store secret version, include key id in signature header.- Clock skew: include timestamp tolerance window.- Long outages: DLQ retention and replay APIs.This design ensures durable at-least-once delivery, signed payloads, observability, and prevents retry storms via backoff, jitter, circuit breakers, and rate limiting.
HardSystem Design
48 practiced
Design a saga-based approach (or orchestration alternative) for a travel booking flow involving flight, hotel, and car reservations. Cover orchestrator responsibilities, compensation actions for each service, idempotency of saga steps, persistence of saga state, timeouts, and how to allow manual intervention in case of partial failures.
Sample Answer
Requirements & constraints:- Atomic user experience: book flight+hotel+car or provide clear compensation.- Eventually consistent across services (each service is its own bounded context).- Low latency for happy path; robust handling of partial failures, retries, and manual recovery.High-level approach- Use an orchestration-based saga: a central Saga Orchestrator (stateless workers + durable saga store) drives steps in sequence: Reserve Flight -> Reserve Hotel -> Reserve Car -> Confirm All. Each step is a request to the service’s booking API; success advances saga; failure triggers compensation for already-successful steps.Orchestrator responsibilities- Maintain saga state machine (current step, participants, retries, deadlines, compensation plan).- Dispatch commands to services and receive responses (success/failure).- Apply retry/backoff and idempotency semantics when calling services.- Persist saga state (see below) and emit events for monitoring/alerting.- Trigger compensations on failures and escalate to manual workflow if compensation fails or times out.Saga flow & compensation1. Reserve Flight (action: PUT /bookings with tentative flag) Compensation: Cancel flight reservation (DELETE /bookings/:id or POST /bookings/:id/cancel). If flight provider supports "hold" token, release hold.2. Reserve Hotel (action: hold room) Compensation: Release/Cancel room hold and refund any prepayment.3. Reserve Car (action: reserve vehicle) Compensation: Cancel reservation and refund.- Finalize step: once all tentative reservations succeed, orchestrator sends Confirm/Commit to each service (idempotent confirm). If confirm fails for a service, run compensation for that service and others as needed.Idempotency- Every saga command includes: saga_id, step_id, idempotency_key. Services must store request keys and return same result for retries. Booking services expose idempotent endpoints for TentativeReserve, Confirm, Cancel.- Orchestrator retries safe until acknowledgement; services need idempotent compensations too.Persistence & state model- Durable saga store (e.g., PostgreSQL, DynamoDB) with: - saga_id, status (PENDING/COMPLETED/COMPENSATING/FAILED), current_step, steps[] (participant, status, timestamps, attempt_count), logs.- Events published to message bus (Kafka) for observability and to support resurrecting orchestrator workers.- Snapshotting/TTL to GC completed/abandoned sagas.Timeouts & retries- Per-step policy: retry_count, backoff, overall step timeout. Example: 3 retries with exponential backoff, total step timeout 2 minutes.- Global saga timeout: if not resolved within N minutes, mark as TIMED_OUT and start compensation or escalate to manual.- Compensation retries: similar policy; if compensation fails after retries, mark as COMPENSATION_FAILED and escalate.Manual intervention & operator workflow- On partial failure or compensation failure, orchestrator: - Moves saga state to NEEDS_MANUAL_INTERVENTION and emits alert with context (logs, participant states). - Expose a dashboard/UI showing saga timeline and actions: Retry step, Force-Compensate, Approve Partial Booking, Rollback, or Contact provider. - Provide safe manual operations that call the same idempotent APIs with saga context. - Allow operator to attach notes and change saga TTL; include audit trail.- For user-visible UX: present clear status (e.g., "Partial booking — flight confirmed, hotel pending; a specialist will contact you") and automated refund timelines where applicable.Observability & testing- Structured logs, distributed tracing (trace = saga_id), metrics (success rate, avg duration, compensation rate).- Chaos testing for network, service failures, and compensation edge cases.- Contract tests to ensure services honor idempotency and compensation semantics.Trade-offs- Orchestrator centralizes complexity (simpler participants); single point of coordination but can be made highly available using leader-election and durable store.- Alternatively, choreography reduces central state but is harder to reason about for multi-step failure scenarios.This design yields clear compensations, robust idempotency, persisted state for recovery, sensible timeouts, and a manual remediation path for real-world partial failures.
EasyTechnical
38 practiced
Compare authentication mechanisms used for APIs: API keys, OAuth2 (authorization-code and client-credentials), and JWTs. Discuss trade-offs in security, revocation, token rotation, and storage strategies for web and mobile clients.
Sample Answer
High-level comparison:- API keys: simple static secret passed with each request (header/query). Good for service-to-service or low-friction integration. Weaknesses: bearer-only, long-lived, hard to scope, risky if leaked.- OAuth2 Authorization Code (with PKCE): for user-centric flows (web/mobile). Authorization server issues short-lived access token + long-lived refresh token. Supports scopes, user consent, and rotation. With PKCE it’s safe for public clients (mobile/SPAs).- OAuth2 Client Credentials: machine-to-machine; client authenticates with secret and receives access token. No user context.- JWTs: token format (self-contained claims, signed). Can be used as access tokens in OAuth2. Advantages: stateless validation (no DB lookup). Drawbacks: revocation hard if long expiry; must design rotation/short TTL.Trade-offs:- Security: OAuth2+PKCE > API keys. Use short-lived access tokens (minutes) and refresh tokens for security. Store secrets server-side when possible.- Revocation: API keys and refresh tokens can be revoked centrally. For JWT access tokens: use short TTL, token revocation lists (blacklist), or introspection endpoint to check token validity.- Token rotation: rotate refresh tokens (issue new refresh token on use and invalidate old) to limit replay risk. Rotate client secrets periodically.- Storage strategies: - Web (browser): prefer httpOnly, Secure cookies for tokens to mitigate XSS; use SameSite for CSRF protection. For SPAs without backend, use Authorization Code + PKCE and store tokens in memory when possible; avoid localStorage. - Mobile: use platform secure storage (iOS Keychain, Android Keystore/EncryptedSharedPreferences). Never store long-lived secrets in plain storage.Practical best practices: enforce least privilege scopes, monitor usage, detect anomalies, log token issuance, use HTTPS everywhere, and prefer server-side enforcement for sensitive operations.
Unlock Full Question Bank
Get access to hundreds of Data Management and API Design interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.