36SDET System Design
What you will master here
- How to approach an SDET system-design interview
- Designing a test execution platform at scale
- Test data service architecture
- Distributed test runner with smart sharding
- Reporting and analytics backbone
- Self-healing tests / AI-assisted automation
- Trade-offs and capacity estimation
36.1 How interviewers grade this round
SDET system design at 5+ years experience asks: "Design a Y at scale for an org with N tests / N devs / N envs." They look for:
- You ask clarifying questions before drawing — scale, constraints, who uses it
- You separate functional requirements (what it does) from non-functional (latency, throughput, availability)
- You sketch components and APIs explicitly
- You discuss trade-offs (queue vs sync; SQL vs key-value; push vs poll)
- You estimate capacity (back-of-envelope numbers)
- You spot bottlenecks and propose mitigations
36.2 Common SDET system-design prompts
- "Design a test execution platform for 10K tests across 50 microservices, running on every PR within 10 minutes."
- "Design a test data management service that gives each test isolated, realistic data on demand."
- "Design a flaky-test detection and quarantine pipeline."
- "Design a self-healing locator system."
- "Design a unified test results dashboard across teams."
36.3 Worked example — distributed test execution platform
(1) Clarify
- How many tests total? (10,000 UI + 50,000 API)
- How many PRs per day? (500)
- SLA for feedback? (PR feedback < 10 min)
- Browsers / OSes? (Chromium / Firefox / WebKit on Linux)
- Cloud or self-hosted? (AWS, can use spot instances)
- Who runs it? (Platform team, ~5 SDETs)
(2) Functional requirements
- Trigger on PR / push / cron / manual
- Select which tests to run for a given diff (test selection)
- Distribute across N workers (smart sharding)
- Collect results, traces, screenshots
- Merge into one unified report
- Gate PR merge on result
- Detect and quarantine flakes automatically
(3) Non-functional
- p95 feedback time < 10 min
- 5000 tests/PR × 500 PRs/day = 2.5M test runs/day. Burst capacity at peak hours.
- Availability: 99.9% (downtime blocks all merges)
- Cost: target < $X per PR run
(4) Architecture sketch
Figure 15 — Components of a distributed test execution platform.
(5) Key design decisions
| Decision | Why |
|---|---|
| Queue (SQS / Kafka) between API and workers | Backpressure during peak hours; workers scale independently |
| Smart test selection (file-touched mapping) | 10K tests / PR is wasteful; map diff to affected tests |
| Autoscaling worker fleet on spot instances | Cost — 70% savings vs on-demand |
| Blob report per shard, merged centrally | Parallel shards write independently, no contention |
| Artifacts to S3, metadata to Postgres | Binary big, queries small — split storage |
| Test Data Service tenants per worker slot | Isolation + reuse — no data collisions |
| Flake detector queries result history | Test that fails 5% of times = quarantine candidate |
(6) Capacity estimate
- 2.5M tests/day. Peak ~10× average over 1 hour = 700 tests/sec.
- Each test = ~30 s in browser. So ~21,000 concurrent worker slots at peak.
- ~700 worker pods × 30 slots/pod (40 GB nodes). Manageable on EKS spot fleet.
- Queue throughput: SQS handles 700/sec trivially; Kafka if we need ordering / replay.
- Artifact store: ~10 MB trace/video per test × 2.5M = 25 TB/day. Set 14-day retention.
(7) Trade-offs to discuss
- SQS vs Kafka — SQS simpler, no ordering. Kafka if we need replay or strict ordering.
- Diff-based selection vs run-all — selection risks missing affected tests; combine with nightly full regression.
- Spot vs on-demand — spot cheaper, can be reclaimed mid-test. Build retry/redispatch.
- Synchronous result vs polling — GitHub status check polls; webhooks on result table can push updates.
36.4 Smart test selection
Run only tests likely affected by the diff. Several approaches:
- Path-based — file change in
/checkout/triggers tests tagged@checkout. Static mapping, simple, brittle. - Coverage-based — at last green run, capture coverage per test. Map line changes to tests. Accurate but heavy infrastructure.
- ML-based — train on historical PR → broken-test pairs. Probabilistic. Powerful but needs data.
- Test impact analysis (TIA) — combination above + safety net of full regression nightly.
36.5 Test Data Service
Goal: every test gets isolated, realistic data, in milliseconds. Options:
| Pattern | Pros | Cons |
|---|---|---|
| Fresh seed per test | Pure isolation | Slow if seed is heavy |
| Snapshot / restore per worker | Fast restore | Snapshot maintenance |
| Tenant pool per worker slot | Reuse + isolation | Stuck tests can corrupt slot |
| Synthetic data on the fly | Flexible | Hard to ensure realism |
36.6 Self-healing locators (AI-assisted)
When a locator breaks because the DOM changed, the system tries to find the best alternative automatically. Approach:
- Capture a rich snapshot per locator on green runs (role, name, neighbours, text, position, attributes).
- On failure, query the current DOM for candidates that score highly against the snapshot.
- Suggest top match in the report; allow auto-apply if confidence > 0.9.
- Surface healed locators in PR for human review — never silently change tests.
36.7 Reporting analytics
- Time-series of pass rate, retry rate, mean run time per test
- Top flaky tests this week
- Top slowest tests
- Coverage trend
- PR feedback latency histogram
- Cost per PR
36.8 Common follow-up questions
- "What if a worker crashes mid-test?" — retry on a different worker, mark the original as failed/retried.
- "How do you handle test data tied to a specific tenant?" — tenant pool with leases; expire stale leases.
- "How do you keep auth tokens fresh across thousands of tests?" — central token service issues short-lived tokens; cache per worker.
- "How do you handle a flaky third-party API in tests?" — proxy with HAR cache; replay if upstream slow.
- "How do you debug a single failing test in this system?" — trace stored per test, URL surfaced in PR comment.
Module 26 — SDET System Design Q&A
How do you approach an SDET system-design interview?
Ask clarifying questions about scale, SLA, environments. List functional + non-functional requirements explicitly. Sketch components with arrows. Discuss trade-offs (queue vs sync, push vs poll, SQL vs KV). Estimate capacity with rough numbers. Identify bottlenecks and mitigations.
How would you cut a 30-min test suite to 5 min?
Combine: (1) smart test selection (only affected tests), (2) parallel sharding (10× workers), (3) move UI assertions to API where possible, (4) storageState auth instead of UI login, (5) mock unstable third-party services, (6) cache browser binaries. Each is multiplicative.
What's smart test selection?
Running only the subset of tests likely affected by a diff. Path-based mapping (folder → test tag), coverage-based (per-test line coverage), or ML-based (historical PR-to-failure correlation). Combine with nightly full regression as safety net.
How do you isolate test data at scale?
Per-worker tenant pool — each parallel slot owns a tenant. Or per-test seeding via API with cleanup. For very heavy fixtures, snapshot-and-restore DB between tests. Always reset between tests; never share.
What is self-healing locator and what's the risk?
When a locator stops matching, the system tries to find the closest match in the new DOM using captured context (role, neighbours, attributes). Risk: silently masking bugs (the page changed in a meaningful way, not just markup). Mitigation: report healed locators in PR for review, never auto-merge.
How do you keep auth tokens fresh across thousands of tests?
Central token service that issues short-lived tokens on request. Workers cache them; refresh on 401. Avoids hammering the auth provider with login flows from every test.
Why use a queue between API and workers?
Decouples submission rate from processing rate. Burst-friendly — API stays fast even when workers are saturated. Workers scale independently based on queue depth. Failed jobs can be retried automatically.
How do you store and retrieve test artifacts?
Traces, videos, screenshots are large — store in S3 (or equivalent object storage). Metadata (which test, status, duration, link) in a SQL DB for queries. PR comment shows a link to the artifact in S3.
What's the right back-end for the reports DB?
Postgres for normalised data (test runs, status, duration). ClickHouse or a TSDB for high-volume time-series analytics (pass rate trends, flakiness scores). Use both; route reads to whichever is suited.
How do you handle network flakiness in tests at scale?
HAR-based replay for third-party APIs in CI. Retry with exponential backoff on transient errors. Health-check upstream before starting a shard. Quarantine tests that depend on known-flaky upstreams.
What KPIs would you put on the SDET dashboard?
PR feedback p95 latency, pass rate, retry rate, top 10 flaky tests, top 10 slowest tests, escaped defect rate (bugs that reached prod), test maintenance time per dev. Numbers, not just charts — tied to business outcomes.
How would you design a multi-tenant test data service?
Tenant per parallel slot (parallelIndex). Each tenant has its own DB schema or namespace. API:
POST /tenants/lease returns a tenant id with TTL. Tests use it; release on teardown. Stale leases auto-expire. Catches "noisy neighbour" bugs.How would you design a self-healing locator service?
On green runs, snapshot each locator's neighbourhood (role, name, neighbours' tags/text, position). Store keyed by test+selector. On failure: query current DOM, score candidates against snapshot, return top match. Auto-apply only if confidence > 0.9; otherwise suggest in PR for human review.
How would you build cross-team test result visibility?
Every test run pushes results to a central event bus → reports DB. Per-team dashboard (Grafana). Global dashboard: flakiness leaderboard, slowest tests, escaped defects per service. Email digest weekly. Quarterly review with eng leadership.
How would you scale test execution across 50 microservices?
Per-service contract tests (Pact) verified independently in each service's CI. A small set of cross-service E2E in a global pipeline (synthetic monitoring against staging). Service-mesh / preview environments per PR to scope blast radius.
How would you migrate from a 30-min E2E suite to a 5-min one?
Audit + categorize (kill, migrate to API, migrate to unit, keep as E2E). Implement smart test selection (only tests touching changed code) for PRs; full nightly. Parallelise via sharding + worker count tuning. Auth via storageState. Mock unstable third-parties. Profile slowest tests and refactor. Expect 60-80% reduction.
How would you design a flaky-test detector that doesn't lie?
Capture every retry, not just the final result. Compute failure rate per test over rolling 7d window weighted by total runs. Threshold + auto-open JIRA. Quarantine after N consecutive flake days. Nightly job to re-run quarantined tests; auto-lift if green for K runs.
How would you design a perf-test gate in CI?
Per-PR: synthetic Lighthouse on critical routes; compare against main baseline; fail if regression > threshold (e.g. LCP +200 ms). Nightly: full k6 load test with budgets. Production: RUM with web-vitals → alerts on p95 degradation.
36.4 Implementation Patterns
Test Execution Queue — Python + Redis
import redis
import json
import uuid
from dataclasses import dataclass, field
from typing import List
r = redis.Redis(host="localhost", port=6379)
@dataclass
class TestJob:
job_id: str = field(default_factory=lambda: str(uuid.uuid4()))
spec_file: str = ""
browser: str = "chromium"
shard: int = 1
total_shards: int = 1
env: str = "staging"
def enqueue_test_run(specs: List[str], browsers: List[str], total_shards: int = 4) -> str:
run_id = str(uuid.uuid4())
jobs = []
for spec in specs:
for browser in browsers:
for shard in range(1, total_shards + 1):
job = TestJob(spec_file=spec, browser=browser,
shard=shard, total_shards=total_shards)
jobs.append(job)
pipe = r.pipeline()
for job in jobs:
pipe.lpush("test:queue", json.dumps(job.__dict__))
pipe.set(f"run:{run_id}:total", len(jobs))
pipe.set(f"run:{run_id}:pending", len(jobs))
pipe.execute()
return run_id
def worker_loop():
while True:
raw = r.brpop("test:queue", timeout=30)
if raw is None:
break
job = TestJob(**json.loads(raw[1]))
run_playwright_shard(job)
def run_playwright_shard(job: TestJob):
import subprocess
cmd = [
"npx", "playwright", "test", job.spec_file,
"--project", job.browser,
f"--shard={job.shard}/{job.total_shards}",
"--reporter=json"
]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
r.lpush(f"run:results", json.dumps({
"job_id": job.job_id,
"spec": job.spec_file,
"browser": job.browser,
"exit_code": result.returncode,
"stdout": result.stdout[-2000:],
}))
Test Data Service — FastAPI
from fastapi import FastAPI, HTTPException
import psycopg2, uuid, json
from contextlib import contextmanager
app = FastAPI()
DB_DSN = "postgresql://user:pass@localhost/testdb"
@contextmanager
def get_conn():
conn = psycopg2.connect(DB_DSN)
try:
yield conn
conn.commit()
finally:
conn.close()
@app.post("/v1/test-data/user")
def create_test_user(template: str = "standard"):
uid = str(uuid.uuid4())[:8]
user = {
"email": f"test_{uid}@example.com",
"password": "TestPass123!",
"role": "user" if template == "standard" else "admin",
}
with get_conn() as conn:
cur = conn.cursor()
cur.execute(
"INSERT INTO users(email,password,role,test_session_id) VALUES(%s,%s,%s,%s) RETURNING id",
(user["email"], user["password"], user["role"], uid)
)
user["id"] = cur.fetchone()[0]
return user
@app.delete("/v1/test-data/cleanup/{session_id}")
def cleanup_session(session_id: str):
with get_conn() as conn:
cur = conn.cursor()
cur.execute("DELETE FROM users WHERE test_session_id = %s", (session_id,))
deleted = cur.rowcount
return {"deleted": deleted}
Self-Healing Locator — TypeScript
import { Page, Locator } from "@playwright/test";
interface LocatorStrategy {
strategy: string;
value: string;
score: number;
}
export async function selfHealingLocator(
page: Page,
strategies: LocatorStrategy[]
): Promise<Locator> {
const sorted = strategies.sort((a, b) => b.score - a.score);
for (const s of sorted) {
let loc: Locator;
if (s.strategy === "data-testid") loc = page.locator(`[data-testid="${s.value}"]`);
else if (s.strategy === "role") loc = page.getByRole(s.value as any);
else if (s.strategy === "text") loc = page.getByText(s.value);
else loc = page.locator(s.value);
if ((await loc.count()) > 0) {
console.log(`Healed via ${s.strategy}: ${s.value}`);
return loc;
}
}
throw new Error(`All locator strategies failed: ${JSON.stringify(strategies)}`);
}
// Usage in test
// const btn = await selfHealingLocator(page, [
// { strategy: "data-testid", value: "submit-btn", score: 100 },
// { strategy: "role", value: "button", score: 80 },
// { strategy: "text", value: "Submit", score: 60 },
// ]);