← PaddySpeaks
Interview Studio · Practice · Q&A Design Senior DE Prep 8 sections · case-study notebook
About this guide. Originally written as a Netflix-specific prep notebook, this guide uses Netflix as the worked case study but the patterns hold for any senior data-engineering interview loop — the four scoring axes, the SQL families, the Python reflexes, the streaming vocabulary, and the day-of micro-behaviors are universal. For multi-industry coverage of data modeling specifically, see the companion 12-scenario data-modeling deep-dive.
Interview Studio · Design · Senior DE Prep

The interview is a conversation, not a quiz.

Senior interviewers care more about your logic than your memorization of SQL engine-specific quirks. This notebook is built around that premise — eight deep sections (Calibration, Modeling, SQL, Python, Streaming, Behavioral, System-Design Lite, Day-Of), each with the patterns these rounds actually probe, worked solutions, and the questions you should be asking them. Drawn from a Netflix case study; reads as a generic senior-DE prep.

§ 01 — Calibration

The Netflix bar, and what they're actually probing.

Before any code, understand the rubric. Netflix DE rounds aren't LeetCode contests. They're structured conversations where the interviewer is silently scoring four things on every answer:

  1. Clarification reflex. Do you ask sharp questions before coding? "What's the grain?" "Is this batch or stream?" "Are user_ids stable?" Silence = junior signal.
  2. Trade-off articulation. Every answer has a cost. Star schema vs. wide table. Window function vs. self-join. Can you name the trade and pick one with reasoning?
  3. Failure thinking. What happens when this pipeline runs at 3am, the upstream is late, and the watermark is wrong? Senior candidates volunteer this.
  4. Communication under pressure. Narrate as you code. "I'm writing a CTE first to isolate qualified sessions." Silent typing = unscoreable.
The recruiter's quote, decoded. "We care more about your logic than your memorization of SQL engine-specific quirks." — This means: don't panic if you forget exact syntax. Pseudocode the logic, name the function family ("I'd use a window function with PARTITION BY user_id ORDER BY ts, then LAG to compute gaps"), and the interviewer will nod. Spending 2 minutes recalling whether it's DATE_DIFF or DATEDIFF is a waste. Say "in Snowflake syntax this is roughly…" and move on.

What Netflix DE rounds typically look like

  • Data Modeling (45–60 min): One open-ended scenario. They want to see you go from ambiguous prompt → clarifying questions → conceptual model → physical model → indexing/partitioning → "how does this break?"
  • SQL (45–60 min): 3–6 questions of escalating difficulty against a shared schema. Expect window functions, sessionization, and at least one "data quality" or "deduplication" question.
  • Programming (45–60 min): CodeSignal-style. 1–2 problems. Then 15–20 min deep-dive on streaming, data systems, or a project from your resume.

The articulation framework — the four beats on every answer

BeatWhat you say (out loud)Time
Clarify"Before I model — who reads this? What's the SLA — minutes, hours, or days? Are user_ids stable across devices?"30–60s
Frame"I'll treat this as a star schema with one fact — fact_X — at the grain of one row per Y per Z."15s
BuildWalk the model / write the SQL / type the code, narrating each decision.bulk
Stress"This breaks if late events arrive after watermark, if dim_title is reloaded mid-day, or if a GDPR delete cascades." Volunteer two failure modes unprompted.60s

The three phrases that signal seniority:

  1. "The grain of this fact is…"
  2. "The trade-off here is X vs Y; I'm picking X because…"
  3. "This breaks when…" (volunteered, not asked)

The one phrase that signals junior: silence + typing.

· · ·
§ 02 — Data Modeling

Modeling is a conversation — five moves, every time.

The modeling round is the hardest to fake and the easiest to fail by jumping straight to tables. Use the same five-move sequence on any prompt — viewing history, billing, A/B experiments, content metadata, payouts — and you will look senior even when the domain is unfamiliar.

The five-move sequence

  1. Clarify the use cases. "Who reads this? What questions does it answer? What's the SLA — minutes, hours, days?" Don't model anything until you have 3–5 named consumers.
  2. Pin the grain. One sentence per fact table: "One row per user per title per session." If you can't say this cleanly, the model isn't ready.
  3. Conceptual → logical → physical. Entities and relationships first. Then attributes and types. Then partitioning, clustering, and SCD strategy.
  4. SCDs and time. Which dimensions change? How do you preserve history — Type 1 overwrite, Type 2 versioned rows, or Type 6 hybrid? Most candidates skip this and lose senior signal.
  5. Stress test. "How does this break?" Late-arriving facts, time zones, GDPR delete requests, schema drift, hot partitions. Volunteer at least two failure modes — don't wait to be asked.
The grain trap. The single most common mid-level mistake: writing fact_views with no clear grain, then later realizing one user can have multiple device sessions per title and the row counts don't add up. Say the grain out loud before writing a single column.

The vocabulary you must use fluently

TermOne-sentence definition
GrainThe meaning of one row, stated as a sentence.
SCD Type 1 / 2 / 6Overwrite vs. versioned-rows vs. hybrid (current + history columns).
Conformed dimensionSame dim used by multiple fact tables (dim_date, dim_geography). Enables cross-fact analysis.
Bridge / factless factM:N resolver, or an event-occurrence table with no measure.
Late-arriving dimensionFact arrives before the dim is loaded. Use a placeholder SK (-1) and back-fill, or block the load.
Slowly-changing factRestating financials. Append corrections; never UPDATE in place.
Idempotent loadRe-running yesterday's batch produces the same result. Always achievable via MERGE on a natural key.
WatermarkThe latest event time you trust as "complete enough" to materialize.
Surrogate keySynthetic integer PK (decoupled from natural key); enables SCD2.
Star vs snowflakeStar = denormalized dims. Snowflake = normalized. Star is default for analytics.
Wide tableOne denormalized fact pre-joined to dims. Trade: read speed vs storage + drift.
Activity schemaSingle-table model where every event is a row. Useful for product analytics.

Trade-offs you must articulate

DecisionTrade-off you say out loud
Star vs Snowflake"Star — fewer joins, faster reads, more storage. Snowflake — normalized, less drift, more joins. Default to star for analytical workloads."
Wide table vs star"Wide is faster for one consumer; drifts when 5 consumers want different cuts. Star wins long-term."
SCD1 vs SCD2"Type 1 if no one queries history. Type 2 the moment someone says 'as of date X'. Type 6 if 90/10."
Surrogate vs natural PK"Surrogate decouples PK from upstream changes. Required for SCD2. Always use both — surrogate as PK, natural as a column."
Partition by date vs entity"Date partitioning fits time-series scans. Entity partitioning fits per-customer queries. Default: partition by date, cluster by entity."
Append-only vs upsert"Append-only is cheap, idempotent, replay-friendly. Upsert/MERGE gives latest-state cheaply. Modern lakehouse: append + materialized view."
Materialize vs view"Materialize when read frequency × cost > rebuild cost. View for ad-hoc, materialize for dashboards."

Five real-life scenarios — full walk-throughs

Scenario A — Viewing history (the canonical Netflix prompt)

Design a data model to support Netflix's viewing history. Stakeholders: recommendations team, content team (renewals), finance (royalty payouts).

Move 1 — Clarify (out loud)

  • "Is a 'view' play-start or qualified-view (≥120s)? Both — qualified is a derived flag."
  • "Grain: per session or per heartbeat? Heartbeats land in fact_play_event_raw; I'll roll up to fact_viewing_session."
  • "Latency: recs need <5min, finance is monthly. One model, two consumers."
  • "Stable IDs: profile_id stable, title_id stable, device_id rotates."

Move 2 — Grain

One row per profile per title per device per session in fact_viewing_session. A session = continuous play with gaps ≤ 30 min.

Move 3 — Conceptual

dim_profile (SCD2)  ─┐
dim_title   (SCD2)  ─┤
dim_device  (SCD1)  ─┼─▶ fact_viewing_session ─▶ fact_royalty_event
dim_geography       ─┤   (one row per session)   (one row per title per studio per period)
dim_date            ─┘

Move 3 — Physical (Snowflake / Iceberg)

