Design problems
Platform & infrastructureMediumSDE-3 30 min

Design ephemeral preview environments per pull request

Asked at
AtlassianFlipkartFreshworksRazorpaySwiggyZomato

The interviewer asks

“Design a platform that gives every pull request its own running copy of the application (API, workers, database with seed data, queue) at a unique URL, so reviewers and QA can click through it. It is destroyed when the PR closes. The company has 400 engineers, about 300 PRs open at any time, and a microservice system of 40 services.”

What a strong answer looks like

A strong answer avoids the trap of cloning full production infrastructure per PR (15+ minutes and huge cost) and instead runs previews as namespaces on a shared Kubernetes cluster: base infrastructure (VPC, cluster, ingress, wildcard DNS, registry) is managed by Terraform and changes rarely; per-PR environments are Helm releases or Argo CD ApplicationSets driven by PR events. Only the services changed in the PR are built and deployed; the other 35 services are shared from a stable baseline environment, with request routing by header (the 'sandbox' or 'branch routing' approach) so a PR does not need 40 pods. Data comes from seed scripts or sanitised snapshots, never production PII. Cleanup is guaranteed by close events plus a TTL reaper and cost tags. Deep dives: full copy vs shared baseline with header routing, data and migrations, speed (build cache, warm pools, target under 10 minutes) and cost (scale to zero, spot nodes, quotas). Senior candidates mention AI coding tools increasing PR volume, which makes review on a running system more valuable.

Concepts to mention

Terraform for base infra, Helm/Argo CD per PR namespaceshared baseline with header-based request routingrouting key propagation via baggage, including Kafka headersbuild only affected services with remote cachesanitised or synthetic seed data, never production PIIcopy-on-write database clonesTTL reaper, cost tags, scale to zerosecrets via External Secrets, preview-only credentialsSSO-protected wildcard DNS and TLS

1 Requirements

Functional

  • Opening or updating a PR creates or updates a preview at https://pr-142.preview.example.com and comments the URL on the PR
  • Each preview has the changed services built from the PR commit, plus all other services it depends on
  • Isolated data: its own database schema/instance with seed data and migrations from the PR applied
  • Previews are destroyed when the PR is merged or closed, and after N days of inactivity
  • Logs, traces and a shell/debug view per preview; status checks on the PR (deploying, ready, failed smoke test)
  • Optional: a preview can pin a specific branch of another service for cross-repo changes
  • Access restricted to company SSO; no public exposure

Non-functional

  • Time from push to working URL: p50 under 6 minutes, p95 under 10 minutes
  • Support ~300 concurrent previews, ~1,500 deploys/day (each push updates a preview)
  • Cost per preview-day low enough that previews are always on by default (target under ~₹100-200/day each)
  • No production secrets or PII in previews
  • A broken preview never affects other previews or the shared baseline

Out of scope (say it out loud)

  • Production deployment and canary releases
  • Performance/load testing environments (need production-like sizing)
  • Developer laptops / remote dev environments

2 Estimates

Concurrent previews~300400 engineers, ~0.75 open PRs each
Deploys~1,500/day, peak ~5/min300 PRs × ~5 pushes/day; concentrated in working hours 10:00-19:00 IST
Full-copy footprint~2,400 vCPU if every preview runs all 40 services40 services × ~0.2 vCPU requests × 300 previews; plus Postgres, Kafka, Redis each → too expensive
Shared-baseline footprint~150-250 vCPUAvg 2-3 changed services per PR × 0.2 vCPU × 300 + a small DB per preview (~0.25 vCPU) ≈ 150-250 vCPU
Cost~₹4-6 lakh/month on spot~200 vCPU × 730 h × ~₹3/vCPU-hour on spot ≈ ₹4.4 lakh; scale-to-zero at night and weekends cuts about half
Build time budget~3-4 min of the 10 minDocker build with remote layer cache ~1-3 min, deploy + DB seed ~1-2 min, smoke test ~1 min

3 API

EndpointWhat it does
POST/webhooks/github (pull_request, push events)Signature-verified webhook; enqueues create/update/destroy jobs keyed by repo + PR number.
GET/v1/previews?repo=&pr=Status, URL, deployed SHAs per service, age, cost so far.
POST/v1/previews/{id}:extend | :sleep | :wake | :reset-dataDeveloper actions: extend TTL, scale to zero, wake up, reload seed data.
PUT/v1/previews/{id}/overridesPin another service to a branch or image for cross-repo changes.
DELETE/v1/previews/{id}Manual teardown; also called on PR close.

4 Data model

previewsPostgres
preview_id (PK)repopr_number (unique together)namespacestatusurlservices jsonb {name: image_sha}overridescreated_atlast_activity_atexpires_atowner

Source of truth for the controller; drives the reaper and cost reports.

deploy_jobsPostgres (or a durable workflow engine)
job_idpreview_idcommit_shatype (create|update|destroy)stateattemptslogs_url

