Cloud-Native Patterns
Cloud-native patterns are the recurring solutions to the hard problems of distributed systems on cloud platforms: retries with backoff, circuit breakers, health checks, idempotency, configuration injection and observability. They are the difference between an app that runs in the cloud and one that is resilient in it.
What is this?
Cloud-native patterns are battle-tested recipes for building applications that treat the infrastructure as a platform rather than a machine: stateless processes configured by environment, resilient to partial failure, auto-scaled, and observable by default. They cover the 12-Factor principles plus the distributed-system defenses (retries, circuit breakers, bulkheads) that keep a cloud app alive when dependencies degrade.
Why do you need it?
- Cloud infrastructure fails regularly and silently: instances die, networks drop, regions degrade — resilience must be designed in.
- Stateless, horizontally scaled processes make the autoscaler's job trivial and turn capacity into a solved problem.
- Configuration via environment keeps the same artifact safe in dev, staging and prod with zero code changes.
- Health checks and readiness probes let orchestrators route traffic away from sick instances before users feel it.
- Observability (metrics, logs, traces) is the only way to debug a fleet of instances you never SSH into.
Mental model
A single employee in an office
A cloud-native service on a platform
One employee works until they burn out and the office notices too late. A cloud-native service publishes a heartbeat, takes on only what it can carry, steps aside for replacements when unhealthy, and reports its own vital signs — so the platform can act automatically.
How it works
- Stateless instances are started from an immutable artifact; all config is injected via environment variables at boot.
- The service exposes /health (liveness) and /ready (readiness) endpoints the platform polls to route traffic and restart instances.
- Dependencies are guarded: calls get timeouts, bounded retries with jittered exponential backoff, and circuit breakers that fail fast when a dependency is down.
- Writes are made idempotent with client-supplied keys so retried requests cannot double-apply.
- The service logs structured events, emits metrics and exports traces, and shuts down gracefully on SIGTERM, draining in-flight work.
A request's journey through the resilience stack around a cloud-native service.
Key commands
kubectl get pods -o wideSee which instances exist and their ready state.
kubectl rollout restart deployment/apiRoll new instances in a stateless fleet without downtime.
curl -s localhost:8080/readyProbe the readiness endpoint to see if the instance accepts traffic.
{"status":"ready"}curl -s localhost:8080/healthProbe liveness; the platform restarts instances that fail this.
{"status":"alive"}kubectl get hpaCheck the horizontal autoscaler's current and target replicas.
NAME REFERENCE TARGETS MINPODS MAXPODS
kubectl logs deploy/api --tail=100Stream the last 100 structured log lines from the deployment.
Examples
Retry with jittered exponential backoff and a circuit breaker
// resilience/retry.tsexport async function withRetry<T>(fn: () => Promise<T>, opts: { attempts?: number baseMs?: number maxMs?: number} = {}): Promise<T> { const { attempts = 4, baseMs = 100, maxMs = 2000 } = opts let lastErr: unknown for (let attempt = 1; attempt <= attempts; attempt++) { try { return await fn() } catch (err) { if (attempt === attempts) throw err lastErr = err const backoff = Math.min(maxMs, baseMs * 2 ** (attempt - 1)) const jitter = Math.random() * backoff * 0.5 await sleep(backoff + jitter) // jitter prevents thundering herd } } throw lastErr} export function circuitBreaker(fn: () => Promise<void>, { threshold = 5, resetMs = 10_000 } = {}) { let failures = 0 let openUntil = 0 return async () => { if (Date.now() < openUntil) throw new Error("circuit open") try { await fn() failures = 0 } catch (err) { failures += 1 if (failures >= threshold) openUntil = Date.now() + resetMs throw err } }}withRetry retries transient failures with exponential backoff plus random jitter so simultaneous retries do not stampede the recovering dependency. circuitBreaker trips open after five consecutive failures and refuses calls for ten seconds, giving the dependency room to recover while the caller fails fast.
In production
- A payments API uses idempotency keys so a retried debit cannot charge twice.
- Order ingestion breaks failures by tenant with bulkheads so one abusive tenant cannot take down everyone.
- A video platform scales from 1 to 200 replicas on a view spike, then back to 1 at 3 a.m., paying only for what runs.
- Health and readiness probes let a mesh canary 5% of traffic to a new version and roll back automatically on failure.
Best practices
- Separate /health (liveness, always cheap) from /ready (readiness, checks dependencies) — they drive different orchestrator actions.
- Use client-generated idempotency keys for every mutating API call and store them with a TTL.
- Cap retries and always use exponential backoff with jitter; never retry on 4xx client errors, only on 5xx and network errors.
- Put all config in environment variables and fail fast at boot if a required variable is missing.
- Design for graceful shutdown: handle SIGTERM, stop accepting traffic, finish in-flight requests, then exit.
Common mistakes
- Retrying on non-idempotent endpoints without a key, creating duplicate side effects.
- Health checks that call the database — liveness must never depend on dependencies, or the whole fleet restarts in a DB outage.
- Fixed backoff (or no jitter) that thunders a recovering service with synchronized retries.
- Stateless processes holding state in memory, so scaling and restarts silently lose data.
Common issues
502 Bad Gateway from the load balancer after a deployInstances are starting but failing readiness, or the artifact crashed on boot.
Watch kubectl get pods for CrashLoopBackOff or readiness 0/1, then read startup logs for config or dependency failures.
All retries fail at the same instant after every deploySynchronized clients retrying together without jitter, stampeding the dependency.
Add random jitter to backoff and stagger retry starts across clients.
Duplicate payment records after a client retryThe endpoint is not idempotent and the retried request applied twice.
Require an idempotency key header, dedupe on the key with a unique constraint, and return the stored result on replay.
Debugging tips
- Start with the trace: a p95 spike is a dependency slowdown, while an error-rate spike with healthy deps is usually code.
- Check circuit state — an open circuit turns all calls into fast errors that look like outages.
- Confirm the readiness endpoint flips false before a dependency actually fails; if not, add dependency checks to readiness.
- Look for thundering-herd patterns: synchronized spikes in retry logs usually mean missing jitter.
Interview checkpoint
Hands-on lab
- Build a simulated flaky dependency
- Implement retry with jittered exponential backoff
- Add a circuit breaker that fails fast
- Observe how each layer changes failure behavior
Build a Resilient Gateway with Retries and a Circuit Breaker
45 min · advanced
Quick quiz
What does a readiness probe tell the platform?
A liveness probe should check that the service can reach its database.
Your client retried a failed order request and two orders were created. What pattern was missing?
Cheat sheet
TimeoutEvery external call gets oneRetry + jitterExponential backoff, randomizedCircuit breakerFail fast when dependency is downBulkheadIsolate failures per tenant/typeIdempotency keySafe retries for writes/healthLiveness; restart on failure/readyTraffic routing; checks depsSIGTERM handlingDrain in-flight work, then exitEnv configSame artifact everywhere