InterviewKata
System design · Worked design D30 of 46 · Data and infrastructure · Long form

Design a company data platform

Every product team wants answers from data: which feature people use, why revenue dipped, what to recommend next. A data platform collects events from apps and changes from production databases, keeps them cheaply in one place, turns them into trustworthy tables, and serves dashboards, analysts and machine-learning jobs, without ever slowing down production.

1. The prompt

"Design the data platform for a large consumer company: collect app events and data from our production databases, and make it available for dashboards, ad-hoc analysis and machine learning."

What makes it hard:

2. Frame the problem

Which sources?

Product events from web and mobile apps, plus changes from ~200 production databases (orders, users, payments), and a few SaaS tools (CRM, billing).

Two very different ingestion paths: an event pipe and change data capture (CDC) from databases.

How fresh must data be?

Most dashboards: by the next morning. A few (fraud, live operations): within minutes.

Mostly batch, with a streaming path only where minutes matter. Don't make everything real time.

Who uses it?

~500 analysts writing SQL, ~2,000 dashboards, data scientists training models, and finance reports that must reconcile to the cent.

Different workloads compete for the same compute; finance needs exact, auditable numbers.

How long do we keep data?

Raw events 2 years, curated tables for years, some finance data 7+ years.

Long retention favours cheap object storage over database storage.

Personal data?

Yes: users can ask for their data to be deleted, and only some teams may see emails or addresses.

Deletion in files that are normally never rewritten, plus column-level access control.

Build or buy?

Assume we design the architecture; managed services (a cloud warehouse, managed Kafka) are allowed.

The interview is about data flow, correctness and cost, not about writing a query engine.

3. What it must do

  1. Ingest app events and database changes continuously, without load on production databases.
  2. Store everything raw and cheaply, then transform it into clean, documented tables (daily active users, orders, revenue).
  3. Serve SQL queries, dashboards and ML training jobs, plus a small set of minute-fresh metrics.

Not now: a feature store for real-time model serving, reverse ETL back into product tools, a data marketplace for external customers.

4. How well it must do it

TargetWhy this number
No silent loss: every event and change lands exactly once in curated tables data quality · consistencyRevenue and user counts drive decisions and finance reports.
Freshness: daily tables ready by 06:00; the fast path under 5 minutes speedMorning reviews use yesterday's numbers; fraud and operations need minutes.
Zero impact on production: ingestion adds no meaningful load to source databases surviving failuresAnalytics must never cause an outage.
Interactive queries: typical dashboard query under 10 s speed · load shapeAnalysts iterate; slow queries mean fewer questions asked.
Privacy: deletions complete within 30 days; personal columns visible only to approved roles securityRegulation (for example the GDPR) and user trust.

5. Numbers that change the design

QuantityWorking→ so we…
App events2 B a day × ~1 KB of JSON ≈ 2 TB a day, ~25 K a second on average, 3–5× at peak→ so a partitioned log (Kafka-like) absorbs bursts in front of the lake
Stored history2 TB × 730 days ≈ 1.5 PB raw; columnar and compressed, roughly 5–10× smaller→ so object storage with a columnar file format, not a database's own disks
Database changes200 databases, ~50 M row changes a day→ so read their replication logs (CDC) instead of running nightly SELECT * dumps
Queries2,000 dashboards refreshed a few times a day, plus ad-hoc queriesIf each scanned a full year it would be petabytes a day → so partitioning, column pruning and pre-aggregated tables decide the bill

6. The nouns

raw.app_events        event_id, event_type, user_id, event_time, received_at, payload (JSON)     partitioned by event_date
clean.app_events      event_id (unique), event_type, user_id, event_time, typed columns…        partitioned by event_date
raw.orders_changes    op (c | u | d), before, after, source_lsn, commit_time                    partitioned by commit_date
clean.orders          order_id, user_id, status, amount, currency, updated_at                   current state, merged from changes
mart.daily_revenue    date, country, currency, orders, revenue                                  small; dashboards read this

7. The contract

Producers and consumers both need interfaces:

POST /v1/events            [{event_id, event_type, user_id, event_time, properties}]   → 202   (batched by the SDK)
schema registry            event_type → versioned schema; incompatible changes are rejected at publish time
SQL endpoint               SELECT … FROM mart.daily_revenue WHERE date >= …           (dashboards, analysts)
table read API             read clean.app_events snapshot @ version N                    (ML training, reproducible)
job definition             inputs, output, SQL or code, schedule, owner, tests, freshness target

8. First working version

App SDKs send batched events to a collector; databases stream changes through CDC; both land in a log, then in object storage as raw tables. Nightly jobs build clean tables and marts; a SQL engine serves queries.

8.1 Getting data in

The collector only validates and appends to the log, so it stays fast and can't lose data in a burst. A CDC connector reads each database's replication log (the same stream a replica uses), so production sees no extra queries. A sink job writes the log into object storage every few minutes as columnar files.

8.2 Turning it into answers

An orchestrator runs transformation jobs in dependency order: clean tables first, then marts. Dashboards read the small marts; analysts can still query the big clean tables when they need detail.

Parked for laterbatch vs streaming (9.1) · table format and small files (9.2) · CDC done right (9.3) · bad data and contracts (9.4) · late and duplicate events (9.5) · deleting personal data (9.6) · query cost (9.7).

9. Make it hold up

9.1 Batch, streaming, or both freshness

ApproachWhy
✗ BadEverything streaming, every table updated in secondsMost questions are about yesterday; streaming costs more to build, run and debug, and joins across late data get hard.
~ GoodTwo separate pipelines: nightly batch for accuracy, streaming for speed (the Lambda architecture)Works, but the same logic lives twice and the two numbers disagree.
✓ GreatOne log as the source; the lake is written in small increments every few minutes; daily jobs read the same tables; a streaming job only for the handful of minute-fresh metricsOne definition of each metric; streaming is a targeted exception, not the default (Module 23).

9.2 Files on object storage that behave like tables interactive queries

Object storage gives cheap, durable files but no transactions: a job that dies halfway leaves half-written output, and readers can see a mix. An open table format (Apache Iceberg, Delta Lake or Apache Hudi) adds a metadata layer: each commit writes new files, then atomically swaps a pointer to a new snapshot listing them.

9.3 Change data capture without surprises no silent loss

9.4 Bad data and data contracts data quality

