Skip to main content
Data Engineering Interviews advanced Lesson 9 of 10

Data System Design

The 45-minute whiteboard round, sequenced — requirements, back-of-envelope sizing, storage choice, and the tradeoffs you volunteer before being asked.

The design round is 45 minutes and most candidates spend 35 of them drawing boxes. The sequence below is what actually gets scored, in the order to do it.

The question

“Design a system that ingests clickstream events from a website and gives analysts next-day dashboards, plus a real-time view of orders in the last five minutes.”

Note the two latency requirements in one question. That is deliberate — it is testing whether you notice, and whether you build one system or two.

Minutes 0-8: requirements and sizing

Ask before drawing. Six questions, each of which changes the design:

Volume        how many events/day? peak events/second?
Latency       "next-day dashboards" and "last 5 minutes" — two different systems?
Retention     how long must raw events be kept? regulatory constraints?
Correctness   can we drop 0.1% of events, or must it reconcile exactly?
Consumers     analysts in SQL? a product surface? both?
Team          who operates this at 3am, and what do they already run?

Then do the arithmetic out loud. This is the step that most separates candidates:

events_per_day   = 500_000_000
bytes_per_event  = 1_200          # JSON envelope, before compression
peak_multiplier  = 4              # traffic is not uniform across 24h

raw_gb_day   = events_per_day * bytes_per_event / 1024**3
avg_eps      = events_per_day / 86_400
peak_eps     = avg_eps * peak_multiplier
parquet_gb   = raw_gb_day * 0.12                    # ~8x with zstd + columnar
yearly_tb    = parquet_gb * 365 / 1024

print(f"raw JSON        {raw_gb_day:8.0f} GB/day")
print(f"as parquet+zstd {parquet_gb:8.0f} GB/day")
print(f"one year        {yearly_tb:8.1f} TB")
print(f"average         {avg_eps:8,.0f} events/sec")
print(f"peak            {peak_eps:8,.0f} events/sec")
print(f"S3 storage/yr   ${yearly_tb * 1024 * 0.023 * 12 / 2:8,.0f}  (avg half-year held)")
raw JSON             559 GB/day
as parquet+zstd       67 GB/day
one year            23.9 TB
average            5,787 events/sec
parquet+zstd peak 23,148 events/sec
S3 storage/yr     $3,377  (avg half-year held)

Those numbers now decide things. 23,000 events/sec at peak rules out a single-instance consumer and justifies Kafka partitioning. 67 GB/day rules in a warehouse and rules out loading it into pandas. $3,377/year for storage says retention is not the cost driver — compute will be — so that is where to spend design effort.

Say one of these out loud: “So this is a mid-size problem — hundreds of gigabytes a day, not petabytes. That means I do not need a specialised system; a standard lakehouse handles it.”

Minutes 8-23: the design

                          ┌─────────────────────────────────────┐
  web / app SDK ──────────►  Kafka  (topic: events, 24 partitions)
                          │  key = session_id, retention 7 days  │
                          └───────┬──────────────────┬───────────┘
                                  │                  │
                    ┌─────────────▼──────┐   ┌───────▼─────────────┐
                    │ Sink connector     │   │ Flink / Spark SS    │
                    │ → S3 bronze        │   │ 5-min tumbling agg  │
                    │ parquet, hourly    │   │ → Redis / Pinot     │
                    │ partition=dt/hour  │   │ (real-time view)    │
                    └─────────────┬──────┘   └─────────────────────┘

                         ┌────────▼─────────┐
                         │ dbt on warehouse │  bronze → silver → gold
                         │ daily, 03:00 UTC │  + tests as gates
                         └────────┬─────────┘

                              BI / analysts

Two paths from one log, and say why: “The batch path and the streaming path read the same Kafka topic rather than the streaming path feeding the batch one. That way a bug in the real-time aggregation cannot corrupt the historical record, and the batch path is the source of truth for anything reconciled.”

Decisions to state as you draw, each with a reason and an alternative:

DecisionReasonAlternative
Kafkareplay, multiple independent consumers, back-pressureKinesis — less to operate, 7-day cap, harder replay
Key by session_idsessionisation needs ordering per sessionkey by user_id if the analytics are user-centric
24 partitions~1,000 events/sec each at peak, room to growfewer if consumers cannot keep up
Parquet + zstd8× smaller, typed, column pruningAvro on the wire, Parquet at rest
Partition by dt/hourqueries filter by time; ~2.8 GB/hour is a healthy sizedaily if hourly files get small
dbt for transformstests gate publication; build skips downstream on failureSpark SQL if the warehouse is the bottleneck
Redis for real-timesub-ms reads of a small aggregateApache Pinot if analysts need to slice it