CREATE TABLE fact_viewing_session (
  session_sk        BIGINT,                -- surrogate
  session_id        STRING,                -- natural, from client SDK
  profile_sk        BIGINT,                -- FK to SCD2 effective row
  title_sk          BIGINT,
  device_sk         BIGINT,
  geo_sk            BIGINT,
  date_sk           INT,
  session_start_ts  TIMESTAMP_TZ,
  session_end_ts    TIMESTAMP_TZ,
  watch_seconds     INT,
  qualified_view    BOOLEAN,
  bitrate_avg_kbps  INT,
  ingest_ts         TIMESTAMP_TZ,          -- for idempotent MERGE
  source_system     STRING
)
PARTITION BY date_sk
CLUSTER BY (title_sk, profile_sk);

Move 4 — SCD strategy

  • dim_profileType 2 (plan changes, country moves affect royalty + recs).
  • dim_titleType 2 (rating changes, regional availability changes).
  • dim_deviceType 1 (current fingerprint is enough).
  • dim_geographyType 1.

Move 5 — Stress tests (volunteer)

  1. Late events: stream-side watermark 60s; daily batch backfills 48h late tail.
  2. Time zones: UTC at storage; royalty uses contract TZ.
  3. GDPR delete: token mapping table — invalidate token, sessions become un-rejoinable.
  4. Schema drift: Avro + Confluent Schema Registry, backward-compat mode.
  5. Hot partitions: popular new releases — sub-partition by HASH(title_sk) MOD 16 on release week.
Senior framing line. "Once fact_viewing_session is conformed with fact_royalty_event through dim_title, finance can do per-territory amortization without rebuilding the join — and that's the unlock for content leadership."

Scenario B — Production lifecycle (Studio/Content)

Track every title's stage: greenlight → development → pre-prod → production → post → QC → release. Spot stuck projects.

The trap: This looks like a "status" column on dim_title. It's not — you lose history. The right answer is a factless fact.

dim_title ──┐
dim_stage ──┼──▶ fact_title_stage_transition (factless)
dim_user  ──┤      grain: one row per title per stage entry
dim_date  ──┘
            └──▶ snap_title_stage_daily (daily snapshot for "where is title X now?")
CREATE TABLE fact_title_stage_transition (
  title_sk          BIGINT,
  from_stage_sk     INT,           -- nullable for greenlight (first stage)
  to_stage_sk       INT,
  transition_ts     TIMESTAMP_TZ,
  actor_user_sk     BIGINT,        -- who moved it
  comment           STRING
);

CREATE TABLE snap_title_stage_daily (
  snap_date              DATE,
  title_sk               BIGINT,
  current_stage_sk       INT,
  days_in_stage          INT,
  total_days_in_pipeline INT
)
PARTITION BY snap_date;

The "stuck project" query reduces to:

SELECT title_sk, current_stage_sk, days_in_stage
FROM snap_title_stage_daily s
WHERE snap_date = CURRENT_DATE
  AND days_in_stage > (
    SELECT PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY days_in_stage)
    FROM snap_title_stage_daily
    WHERE snap_date = CURRENT_DATE - 7
      AND current_stage_sk = s.current_stage_sk
  );
Senior signal. "I'd add a dim_stage_sla so 'stuck' is a per-stage threshold, not global — pre-prod normally takes 6 months, post takes 3."

Scenario C — Talent contracts & royalties (the SCD2 trap)

Actor has overall deal → series deal → episode fee + backend (residuals, success bonus). Legal needs total liability per actor as of any date. Finance wants next-quarter forecast.

Why it's hard: Deal terms get renegotiated. A query for "as of 2025-Q1" must use the deal terms current at that moment.

dim_talent (SCD1)

dim_deal_overall (SCD2)         -- talent ↔ Netflix master agreement
  deal_id, talent_sk, term_start, term_end,
  effective_from, effective_to, is_current

dim_deal_show (SCD2)            -- deal under an overall deal, per show
  show_deal_id, overall_deal_id, title_sk,
  base_episode_fee, residual_pct, mfn_flag,
  effective_from, effective_to, is_current

fact_payout_accrued (grain: one row per deal per period)
  payout_id, talent_sk, show_deal_id, accrual_date,
  base_amount_usd, residual_amount_usd, bonus_amount_usd,
  fx_rate_to_usd, source_currency

fact_payout_disbursed (grain: one row per actual payment)
  payout_id, accrual_id, paid_date, amount_usd, status

The "as of date D" query — show this verbatim

SELECT
  d.talent_sk,
  SUM(d.base_episode_fee * episodes_aired_by(D, d.title_sk))
    + SUM(estimated_residual(d, D)) AS total_liability_usd
FROM dim_deal_show d
WHERE d.effective_from <= :as_of_date
  AND (d.effective_to IS NULL OR d.effective_to > :as_of_date)
GROUP BY d.talent_sk;

Things to volunteer

  • MFN clauses: If Actor A has MFN, raising Actor B's fee triggers a backfilled adjustment. Need an audit log of fee changes; recompute affected MFN deals.
  • Multi-currency: dim_fx_rate is itself SCD2. Lock to accrual date FX rate, not today's.
  • Forecast: Monte Carlo on residuals — separate ML model produces forecast_payout_low/mid/high; DE owns the join, not the model.

Scenario D — A/B experimentation at scale

Thousands of concurrent experiments. Compute lift on any metric for any test.

The model is two facts joined late:

fact_assignment (grain: one row per profile per experiment)
  profile_sk, experiment_id, variant, assigned_ts, bucketing_hash
  -- sticky on profile_sk + experiment_id

fact_exposure (grain: one row per profile per experiment per day)
  profile_sk, experiment_id, variant, exposure_date, exposure_count

dim_experiment (SCD2)
  experiment_id, name, primary_metric,
  start_date, end_date, traffic_alloc_pct, mex_layer

Lift compute is metric-agnostic

WITH exposed AS (
  SELECT profile_sk, variant
  FROM fact_assignment
  WHERE experiment_id = :exp
    AND assigned_ts >= :start AND assigned_ts < :end
),
metric AS (
  SELECT profile_sk, SUM(qualified_view_seconds) AS m
  FROM fact_viewing_session
  WHERE session_start_ts BETWEEN :start AND :end
  GROUP BY profile_sk
)
SELECT
  e.variant,
  COUNT(*) AS users,
  AVG(COALESCE(m.m, 0)) AS metric_avg,
  STDDEV(COALESCE(m.m, 0)) AS metric_std
FROM exposed e LEFT JOIN metric m USING (profile_sk)
GROUP BY e.variant;
Senior signal. Mention CUPED (variance reduction using pre-period covariate) and SRM (sample ratio mismatch as a first-class quality monitor). "If observed split deviates >0.5% from expected, the experiment is suspect."

Scenario E — Renewal scorecard (Sr/Staff differentiator)

One scorecard per series after season N: total viewing, viewing per dollar, amortization status, talent renewal cost, comparable shows.

This is a data product, not a table. The right answer is to design the layer above the warehouse:

mart_series_renewal_scorecard (one row per series per snapshot_date)
├── viewing_signal     — fact_viewing_session, normalized for catalog effect
├── cost_signal        — fact_production_spend + amortization curve
├── talent_signal      — projected renewal cost from dim_deal
└── benchmark_signal   — k-NN on dim_title features → top 5 comparable
"This isn't a dashboard — it's a versioned data product the renewal committee makes decisions against. So I'd treat it like a feature store: every scorecard is reproducible — running today's pipeline against last month's facts gives last month's recommendation, not today's."

That sentence alone moves you from Senior to Staff signal.

The eight modeling traps — and the senior counter-move

TrapCounter-move
Writing columns before stating grainSay grain first, every time.
Using natural keys as PKsSurrogate as PK, natural as a column. Mandatory for SCD2.
UPDATE on factsAppend corrections; let MV pick latest.
One status column on a dim ("current_stage")Factless fact for transitions + daily snapshot for current.
Hierarchies via recursive joins for hot queriesDenormalize parent IDs onto leaf, drift-check nightly.
GDPR via cascade DELETEsTokenize at ingest; invalidate token on delete.
"We'll add SCD later"Adding SCD2 to a populated dim is a 6-month migration. Decide upfront.
Per-team conformed dim copiesOne conformed dim, owned by platform. Otherwise joins lie.
"Before I start — let me confirm: [3 clarifying questions]. Okay, assuming [X], the grain of the primary fact is [one sentence]. I'd model this as a star — [fact] surrounded by [3–4 dims] — with [dim_X] as Type 2 because [reason]. Physically, partition by [date_sk], cluster by [hot key]. Two failure modes I'd plan for: [late data + watermark] and [GDPR + tokenization]. The unlock for the business is [decision the data product enables]."
· · ·
§ 03 — SQL