ApproachWhy
✗ BadFind out from a stakeholder that yesterday's revenue is zeroBy then, wrong numbers have spread into decisions and other tables.
~ GoodTests on output tables (not null, unique keys, row counts within a range)Catches many problems, but after the bad data has landed.
✓ GreatContracts at the source (registered schemas, owners, compatibility checks in the producer's CI), tests between layers that block publishing a table, a quarantine table for rejected rows, and freshness alerts per tableBad data is stopped early and visibly; consumers keep the last good version instead of a broken one.

9.5 Late and duplicate events exactly once in curated tables

Mobile apps send events hours or days late (offline phones), and SDKs retry, so duplicates arrive. Partition by event time (when it happened), not arrival time; deduplicate on event_id when building clean.app_events; and re-run the last few days' partitions each night so late arrivals are included. Daily numbers can change slightly for a few days: say so on the dashboard, and freeze finance numbers only after the late window closes (Module 20).

9.6 Deleting personal data in files that never change privacy

9.7 Query cost and noisy neighbours interactive queries

ApproachWhy
✗ BadDashboards query raw JSON events directlyEvery refresh reads every column of every day: slow and expensive.
~ GoodColumnar files partitioned by dateQueries read only the columns and days they need, often 100× less.
✓ GreatPlus compacted files, small pre-aggregated marts for dashboards, separate compute pools (dashboards, ad-hoc, nightly jobs) with quotas, and cost shown per teamA runaway query can't starve the morning dashboards, and teams see what their queries cost. Try it in section 13.

9.8 The whole design after the deep dives

10. Interviewer follow-ups

A producer ships a bug that sends user_id as null for a day.

The contract test in the clean layer fails, rows go to quarantine, and the table keeps yesterday's good snapshot. After the fix, replay that day from the raw layer (which kept everything) and rebuild downstream tables.

Finance says revenue in the warehouse doesn't match the billing system.

Reconcile daily: compare counts and sums per day and currency between the source and clean.orders, alert on any gap, and trace it with the CDC log positions. Finance tables are frozen only after reconciliation passes.

Add a real-time "orders in the last 5 minutes" metric.

A streaming job reads the orders change stream, aggregates in event-time windows with a watermark, and writes to a small serving store. The daily mart remains the source of truth for history.

ML wants features computed the same way for training and serving.

Define each feature once, compute it in batch for training (reproducible via table snapshots) and in a stream for online serving, and check that the two agree. That's the next step: a feature store.

A source database is migrated to a new cluster.

Snapshot the new cluster at a log position, run both CDC connectors in parallel, compare the resulting tables, then switch. The clean layer hides the change from consumers.

Storage costs keep rising.

Expire old snapshots, move raw data older than 90 days to a colder storage class, drop raw copies once clean tables are verified (if replay isn't needed), and find tables nobody has queried in 6 months.

11. When things break

FailureWhat users seeHow we detect itWhat limits the damage
Collector or log overloadedNothing, if SDKs bufferProducer errors, log lagSDKs batch and retry with backoff; the log is replicated and partitioned
CDC connector stopsTables go staleFreshness alert, connector lagResume from the last committed log position; source keeps enough log
Nightly job failsYesterday's numbers missingOrchestrator alert, freshness SLARetries; dashboards show "data as of" and keep the last good snapshot
Bad data in a sourceWrong numbersContract and range testsPublishing blocked; quarantine; replay from raw after the fix
Runaway ad-hoc querySlow dashboardsQueue time in the dashboard poolSeparate compute pools, query time and scan limits
Small-file explosionSlow queriesFiles per partitionScheduled compaction

12. Wrap-up and the clock

Trade-offs we made:

What we'd watch: freshness per table · rows quarantined · CDC lag · files per partition · bytes scanned per team · dashboard query p95 · reconciliation gaps.

At 10×: more log partitions, more aggressive compaction and clustering, more marts in front of dashboards, and chargeback per team.

A 60-minute interview

0–5Frame: sources, freshness, users, privacy
5–10Must do, how well, numbers
10–15Nouns (layers), contract
15–25First version: in, store, transform, serve
25–50Deep dives: batch vs stream (9.1), table format (9.2), CDC (9.3)
50–60Quality or privacy, follow-ups, wrap-up

A 90-minute interview

0–8Frame, including finance and privacy
8–15Must do, how well, numbers
15–22Nouns, contract
22–35First working version
35–75Deep dives 9.1–9.7 and the final picture
75–85Follow-ups: reconciliation, real-time metric
85–90Trade-offs, metrics, 10×
LevelWhat a strong answer shows
Mid-levelEvents through a queue into storage, nightly ETL into a warehouse, dashboards on top, partitioning by date.
SeniorCDC instead of dumps, raw/clean/mart layers, deduplication and late data by event time, a table format with compaction, quality tests that block publishing.
Staff+Data contracts and ownership, reconciliation for finance, privacy deletion in immutable files, cost visibility and workload isolation, choosing where streaming is worth it.

Common mistakes:

13. Try it, check yourself, read more

Query-cost lab. One year of app events (2 TB of raw JSON a day). A dashboard asks for the last 7 days, 3 of the 40 columns. Change how the table is stored and watch how much data the query has to read. Costs use an illustrative $5 per TB scanned.

50

Model: columnar files are ~6× smaller than JSON and a query reads only the columns it needs; each file costs ~20 ms to open and plan, spread over 200 parallel readers that each read 200 MB/s.

How do you copy a busy production database into the lake without slowing it down?

CDC reads the log the database already writes for replicas, so it adds no query load.

Phones send events up to 3 days late. How do daily counts stay right?

Each event lands on the day it happened; recent days are rebuilt until late data stops arriving.

What stops readers from seeing half-written output when a job dies midway?

New files only become visible when the commit swaps the snapshot pointer.

Dashboards are slow and the bill is high. What helps most?

Reading less data beats reading the same data faster.
ResourceWhy
Designing Data-Intensive Applications (Martin Kleppmann)Chapters 10–11 on batch and stream processing and derived data.
Apache IcebergSnapshots, hidden partitioning and row-level deletes on object storage.
Delta Lake and Apache HudiThe other two widely used open table formats.
DebeziumOpen-source change data capture: snapshots, log positions and delete events.
dbt and Apache AirflowSQL transformations with tests, and dependency-ordered scheduling.