The one to volunteer: “I would not use the streaming path to serve the daily dashboards. Streaming state is harder to backfill and reprocessing means replaying a topic rather than re-running a day.”

Minutes 23-33: the deep dive

The interviewer picks a component. Prepare for the two most common.

“Walk me through exactly-once into S3.”

“The sink connector commits Kafka offsets and the file write together, so a crash replays the batch rather than losing it. On S3 that means writing to a temp key and renaming, or using a table format — Iceberg or Delta — where the commit is an atomic metadata operation and a duplicate batch id is recognised and skipped. Without a table format I would accept at-least-once and deduplicate on event_id in the bronze-to-silver step, because duplicates are recoverable and lost events are not.”

“How do you handle a 10× traffic spike?”

for mult in (1, 3, 10):
    eps = 23_148 * mult
    partitions_needed = eps / 1_000                # ~1k events/sec per consumer task
    print(f"{mult:>2}x peak: {eps:>7,.0f} eps  →  {partitions_needed:>4.0f} partitions/consumers")
 1x peak:  23,148 eps  →    23 partitions/consumers
 3x peak:  69,444 eps  →    69 partitions/consumers
10x peak: 231,480 eps  →   231 partitions/consumers

“Kafka absorbs the spike as lag rather than dropping events — that is the point of the buffer — so the immediate risk is retention, not throughput. At 7 days I have plenty of headroom to catch up. Consumers scale to the partition count and no further, so I would over-partition now: 48 rather than 24, since increasing partitions later breaks key ordering for existing keys. The batch path is unaffected because it reads from S3 at its own pace.”

That last clause — over-partitioning because repartitioning breaks ordering — is a detail that only comes from having done it.

Minutes 33-40: failure modes, volunteered

Do not wait to be asked. Go through them briskly:

failures = [
    ("Kafka broker loss",     "replication factor 3, min.insync.replicas 2 — writes continue"),
    ("Consumer crash",        "offsets committed with the write; replay is idempotent"),
    ("Schema change",         "contract check in producer CI; unknown fields land in raw payload"),
    ("Late events",           "batch path re-reads a 3-day window and merges on event_id"),
    ("Warehouse down",        "S3 bronze is unaffected; backfill when it returns"),
    ("Bad transform deployed","dbt tests gate publication; previous gold tables stay live"),
    ("Traffic spike",         "Kafka buffers; lag alert at 15 minutes; consumers autoscale"),
    ("Real-time path down",   "batch path unaffected — dashboards degrade, history does not"),
]
for f, mitigation in failures:
    print(f"{f:<24} {mitigation}")
Kafka broker loss        replication factor 3, min.insync.replicas 2 — writes continue
Consumer crash           offsets committed with the write; replay is idempotent
Schema change            contract check in producer CI; unknown fields land in raw payload
Late events              batch path re-reads a 3-day window and merges on event_id
Warehouse down           S3 bronze is unaffected; backfill when it returns
Bad transform deployed   dbt tests gate publication; previous gold tables stay live
Traffic spike            Kafka buffers; lag alert at 15 minutes; consumers autoscale
Real-time path down      batch path unaffected — dashboards degrade, history does not

The last line is the design justifying itself: because the two paths are independent, losing the real-time view costs a dashboard rather than the historical record.

Then monitoring, briefly and specifically: “Consumer lag, S3 object count per hour against trend, dbt test results per run, and a freshness check on the gold tables run from a separate monitor — because if the scheduler never fires, a pipeline-side alert never fires either.”

Minutes 40-45: cost and tradeoffs

monthly = {
    "Kafka (3 brokers, m5.2xlarge)": 3 * 0.384 * 730,
    "S3 storage (avg 12 TB)":        12 * 1024 * 0.023,
    "S3 PUT requests (24k/day)":     24_000 * 30 / 1000 * 0.005,
    "Warehouse compute (2h/day)":    2 * 30 * 4 * 3.00,
    "Streaming job (2 nodes)":       2 * 0.192 * 730,
}
for k, v in monthly.items():
    print(f"{k:<34} ${v:>9,.0f}")