SQL — the seven patterns + the dialects.

Netflix's SQL round is engine-agnostic by design. Master these seven patterns and you can adapt any of them to whatever schema they put in front of you.

Reading the room. If the interviewer says "write SQL," they want runnable SQL. If they say "how would you compute…", you can lead with pseudocode and CTEs first. Always narrate: "Step one, get the qualified sessions. Step two, rank within user. Step three, filter rank=1." This is what they're actually grading.

Pattern 1 — Top-N per group

WITH agg AS (
  SELECT profile_id, title_id, SUM(watch_seconds) AS total_seconds
  FROM fact_viewing_session
  WHERE session_start_ts >= CURRENT_DATE - INTERVAL '30 days'
  GROUP BY profile_id, title_id
),
ranked AS (
  SELECT profile_id, title_id, total_seconds,
    ROW_NUMBER() OVER (
      PARTITION BY profile_id
      ORDER BY total_seconds DESC, title_id ASC
    ) AS rn
  FROM agg
)
SELECT profile_id, title_id, total_seconds
FROM ranked WHERE rn <= 3
ORDER BY profile_id, rn;
  • ROW_NUMBER for exactly N rows. RANK if ties should both qualify (you might get >N). Mention the trade.
  • Tiebreaker: always add deterministic secondary sort (title_id ASC). Without it, results are non-deterministic and tests flake.
  • Snowflake shortcut: QUALIFY ROW_NUMBER() OVER (...) <= 3. Mention it; don't depend on it.
  • Performance: at 10B sessions, GROUP BY is the expensive step — partition pruning on session_start_ts does the heavy lifting.

Pattern 2 — Sessionization (gaps and islands)