Only one active job per preview; a newer push cancels an older running job (latest SHA wins).

desired stateGit (env repo) or Argo CD ApplicationSet
one Helm values file per preview: image tagsrouting keyDB nameresources

GitOps: Argo CD reconciles the cluster to this; drift is corrected automatically.

base infra stateTerraform state in S3 with locking (DynamoDB or native S3 lock)
VPCEKS clusternode poolsingresswildcard DNS and TLS certregistry

Changes rarely; never per PR.

seed snapshotsS3
versioned pg_dump of a synthetic or sanitised dataset (~1-5 GB)

Rebuilt weekly by a job that anonymises data; restored into each preview's database.

5 Architecture

CLIENTSEDGESERVICESASYNCDATAEXTERNALwebhookpreview statecreate/update/destroybuild changed svcspush image:shacommit valueshelm syncExternal Secretspr-142.preview...x-preview: pr-142changed serviceseverything elsesleep / deletestatus + URLGitHub PR eventsReviewers / QA browserSSO proxy + ingressPreview controllerHeader router (mesh)CI build (remotecache)TTL reaper + costDeploy job queueArgo CD (GitOps)Controller PostgresPR namespaces (pods,DB)Shared baseline envRegistry + S3 seedsSecrets Manager
ClientEdge / gatewayService / workerQueue / streamCacheDatabase / storageAI / modelThird partyrequestasync (event / queue)

Hover a box to highlight its connections.

6 Request flows

Push to PR 142 creates a preview

  1. 1GitHub sends a pull_request 'synchronize' event; the controller verifies the HMAC signature, upserts previews(repo, 142) and enqueues a job for commit abc123, cancelling any older running job for the same PR.
  2. 2The job computes which services changed (path filters or the build graph, e.g. Bazel/Nx affected targets): checkout-api and checkout-worker.
  3. 3CI builds only those images with a remote layer cache and pushes checkout-api:abc123 to the registry.
  4. 4The job writes a values file for namespace pr-142 (image tags, routing key pr-142, DB name) to the environment repo; Argo CD's ApplicationSet syncs it.
  5. 5In pr-142, a small Postgres (or a schema on a shared preview DB server) is restored from the latest seed snapshot, then the PR's migrations run as a Helm pre-install hook job.
  6. 6Secrets come from AWS Secrets Manager via External Secrets Operator, using preview-only credentials (sandbox payment keys, test SMS provider).
  7. 7A smoke test runs against the preview URL; on success the controller sets the GitHub check to green and comments https://pr-142.preview.example.com.

Reviewer opens the preview

  1. 1The reviewer opens pr-142.preview.example.com; wildcard DNS and a wildcard TLS cert point all previews to the ingress, which requires company SSO.
  2. 2The ingress maps the host to routing key pr-142 and adds header x-preview: pr-142.
  3. 3Requests to checkout-api go to the pr-142 version; calls to catalog, user and 35 other services go to the shared baseline, which propagates the x-preview header (via OpenTelemetry baggage) on every call.
  4. 4When the baseline cart service calls checkout-worker, the mesh sees x-preview: pr-142 and routes to the pr-142 worker instead of baseline.

PR merged

  1. 1GitHub sends 'closed'; the controller enqueues a destroy job.
  2. 2The values file is removed; Argo CD prunes the namespace (pods, DB, secrets, ingress rules).
  3. 3The reaper verifies zero resources remain with tag pr=142 and records final cost; a nightly sweep deletes anything older than its TTL or without a matching open PR.

7 Deep dives

Full copy per PR vs shared baseline with request routing

The simplest model gives each PR a full copy of all 40 services, plus its own Postgres, Kafka and Redis. It is perfectly isolated and easy to reason about, but at 300 PRs it means about 2,400 vCPU, thousands of pods, slow startup (every service and its dependencies must boot and become healthy), and constant flakiness because one broken unrelated service breaks every preview. A shared baseline with request routing deploys only the changed services into the PR namespace. A stable baseline environment runs the main branch of every service. Each request carries a routing key (x-preview: pr-142), set at the ingress from the hostname and propagated through every hop via trace context baggage. The service mesh (Istio, Linkerd with header routing) or an in-house router sends a request to the PR's version of a service if one exists, and to baseline otherwise. This is how tools like Signadot and Uber's and Lyft's staging approaches work. The cost is that propagation must be universal: any service that drops the header breaks routing, and async paths (Kafka) need the key in message headers with consumers that filter or re-route. Data isolation is also weaker, because baseline services share a database. Use full copies for small systems (under ~10 services) and the shared model beyond that.

Data, migrations and async messaging