print(f"{'TOTAL':<34} ${sum(monthly.values()):>9,.0f}/month")
Kafka (3 brokers, m5.2xlarge)      $      841
S3 storage (avg 12 TB)             $      283
S3 PUT requests (24k/day)          $        4
Warehouse compute (2h/day)         $      720
Streaming job (2 nodes)            $      280
TOTAL                              $    2,128/month

Very few candidates cost their design, and it changes the conversation: “Kafka is 40% of the bill and it exists mainly for replay and multiple consumers. If the real-time requirement were dropped, Kinesis Firehose straight to S3 would cut this to about $1,000 and remove a system to operate. That is the tradeoff I would put to the product team.”

Close by naming what you would push back on:

“I would ask how the five-minute view is actually used. If it is a wall dashboard nobody acts on within the hour, the streaming path is buying latency nobody needs and costing a system to run. If it drives fraud checks or live inventory, it is clearly worth it. That single question changes about a third of this design.”

The scoring

BehaviourSignal
Asked for volume and latency before drawingsenior
Did arithmetic and used it to justify choicessenior
Volunteered failure modes without being askedsenior
Costed the design, even roughlysenior
Pushed back on a requirement with a reasonsenior
Named alternatives alongside each choicemid-to-senior
Correct architecture, discussed failures when promptedmid
Drew boxes for 30 minutes, no numbersjunior

Two failure patterns worth avoiding. Over-engineering: proposing Kafka, Flink, a lakehouse and a serving layer for 50 MB a day. And tool-listing: naming eight technologies with no reason for any, which reads as a résumé rather than a design.

A reusable skeleton

1. Requirements   volume, latency, retention, correctness, consumers, team
2. Sizing         GB/day, events/sec, storage/year, rough cost
3. Data flow      ingest → land raw → transform → serve
4. Storage        format, partitioning, table format, retention
5. Processing     batch or stream, engine, idempotency, backfill
6. Serving        warehouse, cache, API — who reads it and how often
7. Failure modes  each component, and what degrades vs what breaks
8. Observability  freshness, volume, quality, lag — and who is paged
9. Cost           the big line items, and the cheaper alternative
10. Tradeoffs     what you would push back on, and what you would do differently at 10x

Steps 7 to 10 are where most candidates run out of time, and where the round is decided. If you are 25 minutes in and still drawing, skip detail and get to failure modes.

Practice

1. Size a 500M events/day system before designing it.
raw JSON        559 GB/day
as parquet+zstd  67 GB/day
peak         23,148 events/sec

Those three numbers rule several architectures in and out. Two minutes of arithmetic turns “I’d use Kafka” into “I need at least 23 partitions”.

2. Work out what a 10× spike requires.
10x peak: 231,480 eps → 231 partitions/consumers

And the insight: Kafka absorbs the spike as lag, so retention is the constraint, not throughput. Over-partition now, because repartitioning later breaks key ordering.

3. Cost the design.
TOTAL  $2,128/month  — Kafka is 40% of it

Which makes the tradeoff concrete: dropping the real-time requirement halves the bill and removes a system to operate. Almost no candidate does this.

4. Name what you would push back on.
"How is the five-minute view actually used?"

If nobody acts within the hour, the streaming path buys latency nobody needs. Questioning a requirement — with a reason — is the strongest single signal in this round.

Next: the take-home project — what is actually being marked.

Frequently Asked Questions

How should I spend the 45 minutes in a data system design round?
Roughly 8 minutes on requirements and sizing, 15 on the core design, 10 on deep-diving one component the interviewer picks, 7 on failure modes and cost, and 5 on tradeoffs. Candidates who draw boxes for 30 minutes and never reach failure modes score poorly regardless of the architecture.
Do I need to do back-of-envelope calculations?
Yes, and they change the answer. Whether the daily volume is 5 GB or 5 TB determines the storage format, whether you need a distributed engine, and what the thing costs. Two minutes of arithmetic converts an opinion into a decision.
How specific should I be about technology choices?
Specific, with a reason and a stated alternative. 'Kafka because we need replay and multiple consumers; Kinesis if we want less operational burden and accept 7-day retention' shows a decision. Naming one tool with no reason reads as a default.
What separates a senior data system design answer?
Volunteering the failure modes and the cost. Anyone can draw a working pipeline; seniors say what happens on a re-run, on a schema change, on a 10× traffic spike, what it costs per month, and which requirement they would push back on.