WITH with_prev AS (
  SELECT profile_id, ts, event_type,
    LAG(ts) OVER (PARTITION BY profile_id ORDER BY ts) AS prev_ts
  FROM fact_play_event
),
flagged AS (
  SELECT profile_id, ts, event_type,
    CASE
      WHEN prev_ts IS NULL OR TIMESTAMPDIFF('second', prev_ts, ts) > 1800
      THEN 1 ELSE 0
    END AS is_new_session
  FROM with_prev
),
sessionized AS (
  SELECT profile_id, ts, event_type,
    SUM(is_new_session) OVER (
      PARTITION BY profile_id ORDER BY ts
      ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS session_num
  FROM flagged
)
SELECT profile_id, session_num,
  MIN(ts) AS session_start, MAX(ts) AS session_end,
  COUNT(*) AS event_count
FROM sessionized
GROUP BY profile_id, session_num
ORDER BY profile_id, session_start;
  • Canonical gaps-and-islands: LAG → flag boundary → cumulative SUM = session_id → GROUP BY. Memorize the shape.
  • Dialect note: TIMESTAMPDIFF is Snowflake/MySQL. BigQuery: TIMESTAMP_DIFF(ts, prev_ts, SECOND). Postgres: EXTRACT(EPOCH FROM (ts - prev_ts)).
  • Senior signal: for a 30-day stream, run incrementally with a backfill window of (30min × 2) at the boundary to avoid splitting sessions across batches.

Pattern 3 — Funnels and conversion

WITH per_user AS (
  SELECT profile_id,
    MAX(CASE WHEN step_name = 'landing'      THEN 1 END) AS hit_landing,
    MAX(CASE WHEN step_name = 'plan_select'  THEN 1 END) AS hit_plan,
    MAX(CASE WHEN step_name = 'payment_info' THEN 1 END) AS hit_pay,
    MAX(CASE WHEN step_name = 'confirm'      THEN 1 END) AS hit_confirm
  FROM fact_signup_event
  GROUP BY profile_id
)
SELECT
  SUM(hit_landing)  AS landing_n,
  SUM(hit_plan)     AS plan_n,
  SUM(hit_pay)      AS pay_n,
  SUM(hit_confirm)  AS confirm_n,
  SUM(hit_plan)::FLOAT    / NULLIF(SUM(hit_landing), 0) AS conv_landing_to_plan,
  SUM(hit_pay)::FLOAT     / NULLIF(SUM(hit_plan),    0) AS conv_plan_to_pay,
  SUM(hit_confirm)::FLOAT / NULLIF(SUM(hit_pay),     0) AS conv_pay_to_confirm,
  SUM(hit_confirm)::FLOAT / NULLIF(SUM(hit_landing), 0) AS overall_conversion
FROM per_user;
  • Conditional aggregation (CASE inside aggregate) beats self-joining four times. Always preferred for funnels.
  • Strict vs inclusive: above is inclusive. For strict (must complete previous in order), add ts > landing_ts checks.
  • NULLIF on denominator prevents division-by-zero. Always.
  • Senior signal: real funnel has a per-step time window — "did they reach plan_select within 7 days of landing?" — via FIRST_VALUE per step.

Pattern 4 — Deduplication / data quality

WITH ranked AS (
  SELECT *,
    ROW_NUMBER() OVER (
      PARTITION BY event_id
      ORDER BY ingest_ts DESC
    ) AS rn
  FROM fact_play_event_raw
)
SELECT event_id, profile_id, ts, event_type, ingest_ts
FROM ranked WHERE rn = 1;
  • Why ROW_NUMBER over DISTINCT: DISTINCT collapses on all columns; if anything else differs, dupes survive. ROW_NUMBER picks one deterministically.
  • Why ingest_ts not event_ts: producer's clock may be wrong. Trust broker's ingest time for "which write wins."
  • Senior signal: make idempotent at write time — MERGE INTO target USING source ON event_id WHEN MATCHED AND source.ingest_ts > target.ingest_ts THEN UPDATE.

Pattern 5 — Cohort retention

WITH cohort AS (
  SELECT profile_id, DATE_TRUNC('week', signup_ts) AS cohort_week
  FROM dim_profile
),
activity AS (
  SELECT DISTINCT profile_id,
    DATE_TRUNC('week', session_start_ts) AS activity_week
  FROM fact_viewing_session
  WHERE qualified_view_flag = TRUE
),
sized AS (
  SELECT cohort_week, COUNT(DISTINCT profile_id) AS cohort_size
  FROM cohort GROUP BY 1
),
joined AS (
  SELECT c.cohort_week, c.profile_id, a.activity_week,
    DATEDIFF('week', c.cohort_week, a.activity_week) AS week_offset
  FROM cohort c
  LEFT JOIN activity a
    ON c.profile_id = a.profile_id
   AND a.activity_week >= c.cohort_week
)
SELECT j.cohort_week, j.week_offset,
  COUNT(DISTINCT j.profile_id) AS retained,
  s.cohort_size,
  COUNT(DISTINCT j.profile_id)::FLOAT / s.cohort_size AS retention_rate
FROM joined j JOIN sized s ON j.cohort_week = s.cohort_week
WHERE j.week_offset BETWEEN 0 AND 12
GROUP BY 1, 2, s.cohort_size
ORDER BY 1, 2;
  • Cohort definition is a choice. Calendar week vs rolling 7-day; first-event vs signup. State your choice.
  • LEFT JOIN keeps cohort users with zero activity — essential for the denominator.
  • Senior signal: materialize as mart_cohort_retention_daily partitioned by cohort_week, refresh trailing 12 weeks. Recomputing from raw on every dashboard load is the rookie move.

Pattern 6 — Running totals and pacing

WITH daily AS (
  SELECT title_id, DATE(session_start_ts) AS view_date,
    COUNT(*) AS daily_views
  FROM fact_viewing_session
  WHERE qualified_view_flag = TRUE
  GROUP BY title_id, DATE(session_start_ts)
),
cumulative AS (
  SELECT title_id, view_date, daily_views,
    SUM(daily_views) OVER (
      PARTITION BY title_id ORDER BY view_date
      ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS cumulative_views,
    SUM(daily_views) OVER (
      PARTITION BY title_id ORDER BY view_date
      ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING
    ) AS prev_cumulative
  FROM daily
)
SELECT title_id, view_date, daily_views, cumulative_views,
  CASE
    WHEN cumulative_views >= 1000000
     AND (prev_cumulative IS NULL OR prev_cumulative < 1000000)
    THEN TRUE ELSE FALSE
  END AS crossed_1m_today
FROM cumulative
ORDER BY title_id, view_date;
  • The ROWS BETWEEN ... PRECEDING trick gets "previous cumulative" without LAG — works even with date gaps.
  • Edge case to volunteer: if a title has no views on day N, densify with generate_series or a date spine before the cumulative — otherwise "crossed today" fires on the wrong day.

Pattern 7 — DAU / MAU and HyperLogLog

-- Exact, expensive:
SELECT DATE(session_start_ts) AS d, COUNT(DISTINCT profile_id) AS dau
FROM fact_viewing_session
WHERE session_start_ts >= CURRENT_DATE - 30
GROUP BY 1;

-- Approximate, cheap:
SELECT DATE(session_start_ts) AS d,
       APPROX_COUNT_DISTINCT(profile_id) AS dau_approx
FROM fact_viewing_session
WHERE session_start_ts >= CURRENT_DATE - 30
GROUP BY 1;

-- HLL sketches for rollup-friendly storage (Snowflake):
CREATE TABLE dau_sketches AS
SELECT DATE(session_start_ts) AS d,
       HLL_ACCUMULATE(profile_id) AS sketch
FROM fact_viewing_session GROUP BY 1;

-- Then MAU is a cheap merge of 30 sketches:
SELECT HLL_ESTIMATE(HLL_COMBINE(sketch)) AS mau_approx
FROM dau_sketches WHERE d >= CURRENT_DATE - 30;
  • Senior signal — name HyperLogLog sketches. Explain they're additive: weekly/monthly/yearly rollups become trivial sketch merges.
  • Typical accuracy: ~1.5% error at default precision. Acceptable for product analytics, not finance.
  • BigQuery: HLL_COUNT.INIT / MERGE / EXTRACT. Snowflake: HLL_ACCUMULATE / COMBINE / ESTIMATE.

Dialect cheatsheet — don't lose 2 minutes recalling syntax

OperationSnowflakeBigQuerySpark SQLPostgres
Date diff (sec)TIMESTAMPDIFF('second',a,b)TIMESTAMP_DIFF(b,a,SECOND)unix_timestamp(b)-unix_timestamp(a)EXTRACT(EPOCH FROM b-a)
Truncate to weekDATE_TRUNC('week',ts)DATE_TRUNC(ts,WEEK)date_trunc('week',ts)DATE_TRUNC('week',ts)
Date addDATEADD(day,7,d)DATE_ADD(d,INTERVAL 7 DAY)date_add(d,7)d + INTERVAL '7 day'
Approx distinctAPPROX_COUNT_DISTINCT(x)APPROX_COUNT_DISTINCT(x)approx_count_distinct(x)(extension)
Top-N shortcutQUALIFY ROW_NUMBER()=1QUALIFY ROW_NUMBER()=1QUALIFY (3.4+)(use CTE)
Array aggARRAY_AGG(x) WITHIN GROUP (...)ARRAY_AGG(x ORDER BY ...)collect_list(x)ARRAY_AGG(x ORDER BY ...)
String concatLISTAGG(x,',')STRING_AGG(x,',')concat_ws(',',collect_list(x))STRING_AGG(x,',')
Null-safe equalIS NOT DISTINCT FROMIS NOT DISTINCT FROM<=>IS NOT DISTINCT FROM
The rule. If you forget syntax, name the function family ("I'd use DATE_TRUNC semantics here") and write what you remember. Never freeze on syntax.

Six real-life SQL scenarios

SQL Q1 — Cross-device sessionization (Studio twist)

A user starts on phone, switches to TV. Should that count as one session?
WITH events AS (
  SELECT profile_id, device_id, ts,
    LAG(ts) OVER (PARTITION BY profile_id ORDER BY ts) AS prev_ts
  FROM fact_play_event
),
flagged AS (
  SELECT *, CASE WHEN prev_ts IS NULL
                  OR TIMESTAMPDIFF('second', prev_ts, ts) > 1800
                THEN 1 ELSE 0 END AS is_new_session
  FROM events
),
sessionized AS (
  SELECT *, SUM(is_new_session) OVER (PARTITION BY profile_id ORDER BY ts) AS session_num
  FROM flagged
)
SELECT profile_id, session_num,
  MIN(ts) AS session_start, MAX(ts) AS session_end,
  COUNT(*) AS event_count,
  COUNT(DISTINCT device_id) AS devices_in_session,
  CASE WHEN COUNT(DISTINCT device_id) > 1 THEN TRUE ELSE FALSE END AS is_handoff
FROM sessionized GROUP BY 1, 2;
  • Partitioning only on profile_id (not device) is what makes device handoff stay in one session.
  • devices_in_session is free senior touch — answers "what % of sessions involve handoff?" without a second query.
  • For 30-day batch: 60-min boundary buffer to avoid splitting sessions across daily partitions.

SQL Q2 — SCD2 point-in-time join (Studio/Finance)

Compute revenue per title as-of 2025-Q1, using the price plan active then, not today's.
SELECT t.title_id,
  SUM(s.watch_seconds * p.price_per_second_usd) AS revenue_usd
FROM fact_viewing_session s
JOIN dim_title t ON s.title_sk = t.title_sk
JOIN dim_pricing_plan p
  ON p.plan_id = t.plan_id
 AND s.session_start_ts >= p.effective_from
 AND s.session_start_ts <  COALESCE(p.effective_to, '9999-12-31')
WHERE s.session_start_ts >= '2025-01-01'
  AND s.session_start_ts <  '2025-04-01'
GROUP BY t.title_id;
  • Interval-overlap predicate on the dim is the SCD2 point-in-time idiom. WHERE p.is_current = TRUE would be wrong for historical compute.
  • COALESCE(effective_to, '9999-12-31') — sentinel for current rows. Some shops use NULL; ask which convention.

SQL Q3 — Tiered deduplication with quality preference

fact_play_event_raw has dupes from a flaky producer. Pick the "best" record — prefer non-null watch_seconds, then latest ingest.
SELECT *
FROM (
  SELECT *,
    ROW_NUMBER() OVER (
      PARTITION BY event_id
      ORDER BY
        CASE WHEN watch_seconds IS NOT NULL THEN 0 ELSE 1 END,
        ingest_ts DESC
    ) AS rn
  FROM fact_play_event_raw
)
WHERE rn = 1;
  • Multi-tier ORDER BY — quality first, recency second. Portable trick when NULLS LAST is unavailable.
  • Push into the MERGE at write time; daily dedup scan is the rookie option.

SQL Q4 — Density-aware running total with date spine

Cumulative qualified views per title since release. Flag the day cumulative crosses 1M. Some days have zero views.
WITH dates AS (
  SELECT DATEADD('day', seq4(), '2025-01-01') AS d
  FROM TABLE(GENERATOR(ROWCOUNT => 365))
),
title_release AS (
  SELECT title_id, MIN(DATE(session_start_ts)) AS release_date
  FROM fact_viewing_session GROUP BY title_id
),
spine AS (
  SELECT t.title_id, d.d
  FROM title_release t CROSS JOIN dates d
  WHERE d.d >= t.release_date
),
daily AS (
  SELECT s.title_id, s.d,
    COALESCE(SUM(CASE WHEN f.qualified_view_flag THEN 1 END), 0) AS daily_views
  FROM spine s
  LEFT JOIN fact_viewing_session f
    ON f.title_id = s.title_id AND DATE(f.session_start_ts) = s.d
  GROUP BY 1, 2
)
SELECT title_id, d, daily_views,
  SUM(daily_views) OVER (PARTITION BY title_id ORDER BY d) AS cumulative,
  CASE
    WHEN SUM(daily_views) OVER (PARTITION BY title_id ORDER BY d) >= 1000000
     AND COALESCE(SUM(daily_views) OVER (
           PARTITION BY title_id ORDER BY d
           ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING), 0) < 1000000
    THEN TRUE ELSE FALSE
  END AS crossed_1m_today
FROM daily ORDER BY title_id, d;
  • Date spine prevents the running total from being wrong on zero-view days.
  • ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING gives previous-cumulative without LAG, robust to gaps.

SQL Q5 — Streaming SQL (Flink tumbling window)

Compute a 5-minute tumbling window of qualified views per title from a Kafka topic.
-- Flink SQL
CREATE TABLE play_events (
  profile_id    BIGINT,
  title_id      BIGINT,
  watch_seconds INT,
  event_ts      TIMESTAMP(3),
  WATERMARK FOR event_ts AS event_ts - INTERVAL '60' SECOND
) WITH ('connector' = 'kafka', 'topic' = 'play_events_v1', ...);

SELECT title_id,
  TUMBLE_START(event_ts, INTERVAL '5' MINUTE) AS window_start,
  TUMBLE_END  (event_ts, INTERVAL '5' MINUTE) AS window_end,
  COUNT(*)                   AS qualified_events,
  COUNT(DISTINCT profile_id) AS unique_viewers
FROM play_events
WHERE watch_seconds >= 120
GROUP BY title_id, TUMBLE(event_ts, INTERVAL '5' MINUTE);
  • Watermark of 60s — promise that events older than (max_seen_ts − 60s) won't arrive.
  • Tumbling for non-overlapping 5-min buckets. Hopping if consumer wanted moving 5-min average updated every minute.
  • Late events past watermark go to a side output → DLQ topic, not /dev/null.
  • State backend: RocksDB — COUNT DISTINCT profile_id state grows; spills to disk.

The eight SQL traps

TrapCounter-move
Forgetting tiebreaker in ROW_NUMBERAlways add deterministic secondary sort.
COUNT(DISTINCT) on petabyte factMention HLL; use APPROX_COUNT_DISTINCT.
Division without NULLIFnumerator / NULLIF(denominator, 0) always.
is_current = TRUE for historical computeInterval-overlap predicate on SCD2 instead.
DISTINCT to dedupeROW_NUMBER + WHERE rn=1 — DISTINCT collapses on all columns.
UNION instead of UNION ALLUNION dedupes (hidden DISTINCT). UNION ALL by default.
LEFT JOIN with WHERE right.x = ...Predicate kills the LEFT JOIN — move to ON, or use right.x IS NULL OR ....
Cumulative running total without date spineDensify dates first; otherwise gaps fire wrong thresholds.
"Pattern-wise this is a [top-N / gaps-and-islands / funnel / dedup / cohort / running total / approximate count]. The shape is [one sentence: window function + partition + frame]. I'll structure it as [N CTEs]. Edge cases: [NULLs / ties / empty groups / div-by-zero]. Performance: [partition pruning / clustering / materialization]. In production I'd [materialize / merge at write / add a date spine]."
· · ·
§ 04 — Programming

Python — fluency over cleverness.

The recruiter explicitly said: "we care more about coding fluency than perfect solutioning." A working O(n²) you can explain beats a clever O(n log n) you can't articulate. Here are the patterns Netflix actually tests.

CodeSignal mechanics. Hidden tests; you see pass/fail per test, not the inputs. Always start brute-force, pass first 2–3 tests, then optimize. A partial solution scores higher than a perfect one you didn't finish. Keep one eye on the timer.

The five reflexes — when to reach for which

NeedReach forWhy
Group + aggregatecollections.defaultdict(int / list / set)No KeyError; clean code.
Top-Kheapq.nlargest(k, ...) or manual min-heap of size kO(n log k); manual heap saves memory.
Sliding window over timecollections.deque + running totals dictO(1) append/popleft.
SessionizationSort within group, walk pairs, track open sessionSingle pass after sort.
O(1) cachecollections.OrderedDict + move_to_end / popitem(last=False)Backed by DLL+hashmap.
K-way mergeheapq.merge(*iterables)Lazy generator; bounded memory.
Countingcollections.CounterBuilt-in most_common(k).
Probabilistic / approxReservoir sample, MinHash, Count-Min SketchSub-linear memory.

Six real-life Python scenarios

Python P1 — Sessionization (the warmup)

Group [(profile_id, ts), ...] into sessions where consecutive same-profile events are within gap_sec. Return [(profile_id, start, end, count)].
from collections import defaultdict

def sessionize(events, gap=1800):
    by_user = defaultdict(list)
    for uid, ts in events:
        by_user[uid].append(ts)

    sessions = []
    for uid, ts_list in by_user.items():
        ts_list.sort()
        start = end = ts_list[0]
        count = 1
        for ts in ts_list[1:]:
            if ts - end <= gap:
                end, count = ts, count + 1
            else:
                sessions.append((uid, start, end, count))
                start = end = ts
                count = 1
        sessions.append((uid, start, end, count))
    return sessions
  • O(n log n) total — dominated by per-user sort. Pre-sorted (Kafka): single pass O(n).
  • Memory O(n) — at scale, partition by profile_id across workers.
  • Edge: two events with same ts → same session. State the assumption.

Python P2 — Top-K from a stream (the Netflix favorite)

Stream of (ts, title_id, watch_seconds). Return top-K most-watched titles in the last hour. Implement add_event(ev) and top_k(k).
from collections import defaultdict, deque
import heapq

class TopKWatched:
    def __init__(self, window_sec=3600):
        self.window = window_sec
        self.events = deque()                 # (ts, title, watch)
        self.totals = defaultdict(int)        # title -> watch in window

    def add_event(self, ts, title, watch):
        self.events.append((ts, title, watch))
        self.totals[title] += watch
        self._evict(ts)

    def _evict(self, now):
        cutoff = now - self.window
        while self.events and self.events[0][0] < cutoff:
            _, t, w = self.events.popleft()
            self.totals[t] -= w
            if self.totals[t] <= 0:
                del self.totals[t]

    def top_k(self, k):
        return heapq.nlargest(k, self.totals.items(), key=lambda x: x[1])
  • add_event O(1) amortized, eviction amortized O(1). top_k O(n log k) — called rarely.
  • If top_k is hot, switch to max-heap with lazy deletion — push (-total, title) on update; pop stale on read.
  • Distributed: partition by title_id, each worker maintains local top-K, then merge — top-K is associative for sums.
  • Out-of-order events: _evict uses last-seen-ts; for true event-time, use a watermark.

Python P3 — Watermark sessionizer (out-of-order stream)

Stream of (ts, profile, watch_sec), events arrive out-of-order. Emit closed sessions only when watermark passes.
from collections import defaultdict
import heapq

class WatermarkSessionizer:
    def __init__(self, gap=1800, lag=300):
        self.gap = gap
        self.lag = lag
        self.buf = defaultdict(list)
        self.max_ts = 0

    def add(self, ts, profile, watch):
        heapq.heappush(self.buf[profile], (ts, watch))
        self.max_ts = max(self.max_ts, ts)

    def poll(self):
        watermark = self.max_ts - self.lag
        closed = []
        for pid, heap in list(self.buf.items()):
            start = end = total = None
            while heap and heap[0][0] < watermark:
                ts, w = heapq.heappop(heap)
                if start is None or ts - end > self.gap:
                    if start is not None:
                        closed.append((pid, start, end, total))
                    start, end, total = ts, ts, w
                else:
                    end, total = ts, total + w
            if start is not None:
                heapq.heappush(heap, (start, total))
            if not heap:
                del self.buf[pid]
        return closed
  • Watermark = max_seen − lag. Close sessions whose end < watermark; everything more recent stays buffered.
  • Exactly Flink's keyed state with event-time. Production: state backed by RocksDB, checkpointed.

Python P4 — Co-occurrence at scale (the trap)

Given [(user_id, title_id), ...], find pairs of users who watched ≥3 titles in common.
from collections import defaultdict
from itertools import combinations

def overlap_pairs(views, threshold=3):
    title_to_users = defaultdict(set)
    for uid, tid in views:
        title_to_users[tid].add(uid)

    pair_counts = defaultdict(int)
    for users in title_to_users.values():
        for u1, u2 in combinations(sorted(users), 2):
            pair_counts[(u1, u2)] += 1

    return [pair for pair, c in pair_counts.items() if c >= threshold]
  • Volunteer the complexity warning: a popular title with M users → M² pairs. Intractable at Netflix scale.
  • Better: MinHash + LSH (locality-sensitive hashing) — approximate set intersection without enumerating pairs. O(n) instead of O(n²).
  • Alternative: cap users per title (sample), or only consider pairs sharing rare titles (more signal anyway).

Python P5 — Reservoir sampling

Sample K events uniformly from a stream of unknown length, single pass, O(K) memory.
import random

def reservoir_sample(stream, k):
    reservoir = []
    for i, item in enumerate(stream):
        if i < k:
            reservoir.append(item)
        else:
            j = random.randint(0, i)
            if j < k:
                reservoir[j] = item
    return reservoir
  • Each item has probability k/n of being kept after seeing n items — provable by induction.
  • Senior signal: mention weighted reservoir (A-Res) when items have unequal weights.

Python P6 — Causal event pairing

Two streams: play_started and play_ended. Pair them by (profile, title). Some started events never get an ended (crashes/timeouts).
def pair_events(events, timeout_sec=14400):
    open_starts = {}
    pairs, orphans = [], []

    for ev in sorted(events, key=lambda e: e['ts']):
        key = (ev['profile_id'], ev['title_id'])
        if ev['type'] == 'play_started':
            if key in open_starts:
                orphans.append(open_starts[key])
            open_starts[key] = (ev['session_id'], ev['ts'])
        elif ev['type'] == 'play_ended':
            if key in open_starts:
                sid, sts = open_starts.pop(key)
                if ev['ts'] - sts <= timeout_sec:
                    pairs.append((sid, sts, ev['ts']))
                else:
                    orphans.append((sid, sts))
    orphans.extend(open_starts.values())
    return pairs, orphans
  • Volunteer the orphan output — interviewer was about to ask "what about events without partners?".
  • Sort once: O(n log n). Pre-sorted Kafka: heapq.merge for O(n) merged iteration.

Three high-leverage patterns to memorize verbatim

Type these from a blank file every drill day. By rep five, your fingers know them.

(a) Sessionization — see Python P1 above. (b) Top-K with sliding window — see Python P2. (c) Counter + sliding window over time:

from collections import Counter, deque

def windowed_counts(events, window_sec):
    """events: iterable of (ts, key). Yields (ts, Counter) at each ts."""
    buf = deque()
    counts = Counter()
    for ts, key in events:
        buf.append((ts, key))
        counts[key] += 1
        cutoff = ts - window_sec
        while buf and buf[0][0] < cutoff:
            _, old = buf.popleft()
            counts[old] -= 1
            if counts[old] == 0:
                del counts[old]
        yield ts, counts.copy()

This third pattern shows up in ~1 of 3 streaming-flavored CodeSignal problems.

The eight Python traps

TrapCounter-move
list.pop(0) on big listUse dequepopleft() is O(1).
Mutable default arg (def f(x=[]))def f(x=None): x = x or []
Sort with cmp_to_keyUse tuple keys: key=lambda x: (-x.score, x.name).
Recursion deeper than 1000Convert to iteration with explicit stack.
for k in dict: del dict[k]Iterate list(dict.keys()) first.
== for floatsmath.isclose(a, b, rel_tol=1e-9)
Forgetting enumeratefor i, x in enumerate(xs)
Reading whole file in memorywith open(...) as f: for line in f: — generator.

Coding under pressure — the script

  1. Restate the problem. "So I'm getting a list of tuples and need to return…" 30 sec.
  2. Walk through one example by hand. Forces edge case confirmation.
  3. Brute-force first. "I'll start with the obvious O(n²), then optimize." Get something passing.
  4. Optimize with reasoning. "The bottleneck is the inner loop. If I precompute a dict, that becomes O(1)…"
  5. Test mentally. Run your code on the example. Catch off-by-ones before the timer does.
  6. Discuss complexity. Time and space, both. State explicitly even if not asked.
"This is a [grouping / streaming / top-K / sessionization / pairing] problem. I'll reach for [defaultdict / heap / deque / OrderedDict] because [O(1) access / sorted by frequency / FIFO / LRU semantics]. My first pass is [brute-force shape], O([n²]) — I'll get it passing, then optimize the [inner loop / sort step] to O(n log n). Edge cases: empty input, single element, ties, duplicate timestamps. Memory: O([k / n / 1]). For Netflix scale, the distributed version partitions by [user / title]; per-partition this code runs as-is."
· · ·
§ 05 — Streaming · Data In Flight

Streaming concepts — the vocabulary that signals seniority.

Netflix runs Keystone (a massive Kafka pipeline) and uses Flink heavily. Even if you don't write a line of Flink, knowing this vocabulary is what separates a senior DE from a SQL specialist.

The vocabulary you must say without hesitation

TermOne-liner
Event-time"When it happened — client clock embedded in the event."
Processing-time"When the pipeline saw it. Fallback for events without reliable timestamps."
Ingestion-time"When the broker received it. Compromise — server clock, not user clock."
Watermark"Promise: 'I have seen all events with timestamp ≤ W.' Lets us close windows."
Allowed lateness"Tolerance budget after watermark for late firings before window is dropped."
Tumbling window"Non-overlapping fixed buckets — every 5 min, distinct."
Hopping (sliding)"Overlapping fixed buckets — every 1 min, look back 5 min."
Session window"Gap-defined dynamic boundaries — sessionization."
Exactly-once"Each event affects state once, even on failure. Flink: checkpoint + 2PC sink."
At-least-once"Events may duplicate on retry. Cheap. Requires idempotent consumers."
Backpressure"Downstream slower than upstream — system pauses upstream automatically."
Checkpoint barrier"Flink's snapshot trigger — flows through DAG; each operator snapshots state."
State backend"RocksDB (disk, big state) vs heap (small, fast)."
Savepoint"Manually-triggered checkpoint for upgrades and version migration."
Keyed state"Partitioned state — one key, one worker. Sessionization needs it."
Schema registry"Centralized schema validation — backward / forward / full compatibility."
DLQ"Dead-letter queue — bad events go here, not /dev/null."
Lambda architecture"Separate batch + stream paths, reconcile in serving. Two codebases."
Kappa architecture"One streaming pipeline. Backfills via log replay. Modern default."
Idempotent sink"Re-applying the same write produces the same result. MERGE on natural key."
Skew"One key dominates traffic; one worker pegged. Salt the key, sub-partition."

Trade-offs you must articulate

DecisionThe line you say
Event-time vs processing-time"Event-time for analytics — otherwise mobile reconnects look like spikes. Processing-time only when client clocks are unreliable."
Exactly-once vs at-least-once"Exactly-once costs latency and storage. At-least-once + idempotent writes is the common shape — same outcome, half the cost."
Tumbling vs hopping vs session"Tumbling for periodic reports. Hopping for moving averages. Session for user-defined boundaries."
Lambda vs Kappa"Lambda is two codebases that drift. Kappa is one path with replay-from-log. Modern lakehouse made Kappa the default."
RocksDB vs heap state"RocksDB for large state — spills to disk. Heap for small state and tight latency. Default RocksDB at scale."
Watermark lag (tight vs loose)"Tight = fresh, late events drop. Loose = output delayed, more captured. Set to p99 lateness + safety buffer."
DLQ vs drop"DLQ always — inspecting bad events is how you find producer bugs."

The 8-step play-event walkthrough — memorize verbatim

Memorize this and rehearse out loud till you can deliver in 90 seconds:

  1. Client SDK emits the event from device — TV, phone, browser. Embeds event_ts (client clock), profile_id, title_id, device_id, watch_seconds. Buffers locally on bad network.
  2. Regional collector (Keystone) — Kafka cluster at the edge. Producer SDK uses schema-registry client to validate before publish.
  3. Schema validation + DLQ. Bad events → DLQ topic, not dropped. DLQ is monitored.
  4. Multi-tier Kafka topics: play_events_rawplay_events_validatedplay_events_enriched. Each step is a Flink job adding metadata.
  5. Two consumers fork: real-time (recommendations, alerting — sub-second SLA) and batch (S3/Iceberg sink for analytics).
  6. S3/Iceberg sink — partitioned by event_date/hour. Iceberg gives ACID + time-travel + multi-engine read. Files compacted nightly.
  7. dbt / Spark builds the dimensional layer (fact_viewing_session) on a daily schedule. 48h backfill window for late events.
  8. BI (Tableau/Superset) queries the modeled layer. Recommendation features re-publish to a feature store (Feast/EVCache) for sub-second model serving.

The credibility-test phrase to drop mid-narration: "…we use Iceberg over Delta because of multi-engine support — Trino and Spark both read it natively."

Eight scenarios — the questions actually asked

S1 — Event-time vs processing-time

A: "Event-time is when the user pressed play — the client clock embedded in the event. Processing-time is when our pipeline received it. The gap is small on good networks, hours on mobile reconnects. Production analytics always uses event-time — otherwise a deferred sync from someone in airplane mode looks like a traffic spike. Choosing event-time forces us to handle out-of-order arrivals, which is what watermarks are for."

S2 — Watermarks

A: "A watermark is the pipeline's promise: 'I have seen all events with timestamp ≤ W.' Choosing it is a trade-off — too tight, late events drop; too loose, output is delayed. Standard heuristic: max-seen-ts minus the 99th percentile of observed lateness, plus a safety buffer. For Netflix mobile traffic where lateness p99 is around 30s, I'd start with a 60s lag. The metric to monitor is late_event_count per topic per hour."

S3 — Exactly-once

A: "Each event affects state exactly once, even on failure. Flink achieves it with checkpoint barriers — periodic markers in input streams. When a barrier reaches an operator, it snapshots state to S3. Sinks use two-phase commit — pre-commit on checkpoint, commit on next checkpoint. Cost: latency equal to checkpoint interval, plus snapshot CPU. Most Netflix pipelines run at-least-once + idempotent sinks — same observable behavior, half the cost. Exactly-once is reserved for billing where retries change the answer."

S4 — Lambda vs Kappa

A: "Lambda runs separate batch and stream pipelines, reconciles in serving — gives correctness from batch and freshness from stream. Cost: two codebases, drift. Kappa runs only streaming, replays log for backfills — single codebase, but you must own the replay tooling. I'd pick Kappa today. Iceberg/Delta gave us reliable replay-from-log on commodity object store, which used to be the missing piece."

S5 — Schema evolution

A: "Avro or Protobuf, plus a centralized schema registry. Producers register a schema; consumers fetch by ID embedded in messages. Compatibility modes: backward (consumer reads old + new), forward (old consumer reads new producer), full (both). Default to backward. Without this, a renamed field on the producer side silently breaks every consumer at 3 AM. CI test is a registry pre-commit hook. Breaking changes go to a new topic version (play_events_v2) with a sunset window."

S6 — Backpressure

A: "Downstream operator slower than upstream. Without handling, upstream buffer overflows — OOM or data loss. Flink handles it natively: when a downstream task can't keep up, network buffers fill, upstream naturally slows. Diagnostic is the backpressure indicator in the Flink UI — red on a task means it's the bottleneck. Fixes: scale that operator, find skew (one key dominating — salt or sub-partition), or simplify the operator."

S7 — Tumbling vs hopping vs session

A: "Tumbling — fixed, non-overlapping. Use for periodic reports — hourly DAU. Hopping — fixed, overlapping. Use for moving averages — '5-minute trending titles, updated every minute'. Session — gap-defined dynamic. Use for user behavior — sessionization, binge sequences. The mistake is using tumbling for trending — gives stepwise jumps; hopping smooths them."

S8 — The 8-step narrative

Cover when asked: "Walk me through how a play event travels from a user's TV to the executive dashboard." Use the 8 steps above. Memorize; deliver in 90 seconds.

Five operational war-stories to have ready

ProbeShape of the answer
Pipeline that broke at 3 AMDAU dropped 40% → watermark stalled because one partition's producer was clock-skewed → fix (split watermark per partition) → result (lateness budget cut from 5min to 60s).
Schema-evolution incidentProducer renamed field; backward-compat missed it because rename = remove + add → consumers crashed → fix (registry pre-commit hook) → outcome (zero schema regressions in 18 months).
Duplicate-events problemAt-least-once + non-idempotent sink → revenue double-counted by 0.3% → fix (MERGE on event_id at sink) → outcome (idempotency invariant added to pipeline contract).
Skew problemOne title (global tentpole) had 100× the events → one Flink task pegged → fix (salt the key with title_id || hash(profile_id) % 16, two-stage aggregation) → outcome (latency p99 cut from 12s to 1s).
Checkpoint-induced latency spikeCheckpoint interval 1 min, RocksDB state 200 GB per task → 30-second pause every minute → fix (incremental checkpoints + smaller state via TTL) → outcome (p99 latency from 30s to 2s).

Six streaming traps

TrapCounter-move
Watermark per topic, not per partitionPer-partition watermarks. One slow partition shouldn't stall the entire job.
Exactly-once everywhere by defaultAt-least-once + idempotent sink is cheaper and observably identical for analytics.
Dropping bad eventsDLQ topic. Always inspectable.
Sessionization in unkeyed stateKeyed state by profile_id. Otherwise sessions cross workers and break.
Single Kafka topic for raw + enrichedMulti-tier topics (raw → validated → enriched). Lets you replay only the failed stage.
Schema changes without registryRegistry + CI hook. Without it, breaking changes hit prod silently.

Ten Netflix-system names worth knowing

SystemWhat it is
KeystoneNetflix's Kafka pipeline backbone.
MantisReal-time stream processing platform (older — partly superseded by Flink).
IcebergTable format for the lakehouse — Netflix originated it.
AtlasTime-series telemetry / metrics store.
GenieJob orchestration / submission service for Spark/Hive/Presto.
MetaflowML workflow tool (Netflix-originated).
MaestroDAG scheduler (replaced Meson).
EVCacheMemcached-based distributed cache.
DBLogChange-data-capture framework.
BulldozerData movement / S3-to-online-store loader.
"This is an event-time problem — the relevant clock is the client's, not ours. I'd model it as a Flink job consuming from Kafka with a watermark of [60s] and a [tumbling / session] window. State backed by [RocksDB] because [size]. Sink is [at-least-once + idempotent MERGE on event_id] because exactly-once buys nothing here. Failure modes I'd plan for: late events past watermark go to a DLQ, schema changes gated by the registry, and per-partition watermarks so one slow producer doesn't stall the job. The unlock for the business is [sub-second freshness for recommendations / hourly accurate accounting for finance]."
· · ·
§ 06 — Behavioral & Deep-Dive Bank

The technical deep-dive — questions to rehearse.

After the coding section, expect 15–20 minutes of "tell me about a time you…" and "how would you design…". Have polished answers. Use STAR (Situation, Task, Action, Result) but compress to 90 seconds each.

From your resume — likely probes

ProbeShape of your answer (90 sec)
Hard data-quality problemSymptom (row counts off 0.3%) → diagnosis (late-arriving events from misconfigured collector) → fix (watermark adjustment + backfill window) → result (SLA met, alert added).
Pipeline you owned end-to-endPipeline name, scale (rows/day, latency SLA), tech stack, what went wrong, how you fixed it.
Model you'd redesign todaySelf-awareness probe. Pick one — wrong grain, over-normalized, missed SCD2 — and what you learned.
Handle PII at scaleTokenization, column-level encryption, separate access tiers, audit logging, GDPR delete cascades. Lean in.
Disagreed and committedNetflix value. Story where you pushed back, lost, executed anyway, captured what you learned.
Influence without authorityCross-team migration; stakeholder you don't manage; how you built the case.

The 90-second STAR template

  1. Situation (15s): "We had a [type of] pipeline running [scale]; the issue was [observable symptom]."
  2. Task (15s): "I needed to [decision / outcome you owned]."
  3. Action (45s): "I [diagnosed by X], decided to [Y because Z trade-off], implemented by [details]."
  4. Result (15s): "[measurable outcome] within [timeframe]; [what changed structurally]."
· · ·
§ 07 — System-Design Lite

Designs to rehearse.

D1 — Real-time recommendation pipeline

Kafka events → Flink for feature aggregation → feature store (Feast/Redis) → model serving (Triton/TF-Serving). Discuss feature freshness SLA (sub-second) vs. batch features (daily). Cold-start handling (use content embeddings + popularity priors for new users).

D2 — Data quality framework

Layered checks: schema (registry), volume (row count vs 7-day average ± stddev), distribution (KS test on key columns), referential integrity (FK orphans). Write to a monitoring topic; route critical failures to PagerDuty. Each check has a severity (block downstream / alert / log).

D3 — GDPR delete-on-request

Token mapping service: replace user_id with token at ingest. On delete, invalidate the token mapping — events become un-rejoinable. Plus a propagation queue for downstream materialized views. SLA: 30 days. For finance/tax (7-year retention), use anonymization (replace name, keep transaction).

D4 — A/B experimentation infra

Assignment service (sticky bucketing on profile_id hash), exposure logging (every served variant logged), metric pipeline (joins exposures to outcome facts), stats engine (CUPED + sequential testing). SRM monitor as a first-class citizen — observed split deviation >0.5% from expected pages on-call.

· · ·
§ 08 — Day-Of Playbook

The interview-day playbook.

The morning

  • Don't cram new material. Re-read your one-page cheat sheet (seven SQL patterns, modeling five-move sequence, streaming vocabulary).
  • Test setup 30 min early. Camera, mic, screen-share, CodeSignal login, scratch paper, water. Have a second laptop or phone ready.
  • Have the recruiter's contact open in another tab. If something breaks, message them immediately.

In the round — micro-behaviors

  • First 30 sec of every problem: ask 1–2 clarifying questions. Even if the prompt seems complete. Muscle memory you should drill.
  • Narrate everything. "I'm going to start with a CTE that aggregates per user. Then I'll rank within user. Then filter." Silent for 60+ seconds = lost signal.
  • If stuck: say so. "I'm trying to remember the exact syntax for the date function in Snowflake — can I write it as date_trunc and we move on?" The interviewer will say yes 100% of the time.
  • If you finish early: don't sit silently. Walk through edge cases. "What if input is empty? What if duplicate timestamps?" Free senior signal.
  • Last 5 minutes: always reserved for your questions. Have 3 ready.

If something goes wrong

  • Wrong answer realized 2 minutes later: say so immediately. "Actually, I want to revise — the LEFT JOIN should be on cohort, not activity, otherwise we drop users with zero activity." Strong signal.
  • You don't know the answer at all: don't fake. "I haven't worked with Iceberg specifically; I've used Delta. Can I tell you how I'd approach it from first principles?"
  • Blank on syntax: "In standard SQL, this is X. I'm forgetting the exact Snowflake function name — would you remind me?" Asking is fine. Stalling is not.

Questions to ask them

Pick 2–3 per round; tailor by the interviewer's role. Your questions are scored — they reveal what you care about.

For an engineering interviewer

  • "What does your team's batch-vs-stream split look like? Are most pipelines moving toward Kappa, or do you still run dual-mode?"
  • "How does the team handle schema evolution across producers and consumers — registry-driven, or convention?"
  • "What's the most painful operational issue your team has dealt with in the last quarter, and how did the team grow from it?"
  • "How is data quality scored at Netflix? Is there a centralized framework, or does each team build its own?"

For a manager / hiring manager

  • "What does success look like in the first 90 days for someone in this role?"
  • "How does the team balance platform work versus product-aligned analytics?"
  • "How autonomous are individual engineers in choosing tools and patterns?"
  • "What's the team's current biggest unsolved problem, and what's blocking progress?"

About culture (Netflix specifically)

  • "How does the 'context, not control' principle play out day-to-day on your team?"
  • "How do you handle the tension between high-performing individuals and team cohesion in practice?"
  • "What kinds of mistakes are tolerated, and which aren't?"

One-page cheat sheet

Modeling — the five moves

MoveWhat you say / do
1. Clarify3–5 questions before drawing. Use cases, SLA, retention, key stability, time zone.
2. GrainOne sentence per fact: "One row per X per Y per Z." Out loud.
3. Conceptual → physicalEntities first. Then partitioning, clustering, SCD strategy.
4. SCDs & timeType 1 (overwrite), Type 2 (versioned rows), Type 6 (hybrid). Justify.
5. Stress testVolunteer 2 failure modes. Late data, GDPR, schema evolution, hot partitions.

SQL — the seven patterns

PatternShape
Top-N per groupROW_NUMBER() OVER (PARTITION BY g ORDER BY metric DESC)WHERE rn ≤ N
Gaps & islandsLAG → flag boundary → cumulative SUM = session_id → GROUP BY
FunnelMAX(CASE WHEN step='X' THEN 1 END) per user → ratios with NULLIF
DedupROW_NUMBER PARTITION BY natural_key ORDER BY ingest_ts DESC = 1
Cohort retentioncohort_week × activity_week LEFT JOIN, weeks_since = DATEDIFF
Running totalSUM() OVER (ORDER BY ... ROWS UNBOUNDED PRECEDING)
HLL / approxAPPROX_COUNT_DISTINCT — mention it for scale

Python — the five reflexes

NeedReach for
Group + aggregatecollections.defaultdict(int / list / set)
Top-Kheapq.nlargest, or manual min-heap of size K
Sliding windowcollections.deque + running totals dict
SessionizationSort within group, walk pairs, track open session
O(1) cachecollections.OrderedDict, move_to_end / popitem(last=False)

Studio/Content vocabulary

TermMeaning
SlatePlanned set of releases for a period
GreenlightFormal approval to begin production
TentpoleHigh-budget flagship release
P&APrints & Advertising — marketing spend
ResidualsOngoing payments to talent post-release
MFN clauseMost-favored-nation: my terms match the best granted to anyone else
AmortizationSpreading content cost over expected useful life
License windowPeriod during which a title is available in a territory
· · ·
§ 09 — 5-Day Drill Pack

The five days before the conversation.

~2.5 hours per day. Not negotiable: do the timed drills. The point isn't to find the answer — it's to get used to the clock running.

  1. Day 1 · Foundation · Modeling (~2.5 hrs)
    • 30 min · read — Re-read the five-move sequence (§02). Practice saying the moves out loud. Sticky note next to monitor.
    • 60 min · drill — Modeling drills #1 and #2 (timed, 25 min each): Production lifecycle + Talent & royalties.
    • 30 min · vocabulary — Studio/Content vocabulary. Look up any term you can't define crisply (P&A, MFN, residuals, slate, tentpole).
    • 30 min · reflect — Did you ask clarifying questions before drawing? State grain explicitly? Volunteer 2 failure modes?
  2. Day 2 · SQL · Window functions & gaps/islands (~2.5 hrs)
    • 45 min · drill — SQL drills #1, #2, #3 (timed, 15 min each). Solve at SQL playground. Don't read solutions — type them.
    • 30 min · pattern reps — Type the gaps-and-islands template (LAG → flag → cumulative SUM) three times from memory.
    • 45 min · drill — SQL drills #4 and #5 (timed, 20 min each). Multi-CTE, finance-flavored. Talk out loud while solving.
    • 30 min · read — Patterns 1, 2, 6. Note the QUALIFY shortcut (Snowflake).
  3. Day 3 · Python · CodeSignal pace (~2.5 hrs)
    • 30 min · warm-up — Type from memory: (a) sessionization, (b) top-K with heap, (c) sliding-window aggregator.
    • 75 min · drill — Python drills #1, #2, #3 (timed, 25 min each). Fresh editor — closer to CodeSignal feel. Narrate while coding.
    • 30 min · solve again, optimize — Take the slowest of three; rewrite for better complexity. Practice the verbal arc: "First pass O(n²); I see I can use a hash map for O(n)…"
    • 15 min · reflect — Where did the clock catch you? Off-by-ones? Edge cases? defaultdict(int) recall?
  4. Day 4 · Streaming · Deep-dive prep (~2.5 hrs)
    • 45 min · drill — Streaming Q&A (§05). Answer each on a 90-sec timer. Then read the model answer.
    • 45 min · mock — Trace a play event from device to dashboard. 8 stages, 30 sec each. Repeat 3 times until it flows.
    • 30 min · resume rehearsal — Pick three projects. Write 90-sec STAR for each. Rehearse out loud.
    • 30 min · catch-up — Re-do any drill from days 1–3 you didn't finish.
  5. Day 5 · Integration · Calm (~2 hrs)
    • 30 min · one-page review — Read the cheat sheet twice. Don't drill new material. Trust the reps.
    • 30 min · one mock end-to-end — Pick one modeling + one SQL + one Python. 15 min each. Light pace.
    • 30 min · logistics — Test camera, mic, screen-share. Clear desktop. Water. Recruiter's number open.
    • 30 min · the 3 questions — Pick your 3 questions per interviewer. Write on paper. Different for engineer vs manager.
    • Then · stop. No drills the night before. Walk, eat, sleep. The reps are already in.
The single biggest signal. Narrate. Constantly. "I'm using a CTE here because the alternative is repeating this aggregation." "I'm picking ROW_NUMBER over RANK because ties shouldn't both qualify." Your interviewer cannot give you a senior score for code they only saw appear in silence.

Interview prep notebook · built for the conversation, not the quiz · go well.

· · ·
▸ Interview Studio · Design · Senior DE Prep ↓ PDF ← All Design topics Practice Q&A →