Data is where previews get hard. Never copy production: it contains PII and payment data, and regulations (and common sense) forbid it. Use synthetic seed data or a sanitised snapshot built by a weekly job that anonymises names, phones and addresses and keeps referential integrity; restore it per preview in about a minute from S3. For speed, keep the database small (1-5 GB) or use copy-on-write clones (Neon branches, Aurora fast clones, ZFS/LVM snapshots) that create a writable copy in seconds regardless of size. Each PR runs its own migrations against its own database or schema, so a destructive migration in one PR cannot break others. If baseline services share a database, a PR that changes the schema of a shared table must deploy that owning service in its namespace with its own DB. Kafka is the other trap: if the PR's consumer and baseline's consumer are in the same consumer group, they steal each other's messages. Either give each preview its own topics (prefix pr-142.orders.events) on a shared cluster, or route by header: consumers ignore messages whose routing key belongs to another preview, and the PR version consumes only its own.

Speed and cost

Previews are only used if they are fast. The 10-minute target breaks down into build, deploy and data. Build: compute affected services from the change and build only those, with a remote Docker/BuildKit layer cache so a code-only change rebuilds one layer in under a minute. Deploy: keep node capacity warm (a small buffer of idle nodes or overprovisioning pods with low priority that get preempted) so new pods are not waiting 2-3 minutes for a node. Data: snapshot restore or copy-on-write clone instead of running seed scripts. Cancel in-flight jobs when a new commit arrives. Cost: run preview node pools on spot instances (acceptable because previews are disposable), set resource requests small with ResourceQuota per namespace, scale previews to zero after 2 hours of no traffic and wake them on the next request (a KEDA HTTP add-on or an activator that holds the request while pods start), shut everything down at night and weekends, and tag every resource with pr= and team= so cost per PR is visible. Forgotten environments are the classic money leak, so the reaper is not optional.

8 Trade-offs

Namespace per PR vs cluster or cloud account per PR

A Terraform-created stack per PR (own RDS, MSK, cluster) is the strongest isolation and tests infrastructure changes too, but takes 15-30 minutes and costs many times more. Namespaces in a shared cluster start in minutes and cost little; isolation is softer (noisy neighbours, shared control plane). Use namespaces for app previews and a separate, rarer pipeline for infrastructure changes.

Helm from CI vs GitOps (Argo CD)

Running helm upgrade from CI is simple and fast. GitOps adds a reconcile loop: the desired state is in Git, drift is corrected, and the cluster can be rebuilt from Git. At 300 environments, ApplicationSets with a pull-request generator give automatic create and prune with less custom code.

Database per preview vs schema per preview

A Postgres pod per preview is simple and isolated but uses memory for 300 instances. A schema per preview on a shared preview database server is cheaper but one heavy test can slow everyone. Copy-on-write branching services give isolation with low cost.

9 Bottlenecks

Monday morning rush

Hundreds of previews wake up or deploy at 10:00. Node autoscaling (Karpenter) needs minutes; keep a warm buffer, stagger wake-ups, and prioritise builds for recently active PRs.

Shared baseline drifting or broken

If baseline (main branch) is broken, every preview looks broken. Deploy baseline only from green main builds, with its own smoke tests, and keep the previous version ready to roll back.

Ingress and DNS limits

Thousands of ingress rules and certificates slow the controller. Use one wildcard certificate and one wildcard DNS record, with host-based routing done in the proxy from a map, not one ingress object per service per preview.

10 Failure handling

Deploy job fails midway

The job is idempotent: re-running helm/Argo sync converges to the desired state. The PR check shows the failure with a link to logs; the previous working version stays live if it existed.

Spot node reclaimed

Pods are rescheduled within minutes; preview databases need a persistent volume or are re-seeded automatically. Previews are explicitly best-effort; reviewers see a 'waking up' page instead of an error.

Cleanup missed (webhook lost)

Webhooks can be lost. The nightly reaper lists open PRs from the GitHub API and deletes any namespace or cloud resource whose PR is closed, using the pr tag, and alerts when orphan count is non-zero.

11 What impresses

  • Does the footprint math for full copies vs shared baseline and picks with numbers
  • Explains header-based routing and why every hop, including Kafka, must propagate the key
  • Catches the Kafka consumer-group stealing problem in shared environments
  • Treats cleanup as a correctness requirement: close event plus reaper plus orphan alert
  • Breaks the 10-minute target into build, deploy and data budgets
  • Mentions sandbox third-party credentials (payment gateway test keys, test SMS)

12 Mistakes

  • Provisioning RDS, MSK and a cluster per PR with Terraform
  • Copying the production database into previews
  • No TTL or reaper, so forgotten previews burn money
  • Ignoring migrations: one PR's migration breaks a shared database
  • Publicly accessible preview URLs with real-looking data
  • Secrets committed in Helm values or printed in CI logs

13 Follow-ups

Q1A PR changes two services in two different repos. How do they end up in one preview?
Q2How do you test a PR that changes a Kafka event schema?
Q3How would you support mobile app previews against these backends?
Q4How do you show cost per team and stop one team from running 100 previews?
Q5What would you change for 3,000 engineers?
Q6How do you run chaos experiments or end-to-end tests against previews safely?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free