« All Roles

Principal / Distinguished Data Infrastructure Engineer — Curriculum

Target Role: Principal / Distinguished Data Infrastructure Engineer — Real-Time Distributed Data Platform, Streaming Systems, and Lakehouse Architecture (the reference JD — jd.md)

Also prepares you for:

  • Netflix / Uber / Stripe / Databricks / Confluent — Staff/Principal Data Platform or Streaming Infrastructure Engineer
  • Amazon / AWS — Principal Engineer, EMR / Kinesis / Glue / Lake Formation
  • Any enterprise building a petabyte-scale, multi-region, real-time + batch data platform that hundreds or thousands of engineers depend on

Duration: 34 weeks core (≈8 months) — extendable to 12+ months with full capstones Seniority Target: Principal / Distinguished IC (the person who is the escalation point for the hardest failures and defines platform standards for the org) Goal: Make you the engineer who can architect the data infrastructure layer at the system level and debug it at the byte, partition, checkpoint, offset, file, and query-plan level — across streaming, batch, lakehouse, serving, governance, and the Scala/JVM platform code that ties it together.


Why This Curriculum Exists

This is not a "write ETL jobs" role and this is not a "learn one tool" course. The JD is explicit: you build the data infrastructure layer itself — the streaming substrate, the lakehouse architecture, the schema-governance system, the reliability controls, the developer SDKs, the observability, and the operational playbooks that hundreds or thousands of engineers, analysts, data scientists, and product teams stand on.

Most engineers specialise in one or two of: distributed systems, stream processing, batch processing, cloud infrastructure, functional programming, data modeling, query optimisation, data reliability, schema evolution, storage formats, operability, security, governance, cost, and developer-platform design. This role requires practical fluency across all of them — and, harder, the judgment to make the right tradeoff when they conflict (low latency vs low cost; exactly-once vs operational simplicity; flexible schemas vs safe contracts; self-service vs governance).

So this track is built around two non-negotiables:

  1. Every technology in the JD gets implemented, not just described. Watermarks, exactly-once sinks, Parquet row-group skipping, Iceberg snapshot isolation, Spark's shuffle, Cassandra's tombstone math, a Cats-style Validated contract engine, IAM least-privilege evaluation — you build a working, tested miniature of each so the concept becomes muscle memory instead of folklore.
  2. Everything is taught to the level interviewers actually probe — the numbers, the failure modes, the corner cases, the production tradeoffs. We assume you are aiming for the platform that serves millions of users with correctness guarantees, not a hobby pipeline.

How Each Phase Teaches (the "many ways of explaining")

Every phase deliberately explains the same material through several lenses, because principal-level understanding is the intersection of all of them:

DocumentVoiceWhat it gives you
README.mdthe syllabusintroduction, concept map, lab specs, integrated-scenario ideas, deliverables checklist, key takeaways
WARMUP.mdthe professorzero-to-principal primer — every term from first principles, the math, lab guidance, success criteria, interview Q&A, references
HITCHHIKERS-GUIDE.mdthe senior who's been therethe compressed practitioner tour — 30-second mental model, the numbers to tattoo on your arm, war stories, vocabulary, beginner mistakes
BROTHER-TALK.mdyour brother 👨🏻, off the recordcandid real-talk — what nobody tells you, the 2 a.m. pager truth, what's worth caring about, the career framing
lab-*/the lab bencha runnable, test-verified implementation: lab.py (TODOs) → your code → solution.py (reference) → test_lab.py (the proof). Validation is pytest.

When the JD demands the JVM (Flink, Scala/Cats/ZIO/FS2/Akka), the phase additionally ships a real, sbt test-verified Scala lab. Scala is first-class here, not an afterthought: it is the JVM language of Spark/Flink/Kafka-Streams and is in heavy demand for data-platform roles. Six runnable Scala labs + four runnable capstones (72 ScalaTest specs, all green via sbt test; JDK + sbt required):

PhaseScala projectStackSpecs
02lab-02-scala-ingestionpartitioned log, offsets, replay, compaction, DLQ7
03lab-02-scala-contractsschema compatibility + Cats Validated contracts11
04lab-02-scala-windowsevent-time windows, watermarks, lateness, dedup6
06lab-02-scala-spark-modelSpark execution model (stages/skew/AQE/spill)6
09lab-02-scala-icebergIceberg commits: snapshots, OCC, time travel, expiry6
12lab-02-scala-sdkCats + Cats-Effect data SDK (Resource/IO/retry)8
16capstone-01-ingestion-platformend-to-end ingest→DLQ→window→sink→query (sbt run)5
16capstone-02-fraud-detectionstateful fraud: rules, broadcast, savepoint, dedup8
16capstone-03-feature-servingCassandra serving: LWW/TTL/tombstones/consistency/backfill8
16capstone-04-unified-platformmulti-module build reusing the real P02 + P09 labs7

What You Will Build

Every phase ships a tested, runnable artifact. By the end you will have hand-built a miniature of essentially every component of a modern data platform:

  1. A failure-domain & exactly-once reasoning kit — partitioners, idempotency keys, a two-generals demonstration, and a dedup/outbox engine
  2. An ingestion gateway — partition assignment, consumer-group rebalancing, offset management, retention/compaction, DLQ + poison-message quarantine, replay-from-offset
  3. A schema-registry & data-contract engine — Protobuf/Avro/JSON compatibility checking (backward/forward/full), a Validated-style accumulating validator, CI gate
  4. A Flink-grade stream processor — event-time windows, watermarks, allowed lateness, keyed state, checkpoint/savepoint + restore, exactly-once sink protocol
  5. Streaming joins & CDC — stream-table enrichment, sessionization, a CDC apply loop with idempotent upserts
  6. A Spark execution simulator — DAG → stages → tasks, shuffle + skew detection, AQE-style coalescing, broadcast-join planning, small-file analysis
  7. An EMR/YARN cost & scheduling model — instance-fleet + spot-interruption planner, YARN container packing, EMR-Serverless vs EKS cost calculator
  8. Parquet & ORC internals — a row-group/page reader with predicate pushdown and dictionary/RLE decoding from first principles
  9. An Iceberg-style table format — snapshots, manifest lists, snapshot isolation, compaction, time travel, partition evolution, and conflict resolution on commit
  10. A query optimiser — predicate pushdown, partition pruning, join reordering, a cost-based plan chooser, and an Athena scan-cost estimator
  11. A Cassandra/DynamoDB data modeler — partition-key design, tombstone & TTL math, compaction-strategy chooser, consistency-level (R+W>N) calculator, hot-partition detector
  12. A typed functional data SDKResource (bracket) safety, Validated/EitherT error modeling, an effect-system mini-runtime, FS2-style streaming, retry semantics
  13. An orchestration & reliability core — a DAG scheduler with content-hash caching, SLO/error-budget tracker, freshness/volume/uniqueness data-quality checks, lineage graph
  14. A governance & security kit — an IAM policy evaluator (least privilege), PII classifier + access policy, Lake-Formation-style column/row masking, audit log
  15. Capstone platforms — the JD's five hard problems, composed end-to-end

Folder Structure

data-engineer/
├── README.md            ← you are here
├── jd.md                ← the target role profile (the source of truth for scope)
├── GLOSSARY.md          ← every term in the JD, defined precisely
├── CHEATSHEET.md        ← the numbers, formulas, and decision rules in one place
├── SUMMARY.md           ← mdBook table of contents
├── phase-00-principal-data-infra-engineer/
├── phase-01-distributed-systems-foundations/
├── phase-02-event-ingestion-messaging/
├── phase-03-serialization-schema-contracts/
├── phase-04-flink-stream-processing/
├── phase-05-streaming-alternatives-cdc/
├── phase-06-spark-internals/
├── phase-07-hive-tez-mapreduce-emr/
├── phase-08-storage-columnar-formats/
├── phase-09-lakehouse-table-formats/
├── phase-10-query-analytics-engines/
├── phase-11-serving-cassandra-dynamodb/
├── phase-12-scala-functional-data-engineering/
├── phase-13-orchestration-reliability-observability/
├── phase-14-security-privacy-governance/
├── phase-15-principal-architecture-strategy/
├── phase-16-capstone-systems/
├── interview-prep/      ← the JD's 5-round evaluation loop + per-domain Q&A banks
└── system-design/       ← full walkthroughs of the JD's example hard problems

Each phase contains README.md, WARMUP.md, HITCHHIKERS-GUIDE.md, BROTHER-TALK.md, and at least one lab-NN-*/ (README.md, lab.py, solution.py, test_lab.py, requirements.txt) where all tests pass against the solution.


34-Week Schedule

WeekPhaseFocus
100The role: platform vs pipeline vs product; the data lifecycle; the tradeoff calculus
2–301Distributed systems: the log, time, partitioning, replication, consistency, exactly-once
4–502Ingestion: Kafka & Kinesis internals, partitioning, retention, replay, DLQ, Flume migration
6–703Schema & contracts: Protobuf/Avro/JSON, registry, compatibility, contract testing in CI
8–904Flink: event time, watermarks, state, checkpoints, savepoints, RocksDB, exactly-once
10–1105Kafka Streams / Spark SS / Akka Streams / FS2; CDC; streaming joins & sessionization
12–1306Spark internals: DAG, shuffle, AQE, skew, spill, memory, broadcast joins, backfills
14–1507Hive/Tez/MapReduce + EMR ops: metastore, YARN, EMR Serverless, EMR on EKS, spot, cost
16–1708Storage & formats: S3, HDFS, Parquet/ORC internals, encodings, predicate pushdown
18–1909Lakehouse: Iceberg/Delta/Hudi — snapshots, compaction, time travel, partition evolution
20–2110Query engines: Trino/Presto/Athena/Spark SQL, CBO, Glue Catalog, Lake Formation
22–2311Serving: Cassandra & DynamoDB modeling, tombstones, TTL, consistency, hot partitions
24–2612Scala/FP: Akka, Cats, Cats Effect, ZIO, FS2, type-safe modeling, SDK design, JVM tuning
27–2813Orchestration & reliability: Airflow/Dagster/Step Functions, SLOs, lineage, data quality
29–3014Security & governance: IAM, KMS, VPC/PrivateLink, Lake Formation, PII, Terraform, DR
31–3215Principal architecture: ADRs, migrations, build-vs-buy, multi-region, design reviews
33–3416Capstone: the five hard problems composed into one production-grade platform

Prerequisites

  • Strong programming — Python 3.10+ (the labs run in Python) and a willingness to read and write Scala (Phase 12 teaches it to platform-author level; you do not need prior Scala, but you need to not fear the JVM)
  • SQL fluency — you will optimise queries at the plan level
  • Comfort with a terminal, git, and Docker — the extension projects spin up real Kafka/Flink/Trino/Cassandra in containers
  • No paid cloud account required — every flagship lab runs locally and simulates the distributed component; the concepts are cloud-portable by design. AWS-specific phases (07, 14) teach the model and provide Terraform you can read and adapt.

Interview Relevance

This track maps directly onto the JD's own five-round evaluation loop:

  • Round 1 — Distributed data systems design ("ingest 5M events/sec → Flink → S3/Iceberg → Athena → Cassandra") → phases 01–04, 08–11 + system-design/
  • Round 2 — Deep debugging (a Flink job with rising checkpoint duration, growing RocksDB state, delayed watermarks, growing lag) → phases 04, 06, 13
  • Round 3 — Spark & lakehouse optimisation (a 9-hour job spilling and producing millions of small files) → phases 06–10
  • Round 4 — Scala platform engineering (a type-safe contracts + stream-processing SDK) → phases 03, 12
  • Round 5 — Architecture leadership (migrate Hive/Tez/Flume → Kafka/Flink/Iceberg) → phases 07, 09, 15

Start with Phase 00, then read each phase's HITCHHIKERS-GUIDE.md (fast), then its WARMUP.md (slow), then build the lab before moving on. The lab is where folklore becomes knowledge.

Principal / Distinguished Data Infrastructure Engineer

Real-Time Distributed Data Platform, Streaming Systems, and Lakehouse Architecture

Role Summary

We are looking for a Principal / Distinguished Data Infrastructure Engineer to design, build, operate, and evolve a mission-critical data platform that processes large-scale event streams, batch workloads, operational data, analytical data, and machine-learning-ready datasets across cloud and hybrid environments.

This role is not focused on writing ordinary ETL jobs. It is focused on building the data infrastructure layer itself: the streaming substrate, lakehouse architecture, schema governance systems, reliability controls, query optimization patterns, storage layout standards, ingestion frameworks, developer tooling, observability systems, and operational playbooks used by hundreds or thousands of engineers, analysts, data scientists, ML engineers, product teams, and business systems.

The ideal candidate has deep expertise in:

EMR, Hive, Tez, Spark, Flume, Flink, Scala, Akka, Cats,
Kafka, Kinesis, Cassandra, Parquet, Protobuf, Athena,
S3, Iceberg, Delta Lake, Hudi, Trino/Presto, Glue,
Airflow/Dagster, Kubernetes, Terraform, observability,
streaming semantics, schema evolution, distributed systems,
data correctness, platform reliability, and cost efficiency.

This is a senior technical leadership role for someone who can both architect at the system level and debug at the byte, partition, checkpoint, offset, file, and query-plan level.


Mission

Build and operate a high-scale, fault-tolerant, real-time and batch data platform that supports:

  • Low-latency event ingestion.
  • Real-time stream processing.
  • Large-scale batch processing.
  • Lakehouse analytics.
  • Operational data serving.
  • Feature generation for ML and AI systems.
  • Data governance, lineage, privacy, and access control.
  • High-confidence data correctness.
  • Cost-efficient cloud execution.
  • Developer-friendly platform abstractions.
  • Safe self-service for internal engineering teams.

The platform must handle workloads ranging from simple analytics pipelines to advanced distributed systems problems involving stateful stream processing, exactly-once guarantees, late-arriving events, backpressure, skew, reprocessing, schema migration, partition evolution, checkpoint recovery, multi-region replication, high-cardinality observability, and petabyte-scale storage optimization.


Technical Environment

The engineer should be comfortable designing and operating systems involving most or all of the following technologies.

Compute and Processing

Apache Spark
Apache Flink
Apache Hive
Apache Tez
Amazon EMR
AWS Glue
Apache Beam
Trino / Presto
Athena
MapReduce legacy systems
Kubernetes-native data workloads

Expected depth:

  • Understand Spark execution internals: DAGs, stages, tasks, shuffles, broadcast joins, adaptive query execution, partitioning, memory management, spill behavior, executor sizing, speculative execution, and failure recovery.
  • Understand Flink internals: job graph, task slots, keyed state, operator state, checkpoints, savepoints, barriers, watermarks, event time, processing time, timers, state TTL, backpressure, RocksDB state backend, rescaling, and exactly-once sinks.
  • Understand Hive and Tez execution: query planning, DAG execution, partition pruning, file formats, metastore behavior, statistics, ORC/Parquet optimization, and legacy warehouse migration.
  • Understand EMR operations: cluster lifecycle, managed scaling, spot interruptions, bootstrap actions, instance fleets, YARN tuning, Spark-on-YARN, EMR Serverless, EMR on EKS, IAM, logging, and cost controls.

Streaming and Messaging

Apache Kafka
Amazon Kinesis
Kafka Streams
Flink DataStream API
Flink SQL
Spark Structured Streaming
Akka Streams
Flume
CDC platforms
Schema Registry
Dead-letter queues
Replay systems

Expected depth:

  • Design durable event ingestion architectures.
  • Choose between Kafka, Kinesis, Pulsar, and cloud-native queues.
  • Understand partitioning, consumer groups, offset management, retention, compaction, replication, ISR, leader election, and broker failure modes.
  • Design replayable pipelines.
  • Handle ordering guarantees and out-of-order events.
  • Implement event-time processing with watermarks.
  • Control backpressure and overload.
  • Design dead-letter queues and quarantine flows.
  • Handle poison messages and malformed payloads.
  • Build idempotent producers and consumers.
  • Understand exactly-once versus effectively-once versus at-least-once semantics.
  • Design transactional sinks and deduplication strategies.
  • Build streaming joins, windowed aggregations, sessionization, enrichment, and stateful processing.
  • Migrate legacy Flume ingestion into modern Kafka/Kinesis/Flink-based ingestion.

Programming Languages and Runtime Systems

Scala
Java
Python
SQL
Akka
Cats
Cats Effect
FS2
ZIO
Functional programming
Type-level modeling
JVM performance tuning

Expected depth:

  • Build production-grade Scala services and libraries.
  • Use functional programming to model data workflows safely.
  • Use Cats abstractions such as Functor, Applicative, Monad, Traverse, Validated, EitherT, OptionT, and Resource.
  • Understand effect systems such as Cats Effect or ZIO.
  • Build concurrent, streaming, and actor-based systems with Akka or Akka Streams.
  • Understand JVM memory, GC, thread pools, async boundaries, blocking calls, serialization, and profiling.
  • Write APIs and frameworks that other engineers can use safely.
  • Design strongly typed domain models for events, schemas, transformations, and data contracts.
  • Build internal SDKs for ingestion, stream processing, data quality, schema validation, and pipeline deployment.

Storage and Data Formats

Amazon S3
HDFS
Cassandra
DynamoDB
Iceberg
Delta Lake
Apache Hudi
Hive Metastore
AWS Glue Data Catalog
Parquet
ORC
Avro
Protobuf
JSON
CSV
RocksDB state backend

Expected depth:

  • Design table formats for lakehouse workloads.
  • Understand Parquet internals: row groups, column chunks, pages, encodings, compression, statistics, predicate pushdown, dictionary encoding, and file sizing.
  • Understand Protobuf schema evolution, field numbering, backward compatibility, unknown fields, optional fields, repeated fields, and binary payload design.
  • Compare Avro, Protobuf, JSON, Parquet, and ORC for different ingestion and analytics use cases.
  • Design partitioning strategies that avoid small files, skew, and high metadata overhead.
  • Implement compaction, clustering, sorting, Z-ordering, bucketing, and file-size optimization.
  • Design Cassandra data models around query patterns, partition keys, clustering keys, tombstones, TTL, compaction strategy, consistency levels, repair, hot partitions, and read/write amplification.
  • Understand S3 consistency, object layout, request costs, prefix scaling, lifecycle policies, encryption, and cross-region replication.
  • Build storage abstractions that support analytics, replay, data science, machine learning, and operational serving.

Query and Analytics Layer

Athena
Trino / Presto
Hive
Spark SQL
Flink SQL
Iceberg SQL
Glue Catalog
Lake Formation
BI tools
Ad hoc analytics

Expected depth:

  • Optimize queries at the plan level.
  • Understand join strategies, predicate pushdown, partition pruning, statistics, CTE materialization, broadcast joins, dynamic filtering, and cost-based optimization.
  • Diagnose slow Athena/Trino/Hive/Spark SQL queries.
  • Design lakehouse tables for high-concurrency analytics.
  • Build semantic layers and data marts without corrupting lineage or duplicating business logic.
  • Support both exploratory analytics and production-grade data products.
  • Understand when Athena is sufficient and when a dedicated Trino/Spark/Flink architecture is needed.
  • Establish standards for table ownership, schemas, SLAs, freshness, retention, privacy, and access control.

Core Responsibilities

1. Architect the End-to-End Data Platform

Design the complete data platform spanning:

event production
ingestion
schema validation
stream processing
batch processing
CDC
data lake storage
warehouse/lakehouse tables
serving stores
query engines
governance
observability
deployment
incident response
cost management

You will define how data flows from operational systems into Kafka/Kinesis, through Flink/Spark jobs, into S3/Iceberg/Parquet tables, into Athena/Trino/Spark SQL, and into downstream systems such as Cassandra, feature stores, dashboards, ML pipelines, and customer-facing applications.

You must be able to reason about:

  • Data correctness.
  • Ordering.
  • Replayability.
  • Idempotency.
  • Deduplication.
  • Schema compatibility.
  • Data freshness.
  • Latency.
  • Throughput.
  • Cost.
  • Operational complexity.
  • Blast radius.
  • Privacy.
  • Security.
  • Failure recovery.

2. Build Real-Time Streaming Infrastructure

Design and implement real-time pipelines using Kafka, Kinesis, Flink, Spark Structured Streaming, and Akka Streams.

Examples of expected systems:

  • Real-time user activity ingestion.
  • Payment/event/fraud detection pipelines.
  • Clickstream processing.
  • IoT/event telemetry processing.
  • Real-time personalization features.
  • Online aggregation systems.
  • Low-latency operational dashboards.
  • CDC-based replication pipelines.
  • Streaming joins between event streams and slowly changing dimensions.
  • Exactly-once transactional sinks into lakehouse tables.
  • Streaming upserts into Cassandra or DynamoDB.
  • Real-time alerting and anomaly detection streams.

You should deeply understand:

  • Event-time processing.
  • Watermarks.
  • Late events.
  • Windowing.
  • Sessionization.
  • Stateful operators.
  • Checkpointing.
  • Savepoints.
  • Backpressure.
  • Offset commits.
  • Rebalances.
  • Consumer lag.
  • Idempotent writes.
  • Deduplication.
  • Reprocessing.
  • Failure recovery.
  • State migration.
  • Stream versioning.

3. Own Batch and Lakehouse Architecture

Design large-scale batch processing using Spark, Hive, Tez, EMR, Athena, Iceberg, Delta Lake, or Hudi.

Expected systems include:

  • Petabyte-scale data lake tables.
  • Incremental ETL and ELT pipelines.
  • Backfill frameworks.
  • Reprocessing frameworks.
  • Batch feature generation.
  • Historical event reconstruction.
  • Data quality validation.
  • Lakehouse compaction systems.
  • Dataset publishing workflows.
  • Multi-tenant data platform APIs.

You should be able to debug:

  • Slow Spark jobs.
  • Shuffle explosions.
  • Data skew.
  • Executor OOMs.
  • Excessive spill.
  • Bad partitioning.
  • Small-file problems.
  • Broken metastore partitions.
  • Athena scan-cost explosions.
  • Hive query regressions.
  • Tez container failures.
  • S3 throttling.
  • EMR spot interruption failures.
  • Corrupt Parquet files.
  • Schema drift.
  • Inconsistent late-arriving data.

4. Design Data Contracts and Schema Governance

Own schema governance across event streams, lakehouse tables, and analytical datasets.

Responsibilities:

  • Define event schema standards.
  • Manage Protobuf, Avro, and JSON schema evolution.
  • Build compatibility checks.
  • Integrate schema registry workflows into CI/CD.
  • Prevent breaking changes in event contracts.
  • Define data contracts between producers and consumers.
  • Build schema migration playbooks.
  • Track field-level ownership and lineage.
  • Support versioned datasets and historical reprocessing.
  • Enforce PII classification and access rules.
  • Design policy-aware event ingestion.

Expected technical depth:

  • Protobuf field compatibility.
  • Backward and forward compatibility.
  • Nullable versus optional fields.
  • Enum evolution.
  • Unknown field behavior.
  • Binary encoding tradeoffs.
  • Schema registry design.
  • Contract testing.
  • Producer/consumer compatibility matrices.
  • Safe deprecation.
  • Schema drift detection.

5. Build Data Quality, Correctness, and Reliability Systems

This role requires treating data pipelines like production software systems with explicit correctness guarantees.

You will define and implement:

  • Data quality checks.
  • Freshness checks.
  • Volume anomaly checks.
  • Referential integrity checks.
  • Duplicate detection.
  • Late-data reconciliation.
  • Exactly-once or effectively-once strategies.
  • Replay validation.
  • Data diffing.
  • Audit tables.
  • Checkpoint verification.
  • End-to-end lineage.
  • Dataset certification.
  • Data incident response.
  • SLOs and SLIs for data products.

Example SLOs:

99.9% of critical streams process events within 60 seconds.
99.99% of accepted events are durably persisted.
Critical lakehouse tables are queryable within 15 minutes of event arrival.
No breaking schema change reaches production without compatibility checks.
Backfills are reproducible and auditable.
Every production dataset has an owner, SLA, lineage, and quality policy.

6. Operate Distributed Systems in Production

You are expected to be an escalation point for the hardest failures.

You should be able to diagnose:

  • Kafka broker instability.
  • Kinesis shard hot spots.
  • Consumer lag spikes.
  • Flink checkpoint failures.
  • Flink RocksDB state growth.
  • Spark executor failures.
  • Spark shuffle service issues.
  • EMR cluster instability.
  • Hive metastore bottlenecks.
  • Athena query failures.
  • Cassandra tombstone storms.
  • Cassandra hot partitions.
  • S3 request throttling.
  • Kubernetes pod eviction.
  • JVM GC pauses.
  • Serialization bottlenecks.
  • Network saturation.
  • Data skew.
  • Backpressure cascades.
  • Cascading pipeline failures.
  • Incorrect event-time semantics.
  • Silent data corruption.

You will design runbooks, dashboards, alerting, and automated mitigations for these systems.


7. Build Internal Developer Platforms

This role includes platform engineering, not just data engineering.

You will build tools that let other engineers safely create and operate pipelines.

Possible platform components:

  • Pipeline templates.
  • Scala SDKs for stream processing.
  • Data contract generators.
  • Protobuf validation tools.
  • CI/CD checks for schemas and pipelines.
  • Self-service pipeline deployment.
  • Backfill orchestration framework.
  • Replay framework.
  • Data quality framework.
  • Dataset registry.
  • Lineage browser.
  • Cost attribution dashboard.
  • Query optimization advisor.
  • Streaming job health dashboard.
  • Data incident management workflow.
  • Lakehouse table creation service.
  • Automated compaction scheduler.
  • EMR/Flink/Spark deployment abstractions.
  • Local development and test harnesses.

The platform should reduce duplicated effort while preserving enough flexibility for advanced use cases.


8. Drive Cloud-Native Data Infrastructure

Own AWS-heavy architecture involving:

EMR
EMR Serverless
EMR on EKS
Kinesis
MSK
S3
Glue
Athena
Lake Formation
IAM
KMS
CloudWatch
CloudTrail
EKS
Lambda
Step Functions
DynamoDB
VPC networking
PrivateLink
Terraform

Expected depth:

  • IAM least privilege.
  • Cross-account access.
  • Encryption at rest and in transit.
  • KMS key policies.
  • Network isolation.
  • Private endpoints.
  • S3 bucket policies.
  • Lake Formation permissions.
  • Glue catalog governance.
  • Athena workgroup controls.
  • Cost allocation.
  • Autoscaling.
  • Spot usage.
  • Multi-region architecture.
  • Disaster recovery.
  • Infrastructure as code.
  • CI/CD promotion across environments.

9. Lead Technical Strategy and Architecture Reviews

You will define the technical direction for the data platform.

Responsibilities:

  • Create architecture decision records.
  • Review major data platform designs.
  • Define streaming and lakehouse standards.
  • Establish data modeling patterns.
  • Decide when to use Spark, Flink, Hive, Athena, Trino, Cassandra, Kinesis, Kafka, or EMR.
  • Evaluate new technologies.
  • Lead migrations away from obsolete systems.
  • Mentor senior engineers.
  • Raise the engineering bar.
  • Create reference architectures.
  • Build prototypes for risky designs.
  • Present tradeoffs to senior engineering leadership.
  • Translate business requirements into platform capabilities.

You should be able to defend architectural decisions using:

  • Latency requirements.
  • Throughput requirements.
  • Failure modes.
  • Team skill constraints.
  • Cost model.
  • Operational burden.
  • Data correctness requirements.
  • Security and compliance needs.
  • Migration complexity.
  • Long-term maintainability.

Example Hard Problems This Role Must Solve

Problem 1: Global Real-Time Event Ingestion

Design a multi-region event ingestion platform that accepts billions of events per day from product services, mobile clients, backend systems, and partner integrations.

Requirements:

  • Kafka or Kinesis ingestion.
  • Protobuf payloads.
  • Schema validation at ingress.
  • Dead-letter routing.
  • Low-latency Flink processing.
  • Durable raw event storage in S3.
  • Partitioned Parquet/Iceberg tables.
  • Athena and Trino query access.
  • Reprocessing support.
  • Regional failover.
  • Exactly-once or effectively-once persistence.
  • End-to-end observability.
  • Cost attribution by producer team.

Problem 2: Stateful Fraud or Risk Stream Processing

Build a Flink-based stateful processing system that detects suspicious behavior in near real time.

Requirements:

  • Event-time semantics.
  • Out-of-order events.
  • Sliding windows.
  • Session windows.
  • Pattern detection.
  • Enrichment from Cassandra.
  • Broadcast state for rules.
  • RocksDB state backend.
  • Checkpointing to S3.
  • Savepoint-based deployment.
  • Stateful job upgrades.
  • Rule versioning.
  • Low-latency alert output.
  • Replay for model/rule validation.

Migrate a legacy Hive/Tez warehouse into a modern lakehouse.

Requirements:

  • Preserve historical data.
  • Convert ORC/Parquet layouts.
  • Maintain backward-compatible schemas.
  • Introduce Iceberg or Delta Lake.
  • Support Athena/Trino/Spark queries.
  • Eliminate small files.
  • Implement compaction.
  • Fix broken partitions.
  • Replace batch-only pipelines with incremental processing.
  • Support streaming writes.
  • Maintain lineage.
  • Reduce query cost.
  • Decommission legacy Flume/Hive dependencies safely.

Problem 4: Cassandra Serving Layer for Real-Time Features

Design a low-latency serving store for derived real-time features.

Requirements:

  • Cassandra schema design.
  • Partition key design.
  • Hot partition avoidance.
  • TTL policy.
  • Compaction strategy.
  • Consistency-level selection.
  • Write idempotency.
  • Streaming upserts from Flink.
  • Backfill from Spark.
  • Read-path latency SLOs.
  • Tombstone monitoring.
  • Repair strategy.
  • Multi-region replication.

Problem 5: Enterprise Data Governance Platform

Design governance for thousands of datasets and event streams.

Requirements:

  • Dataset registry.
  • Event schema registry.
  • Field-level ownership.
  • PII classification.
  • Access-control integration.
  • Lineage graph.
  • Data quality checks.
  • Contract testing.
  • Audit logging.
  • Retention policies.
  • Dataset certification.
  • Breaking-change prevention.
  • Integration with CI/CD.
  • Integration with Lake Formation, Glue, Athena, and IAM.

Required Experience

Distributed Data Systems

You should have extensive experience with:

  • Apache Spark internals and production operations.
  • Apache Flink internals and production operations.
  • Kafka or Kinesis at high scale.
  • EMR, Hive, Tez, Athena, Glue, and S3-based data lakes.
  • Lakehouse table formats such as Iceberg, Delta Lake, or Hudi.
  • Cassandra or another distributed serving database.
  • Batch and streaming pipeline design.
  • Large-scale data modeling.
  • Production incident response.
  • Query optimization.
  • Data correctness and reliability engineering.

Scala and JVM Engineering

You should be strong in:

  • Scala.
  • Java interoperability.
  • Functional programming.
  • Akka or Akka Streams.
  • Cats or Cats Effect.
  • Type-safe API design.
  • JVM performance tuning.
  • Concurrency.
  • Serialization.
  • Library design.
  • Testing distributed data applications.

This role expects more than basic Scala syntax. You should be able to build reusable frameworks and platform libraries that prevent entire classes of bugs.


Cloud and Platform Engineering

You should understand:

  • AWS data services.
  • Kubernetes.
  • Terraform.
  • CI/CD.
  • Secrets management.
  • IAM.
  • Networking.
  • Observability.
  • Autoscaling.
  • Cost optimization.
  • Multi-environment deployment.
  • Disaster recovery.
  • Security controls.

Data Reliability and Operations

You should be comfortable owning:

  • SLOs.
  • SLIs.
  • Error budgets.
  • On-call rotations.
  • Incident response.
  • Postmortems.
  • Runbooks.
  • Capacity planning.
  • Cost reviews.
  • Reliability reviews.
  • Operational maturity roadmaps.

Advanced Technical Expectations

A strong candidate should be able to answer and implement around questions like these.

Spark

  • How does Spark split work into jobs, stages, and tasks?
  • What causes a shuffle?
  • How do you diagnose data skew?
  • How does adaptive query execution change join behavior?
  • When should you use broadcast joins?
  • How do you tune executor memory?
  • What causes executor OOM?
  • How do you design a safe backfill framework?
  • How do you avoid small files when writing Parquet?
  • How do you make Spark writes idempotent?
  • How do watermarks affect correctness?
  • How do checkpoints work?
  • What causes checkpoint alignment delays?
  • How does RocksDB state grow?
  • How do you upgrade a stateful Flink job?
  • What is the difference between savepoints and checkpoints?
  • How do you handle late events?
  • How do you design exactly-once sinks?
  • How do you debug backpressure?
  • How do you rescale keyed state?

Kafka and Kinesis

  • How do you choose partition keys?
  • How do you handle hot partitions?
  • How do you design retention?
  • How do you perform replay safely?
  • How do you manage consumer lag?
  • What happens during consumer group rebalancing?
  • How do Kafka transactions work?
  • How do Kinesis shards affect throughput?
  • How do you design cross-region replication?
  • How do you prevent poison messages from blocking pipelines?

Lakehouse

  • How do Iceberg snapshots work?
  • How do partition evolution and hidden partitioning work?
  • How do you compact small files?
  • How do you design table layouts for Athena and Trino?
  • How do you support streaming writes and batch reads safely?
  • How do you handle schema evolution?
  • How do you implement time travel?
  • How do you recover from bad writes?
  • How do you enforce retention and deletion policies?

Cassandra

  • How do you design tables around query patterns?
  • What creates tombstones?
  • Why are large partitions dangerous?
  • How do compaction strategies affect performance?
  • What consistency levels are appropriate?
  • How do you handle TTL-heavy workloads?
  • How do you model time-series data?
  • How do you support backfills without overwhelming the cluster?

Data Correctness

  • How do you prove a pipeline is correct?
  • How do you detect duplicates?
  • How do you make writes idempotent?
  • How do you reconcile batch and streaming outputs?
  • How do you test late-arriving data?
  • How do you validate replay results?
  • How do you prevent schema-breaking changes?
  • How do you detect silent data loss?

Deliverables Expected in This Role

Within the first 6-12 months, this engineer should be able to deliver systems such as:

  1. Unified event ingestion platform using Kafka/Kinesis, Protobuf, schema validation, DLQs, and replay.
  2. Flink-based stream processing framework with templates, observability, deployment automation, and correctness patterns.
  3. Spark/EMR batch framework for reliable backfills, incremental processing, and cost-efficient execution.
  4. Lakehouse architecture using Iceberg/Delta/Hudi, Parquet, Athena, Trino, and Glue.
  5. Data contract system integrated into CI/CD.
  6. Schema governance platform for Protobuf/Avro/JSON events.
  7. Data quality framework for freshness, volume, uniqueness, referential integrity, and anomaly detection.
  8. Streaming observability dashboard covering lag, throughput, watermark delay, checkpoint health, backpressure, DLQ rate, and failure rate.
  9. Cost optimization program reducing waste from poor partitioning, inefficient queries, overprovisioned clusters, and excessive small files.
  10. Migration strategy from legacy Hive/Tez/Flume systems to modern streaming and lakehouse systems.
  11. Cassandra serving architecture for real-time features and low-latency lookups.
  12. Incident response playbooks for Kafka, Flink, Spark, EMR, Athena, Cassandra, and S3 failures.

Success Metrics

This role is successful when:

  • Critical pipelines have clear SLOs and meet them.
  • Streaming latency is predictable and measurable.
  • Batch backfills are reproducible and safe.
  • Dataset ownership is explicit.
  • Schema changes are controlled.
  • Data quality issues are detected before customers or executives notice them.
  • Engineers can create new pipelines without reinventing architecture.
  • Athena/Trino/Spark query costs are reduced.
  • Lakehouse tables are well-partitioned and compacted.
  • Kafka/Kinesis/Flink/Spark incidents decrease in frequency and severity.
  • The platform can replay historical data safely.
  • Compliance and privacy controls are built into the platform.
  • Senior engineers use the platform because it is better than custom one-off solutions.

Seniority Bar

This is not a senior data engineer role. This is a principal/expert-level infrastructure role.

The candidate should demonstrate:

  • System-wide technical judgment.
  • Ability to operate under ambiguity.
  • Deep distributed systems debugging skill.
  • Strong understanding of failure modes.
  • Ability to influence multiple teams.
  • Strong written architecture communication.
  • Ability to simplify complex systems without making them fragile.
  • Ability to build platforms, not just pipelines.
  • Ability to mentor senior engineers.
  • Ability to define engineering standards.
  • Ability to balance correctness, latency, cost, and operational burden.

What Makes This Role Difficult

This role is difficult because it combines several hard domains:

distributed systems
stream processing
batch processing
cloud infrastructure
functional programming
data modeling
query optimization
data reliability
schema evolution
storage formats
operability
security
governance
cost optimization
developer platform design

Most engineers specialize in one or two of these. This role requires practical fluency across all of them.

The hardest part is not knowing the tools. The hardest part is making correct tradeoffs when the requirements conflict:

  • Low latency versus low cost.
  • Exactly-once semantics versus operational simplicity.
  • Flexible schemas versus safe contracts.
  • Self-service versus governance.
  • Streaming freshness versus replay correctness.
  • Cassandra serving speed versus lakehouse analytical flexibility.
  • Batch accuracy versus real-time responsiveness.
  • Fast delivery versus long-term platform maintainability.
  • Cloud-native services versus open-source portability.
  • Developer freedom versus platform standardization.

Example Interview / Evaluation Loop

Round 1: Distributed Data Systems Design

Design a real-time data platform that ingests 5 million events per second, validates Protobuf schemas, processes events with Flink, stores raw and curated data in S3/Iceberg, supports Athena queries, and serves derived features from Cassandra.

Expected discussion:

  • Kafka versus Kinesis.
  • Partitioning.
  • Schema registry.
  • DLQs.
  • Flink topology.
  • Checkpointing.
  • Storage layout.
  • Exactly-once semantics.
  • Cassandra modeling.
  • Replay.
  • Observability.
  • Failure recovery.
  • Cost model.

Round 2: Deep Debugging

You are given a Flink job with rising checkpoint duration, increasing RocksDB state size, delayed watermarks, and growing Kafka lag.

Expected analysis:

  • Backpressure.
  • State growth.
  • Late events.
  • Key skew.
  • Checkpoint alignment.
  • RocksDB tuning.
  • Network bottlenecks.
  • Sink slowness.
  • Watermark strategy.
  • Partition distribution.
  • Deployment changes.
  • Rollback and savepoint strategy.

Round 3: Spark and Lakehouse Optimization

A Spark job on EMR runs for 9 hours, spills heavily, produces millions of small Parquet files, and causes Athena queries to scan excessive data.

Expected analysis:

  • Spark DAG.
  • Shuffle analysis.
  • Partitioning.
  • AQE.
  • Join strategy.
  • Executor sizing.
  • File sizing.
  • Compaction.
  • Iceberg/Delta/Hudi table layout.
  • Athena partition pruning.
  • S3 cost model.
  • Backfill strategy.

Round 4: Scala Platform Engineering

Design a Scala library that allows product teams to define event schemas, validate data contracts, build stream processors, write to lakehouse tables, and expose observability automatically.

Expected discussion:

  • Type-safe APIs.
  • Cats abstractions.
  • Resource safety.
  • Effect systems.
  • Error modeling.
  • Retry semantics.
  • Backpressure.
  • Testing.
  • Serialization.
  • Compatibility validation.
  • Developer ergonomics.
  • Binary compatibility.
  • Versioning.

Round 5: Architecture Leadership

Lead a design review for migrating from legacy Hive/Tez/Flume pipelines to Kafka/Flink/Spark/Iceberg/Athena.

Expected discussion:

  • Migration phases.
  • Compatibility.
  • Dual writes.
  • Data validation.
  • Replay.
  • Cutover.
  • Rollback.
  • Cost.
  • Governance.
  • Team adoption.
  • Risk management.
  • Deprecation plan.

Book Baseline: Suggested Chapter Structure

You can turn this role into a serious expert-level book using the following structure.

Part 1: Foundations of Modern Data Infrastructure

  1. What a principal data infrastructure engineer actually does.
  2. Data platform versus data pipeline versus data product.
  3. Batch, streaming, serving, and lakehouse architectures.
  4. Distributed systems principles for data engineers.
  5. Correctness, latency, throughput, cost, and operability.

Part 2: Event Ingestion and Messaging

  1. Kafka internals.
  2. Kinesis internals.
  3. Partitioning, ordering, retention, and replay.
  4. Protobuf, Avro, schema registry, and data contracts.
  5. Dead-letter queues, poison messages, and ingestion safety.
  6. Flume and legacy ingestion migration.

Part 3: Stream Processing

  1. Flink architecture and execution model.
  2. Event time, watermarks, windows, and late data.
  3. State, checkpoints, savepoints, and RocksDB.
  4. Exactly-once and effectively-once processing.
  5. Spark Structured Streaming.
  6. Akka Streams and functional streaming.
  7. Real-time joins, enrichment, and aggregations.
  8. Production debugging for streaming systems.

Part 4: Batch Processing and EMR

  1. Spark internals.
  2. Spark SQL and query optimization.
  3. Hive and Tez internals.
  4. EMR architecture and operations.
  5. Backfills, reprocessing, and replay.
  6. Data skew, shuffle, spill, memory, and file layout.
  7. Cost optimization for batch workloads.

Part 5: Storage, Formats, and Lakehouse Design

  1. S3 and object storage design.
  2. Parquet internals.
  3. ORC, Avro, JSON, and Protobuf tradeoffs.
  4. Iceberg, Delta Lake, and Hudi.
  5. Partitioning, clustering, compaction, and metadata.
  6. Athena, Trino, Presto, and query-engine behavior.
  7. Lakehouse governance and table ownership.

Part 6: Serving Systems

  1. Cassandra data modeling.
  2. DynamoDB and cloud-native serving stores.
  3. Feature serving and real-time lookups.
  4. TTL, tombstones, hot partitions, and consistency.
  5. Connecting streams to serving stores safely.

Part 7: Scala and Functional Data Engineering

  1. Scala for data infrastructure.
  2. Akka and actor-based systems.
  3. Cats and functional abstractions.
  4. Cats Effect, ZIO, FS2, and resource safety.
  5. Type-safe event modeling.
  6. Building internal data platform SDKs.

Part 8: Reliability, Observability, and Operations

  1. Data SLOs and SLIs.
  2. Streaming observability.
  3. Batch observability.
  4. Lineage and auditability.
  5. Incident response.
  6. Data quality systems.
  7. Detecting silent data corruption.
  8. Capacity planning and cost management.

Part 9: Security, Privacy, and Governance

  1. IAM and least privilege.
  2. Encryption, KMS, and network isolation.
  3. Lake Formation and Glue governance.
  4. PII classification and retention.
  5. Access control and audit trails.
  6. Data contracts and organizational governance.

Part 10: Principal-Level Architecture

  1. Architecture decision records.
  2. Platform strategy.
  3. Migration from legacy Hadoop systems.
  4. Build versus buy decisions.
  5. Multi-region data architecture.
  6. Developer experience for data platforms.
  7. Technical leadership and design reviews.
  8. Operating as a principal engineer.

Part 11: Capstone Systems

  1. Build a Kafka/Kinesis ingestion platform.
  2. Build a Flink stream processing platform.
  3. Build a Spark/EMR batch processing platform.
  4. Build an Iceberg/Athena lakehouse.
  5. Build a Cassandra feature-serving layer.
  6. Build schema governance and data contracts.
  7. Build observability and incident response.
  8. Build a full production-grade data platform.

Final Positioning Statement

This job description represents a top-tier expert data infrastructure role.

A person who can perform this role well is not merely a data engineer. They are a hybrid of:

principal software engineer
distributed systems engineer
streaming platform engineer
data architect
cloud infrastructure engineer
Scala/JVM engineer
database/storage engineer
data reliability engineer
technical strategist

The role is intentionally hard because it requires the engineer to understand the entire lifecycle of data:

creation -> ingestion -> validation -> transport -> processing ->
storage -> query -> serving -> governance -> replay -> deletion

And to do that while satisfying production constraints:

correctness
latency
throughput
availability
security
privacy
cost
operability
developer experience
long-term maintainability

That is the bar for a truly challenging principal/expert-level data infrastructure role.

Glossary — Every Term in the JD, Defined Precisely

The JD names ~120 technologies and concepts. This glossary defines each one to the depth an interviewer probes, and points to the phase where you build it. If a term in the JD is not here yet, that is a bug — open it as a gap.

Legend: → PNN = the phase that implements/teaches it.


A. Compute & Processing Engines

  • Apache Spark — a distributed, in-memory batch+micro-batch engine. Work is a DAG of stages split at shuffle boundaries; each stage is a set of tasks (one per partition) run on executors. Key internals: shuffle, broadcast joins, Adaptive Query Execution (AQE), Tungsten memory management, spill. → P06
  • Apache Flink — a true streaming (record-at-a-time) engine with managed keyed/operator state, event-time processing, checkpoints/savepoints, and exactly-once sinks. The reference engine for low-latency stateful stream processing. → P04
  • Apache Hive — SQL-on-Hadoop: a metastore (table→files mapping) + a query compiler that historically generated MapReduce, then Tez, DAGs. The ancestor of the lakehouse catalog. → P07
  • Apache Tez — a DAG execution framework that replaced MapReduce under Hive/Pig: avoids writing intermediate results to HDFS between steps, enabling pipelined DAGs. → P07
  • MapReduce — the original Hadoop batch model: map → shuffle/sort → reduce, with every stage boundary materialised to disk. Slow but the conceptual root of all the above. → P07
  • Amazon EMR — AWS's managed Hadoop/Spark/Flink/Hive cluster service. Flavours: EMR-on-EC2 (YARN), EMR Serverless (no cluster to manage), EMR on EKS (run Spark on a Kubernetes cluster). → P07
  • AWS Glue — serverless Spark (Glue ETL) + the Glue Data Catalog (a managed Hive metastore) + crawlers. → P07, P10
  • Apache Beam — a unified batch+stream programming model (the Dataflow model) that runs on multiple engines (Flink, Spark, Dataflow) via runners. → P05
  • Trino / Presto — a distributed MPP query engine (no storage of its own) that federates queries over S3/Hive/Iceberg/Cassandra/etc. Trino is the actively-developed fork of PrestoDB. → P10
  • Athena — AWS's serverless, pay-per-TB-scanned managed Trino over S3 + Glue Catalog. → P10
  • Kubernetes-native data workloads — running Spark/Flink as K8s pods (Spark-on-K8s, Flink Kubernetes Operator) instead of YARN. → P07, P14

B. Streaming & Messaging

  • Apache Kafka — a distributed, partitioned, replicated commit log. Topics → partitions; ordering is guaranteed within a partition only; consumers track offsets in consumer groups; durability via replication (ISR) + acks=all. → P02
  • Amazon Kinesis — AWS's managed streaming service. Shards are the unit of throughput (1 MB/s in, 2 MB/s out, 1000 records/s in); the partition key maps records to shards. Data Streams vs Firehose vs Data Analytics. → P02
  • Amazon MSK — managed Kafka on AWS (MSK Provisioned / MSK Serverless). → P02
  • Apache Pulsar — a Kafka-class log with a segment/broker separation (BookKeeper storage) and built-in tiered storage + multi-tenancy. The "third option." → P02
  • Kafka Streams — a JVM library (not a cluster) for stateful stream processing on top of Kafka, using changelog topics + RocksDB local state. → P05
  • Flink DataStream API / Flink SQL — the low-level (operators, state, timers) and declarative (SQL over streams) Flink programming surfaces. → P04
  • Spark Structured Streaming — micro-batch (and continuous) streaming on Spark's SQL engine, with checkpointing to a durable store. → P05
  • Akka Streams — a Reactive-Streams implementation on the Akka actor runtime; back-pressured, typed stream graphs in Scala/Java. → P05, P12
  • Flume — a legacy log/event ingestion agent (source → channel → sink). The thing you migrate off of into Kafka/Kinesis. → P02
  • CDC (Change Data Capture) — turning a database's row changes (via binlog/WAL, e.g. Debezium) into an event stream. The bridge from OLTP to the lakehouse. → P05
  • Schema Registry — a service that stores versioned schemas and enforces compatibility rules so producers and consumers evolve safely. → P03
  • Dead-letter queue (DLQ) / quarantine — a side destination for messages that fail validation/processing, with the reason attached, so the pipeline keeps flowing and bad data is preserved for diagnosis. → P02, P03
  • Replay — re-reading the log from an older offset to reprocess history (the property that makes backfills and bug-recovery possible). → P02, P06
  • Consumer group / rebalance — a set of consumers sharing a subscription; partitions are divided among members and rebalanced on join/leave/failure. → P02
  • ISR (in-sync replicas) — the replica set caught up to the leader; acks=all + min.insync.replicas defines durability. → P02
  • Leader election — choosing the partition replica that serves reads/writes; happens on broker failure (KRaft/ZooKeeper-coordinated). → P02
  • Idempotent producer / transactions — Kafka features giving exactly-once append per partition and atomic offset+output commits within Kafka. → P02, P03
  • Poison message — a malformed/unprocessable record that, without a DLQ, blocks a partition forever. → P02

C. Streaming Semantics

  • Event time vs processing time — when the event happened vs when the system saw it. Aggregate in event time; use processing time only for pipeline metrics. → P04
  • Watermark — a monotonic estimate flowing with the stream asserting "no events with timestamp ≤ W are still coming"; drives window emission. → P04
  • Windows — tumbling (fixed, non-overlapping), sliding/hopping (overlapping), session (gap-based, dynamic). → P04
  • Allowed lateness — how long after a window fires it stays open to absorb late events (re-emitting corrections) before expiring. → P04
  • Checkpoint vs savepoint — automatic, engine-owned recovery snapshots vs manual, user-owned, portable snapshots for upgrades/migrations. → P04
  • Barrier / checkpoint alignment — Flink injects barriers into streams; aligning them across inputs is what makes checkpoints consistent (and can stall under skew). → P04
  • RocksDB state backend — an embedded LSM key-value store Flink uses to keep large keyed state on local disk (spilling beyond heap). → P04, P08
  • Backpressure — downstream slowness propagating upstream as flow control; the central streaming health signal. → P01, P04
  • Exactly-once / at-least-once / at-most-once — delivery+effect guarantees. "Exactly-once" = at-least-once delivery + idempotent or transactional processing = exactly-once effect. → P01
  • Sessionization — grouping a user's events into sessions by inactivity gaps. → P05

D. Languages, Runtime & Functional Programming

  • Scala — a JVM language blending OO and FP; the lingua franca of Spark/Flink/Kafka Streams internals and data-platform SDKs. → P12
  • Java interop / JVM — Scala compiles to JVM bytecode and interops with Java; you must understand JVM memory, GC, thread pools, serialization. → P12
  • Akka — an actor-model runtime for concurrent, distributed, supervised systems. → P12
  • Cats — a Scala library of functional abstractions: Functor, Applicative, Monad, Traverse, Validated, EitherT, OptionT, Resource. → P12
  • Cats Effect — an effect system (IO) giving referentially-transparent, cancelable, resource-safe concurrency. → P12
  • ZIO — an alternative effect system: ZIO[R, E, A] (environment, typed error, value) with built-in dependency injection and structured concurrency. → P12
  • FS2 — Functional Streams for Scala: pure, back-pressured streaming built on Cats Effect. → P12
  • Validated — an applicative that accumulates errors (unlike Either's fail-fast) — the right tool for data-contract validation. → P03, P12
  • Resource / bracket — guaranteed acquire/release of resources even under failure or cancellation. → P12
  • Type-level modeling / data contracts in types — making illegal states unrepresentable so whole bug classes can't compile. → P12

E. Storage & Data Formats

  • Amazon S3 — object storage; the lake's substrate. Strong read-after-write consistency (since 2020), prefix-based request scaling, request costs, lifecycle policies, encryption, cross-region replication. → P08
  • HDFS — the Hadoop Distributed File System; block-based, NameNode metadata + DataNode blocks. The on-prem ancestor of S3-as-lake. → P08
  • Parquet — a columnar file format: file → row groups → column chunks → pages, with per-chunk statistics, dictionary/RLE encoding, and predicate pushdown. → P08
  • ORC — Optimised Row Columnar: stripes → row index → streams, with built-in indexes and bloom filters; Hive-native. → P08
  • Avro — a row-oriented, schema-carrying binary format ideal for streaming/ingestion and schema evolution. → P03, P08
  • Protobuf — Protocol Buffers: compact tag-based binary encoding with strict field-number-based evolution rules. The JD's preferred event payload. → P03
  • JSON / CSV — text formats: ubiquitous, self-describing (JSON) or positional (CSV), but expensive and weakly typed for analytics. → P03, P08
  • Cassandra — a wide-column, masterless (Dynamo-style) distributed database. Model around queries; partition key chooses distribution; tombstones, TTL, compaction strategies, tunable consistency. → P11
  • DynamoDB — AWS's managed key-value/document store; partition-key + sort-key model, on-demand/provisioned capacity, GSIs/LSIs, streams. → P11
  • Iceberg — an open table format: metadata files → manifest lists → manifests → data files, giving snapshot isolation, hidden partitioning, partition evolution, time travel. → P09
  • Delta Lake — a table format using a transaction log (_delta_log JSON + checkpoints) for ACID, time travel, and MERGE. → P09
  • Apache Hudi — a table format optimised for upserts/incremental: Copy-on-Write vs Merge-on-Read, timeline, record-level indexes. → P09
  • Hive Metastore — the catalog mapping logical tables/partitions to physical files + statistics. → P07, P10
  • Glue Data Catalog — AWS's managed, Hive-compatible metastore. → P10
  • RocksDB — an embeddable LSM-tree KV store; Flink/Kafka-Streams state backend. → P04

F. Lakehouse / Table-Format Concepts

  • Snapshot / time travel — each commit creates an immutable snapshot; you can query "as of" a snapshot/timestamp. → P09
  • Snapshot isolation / optimistic concurrency — readers see a consistent snapshot; concurrent writers detect conflicts at commit and retry. → P09
  • Hidden partitioning / partition evolution — Iceberg derives partition values from columns (no extra columns) and lets you change the partition spec without rewriting old data. → P09
  • Compaction / clustering / Z-order / bucketing / sorting — rewriting many small files into fewer large ones and laying data out so queries skip more of it. → P06, P09
  • Small-file problem — too many tiny files → metadata + open-cost explosion + bad scan performance. The lakehouse's most common disease. → P06, P09

G. Query & Analytics Layer

  • Predicate pushdown — pushing filters down to the scan so the engine reads fewer files/row-groups/pages. → P08, P10
  • Partition pruning — skipping whole partitions whose values can't match the filter. → P10
  • Dynamic filtering — building a filter from one side of a join at runtime to prune the other side's scan. → P10
  • Broadcast join — shipping the small side to every node to avoid a shuffle. → P06, P10
  • Cost-based optimisation (CBO) — using table/column statistics to choose join order and strategy. → P10
  • CTE materialisation — computing a common sub-expression once and reusing it. → P10
  • Lake Formation — AWS's fine-grained (table/column/row) access control + governance layer over the Glue Catalog and S3. → P10, P14
  • Semantic layer / data mart — a curated, business-logic-bearing view layer over raw tables. → P10

H. Cloud-Native (AWS) Infrastructure

  • IAM — Identity and Access Management: principals, policies, roles, least privilege, cross-account access via AssumeRole. → P14
  • KMS — Key Management Service: envelope encryption, key policies, grants. → P14
  • CloudWatch / CloudTrail — metrics+logs+alarms / API audit logging. → P13, P14
  • EKS / Lambda / Step Functions — managed Kubernetes / serverless functions / serverless workflow orchestration. → P07, P13, P14
  • VPC networking / PrivateLink — network isolation / private service endpoints that keep traffic off the public internet. → P14
  • Terraform — declarative infrastructure-as-code; the way platform infra is versioned, reviewed, and promoted across environments. → P14

I. Reliability, Correctness & Governance

  • SLO / SLI / error budget — the target (objective), the measured indicator, and the allowed failure budget that gates change velocity. → P13
  • Lineage — the dataset/field-level graph of "what was computed from what." → P13
  • Data quality dimensions — freshness, volume, uniqueness, referential integrity, schema validity, semantic validity, cardinality drift. → P13
  • Idempotency / dedup / outbox — making re-processing safe (no double effects). → P01, P05
  • Backfill / reprocessing — recomputing historical outputs correctly and reproducibly. → P06, P13
  • Data contract — an enforced agreement (schema + semantics + SLA + ownership) between a producer and its consumers. → P03
  • PII classification — tagging fields by sensitivity to drive masking, access, and retention. → P14
  • Blast radius / failure domain — the set of things that fail together; the first question in any incident. → P01, P13
  • ADR (Architecture Decision Record) — a short, durable record of a decision and its context/tradeoffs/consequences. → P15

Cross-reference: the one-page numbers and decision rules live in CHEATSHEET.md.

Cheat Sheet — The Numbers, Formulas & Decision Rules

One page of the things you should be able to recite. Each links to the phase that derives it. Memorising the number is worthless without the why — but in an interview, having both is the difference between "sounds senior" and "is senior."


Delivery semantics (P01)

at-most-once   = fire & forget                 → may lose data
at-least-once  = retry until acked             → may duplicate   ← practical default
exactly-once   = at-least-once + idempotency/transactions = exactly-once EFFECT

Rule: "exactly-once" is a property of the effect on state, re-established at every boundary — not a network guarantee. The moment you write to an external store, you re-earn it (upsert by key / dedup id / outbox).

Watermarks & windows (P04)

watermark W      = max_event_time_seen − bounded_delay
window fires when  W ≥ window.end
window expires when W ≥ window.end + allowed_lateness   → late events → side output
tumbling start   = ts − (ts % size);  interval is [start, start+size)  (end exclusive)

The triangle you trade: first-result delay ↔ completeness ↔ state size.

Kafka / Kinesis sizing (P02)

QuantityNumber
Kafka ordering guaranteeper-partition only
Durabilityacks=all + min.insync.replicas ≥ 2 (RF=3)
Kinesis shard1 MB/s & 1000 rec/s in, 2 MB/s out; GetRecords 5 tx/s
Partitions neededmax(target_in / per_partition_in, target_out / per_consumer_out)
Hot partition causelow-cardinality or skewed key → fix the key, add salt, or re-shard

Spark tuning (P06)

#tasks in a stage      = #input partitions (post-shuffle: spark.sql.shuffle.partitions, default 200)
target partition size  ≈ 128–256 MB
broadcast join when     small side ≲ spark.sql.autoBroadcastJoinThreshold (default 10 MB; raise w/ care)
executor memory split  = heap × spark.memory.fraction(0.6) → execution+storage (unified)
spill cause            = partition data > available execution memory → tune partitions, not just memory
skew fix               = AQE skew join (split big partitions) / salting / broadcast
small-file fix         = repartition/coalesce to target size before write; compaction job after

Idempotent write recipe: write to a staging path → atomic rename/commit → or use a table format (Iceberg/Delta) whose commit is atomic.

EMR / cloud cost (P07, P14)

RuleValue
Spot savingsup to ~90% vs on-demand; expect 2-min interruption notice
Spot safetycore nodes on-demand, task nodes on spot; checkpoint long jobs
Athena cost$5 per TB scanned → partition + columnar + projection = the bill
S3 request scaling~3,500 PUT/s & 5,500 GET/s per prefix → spread prefixes
Continuous-load breaker derate×0.8 (also the mental model for headroom planning)

Parquet / file layout (P08)

file → row groups (≈128 MB) → column chunks → pages (≈1 MB)
skipping: footer + row-group stats (min/max/null count) → predicate pushdown
encodings: dictionary (low-cardinality) → RLE/bit-packing; then compression (zstd/snappy)
target file size: 128 MB–1 GB;  avoid << 128 MB (small-file tax) and >> 1 GB (poor parallelism)

Cassandra (P11)

strong consistency: R + W > RF   (e.g. RF=3, W=QUORUM(2), R=QUORUM(2) → 2+2>3 ✓)
partition size: keep < 100 MB and < ~100k cells; model around the read query
tombstones: deletes/TTL create tombstones → read amplification; alert on tombstones/read
compaction: STCS (write-heavy) | LCS (read-heavy, more write amp) | TWCS (time-series/TTL)
hot partition: too-coarse partition key → add a bucketing component to the key

Iceberg / lakehouse (P09)

commit = new metadata.json → manifest list → manifests → data files (immutable snapshot)
isolation = optimistic concurrency; conflicting commits retry against the new snapshot
time travel = query AS OF snapshot-id / timestamp
maintenance = rewrite_data_files (compaction) + expire_snapshots + remove_orphan_files
partition evolution = change spec without rewriting history (hidden partitioning)

Consistency / distributed systems (P01)

CAP: under a network Partition, choose Availability or Consistency
PACELC: Else (no partition), choose Latency or Consistency  ← the everyday tradeoff
quorum: R + W > N gives read-your-writes on that register
clocks: never trust wall clocks for ordering → use logical/Lamport/vector clocks or sequence ids

Reliability targets (P13) — example SLOs from the JD

99.9%  of critical streams process events within 60s
99.99% of accepted events are durably persisted
critical lakehouse tables queryable within 15 min of event arrival
no breaking schema change reaches prod without a compatibility check
every production dataset has an owner, SLA, lineage, and quality policy

The tradeoff dials you will always be asked to turn (P00, P15)

low latency ⟷ low cost
exactly-once ⟷ operational simplicity
flexible schema ⟷ safe contract
self-service ⟷ governance
streaming freshness ⟷ replay/batch correctness
cloud-native lock-in ⟷ open-source portability
developer freedom ⟷ platform standardization

Principal move: name the dial, quantify both ends with the workload's numbers, then choose — and write it in an ADR.


Definitions for every term above are in GLOSSARY.md.

Phase 00 — The Principal Data Infrastructure Engineer

Difficulty: ⭐⭐☆☆☆ (conceptual) → the judgment is ⭐⭐⭐⭐⭐ Estimated Time: 1 week (12–18 hours) Prerequisites: none — this is the orientation that makes the other 16 phases cohere


Why This Phase Exists

Before you touch Kafka or Flink or Iceberg, you have to understand what job you are actually being hired to do — because at the principal level the job is not "operate tools," it is "make correct tradeoffs across the entire data lifecycle when the requirements conflict, and encode those tradeoffs into platforms other people build on."

The JD says it directly: "This role is not focused on writing ordinary ETL jobs. It is focused on building the data infrastructure layer itself." And: "The hardest part is not knowing the tools. The hardest part is making correct tradeoffs when the requirements conflict."

So Phase 00 builds the mental model and the calculator for those tradeoffs. Every later phase plugs a real technology into a slot in the model you build here. If you skip this phase you will learn 16 tools and still not be able to architect — which is exactly the failure mode that keeps strong engineers stuck at senior.

Concepts

  • Data pipeline vs data platform vs data product — three different things people call "data engineering," with three different success metrics and blast radii.
  • The data lifecycle: creation → ingestion → validation → transport → processing → storage → query → serving → governance → replay → deletion. Every technology in this track lives at one or two of these stages; principals reason about the seams.
  • The five forces: correctness, latency, throughput, cost, operability — and how every architecture decision trades them against each other.
  • The tradeoff dials (low-latency↔low-cost, exactly-once↔simplicity, flexible schema↔safe contract, self-service↔governance, freshness↔replay-correctness, …) — and how to quantify both ends with the workload's numbers instead of arguing taste.
  • Latency budgets — decomposing an end-to-end SLA across lifecycle stages so you know where the milliseconds (and dollars) actually go.
  • Blast radius / failure domains — the unit of thinking that turns a 30-alarm storm into one root cause (developed fully in Phase 01 & 13).
  • The ADR (Architecture Decision Record) — how principals make a decision durable and reviewable instead of tribal knowledge.

Labs

Lab 01 — Tradeoff & Lifecycle Modeler (flagship, implemented)

FieldValue
GoalBuild the calculator behind Round-1 architecture interviews: allocate an end-to-end latency budget across lifecycle stages, compute Athena scan cost and storage cost, size Kafka partitions / Kinesis shards, check a quorum for strong consistency, classify delivery semantics, and run a weighted tradeoff resolver that picks between two architectures given the workload's requirements
ConceptsThe data lifecycle as a typed pipeline; latency budgeting; cost models; the five forces as a scoring function; "name the dial, quantify both ends, then choose"
Steps1. Lifecycle stage model + budget allocation; 2. cost functions (Athena $5/TB, S3 storage, request scaling); 3. sizing (partitions, shards, quorum); 4. delivery-semantics classifier; 5. weighted tradeoff resolver with explainable output
How to Testpytest test_lab.py -v — covers budget conservation, cost math, sizing edge cases, quorum boundaries, and resolver tie-breaks
Talking Points"Where does the latency go?" "What does exactly-once cost you operationally?" "Defend Kinesis over Kafka with numbers."
Resume bulletBuilt a decision-support model that allocates end-to-end latency budgets and quantifies cost/correctness/operability tradeoffs to drive platform architecture choices

→ Lab folder: lab-01-tradeoff-modeler/

Integrated-Scenario Suggestions (carried through the whole track)

These five scenarios (the JD's "Example Hard Problems") are the spine of the capstone (Phase 16). Keep them in mind from now — each phase contributes a real component:

  1. Global real-time event ingestion — billions of events/day, multi-region, Protobuf, schema validation at ingress, DLQ, Flink, S3/Iceberg, Athena, cost attribution by team.
  2. Stateful fraud/risk stream processing — event-time, out-of-order, sliding+session windows, Cassandra enrichment, broadcast rules, RocksDB, savepoint upgrades, replay.
  3. Lakehouse migration — Hive/Tez/Flume → Kafka/Flink/Spark/Iceberg/Athena, small-file elimination, compaction, dual-write, validation, cutover, rollback.
  4. Cassandra serving layer — low-latency derived features, partition design, TTL, compaction, consistency, streaming upserts from Flink, Spark backfill, multi-region.
  5. Enterprise governance platform — dataset/schema registry, field ownership, PII, lineage, contract testing, Lake Formation/Glue/IAM integration, breaking-change prevention.

Guides in This Phase

Key Takeaways

  • The job is tradeoffs across a lifecycle, encoded into platforms, defended in ADRs — not pipelines.
  • Every later technology fits a lifecycle slot; learn the slot first, then the tool.
  • "Make it fast / cheap / correct" is not a requirement until you attach numbers; Phase 00's lab is where you learn to attach them.

Deliverables Checklist

  • Lab 01 implemented; all tests pass against solution.py
  • You can draw the 11-stage lifecycle from memory and name the tech at each stage
  • You can take any "design X" prompt and immediately list the dials it forces
  • You can write a one-page ADR for a real decision (template in WARMUP)

Warmup Guide — The Principal Data Infrastructure Engineer

Zero-to-principal primer for Phase 00: what the role actually is, the data lifecycle that organises the entire track, the five forces and the tradeoff dials you will spend your career turning, and the artifacts (latency budgets, cost models, ADRs) that turn opinion into engineering.

Table of Contents


Chapter 1: Pipeline vs Platform vs Product

From zero. Three things get called "data engineering," and conflating them is the single most common reason designs are over- or under-built:

  • A data pipeline moves/transforms data from A to B. Success = "the job ran and the numbers look right." Blast radius = one dataset. A senior engineer owns pipelines.
  • A data platform is the substrate other people build pipelines on: the ingestion SDK, the schema registry, the table-creation service, the orchestration framework, the observability that every pipeline inherits. Success = "hundreds of engineers ship correct pipelines without reinventing architecture." Blast radius = the whole org. A principal engineer owns platforms.
  • A data product is a dataset/stream treated as a product: it has an owner, an SLA, a schema contract, documented semantics, a freshness guarantee, and consumers who depend on it. Success = "downstream teams trust it enough to build on it without asking."

The JD is unambiguous that this role is the middle one with strong opinions about the third: "building the data infrastructure layer itself … used by hundreds or thousands of engineers." Why does the distinction matter so much? Because the right answer changes with the noun. Exactly-once semantics in one pipeline is a code review; exactly-once as a platform guarantee every team inherits is an architecture program. Schema validation in one job is an if statement; schema governance as a platform is a registry, a compatibility checker, a CI gate, and an org-wide contract. You will be asked to build the second kind. Everything in this track is "build the platform version of X."

Chapter 2: The Data Lifecycle — The Spine of the Track

Every piece of data, in every system you will ever build, walks this path:

creation → ingestion → validation → transport → processing →
storage → query → serving → governance → replay → deletion

This is not a metaphor; it is the literal table of contents of the JD and of this track. Memorise it, because the principal skill is reasoning about the seams between stages — that's where production failures live:

StageWhat happensTrack phaseClassic seam failure
creationa service emits an eventP00/P03wrong/absent event-time stamp
ingestionKafka/Kinesis accepts it durablyP02hot partition; lost acks
validationschema + semantic checks at ingressP03schema drift slips through
transportthe log carries it, partitioned & orderedP01/P02reordering breaks aggregation
processingFlink/Spark compute over itP04–P07processing-time logic double-counts on replay
storageS3/Iceberg/Parquet persist itP08/P09small files; bad partitioning
queryTrino/Athena read itP10full-table scans; cost explosion
servingCassandra/DynamoDB low-latency lookupsP11hot partition; tombstone storm
governancelineage, PII, access, contractsP13/P14PII leaks to an open table
replayreprocess history correctlyP02/P06/P13non-deterministic, non-idempotent recompute
deletionretention, GDPR erasureP14"delete" that time-travel can resurrect

The reason a principal can interview across all of Kafka and Spark and Iceberg and Cassandra is not that they memorised four manuals — it's that they hold one lifecycle and ask the same five questions (the next chapter) at every stage.

Chapter 3: The Five Forces

Every data-infrastructure decision is scored on five axes. There is no architecture that maxes all five; the art is knowing which the workload actually needs.

  1. Correctness — does the output match reality? (no loss, no duplication, no skew, no silent corruption, schema-valid). The one force you can't trade away on critical data, only define precisely (exactly-once-effect, freshness SLA, quality policy).
  2. Latency — how fast from event to usable result? (p50/p99; "real-time" is meaningless without a number and a percentile).
  3. Throughput — how much per second, sustained and at peak? (events/s, MB/s, and the ratio of read to write).
  4. Cost — $/event, $/TB stored, $/TB scanned, $/engineer-hour to operate.
  5. Operability — how hard is it to run, debug, upgrade, and stay on-call for? (the force juniors ignore and principals obsess over, because it dominates total cost over a system's life).

The senior framing: "requirements" are not requirements until they are numbers on these five axes. "Make it fast and cheap and correct" is a wish. "p99 < 60s, 5M events/s peak, < $X/month, exactly-once into the lakehouse, runnable by a 4-person on-call" is a spec you can architect against — and the lab makes you compute it.

Chapter 4: The Tradeoff Dials

The five forces conflict in recurring, nameable ways. A principal names the dial, quantifies both ends with the workload's numbers, and chooses — out loud, in an ADR. The canonical dials (straight from the JD):

  • Low latency ↔ low cost. Real-time Flink + Cassandra serving is fast and expensive; hourly Spark + Athena is cheap and slow. The number that decides it: what does a minute of staleness cost the business?
  • Exactly-once ↔ operational simplicity. Transactional sinks and checkpoint barriers buy correctness and cost you debuggability and throughput. The number: what does one duplicate or one lost event cost? (a duplicated fraud alert vs a duplicated ad impression are not the same question).
  • Flexible schema ↔ safe contract. Schemaless JSON ships fast and breaks consumers silently; strict Protobuf contracts slow producers and prevent 3 a.m. outages. The number: how many consumers, and how expensive is their breakage?
  • Self-service ↔ governance. Letting teams create tables freely maximises velocity and guarantees a small-file/PII/lineage mess; gating everything is safe and becomes the bottleneck you were hired to remove. The principal answer is almost always "self-service with guardrails the platform enforces."
  • Streaming freshness ↔ replay/batch correctness. The Lambda-vs-Kappa argument: maintain two code paths (fast+approximate, slow+correct) or one replayable streaming path. The number: can your streaming path be replayed deterministically? (if yes, Kappa; if no, you've bought Lambda's double maintenance).
  • Cassandra serving speed ↔ lakehouse analytical flexibility. A serving store answers one query shape in single-digit ms; the lakehouse answers any query in seconds. You need both, wired safely (P11).
  • Cloud-native lock-in ↔ open-source portability. Athena/Kinesis/Glue are fast to adopt and hard to leave; Trino/Kafka/Iceberg are portable and yours to operate.

Hold this: an interviewer is not testing whether you know Flink. They are testing whether, when they add "…and it must also be cheap," you can name which dial that turns and what it costs. That is the whole Round-1 signal.

Chapter 5: Latency Budgets — Where the Milliseconds Go

An end-to-end SLA (say "p99 event-to-queryable < 15 minutes") is meaningless until you decompose it across lifecycle stages, because that's how you find the stage that's eating your budget and the stage where spending more buys nothing:

ingest commit         ▏  50 ms   (acks=all to Kafka)
transport / consumer  ▏ 200 ms   (consumer poll + network)
stream processing     ▏   2 s    (Flink window + watermark delay)
write to S3/Iceberg   ▏  30 s    (batch the writes; small-file avoidance)
catalog commit/refresh▏  10 s    (snapshot commit + metadata)
query availability    ▏   --     (immediate once committed)
                      ───────────
budget consumed       ≈ 42 s     → 15-min SLA has huge headroom; spend it on cost, not speed

Two principal moves fall out of this:

  1. Budget conservation — the sum of stage budgets must be ≤ the SLA; the lab enforces it (you can't allocate 110% of the budget).
  2. Spend the slack. If the SLA is 15 min and you consume 42 s, the remaining 14 min is a cost lever: batch writes harder, use spot instances, compact lazily. Juniors optimise latency they don't need; principals convert unused latency into saved dollars.

Chapter 6: Cost Models — Where the Dollars Go

You cannot be the "cost efficiency" person the JD asks for without arithmetic. The ones that come up constantly (full derivations in later phases; here are the shapes):

  • Athena/Trino scan cost = bytes scanned × $5/TB. This is why partitioning, columnar formats, and projection pruning are cost features, not just speed features. A query that scans 1 TB instead of 10 GB costs 100× — and the fix is layout, not hardware (P08–P10).
  • S3 storage = GB-months × tier price; lifecycle policies move cold data to cheaper tiers. Plus request costs (PUT/GET per 1000) — which is why millions of small files cost money even when idle (P08).
  • Compute = node-hours × instance price, where spot is up to ~90% cheaper but can be reclaimed in 2 minutes (P07). Cost = nodes × hours × $/hour × (1 − spot_discount).
  • The dominant hidden cost is operability — engineer-hours on-call, debugging, and upgrading. A "cheaper" architecture that needs a dedicated team is not cheaper.

Cost attribution by producer team (a JD deliverable) is just tagging every byte and every node-hour with an owner and summing — boring, and the thing that actually changes behaviour, because teams optimise what they're billed for.

Chapter 7: Correctness as a First-Class Requirement

The JD repeats one idea more than any other: treat data pipelines like production software with explicit correctness guarantees. That means correctness is specified, tested, and monitored, not hoped for:

  • Specified: "exactly-once-effect into the lakehouse," "99.99% of accepted events durably persisted," "no breaking schema change reaches prod." These are the JD's example SLOs — testable statements, not vibes.
  • Tested: every lab in this track ships a property test. The flagship example (Phase 04): shuffled, duplicated delivery produces identical results to clean ordered delivery. If you can't write that test, you don't have correctness, you have luck.
  • Monitored: freshness, volume, uniqueness, referential integrity, schema/semantic validity, cardinality drift (P13). The point is to find corruption before customers or executives do.

Correctness is the force you most often see traded away by accident — a processing-time aggregation here, a missing dedup there — and the failures are silent, which is why a principal builds the detection in (P13's silent-corruption chapter).

Chapter 8: Blast Radius and Failure Domains

A failure domain is the set of things that fail together (a partition, a broker, an AZ, a region, a shared metastore, a single hot key). Blast radius is how much breaks when one thing does. The first question in every incident is not "what's wrong with component X?" but "what shared resource explains all these alarms at once?"

This reframing is what lets a principal stay calm in a 30-alarm storm: 12 jobs failing that all read one Hive metastore is one incident (the metastore), not twelve. You design for blast radius (isolate tenants, bulkhead resources, cap fan-out) and you reason with it during incidents (P13). Phase 01 makes this rigorous; Phase 00 just plants the question.

Chapter 9: The ADR — Making Decisions Durable

A principal's decisions outlive their memory of them and must survive their absence. The Architecture Decision Record is the lightweight artifact that makes a decision reviewable, teachable, and reversible-on-purpose. The template you'll reuse all track:

# ADR-NNN: <short title>
Status: proposed | accepted | superseded by ADR-MMM
Date: YYYY-MM-DD

## Context
What forces are in play? What are the numbers (the five forces, the dials)?

## Decision
What we chose, stated as one sentence.

## Alternatives Considered
Each option, and the dial it turned the wrong way for *our* numbers.

## Consequences
What gets better, what gets worse, what we must now monitor, and what would
make us revisit this.

The discipline the ADR enforces is exactly Chapter 4's: name the dial, quantify both ends, choose, and write down what would change your mind. An architecture review (JD Round 5) is just a room full of people pressure-testing your ADRs.

Lab Walkthrough Guidance

Lab 01 — Tradeoff & Lifecycle Modeler, suggested order:

  1. LifecycleStage enum and allocate_latency_budget(total, weights) — split a budget by weights and assert it sums to ≤ total (budget conservation; Ch. 5).
  2. Cost functions: athena_scan_cost_usd(bytes) ($5/TB), s3_storage_cost_usd(...), spot_compute_cost_usd(...) (Ch. 6) — pure arithmetic, exact unit tests.
  3. Sizing: kafka_partitions_needed(...), kinesis_shards_needed(...), quorum_is_strong(n, r, w) (r + w > n) — boundary tests (exactly equal is not strong).
  4. classify_delivery_semantics(retries, idempotent, transactional) → at-most / at-least / exactly-once-effect (Ch. 7).
  5. resolve_tradeoff(option_a, option_b, weights) — score two architectures on the five forces with workload weights; return the winner and the deciding force (explainable output, Ch. 3–4). Test the tie-break and the "weights change the winner" property.

Success Criteria

You are ready for Phase 01 when you can, from memory:

  1. Define pipeline vs platform vs product and say which this role builds.
  2. Recite the 11-stage lifecycle and name a technology and a seam failure for each.
  3. List the five forces and explain why a "requirement" isn't one without numbers.
  4. Name four tradeoff dials and, for each, the single number that decides it.
  5. Decompose a 15-minute end-to-end SLA into a stage budget and identify the slack.
  6. Compute an Athena scan cost and explain why layout (not hardware) is the fix.
  7. Write a one-page ADR for a real decision.

Interview Q&A

Q: "Design a system to ingest 5M events/sec." Where do you start? Not with Kafka. I start by turning the prompt into numbers on the five forces: what's the read throughput vs the 5M write (the ratio drives the storage/serving split); what's the latency SLA and at what percentile; what correctness guarantee (an ad impression and a payment are different); what's the budget; and who operates it. Then I decompose the lifecycle and a latency budget, and only then do tools get chosen — each as an ADR that names the dial it turns. The 5M number alone tells me almost nothing; the ratios and the SLAs tell me everything.

Q: A team wants exactly-once everywhere. Your response? First, "exactly-once" is exactly-once-effect, re-earned at every external boundary — so "everywhere" is a much bigger bill than they think. Second, I'd ask for the cost of a duplicate and the cost of a loss per stream, because most streams don't need it: an analytics impression tolerates at-least-once with dedup at read; a balance transfer does not. Exactly-once is a dial I turn per data product, sized by what an error costs — not a platform-wide default that taxes throughput and operability for streams that never needed it.

Q: How do you make a "make it cheaper" mandate concrete? Attribute cost first — tag every byte stored, byte scanned, and node-hour to an owner, so the bill is a map. Then the levers are mechanical: scan cost falls with partitioning + columnar + projection (often 10–100×); storage falls with compaction (kills the small-file request tax) and lifecycle tiering; compute falls with spot + right-sizing + AQE. Crucially I look for unused latency budget and spend it on cost — most "expensive" pipelines are optimising a freshness no one asked for.

Q: What's the difference between a senior and a principal data engineer, in one sentence? A senior makes one pipeline correct and fast; a principal makes the platform that makes every team's pipelines correct and fast by default, and can defend why with numbers in an ADR.

References

  • Martin Kleppmann, Designing Data-Intensive Applications — the whole book is this role's bible; Ch. 1 (reliability/scalability/maintainability) is this phase
  • Jay Kreps, The Log: What every software engineer should know (2013) — the lifecycle's unifying abstraction
  • Google SRE Book — Service Level Objectives chapter (SLO/SLI/error-budget thinking applied to data in P13)
  • Michael Nygard, Documenting Architecture Decisions (2011) — the ADR pattern
  • The track's own CHEATSHEET.md and GLOSSARY.md

🛸 Hitchhiker's Guide — Phase 00: The Principal Data Infra Engineer

Read this if: you can build a data pipeline but "design our data platform" still sounds like a different job. It is. This is the compressed tour the WARMUP derives slowly. Skim it, then read the WARMUP.


0. The 30-second mental model

You are hired to build the layer other engineers build on, and to be the person who, when latency and cost and correctness fight, picks the winner with numbers and writes down why. One sentence to tattoo: you don't own a pipeline, you own the lifecycle and the tradeoffs across it — encoded into a platform.

1. The lifecycle, one breath

creation → ingestion → validation → transport → processing →
storage → query → serving → governance → replay → deletion

Every technology in this track is one slot in that line. Learn the slot, then the tool. The bugs live in the seams between slots (processing-time logic that breaks on replay; PII that flows from creation to an open query table). Principals think in seams.

2. The five forces (score everything on these)

ForceThe questionThe trap
Correctnessdoes output match reality?silent corruption — you find out from an exec
Latencyp99 event→result?"real-time" with no number
Throughputsustained & peak, read:write ratio?sizing to average, dying at peak
Cost$/event, $/TB scanned, $/TB storedoptimising latency nobody asked for
Operabilitycan a 4-person on-call run it?the cost juniors forget; it dominates

A "requirement" with no number on these axes is a wish, not a spec.

3. The dials you'll turn forever

low latency ⟷ low cost            (what does a minute of staleness cost?)
exactly-once ⟷ simplicity         (what does one dup / one loss cost?)
flexible schema ⟷ safe contract   (how many consumers, how costly is breakage?)
self-service ⟷ governance         (answer is usually: self-service WITH guardrails)
freshness ⟷ replay-correctness    (can you replay deterministically? → Kappa, else Lambda)
serving speed ⟷ analytical reach  (you need both; wire them safely — P11)
cloud lock-in ⟷ OSS portability   (fast to adopt vs yours to leave)

The interview signal: when they add "…and cheap," can you name the dial and price it?

4. The numbers you'll actually use

ThingNumber
Athena/Trino scan$5 / TB scanned → layout is a cost feature
S3 request scaling~3,500 PUT/s, 5,500 GET/s per prefix
Spot savingsup to ~90%, 2-min reclaim warning
Kinesis shard1 MB/s & 1000 rec/s in, 2 MB/s out
Strong quorumR + W > N (equal is not strong)
Parquet target file128 MB – 1 GB (small files = the tax)
Cassandra partition< 100 MB, < ~100k cells

5. Budget the latency, spend the slack

Decompose an end-to-end SLA across stages. If the SLA is 15 min and you consume 42 s, the other 14 min is a cost lever — batch writes harder, use spot, compact lazily. Juniors optimise latency they don't need; principals convert it to dollars.

6. War story shapes you'll relive

  • "Our counts doubled overnight." → a replay hit a processing-time aggregation. Fix is event-time + idempotency, not "stop replaying" (replay is the platform's best feature).
  • "The query bill 10×'d." → someone wrote a million small files / dropped partitioning. The fix is layout, not a bigger warehouse.
  • "12 jobs are down!" → they all read one Hive metastore. One incident, not twelve. Ask "what's the shared resource?" first.
  • "It's exactly-once, we're fine." → only inside Kafka. Crossing into Cassandra you re-earn it (upsert/dedup) or you don't have it.

7. Vocabulary that signals you've held the pager

  • Blast radius / failure domain — what fails together; the first incident question.
  • Exactly-once effect — the only exactly-once that exists.
  • Data product — a dataset with an owner, SLA, contract, lineage.
  • Backpressure — downstream slowness as upstream flow control.
  • ADR — the durable record of a decision and what would reverse it.
  • Kappa vs Lambda — one replayable streaming path vs two code paths.
  • Self-service with guardrails — the only scalable answer to the governance dial.

8. Beginner mistakes that mark you in interviews

  1. Jumping to "use Kafka" before extracting numbers and the read:write ratio.
  2. Saying "real-time" or "scalable" with no percentile / no peak number.
  3. Treating exactly-once as a free platform default instead of a per-product dial.
  4. Optimising latency the SLA didn't ask for (and paying for it).
  5. Designing without a failure-domain picture, then being surprised by blast radius.
  6. Making decisions in chat instead of ADRs — so they're re-litigated every quarter.

9. How this phase pays off later

  • The lifecycle is the spine: every later phase fills one slot.
  • The five forces + dials are the scoring function behind every later architecture choice and every interview round.
  • The latency budget + cost model (the lab) is literally Round-1 mental math.
  • The ADR habit is Round-5 (architecture leadership) and your day job as a principal.

Now read the WARMUP slowly, then build the modeler. You'll use its functions as back-of-envelope checks for the rest of the track.

👨🏻 Brother Talk — Phase 00, Off the Record

No slides, no STAR method. This is me, your brother, telling you the things people only say after the second coffee. Read it once now, and again the week before your interview.


Listen. You've probably been told that becoming a principal data engineer means learning more tools. It doesn't. I've watched brilliant engineers memorise Flink, Spark, Iceberg, Kafka — all of it — and still get stuck at senior, because nobody told them the secret:

The tools are the easy part. The judgment is the job.

Here's the thing that took me too long to learn. When a director says "design our event platform," they are not testing whether you know acks=all. They already assume you do. What they're watching is: do you ask what a duplicate costs before you promise exactly-once? Do you ask the read-to-write ratio before you pick a database? Do you say "a minute of staleness is worth roughly $X to us, so I'll trade freshness for cost here"? That sentence — pricing a tradeoff out loud — is the whole game. Everyone else in the room is reciting features. You're doing engineering.

So let me give you the mindset shifts, brother to brother.

Stop saying "real-time." I mean it. The word is banned for you now. Every time you want to say "real-time," say a number and a percentile instead: "p99 under 60 seconds." The moment you do this, you sound three levels more senior, because you've revealed you know that "real-time" is a budget, not a property. A junior wants real-time everywhere. A principal knows real-time is the most expensive thing on the menu and orders it only where the business pays for it.

Fall in love with the boring force: operability. Everyone optimises latency and throughput because they're sexy and they benchmark well. Nobody brags about "this is easy to debug at 3 a.m." But operability is the force that actually decides whether your platform survives, because a system runs for years and gets debugged thousands of times. The "elegant" exactly-once pipeline that takes a PhD to debug is a liability. The "boring" at-least-once-with-dedup pipeline that any on-call can reason about is an asset. Choose boring on purpose. It's a flex, not a compromise.

Replay is your superpower; protect it. The first time you have an incident — and you will — and you can say "no problem, I'll replay the last six hours from the log and the output will be byte-identical because everything's event-time and idempotent" — that's the moment people start treating you like a principal. So everything you build, ask: can I replay this and get the same answer? If the answer is no, you haven't built a pipeline, you've built a time bomb. The whole reason we obsess over event-time and idempotency in this track is to keep that superpower.

Write the ADR even when nobody asks. Here's a career hack that feels like overhead and pays off enormously: when you make a real decision, write the one-pager — context, decision, alternatives, what would change my mind. Do it even for decisions nobody requested documentation for. Six months later when someone says "why did we do it this way?" you don't defend it from memory and emotion — you point at the doc and say "here were the numbers; have they changed?" You become the person whose decisions are legible. That person gets trusted with bigger decisions. That's the ladder.

The honest truth about this role's difficulty: it's hard because it's wide, not because any one piece is deep beyond reach. Nobody is born knowing Kafka and Spark and Cassandra and Cats. The people who get here didn't have bigger brains; they had a framework (the lifecycle, the five forces, the dials) that let them learn each new tool as "oh, this is the streaming-substrate slot, here's how it answers the same five questions." That framework is this entire phase. Internalise it and the other 16 phases stop being 16 unrelated manuals and become one system you already understand.

One more thing, and it's the most important. You don't have to be the smartest person in the room to be the principal in the room. You have to be the one who turns vague fear ("will this scale? is this correct? what will it cost?") into specific, answerable questions with numbers attached. Calm comes from numbers. When the room is panicking about "will this handle Black Friday," and you say "peak is 4× average, we're sized to 6×, here's the headroom, here's what sheds load first if I'm wrong" — you're not smarter than them. You just did the arithmetic they were too anxious to do. That's leadership in this job. It's learnable. It starts with the lab in this phase.

Go build the modeler. Then come find me in Phase 01 — that's where we make the distributed systems stuff click, the stuff everyone pretends to understand and quietly doesn't.

— your brother 👨🏻

Lab 01 — Tradeoff & Lifecycle Modeler

Phase: 00 — The Principal Data Infrastructure Engineer | Difficulty: ⭐⭐☆☆☆ | Time: 3–4 hours

Round-1 of a data-platform interview is back-of-envelope arithmetic worn as architecture. This lab builds the calculator: latency budgets, cost models, sizing, quorum math, delivery-semantics classification, and a weighted tradeoff resolver whose whole point is that the same two architectures flip winners when the workload's weights change — which is why "best architecture" is a meaningless phrase.

What you build

  • LifecycleStage — the 11-stage spine of the entire track, as an enum
  • allocate_latency_budget / budget_headroom_ms — decompose an SLA across stages with budget conservation, and find the slack that's a cost lever
  • athena_scan_cost_usd / s3_storage_cost_usd / spot_compute_cost_usd — the three cost models that actually decide architectures
  • kafka_partitions_needed / kinesis_shards_needed / quorum_is_strong — the sizing and consistency arithmetic (R + W > N, and the off-by-one that isn't strong)
  • classify_delivery_semantics — at-most / at-least / exactly-once-effect
  • resolve_tradeoff — score two architectures on the five forces under workload weights, and return the deciding force (the one sentence for your ADR)

Key concepts

ConceptWhat to understand
Budget conservationstage budgets must sum to the SLA; you can't allocate 110%
Spend the slackunused latency is a cost lever, not a virtue
Layout is a cost feature$5/TB makes partitioning/columnar a bill decision
R + W > Nstrict inequality; equality is not strong consistency
Exactly-once-effectthe only exactly-once there is; a per-product dial, not a default
Explainable tradeoffsthe deciding force is what you defend in the review

Files

FilePurpose
lab.pyskeleton with # TODO markers
solution.pycomplete reference; python solution.py runs a worked example
test_lab.pythe proof — run it red, make it green
requirements.txtpytest only (pure stdlib otherwise)

Run

pip install -r requirements.txt
pytest test_lab.py -v                      # against your lab.py
LAB_MODULE=solution pytest test_lab.py -v  # against the reference

Success criteria

  • All tests pass against your implementation.
  • You can explain why test_quorum_strong_boundary asserts quorum_is_strong(3,2,1) is False (3 = 3, not > 3).
  • You can explain why test_weights_flip_the_winner is the whole point of the lab.
  • You can take any "design X" prompt and, in 2 minutes, produce a latency budget and a scan-cost estimate on paper.

Extensions

  • Add gdpr_deletion_cost(...) — model the cost of point-deletes across a partitioned lake (foreshadows P09 time-travel-vs-deletion and P14 retention).
  • Add a recommend_engine(requirements) that returns Kafka-vs-Kinesis-vs-Pulsar (or Flink-vs-Spark-SS) with the deciding force — then defend it against a friend.
  • Make resolve_tradeoff accept N architectures and return a ranked list with pairwise deciding forces.

Phase 01 — Distributed Systems Foundations for Data

Difficulty: ⭐⭐⭐⭐☆ Estimated Time: 2 weeks (30–40 hours) Prerequisites: Phase 00 (the lifecycle and the five forces)


Why This Phase Exists

The JD describes a person who can "debug at the byte, partition, checkpoint, offset, file, and query-plan level" and reason about "data correctness, ordering, replayability, idempotency, deduplication, schema compatibility … failure recovery." Every one of those words is a distributed systems word. Kafka, Flink, Spark, Cassandra, and Iceberg are not five things to learn — they are five expressions of the same handful of distributed systems ideas. Learn the ideas once, here, and the rest of the track is "how does this specific engine implement the thing I already understand."

This is the phase that separates engineers who use distributed systems from engineers who reason about them. The former are surprised by reordering, duplication, partial failure, and clock skew. The latter expect all four and design so they don't matter.

Concepts

  • The log as the one abstraction under everything — append-only, ordered, offset-addressed; batch and streaming are two read modes of it (the Kreps/Kleppmann framing).
  • Time: wall-clock vs logical clocks (Lamport, vector); why you must never order distributed events by wall time; happens-before vs concurrent; causality.
  • Partitioning: stable hashing, per-key ordering, skew / hot partitions, and why % N is a resharding trap (consistent hashing as the fix).
  • Replication: leader/follower, quorums (R + W > N), ISR, sync vs async, and the durability/availability arithmetic (tolerated failures = N − quorum).
  • Consistency models: linearizable → sequential → causal → eventual; read-your-writes; CAP (under a partition: C or A) and PACELC (else: latency or consistency — the everyday tradeoff CAP omits).
  • Failure: the eight fallacies of distributed computing; partial failure; the Two Generals / FLP impossibility results and what they mean practically; gray failures.
  • Delivery & effect: at-most/at-least/exactly-once; idempotency, dedup, the outbox pattern; exactly-once-effect as the only achievable "exactly-once."
  • Backpressure & flow control: the queueing-theory reason unbounded buffers are bugs (Little's Law, bounded queues, load shedding).

Labs

Lab 01 — Partitioner, Idempotency & Exactly-Once Kit (flagship, implemented)

FieldValue
GoalBuild the four primitives — deterministic partitioning + skew measurement, Lamport/vector clocks, bounded dedup → exactly-once effect, and quorum/replication math — and prove that reordered + duplicated delivery yields identical state
ConceptsStable hashing, hot partitions, logical time, causality vs concurrency, idempotency, R + W > N, failure tolerance
How to Testpytest test_lab.py -v — 14 tests incl. order-independence and exactly-once under an unreliable channel
Talking PointsWhy never hash() for partitioning? What does a vector clock tell you that a Lamport clock can't? Why is bounded dedup a correctness tradeoff?
Resume bulletImplemented partitioning, logical-clock causality, bounded idempotent dedup, and quorum-durability primitives; verified exactly-once effect under reordered/duplicated delivery

→ Lab folder: lab-01-exactly-once-kit/

Extension Project A — Consistent Hashing Reshard (spec)

Implement a hash ring with virtual nodes; measure the fraction of keys that move when a partition is added vs % N (the difference is the whole reason resharding is survivable).

Extension Project B — Two Generals Demonstrator (spec)

Simulate a lossy channel and prove empirically that no fixed number of acknowledgements achieves certain agreement — then show how at-least-once + idempotency sidesteps the impossibility by changing the goal from "agree" to "same effect."

Integrated-Scenario Hooks

  • Hot partition (this phase's skew math) → Kafka key choice (P02), Kinesis shards (P02), Cassandra partition keys (P11), Spark data skew (P06). It is the same problem four times.
  • Exactly-once effect (this phase's dedup) → Flink sinks (P04), CDC apply (P05), Iceberg commits (P09), Cassandra upserts (P11).
  • Quorum math → Kafka ISR/acks (P02), Cassandra consistency levels (P11), multi-region (P14/P15).

Guides in This Phase

Key Takeaways

  • Five engines, one set of ideas: partitioning, logical time, replication, consistency, failure, idempotency, backpressure.
  • Wall clocks lie; order with logical clocks or sequence ids.
  • "Exactly-once" is exactly-once effect, re-earned at every boundary.
  • Durability and consistency are arithmetic (R + W > N; N − quorum), not adjectives.

Deliverables Checklist

  • Lab 01 implemented; all 14 tests pass against solution.py
  • You can state CAP and PACELC and give a data example of each choice
  • You can explain the same "hot partition" problem in Kafka, Cassandra, and Spark terms
  • Extension A or B attempted

Warmup Guide — Distributed Systems Foundations for Data

Zero-to-principal primer for Phase 01: the log, logical time, partitioning, replication, consistency, failure, idempotency, and backpressure — the small set of ideas that every engine in this track (Kafka, Flink, Spark, Cassandra, Iceberg) is a different expression of. Learn them once; recognise them everywhere after.

Table of Contents


Chapter 1: The Log — One Abstraction Under Everything

From zero. A log is an append-only, totally-ordered sequence of records, each addressed by a monotonic integer offset. That is the entire abstraction — and it is the substrate of the whole field:

  • A Kafka topic-partition is a log.
  • A database's replication stream / WAL is a log.
  • A table is a log compacted by primary key (keep the latest record per key).
  • "Batch" is reading a bounded slice of a log; "streaming" is reading its unbounded tail. They are not two systems — they are two read modes of one log (Kreps's "Kappa" insight; the reason Lambda architectures with two codebases rot).

Why does a principal start here? Because the log is what makes replay possible, and replay is the most valuable property a data platform has: it is how you backfill, how you recover from a bug, how you re-derive a corrupted dataset, how you bootstrap a new consumer. Architectures that discard the log (transform-in-place, no retained source of truth) trade away their own recoverability. Hold this: every "we can't reproduce last month's numbers" incident is a log that wasn't kept or wasn't versioned.

Chapter 2: Time, Clocks, and the Ordering of Events

The single most expensive misconception in distributed data: that you can order events by their wall-clock timestamps. You cannot. Physical clocks on different machines drift, get corrected by NTP (and jump backwards), and are simply unsynchronised at the millisecond scale that matters. Ordering distributed events by wall time produces silently wrong results — duplicated counts, negative durations, "effects before causes."

The principled tools:

  • Lamport clock (scalar): each node keeps a counter; bump it on every local event; on receiving a message stamped t, set local = max(local, t) + 1. Guarantee: if event a happened-before b, then L(a) < L(b). The converse is not true — L(a) < L(b) does not prove causality, which is the clock's limitation.
  • Vector clock: each node keeps a vector of per-node counters. Now you can decide precisely: a happened-before b (every entry ≤ and at least one <), they are equal, or they are concurrent (neither precedes the other). Concurrency detection is what you need to find genuine write conflicts (P11) and to reason about out-of-order events.
  • Sequence numbers / offsets: within a single partition, the log's offset is a total order — which is exactly why per-key ordering (next chapter) is so valuable.
  • Event-time + watermarks (P04): the streaming world's pragmatic answer — trust the event's own timestamp but bound your belief about how out-of-order it can be.

The senior rule: use wall clocks for human-facing display and SLA measurement only; order computation by logical time, offsets, or event-time-with-watermarks — never by arrival.

Chapter 3: Partitioning, Ordering, and Skew

To scale beyond one machine you partition: split the keyspace across N shards by partition = stable_hash(key) % N. Three consequences a principal internalises:

  1. Ordering is per-partition only. A total order across all data does not exist at scale, by design. What you engineer with is per-key ordering: route all events for account_42 to one partition (one key → one partition) and they stay ordered. Choosing the partition key is therefore choosing both your ordering guarantee and your failure-coupling.
  2. The hash must be stable. Language built-in hashes (Python's hash()) are salted per process and change across restarts — using one for partitioning reshuffles every key on every restart and destroys per-key ordering. Use FNV/Murmur/CRC (the lab implements FNV-1a). This is a real, shipped-to-prod bug class.
  3. Skew = hot partitions. If the key is low-cardinality or Zipfian (a few "whale" keys), some partitions get vastly more load. The skew factor = max_load / mean_load is the number you cite ("this key gives a 9× hot partition"). It is the same problem as Kafka hot partitions (P02), Kinesis hot shards (P02), Cassandra hot partitions (P11), and Spark data skew (P06). Fixes: choose a higher-cardinality key, add a salt/bucket component, or use consistent hashing.

Why % N is a resharding trap: changing N remaps almost every key to a new partition (a near-total reshuffle). Consistent hashing (a hash ring with virtual nodes) moves only ~1/N of keys when you add a node — the reason real systems (Cassandra, Dynamo) use it. Extension Project A makes you measure the difference.

Chapter 4: Replication and the Arithmetic of Durability

You replicate data to survive machine loss. The models:

  • Leader/follower (primary/replica): writes go to the leader, which ships them to followers — synchronously (durable but slow, blocks on the slowest follower) or asynchronously (fast but can lose recent writes on leader failure). Kafka's ISR (in-sync replicas) + acks=all + min.insync.replicas is exactly this dial: a write is acknowledged only once it's on ≥ min.insync.replicas members of the ISR.
  • Leaderless / quorum (Dynamo-style): write to W replicas, read from R, and if R + W > N the read set and write set must overlap on at least one up-to-date replica → you read your writes. This is the math behind Cassandra consistency levels (P11). It is a strict inequality: R + W = N is not strong (the lab's boundary test).
  • Durability/availability arithmetic: a quorum operation tolerates N − quorum failures (RF=3, W=2 → survives 1 node down). Sizing redundancy is this subtraction, not a vibe.

The principal framing: replication gives you a dial between durability, latency, and availability, and the right setting is per data product sized by what a lost write costs.

Chapter 5: Consistency Models, CAP, and PACELC

A consistency model is a contract about what a read can return given prior writes. The ladder, strongest to weakest:

  • Linearizable (strong): every operation appears to take effect atomically at a single point between its call and return; there is one real-time order. Expensive; needed for things like leader election and unique-constraint enforcement.
  • Sequential / causal: operations respect program/causal order but not necessarily real-time. Causal consistency (respect happens-before) is often the sweet spot.
  • Read-your-writes / monotonic reads: session guarantees — you never see your own write vanish, never go backwards in time. Often what users actually need.
  • Eventual: replicas converge if writes stop; in between, reads can be stale or conflicting. Cheapest, highest availability.

CAP: when a network Partition happens, you must choose Consistency or Availability — you cannot have both while partitioned. But CAP is a trap because partitions are rare; the everyday tradeoff is PACELC: if Partition then C-or-A, Else (normal operation) then Latency-or-Consistency. The Else clause is the one you tune daily — every "should this read hit the leader or a follower?" is an L-vs-C decision. Saying "PACELC" instead of "CAP" in an interview signals you've operated these systems, not just read the Wikipedia page.

Chapter 6: Failure — The Default State of a Distributed System

In a single process, components either work or the process dies. In a distributed system, the normal state is partial failure: some nodes up, some down, some slow, some lying, the network dropping/reordering/duplicating/delaying messages. Foundational results:

  • The 8 fallacies of distributed computing: the network is not reliable, latency is not zero, bandwidth is not infinite, the network is not secure, topology changes, there is not one admin, transport cost is not zero, the network is not homogeneous. Every one is a bug if you assume otherwise.
  • Two Generals Problem: over a lossy channel, two parties can never be certain they've agreed — no finite number of acknowledgements suffices. Practical consequence: you cannot guarantee "delivered exactly once" over a network. So you change the goal (Ch. 7).
  • FLP impossibility: in a fully asynchronous system with even one faulty process, no deterministic consensus algorithm can guarantee termination. Practical consequence: real consensus (Raft/Paxos, Kafka's KRaft) uses timeouts/randomness to make progress in practice while accepting the theoretical caveat.
  • Gray failure: the worst kind — a node that is slow or partially broken but still "up" to health checks, silently poisoning the system (the stalled follower that's still in the ISR; the disk that's 100× slower but not dead). Detecting gray failure is most of real on-call (P13).

The mindset shift: assume reorder, duplication, loss, delay, and partial failure as the baseline, and design so they don't change the answer. That design is the next chapter.

Chapter 7: Delivery Semantics and Idempotency

Given that the network can lose and duplicate, what can a pipeline promise?

  • At-most-once: send and forget; on failure, data is lost. Rarely acceptable.
  • At-least-once: retry until acknowledged; on failure, duplicates. The practical default — the system must then tolerate duplicates.
  • "Exactly-once" does not mean each message crosses the wire once (Two Generals forbids it). It means at-least-once delivery + idempotent or transactional processing = exactly-once effect. The result is as if each event were processed once.

Two ways to buy it:

  1. Idempotency / dedup: make re-processing a no-op. Dedup by a stable event id (the lab's DedupStore), or use a naturally idempotent sink (upsert by key, set-union, max). Critical subtlety: you can't remember every id forever, so dedup state is bounded (evicted behind a watermark) — which means a duplicate arriving later than the window can slip through. Bounded dedup is a correctness tradeoff, not a free optimisation; you size the window against how late duplicates realistically arrive.
  2. Transactions / atomic commit: commit the output and the input offset atomically, so a crash can't leave them disagreeing. Kafka's transactional producer + read_committed, or the outbox pattern at a service boundary (write the business row and an "event to publish" row in one DB transaction; a relay publishes the outbox).

The interview-grade sentence: "exactly-once is a property of the pipeline's effect on state, purchased with idempotency or atomic offset+output commits, and re-earned at every external boundary — not a network guarantee."

Chapter 8: Backpressure and Flow Control

When a downstream stage is slower than its upstream, something must give. The naive answer — an unbounded in-memory buffer — is always a bug: it converts a throughput problem into an out-of-memory crash, and it hides the slowdown until it's catastrophic. The principled answers:

  • Bounded queues + backpressure: when the buffer fills, slow the producer (block, or signal it to send less). The slowness propagates upstream as flow control instead of silent latency growth. This is Reactive Streams / Akka Streams / Flink's credit-based flow control / Kafka consumer max.poll.records.
  • Little's Law (L = λ × W): the average number of in-flight items equals arrival rate × time in system. It tells you the buffer size you need and what happens when W grows (latency spike) — the queueing-theory floor under all capacity planning (P13).
  • Load shedding: when you can't slow the producer (it's the outside world), drop or sample on purpose, with a metric, rather than collapsing. Better to consciously drop 1% than to topple and lose 100%.

Backpressure is the central health signal in stream processing (P04): rising consumer lag and rising checkpoint duration are backpressure wearing different costumes.

Lab Walkthrough Guidance

Lab 01 — Exactly-Once Kit, suggested order:

  1. fnv1a_32 then hash_partition — verify determinism (same key → same partition across calls); this is the per-key-ordering foundation.
  2. partition_load + skew_factor — feel the difference between uniform keys (~1.0) and a "whale" key (large) — Ch. 3 made tangible.
  3. LamportClock then VectorClock — test that causal chains order under Lamport, and that two independent local ticks are concurrent under vectors (Ch. 2).
  4. DedupStore.check_and_add (first True, then False) and evict (bounded state) — Ch. 7.
  5. exactly_once_apply + simulate_unreliable_delivery — then the payoff test: a reordered, duplicated delivery yields the identical final state (Ch. 7).
  6. quorum_is_strong / tolerated_failures — Ch. 4's arithmetic, including the boundary R + W = N is not strong.

Success Criteria

You are ready for Phase 02 when you can, from memory:

  1. Explain why batch and streaming are read modes of one log, and why that makes replay the platform's key property.
  2. Give a concrete data bug caused by ordering events by wall clock, and the logical-clock fix.
  3. State Kafka's ordering guarantee precisely and explain what choosing the partition key decides.
  4. Compute the durability and read-your-writes properties of RF=3, R=2, W=2.
  5. State CAP and PACELC and give a per-clause data example.
  6. Define exactly-once-effect and both ways to buy it, including why bounded dedup is a correctness tradeoff.
  7. Explain why an unbounded buffer is a bug and what backpressure does instead.

Interview Q&A

Q: A consumer was restarted and now your hourly counts are wrong. Walk me through the suspects. First, duplication: at-least-once redelivery after the restart re-counted events — fixed by dedup on a stable id (and I'd check whether dedup state was bounded such that post-restart duplicates fell outside the window). Second, ordering/partitioning: if the partition key or hash changed (e.g. someone used a salted hash()), keys remapped and per-key aggregation broke. Third, time: if the aggregation keyed on processing time, the restart's catch-up replayed old events into the wrong buckets. The permanent fix is event-time windows + idempotent dedup + a stable partitioner — the three together make the pipeline replayable, which is the actual goal.

Q: Your team wants "strong consistency and high availability." What do you say? That under a network partition they must pick one (CAP) — but I'd reframe to PACELC because partitions are rare and the daily tradeoff is latency vs consistency in normal operation. Then I'd ask what the data is: a balance check needs read-your-writes (route to leader, accept the latency); a recommendation feed is fine eventual (read any replica, win the latency). "Strong + available" isn't one setting; it's a per-data-product dial, and most products don't need the strong end.

Q: What does a vector clock give you that a Lamport clock doesn't, and when do you care? A Lamport clock gives a total order consistent with causality, but L(a) < L(b) can't tell you whether a actually caused b or they were independent. A vector clock can distinguish happens-before from concurrent — which is exactly what you need to detect a genuine write conflict in a leaderless store (two replicas updated the same key independently): concurrent writes need conflict resolution (LWW, or a CRDT merge), causal ones don't. So I care the moment I have multi-writer state — Cassandra, Dynamo, multi-region.

Q: Define exactly-once and then tell me why it's "a lie." Exactly-once delivery over a network is impossible (Two Generals). What's real is exactly-once effect: at-least-once delivery plus idempotent or transactional processing, so retries don't change the result. It's "a lie" only if someone sells it as a network property or a free platform default — it's an end-to-end property of effects on state, re-established at every boundary (and it costs throughput and operability, so you spend it where an error is expensive, not everywhere).

References

  • Martin Kleppmann, Designing Data-Intensive Applications — Ch. 5 (replication), 6 (partitioning), 7 (transactions), 8 (the trouble with distributed systems), 9 (consistency & consensus). This phase is those chapters.
  • Leslie Lamport, Time, Clocks, and the Ordering of Events (1978) — logical clocks
  • Gilbert & Lynch, Brewer's Conjecture (2002) — the CAP proof; Abadi, PACELC (2012)
  • Fischer, Lynch, Paterson, Impossibility of Distributed Consensus with One Faulty Process (FLP, 1985)
  • Jay Kreps, The Log (2013) — the unifying abstraction
  • Nygard, Release It! — bulkheads, circuit breakers, backpressure in practice

🛸 Hitchhiker's Guide — Phase 01: Distributed Systems Foundations

Read this if: you want the distributed-systems vocabulary and numbers fast, before the WARMUP derives them slowly. These are the ideas that make Kafka, Flink, Spark, Cassandra, and Iceberg stop being five manuals and become one.


0. The 30-second mental model

Everything is a log (append-only, offset-addressed). You scale it by partitioning (per-key order only). You survive machine death by replicating (R + W > N math). The network lies (loses, dups, reorders, delays), so you never trust wall clocks and you make re-processing a no-op (idempotency) — which is the only "exactly-once" that exists. One sentence: assume reorder + duplication + partial failure as normal, and design so they don't change the answer.

1. The five engines, one idea each

EngineThe distributed-systems idea it is
Kafka / Kinesisa partitioned, replicated log
Flinkevent-time + bounded-disorder ordering over a log
Sparkpartitioned parallelism + shuffle (repartition)
Cassandraquorum replication + consistent hashing + LWW
Iceberga log of immutable snapshots (MVCC) over files

Learn the idea, recognise the engine.

2. The numbers to tattoo

strong consistency:    R + W > N      (R + W = N is NOT strong — off by one)
failure tolerance:     N − quorum     (RF=3, W=2 → survive 1)
skew factor:           max_load / mean_load   (1.0 = balanced)
consistent hashing:    add a node → ~1/N keys move  (vs % N → ~all move)
Little's Law:          L = λ × W      (in-flight = rate × latency)
ordering:              per-partition only; never total at scale

3. Time: the rule that saves you

Never order distributed events by wall clock. Clocks drift, NTP jumps backwards. Order by: offsets (within a partition), Lamport (causality ⇒ order), vector clocks (also detects concurrent), or event-time + watermarks (P04). Wall clocks are for human display and SLA stopwatches only.

  • Lamport: local = max(local, received) + 1. Gives order; can't prove causality.
  • Vector: per-node counters. Tells you happens-before vs concurrent = conflict detection.

4. Partitioning gotchas (the same bug, four costumes)

  • Salted hash bug: using hash() (salted per process) for partitioning → keys reshuffle on restart → per-key ordering destroyed. Use FNV/Murmur/CRC.
  • Hot partition: low-cardinality / Zipfian key → one shard melts. Same in Kafka, Kinesis, Cassandra, Spark. Fix: better key, salt/bucket, or consistent hashing.
  • % N resharding: changing N remaps almost every key. Consistent hashing moves ~1/N.

5. CAP is a trap; say PACELC

  • CAP: during a partition, choose C or A. (Rare event.)
  • PACELC: if Partition → C/A; ElseLatency or Consistency. The Else clause is the tradeoff you tune every day (leader read = consistent+slow; follower read = fast+maybe stale).

6. The impossibility results (and the practical escape)

  • Two Generals: can't guarantee agreement over a lossy channel → can't do exactly-once delivery. Escape: change the goal to exactly-once effect (idempotency).
  • FLP: async consensus can't guarantee termination with a faulty node. Escape: real systems (Raft/KRaft) use timeouts to make progress in practice.
  • Gray failure: a node that's slow, not dead, still passing health checks. The worst outages. Most of on-call is catching these.

7. Exactly-once, decoded

at-most-once   = no retries        → may lose
at-least-once  = retry till acked  → may dup   ← practical default
exactly-once   = at-least-once + (idempotency | transactions) = exactly-once EFFECT

Buy it with: dedup-by-stable-id (bounded! late dups past the window slip through — a real tradeoff), idempotent sinks (upsert/set-union/max), or the outbox pattern. Re-earned at every boundary.

8. Backpressure: unbounded buffer = bug

When downstream is slow, slow the producer (bounded queue + backpressure) — don't grow an unbounded buffer (that's an OOM with extra steps). When you can't slow the source, shed load on purpose with a metric. Rising lag and rising checkpoint time are backpressure in disguise (P04).

9. Beginner mistakes that mark you in interviews

  1. Ordering events by event.timestamp (wall clock) and trusting it.
  2. Saying "exactly-once" as if it's a network/platform freebie.
  3. hash() (or any salted/unstable hash) for partitioning.
  4. "Strong + highly available" with no mention of partitions / PACELC.
  5. Quorum off-by-one: thinking R + W = N is strong.
  6. Unbounded queues "for performance."
  7. Assuming a node is either up or down (forgetting gray/slow failure).

10. How this phase pays off later

  • Partitioning + skew → P02 (Kafka keys, Kinesis shards), P06 (Spark skew), P11 (Cassandra partitions). One mental model, four phases.
  • Exactly-once effect → P04 (Flink sinks), P05 (CDC), P09 (Iceberg commits), P11 (upserts).
  • Quorum math → P02 (ISR/acks), P11 (consistency levels), P14/P15 (multi-region).
  • Backpressure → P04 (the central streaming health signal).
  • Logical time → P04 (watermarks), P11 (conflict resolution).

Read the WARMUP slowly, build the kit, then go to P02 where the log becomes Kafka for real.

👨🏻 Brother Talk — Phase 01, Off the Record

The distributed-systems stuff everyone nods along to and quietly doesn't get. Let me save you the years it took me.


Brother, here's the thing nobody says out loud: most engineers, including senior ones, don't actually understand distributed systems. They've memorised the words. They'll say "eventually consistent" and "exactly-once" and "CAP theorem" in the right sentences, and if you push one inch — "okay, why is R + W = N not strong?" — the room goes quiet. I was that person for longer than I'd like to admit. So let me give you the handful of clicks that turned the words into understanding.

Click #1: The network is an adversary, not a pipe. Stop picturing the network as a slightly-slow wire that occasionally breaks. Picture a malicious gremlin who can drop your message, deliver it twice, deliver it out of order, deliver it ten minutes late, or — the evil one — make a machine slow instead of dead so your health checks say it's fine. Once you internalise the gremlin, every design question becomes "what does the gremlin do to this?" and suddenly idempotency and timeouts and bounded queues aren't "best practices" you memorise — they're obvious defenses against a specific enemy. The engineers who seem to "just get it" aren't smarter. They just stopped trusting the network years ago.

Click #2: Wall clocks are a lie you're emotionally attached to. This one's hard because timestamps feel so real. There's a number, it says 14:32:07.881, surely that's when it happened? No. That's when some machine's clock, which drifts and gets yanked around by NTP, thought it happened. I've personally debugged "negative event durations" (end before start) and "events from the future" — both from trusting wall clocks across machines. The day you truly stop trusting them and reach for offsets / logical clocks / event-time, you graduate. Say it with me: the timestamp is data the event carries, not the truth about when anything happened.

Click #3: "Exactly-once" is the field's most successful marketing lie. Vendors put it on slides. It sells. And it's technically impossible as delivery — the Two Generals proof is short and airtight; go read it, it'll take ten minutes and immunise you forever. What's real is exactly-once effect, and once you see that the trick is just "make doing it twice the same as doing it once" (idempotency), it stops being magic and becomes a design habit: every sink you build, you ask "what happens if this runs twice?" If the answer isn't "same result," you're not done. When an interviewer says "we need exactly-once," the senior answer is "into where, and what does a duplicate cost?" — because crossing into Cassandra or S3 you re-earn it, and most streams don't need the expensive version.

Click #4: hot partitions are one enemy wearing four masks. You'll spend your career fighting "this one shard is on fire." In Kafka it's a hot partition. In Kinesis it's a hot shard. In Cassandra it's a hot partition (again) plus a fat partition. In Spark it's data skew that makes one task run for 9 hours while 199 finish in seconds. It is the same problem every time: a key with too few distinct values or a Zipfian "whale." And the fixes rhyme: better key, add a salt/bucket, or spread differently. When you realise it's one enemy, you stop relearning it in each tool and start recognising it on sight. That recognition is what makes you look like a wizard in design reviews.

Click #5: the quorum math is just overlap. Don't memorise "R + W > N" as a spell. See it: if your write went to W replicas and your read asks R replicas, then as long as those two sets are forced to share at least one replica, your read can't miss your write. Two sets out of N must overlap exactly when R + W > N. That's it. That's the whole "consistency level" feature in Cassandra, the whole min.insync.replicas thing in Kafka. It's set overlap. Once you see it as overlap, you'll never get the off-by-one wrong, and you'll be able to derive the right consistency level live in an interview instead of recalling it.

Now, the career truth. This phase is the one that compounds the most, and it's the one most people skip because it's not a shiny tool you can put on your résumé. "Distributed systems foundations" isn't a line item; "Apache Flink" is. So everyone rushes to Flink and builds on sand. Don't. The reason a principal can walk into a Kafka problem and a Cassandra problem and a Spark problem and sound deep in all three is not that they read three manuals — it's that they understand partitioning, replication, time, and failure once, deeply, and see each tool as a variation. You're building that "once" right now. The lab feels small — a few hundred lines, hashing and dedup and quorum checks. But you are literally implementing the load-bearing ideas of every system in the next fifteen phases. Build it by hand. Make the exactly-once-under-chaos test pass. Feel the skew factor spike when you add a whale. That muscle memory is the foundation everything else screws into.

One last thing. When you're on-call at 3 a.m. and twelve alarms are screaming, the engineers who panic are the ones who see twelve problems. The ones who stay calm see the shared resource and ask "what one thing, failing, explains all of these?" That calm isn't a personality trait. It's this phase. It's knowing failure domains and blast radius in your bones, so a storm resolves into a single root cause. That calm is worth more than any tool on your résumé.

See you in Phase 02, where the log stops being a concept and becomes Kafka — with all its beautiful, sharp edges.

— your brother 👨🏻

Lab 01 — Partitioner, Idempotency & Exactly-Once Kit

Phase: 01 — Distributed Systems Foundations | Difficulty: ⭐⭐⭐☆☆ | Time: 5–6 hours

Every distributed data system is built from four primitives: partition the work, order it without trusting wall clocks, make re-processing safe, and replicate for durability without lying about consistency. This lab implements all four — and proves the one property the whole field exists to deliver: an unreliable, reordered, duplicated delivery produces the identical final state as a clean one.

What you build

  • fnv1a_32 / hash_partition / partition_load / skew_factor — deterministic partitioning (the basis of per-key ordering) and the skew measurement that names a hot partition with a number
  • LamportClock / VectorClock — logical time: Lamport (causality ⇒ order) and vector (distinguish happens-before from concurrent, the basis of conflict detection)
  • DedupStore — bounded idempotency: first-seen is True, duplicates are no-ops, and state is evicted behind a watermark so it can't grow forever
  • exactly_once_apply / simulate_unreliable_delivery — at-least-once delivery + dedup = exactly-once effect, demonstrated under reorder + duplication
  • quorum_is_strong / tolerated_failures / quorum_available — replication math: R + W > N, and how many failures a quorum survives

Key concepts

ConceptWhat to understand
Stable hashinghash() is salted per process; partitioning needs a stable hash or per-key order breaks on restart
Skew factormax/mean load — the number behind "hot partition"
Lamport vs vectorscalar gives order from causality; vector also detects concurrency
Exactly-once effectthe only exactly-once that exists; dedup makes duplicates no-ops
Bounded dedupyou can't remember every id forever — eviction is a real correctness tradeoff
R + W > Nstrict; equality is not strong; tolerated failures = N − quorum

Run

pip install -r requirements.txt
pytest test_lab.py -v
LAB_MODULE=solution pytest test_lab.py -v

Success criteria

  • All tests pass — test_exactly_once_effect_under_duplicates_and_reorder and test_exactly_once_is_order_independent are the ones that certify understanding.
  • You can explain why bounded dedup (evict) is a correctness tradeoff, not a free optimisation (a duplicate later than the retention window slips through).
  • You can state, with numbers, the durability of RF=3 / W=2 / R=2.

Extensions

  • Replace modulo partitioning with consistent hashing (a hash ring with virtual nodes) and measure how many keys move when you add a partition — the reason real systems don't use % N for resharding (foreshadows P02 rebalancing, P11 Cassandra).
  • Add a transactional outbox: apply a state change and record its id atomically, then show crash-recovery doesn't double-apply (foreshadows P05 CDC, P04 exactly-once sinks).
  • Implement read-repair: given replicas with vector-clocked values, return the merged value and detect a genuine conflict (foreshadows P11 last-write-wins vs CRDTs).

Phase 02 — Event Ingestion & Messaging

Difficulty: ⭐⭐⭐⭐☆ Estimated Time: 2 weeks (30–40 hours) Prerequisites: Phase 01 (the log, partitioning, replication, exactly-once)


Why This Phase Exists

Ingestion is the front door of the platform: if events are lost, reordered, duplicated, or unschematized here, nothing downstream can fix it. The JD's first hard problem is exactly this — "a multi-region event ingestion platform that accepts billions of events per day … Kafka or Kinesis … schema validation at ingress … dead-letter routing … reprocessing support … exactly-once or effectively-once persistence." This phase turns Phase 01's "the log" from theory into the two systems you'll actually run — Kafka and Kinesis — plus the operational machinery around them: consumer groups, retention, compaction, replay, DLQs, idempotent/transactional producers, and the migration off legacy Flume.

Concepts

  • Kafka internals: brokers, topics, partitions, the segment-based log, the page cache + zero-copy path, replication & ISR, acks & min.insync.replicas, leader election, KRaft (vs ZooKeeper), controller, consumer groups & rebalancing protocols (eager vs cooperative-sticky), __consumer_offsets, retention vs compaction.
  • Kinesis internals: shards (1 MB/s & 1000 rec/s in, 2 MB/s out), the partition key → shard hash, resharding (split/merge), enhanced fan-out, KCL & lease tables, vs Kafka.
  • Choosing the substrate: Kafka (MSK) vs Kinesis vs Pulsar (broker/storage split, tiered storage, multi-tenancy) vs cloud queues — the decision matrix and its dials.
  • Delivery & correctness on the wire: idempotent producers (PID + sequence), transactions (atomic offset+output within Kafka), read_committed, exactly-once vs effectively-once, and where the guarantee stops (the external sink).
  • Ingestion safety: schema validation at ingress (handoff to P03), dead-letter queues / quarantine, poison messages, redrive, backpressure & overload, quotas.
  • Replay & reprocessing: offset seek, retention windows, compacted topics as state, replay correctness (event-time + idempotency from P01).
  • Hot partitions/shards: key design, salting, resharding — the P01 skew problem in its ingestion costume.
  • Legacy migration: Flume (source→channel→sink) → Kafka/Kinesis; dual-write & cutover.

Labs

Lab 01 — Mini Broker (flagship, implemented)

FieldValue
GoalBuild a faithful partitioned log: keyed partitioning, absolute offsets surviving retention, consumer-group offsets + replay (seek), range/round-robin assignment + rebalance, log compaction (changelog→table), consumer lag, and a DLQ ingestion path
ConceptsPer-key ordering, log_start_offset, consumer lag, replay, compaction, poison-message quarantine
How to Testpytest test_lab.py -v — 15 tests
Talking PointsWhy do offsets never renumber after retention? When is a compacted topic the right design? Why does a DLQ keep the pipeline flowing?
Resume bulletImplemented a partitioned commit log with consumer-group offset management, replay, log compaction, and DLQ-based ingestion safety

→ Lab folder: lab-01-mini-broker/

Lab 02 — Partitioned-Log Ingestion in Scala (implemented, sbt test)

FieldValue
GoalThe Scala twin of Lab 01: stable FNV-1a partitioning (per-key ordering), monotonic offsets, consumer-group poll/commit/replay, retention, log compaction, and a DLQ ingest gateway
ConceptsSame ingestion substrate, in idiomatic JVM Scala
How to Testsbt test — 7 ScalaTest specs (runs here)
Resume bulletBuilt a partitioned-log ingestion library in Scala (stable partitioning, offsets, replay, compaction, DLQ) with ScalaTest coverage

→ Lab folder: lab-02-scala-ingestion/

Extension Project A — Run real Kafka (spec)

docker compose single-broker Kafka; create a 6-partition topic; produce keyed events; demonstrate per-key ordering, consumer-group rebalancing (start/stop consumers), replay-from-offset, and a compacted topic. Wire a validating consumer with a quarantine topic.

Extension Project B — Flume → Kafka migration memo (spec)

One page + diagram: migrate a Flume spooldir → memory channel → HDFS sink ingest into Kafka/Flink, dual-writing during cutover, validating parity, and decommissioning Flume safely (the JD's explicit "migrate legacy Flume" requirement).

Integrated-Scenario Hooks

  • This phase's DLQ + validation front-ends the schema registry (P03).
  • Its replay feeds Flink reprocessing (P04) and Spark backfills (P06).
  • Its compacted topics become CDC/KTable state (P05) and serving snapshots (P11).
  • Its partitioning/shard sizing is the ingress of the capstone's global platform (P16).

Guides in This Phase

Key Takeaways

  • Ingestion is unfixable downstream — get ordering, durability, and schema right here.
  • Kafka and Kinesis are the same log idea with different operational models; choose by the dials, not by fashion.
  • Replay + compaction + DLQ are the three operational superpowers; build them in from day 1.
  • Exactly-once on the wire stops at Kafka's edge; the external sink re-earns it (P04/P09/P11).

Deliverables Checklist

  • Lab 01 implemented; all 15 tests pass
  • You can size partitions/shards for a stated throughput and consumer parallelism
  • You can explain ISR + acks=all + min.insync.replicas durability precisely
  • Extension A demo or Extension B memo

Warmup Guide — Event Ingestion & Messaging

Zero-to-principal primer for Phase 02: Kafka and Kinesis from the inside, the durability and ordering knobs that decide correctness, consumer groups and rebalancing, retention vs compaction, transactions and idempotent producers, dead-letter safety, replay, and the migration off legacy Flume.

Table of Contents


Chapter 1: Kafka's Anatomy

Kafka is a cluster of brokers storing topics, each split into partitions — and a partition is exactly the log from P01: append-only, offset-addressed, ordered. The pieces:

  • Broker: a server holding some partitions (as leader for some, follower for others).
  • Partition: the unit of parallelism, ordering, and replication. More partitions = more consumer parallelism and more producer throughput, but also more open file handles, more rebalance cost, more end-to-end latency, and (until recently) more controller load. Sizing partitions is a real design decision, not a default.
  • Replica / leader / follower: each partition has a replication factor (RF, usually 3) copies; one is the leader (handles all reads/writes) and the rest are followers that replicate. Clients only talk to the leader.
  • Controller: one broker coordinates leadership and cluster metadata. Historically this lived in ZooKeeper; modern Kafka uses KRaft (a self-managed Raft quorum) — fewer moving parts, faster failover, the thing to mention as "current."
  • __consumer_offsets: an internal compacted topic where consumer groups store their committed offsets — Kafka eating its own dog food (offsets are just a compacted log).

The mental model to keep: a topic is a sharded log; a partition is a single log; ordering is per-partition; everything else is operations around that.

Chapter 2: The Log on Disk — Segments, Page Cache, Zero-Copy

Why is Kafka fast despite writing everything to disk? Three mechanical reasons worth knowing because they explain its limits:

  • Sequential I/O: appending to a log is sequential disk write, which is ~hundreds of MB/s even on spinning disks — orders of magnitude faster than random I/O. Kafka's whole performance story is "never do random I/O."
  • Segments: each partition is split into segment files (+ an offset index and time index). Retention deletes whole old segments (cheap), which is why retention is coarse (segment-granular), not per-record — and why log_start_offset advances in chunks.
  • Page cache + zero-copy: Kafka doesn't maintain its own cache; it relies on the OS page cache, and serves consumers via sendfile (zero-copy) — bytes go disk→NIC without passing through user space. This is why a consumer reading the tail (hot in page cache) is nearly free, and why a consumer reading old data (cold, e.g. a big replay) causes disk I/O and can hurt the brokers — a real operational gotcha during large backfills.

The lab models offsets and start_offset faithfully so this maps directly: retention advancing start_offset is segments being deleted.

Chapter 3: Durability — acks, ISR, and Leader Election

This is the chapter interviewers probe hardest because it's where correctness lives.

  • ISR (in-sync replicas): the set of replicas currently caught up to the leader. A follower that falls too far behind is removed from the ISR (and re-added when it catches up).
  • acks (producer): acks=0 (fire-and-forget, may lose), acks=1 (leader only — lost if the leader dies before a follower replicates), acks=all (leader waits for all in-sync replicas).
  • min.insync.replicas (topic): with acks=all, a write only succeeds if at least this many replicas are in-sync. The classic safe config is RF=3, min.insync.replicas=2, acks=all: you can lose one broker and still accept writes and never lose an acknowledged write. This is P01's quorum math (R + W > N → 2 + 2 > 3) wearing Kafka's clothes.
  • unclean.leader.election: if all in-sync replicas die, do you (a) promote an out-of-sync replica (available but loses data) or (b) refuse writes until an in-sync one returns (consistent but unavailable)? This toggle is the CAP choice, exposed as a config. Default it false for any data you care about.

The durability sentence to own: "RF=3, acks=all, min.insync.replicas=2 survives one broker loss with no acknowledged-write loss; the moment unclean leader election is on, you've traded that for availability."

Chapter 4: Producers — Partitioning, Idempotence, Transactions

  • Partitioning: a record with a key hashes to a partition (per-key ordering); a record without a key is distributed (sticky-partitioner batching in modern clients). Key choice = ordering + skew. A low-cardinality key (e.g. country) creates hot partitions (P01's skew problem); fix with a higher-cardinality key or a key + bucket salt.
  • Idempotent producer (enable.idempotence=true, now default): the producer gets a producer id (PID) and stamps each record with a sequence number per partition, so the broker can drop a retried duplicate — exactly-once append under producer retries. Note the scope: this dedupes producer retries, not application-level duplicates.
  • Transactions: a producer can write to multiple partitions and commit its consumer offsets atomically (initTransactions / sendOffsetsToTransaction / commit). Consumers with isolation.level=read_committed only see committed records. This is how Kafka offers exactly-once within Kafka (consume→process→produce). The instant you write to an external sink (S3, Cassandra), you leave Kafka's transaction and re-earn the guarantee yourself (P04 two-phase-commit sinks, P09 Iceberg commits, P11 upserts).

Chapter 5: Consumer Groups and Rebalancing

A consumer group is a set of consumers cooperatively reading a topic; each partition is assigned to exactly one consumer in the group, so group parallelism is capped at the partition count (extra consumers sit idle — the lab tests this). Mechanics:

  • Assignment strategies: range (contiguous blocks per topic — can imbalance across multiple topics), roundrobin (even but reshuffles more), sticky / cooperative-sticky (minimise partition movement on rebalance — the modern default).
  • Rebalancing: when a member joins/leaves/dies (or partitions change), the group reassigns partitions. Eager rebalancing stops the world (everyone revokes, then re-joins) — a latency hit; cooperative rebalancing moves only the partitions that actually need to move. Frequent rebalances (from max.poll.interval.ms timeouts because a consumer is slow) are a classic production pain — the symptom is "lag sawtooths and throughput drops."
  • Offset commits: a consumer commits the next offset to read (the lab's commit_through(last) = last+1). Commit after processing for at-least-once; commit before for at-most-once. Auto-commit is convenient and a duplicate/loss footgun under failure — principals usually commit manually around the processing boundary.

Chapter 6: Retention vs Compaction

Two completely different ways a topic forgets:

  • Delete retention (cleanup.policy=delete): drop data older than retention.ms or beyond retention.bytes. The log is a time window of events. log_start_offset advances as old segments are deleted; offsets never renumber (the lab models this exactly).
  • Log compaction (cleanup.policy=compact): keep at least the latest value per key forever; older values for a key are garbage-collected; a record with a null value (tombstone) marks a key for deletion. A compacted topic is a changelog that doubles as a table — the foundation of Kafka Streams' KTables, CDC snapshots, and Kafka's own __consumer_offsets. You can also combine (compact,delete).

When to use which: events → delete retention (you want the history window); current-state / config / CDC → compaction (you want the latest per entity, compactly). The lab's compacted_view is this fold.

Chapter 7: Kinesis — The Managed Cousin

Kinesis Data Streams is "Kafka as an AWS service," with different vocabulary and limits:

  • Shard ≈ partition, but with hard quotas: 1 MB/s and 1000 records/s ingress, 2 MB/s egress per shard. Throughput is shards × those limits — so you size in shards (the lab's kinesis_shards_needed).
  • Partition key → shard: an MD5 hash of the partition key maps records to a shard's hash range; ordering is per-shard. Same hot-key problem, same fixes.
  • Resharding: split a hot shard or merge cold ones — but resharding is a manual, rate-limited operation (vs Kafka's fixed partitions), and consumers must handle parent→ child shard lineage. On-Demand mode auto-scales shards at a cost premium.
  • Consumption: classic GetRecords is polled and shared (5 tx/s, 2 MB/s per shard across all consumers); Enhanced Fan-Out gives each consumer a dedicated 2 MB/s push pipe (lower latency, more cost). The KCL (Kinesis Client Library) manages leases & checkpoints in a DynamoDB lease table — the moral equivalent of consumer groups + __consumer_offsets.
  • Retention: 24h default, extendable to 365 days; replay = re-read by sequence number / timestamp.
  • Firehose is a different product: managed delivery to S3/Redshift/OpenSearch with buffering — no replay, no custom processing; great for "just land it," wrong for stream processing.

Chapter 8: Choosing the Substrate — Kafka vs Kinesis vs Pulsar

The decision matrix (and the dial each row turns — P00):

FactorKafka (self/MSK)KinesisPulsar
Ops burdenhigh (or medium w/ MSK)lowest (serverless)high
Throughput ceilingvery high, you tune itshard-quota'd, reshardingvery high
Scaling modelfixed partitions, manual addsplit/merge shards / On-Demandbroker/storage split → elastic
Ecosystemrichest (Connect, Streams, ksqlDB)AWS-native (Lambda, Firehose)growing
Geo / multi-tenancyDIY (MirrorMaker)AWS regionsbuilt-in geo-replication & tenants
Lock-inportable (OSS)AWS lock-inportable (OSS)
Cost shapeinfra + opsper-shard-hour + PUT unitsinfra + ops

The principal answer is never "use X." It's: "all-AWS team optimising for low ops and we're already in the ecosystem → Kinesis/MSK; need maximum throughput control, the Connect/Streams ecosystem, or cloud portability → Kafka; need elastic storage decoupled from compute and first-class multi-tenancy/geo → Pulsar." Then write the ADR.

Chapter 9: Ingestion Safety — DLQs, Poison Messages, Backpressure

The front door must stay open under bad input and overload:

  • Poison message: a record that can't be processed (malformed, fails schema, triggers a bug). Without handling, it blocks its partition forever — the consumer retries, fails, retries, and every record behind it starves. One bad message takes down a partition.
  • Dead-letter queue / quarantine (the lab's ingest): route the failing record to a DLQ topic with the reason attached, and move on. The pipeline keeps flowing; the bad data is preserved for diagnosis; the DLQ rate becomes an alert (a step change = upstream broke something — you find out in minutes, not at model-metric time). After fixing the cause, redrive the DLQ back into the main flow.
  • Schema validation at ingress (P03): the most common DLQ trigger; validate against the contract at the door so bad data never enters the lake.
  • Backpressure & quotas (P01 Ch. 8): when consumers can't keep up, lag grows — that's the signal. Kafka offers client quotas (bytes/s per principal) to stop one noisy producer from starving others; on overload you slow producers or shed load on purpose, never grow an unbounded buffer.

Chapter 10: Replay, Reprocessing, and Legacy Flume Migration

  • Replay is just seek to an older offset and re-consume — possible because the log is retained (or compacted). It's how you backfill a new consumer, recover from a processing bug, or re-derive a corrupted dataset. Replay is only correct if processing is event-time + idempotent (P01): otherwise the replay double-counts or lands in the wrong windows. The test of a well-built pipeline is literally "can you replay it and get the same answer."
  • Flume migration (a JD requirement): Flume is the legacy Hadoop-era agent (source → channel → sink, e.g. spooldir/tail → memory/file channel → HDFS sink). It's push-based, has weak ordering/replay, and couples producers to HDFS. The migration pattern: (1) stand up Kafka/Kinesis alongside; (2) dual-write — keep Flume running while a new producer also writes to Kafka; (3) build the new consumer (Flink/Spark) reading Kafka and validate parity against the Flume-fed HDFS output; (4) cut over consumers to the Kafka path; (5) decommission Flume once parity holds for a soak period. The principles are the same as any migration (P15): dual-write, validate, cut over, keep rollback.

Lab Walkthrough Guidance

Lab 01 — Mini Broker, suggested order:

  1. fnv1a_32 + partition_for_key, then Partition.append/end_offset/read with absolute offsets — verify offsets are monotonic and stable.
  2. retain_count / retain_time — confirm start_offset advances but end_offset doesn't (Ch. 2/6).
  3. compacted_view — latest-per-key, tombstone deletes (Ch. 6).
  4. assign_partitions (range, then roundrobin) and re-run with a new member = rebalance (Ch. 5); confirm extra consumers go idle.
  5. ConsumerGroup position/poll/commit_through/seek/lag — then the replay test: seek(0,0) makes lag jump back (Ch. 10).
  6. ingest with a DLQ — poison messages quarantined with a reason; good records after a poison one still land (Ch. 9).

Success Criteria

You are ready for Phase 03 when you can, from memory:

  1. Draw Kafka's anatomy (broker/topic/partition/replica/leader/ISR/controller).
  2. Explain acks=all + min.insync.replicas durability as quorum math, and what unclean leader election trades.
  3. Explain idempotent producers and Kafka transactions, and exactly where the guarantee stops.
  4. Describe consumer-group rebalancing (eager vs cooperative) and why frequent rebalances hurt.
  5. Contrast delete-retention and compaction and pick the right one for events vs state.
  6. Size Kinesis shards (and Kafka partitions) for a stated throughput.
  7. Choose Kafka vs Kinesis vs Pulsar for a stated context, with the deciding dial.
  8. Explain the DLQ pattern and the Flume→Kafka migration steps.

Interview Q&A

Q: A single bad message is stalling a partition and lag is climbing. Walk me through it. That's a poison message: the consumer fails on it, retries, and starves everything behind it in that partition. Immediate mitigation: route the failing record to a DLQ with the error attached and advance past it, so the pipeline flows and the bad data is preserved. Permanent fix: validate at ingress against the schema contract (so it never enters), alert on DLQ rate (a step change is an upstream regression), and add a redrive to reprocess the DLQ once fixed. The anti-pattern is infinite in-place retry, which converts one bad record into a partition outage.

Q: Producers are creating hot partitions. Diagnose and fix. The partition key is too low-cardinality or skewed (Zipfian "whale" keys), so the hash piles many keys onto few partitions — P01's skew factor, now in Kafka. Confirm by per-partition byte/record rate. Fixes, in order of preference: choose a higher-cardinality key that still preserves the ordering you need; if a single real key is hot (one giant account), add a key + bucket salt and reconcile downstream; only then consider more partitions (doesn't help a single hot key). The thing not to do is raise hardware — skew is a layout problem.

Q: When would you choose a compacted topic? When consumers need the current state per entity, not the event history: a config topic, a CDC snapshot of a table, a feature/profile keyed by user, or offsets themselves. Compaction keeps the latest value per key (tombstones delete), so a new consumer can bootstrap the full current state by reading the topic once — a changelog that doubles as a table. For genuine event streams (clicks, payments) you want delete-retention because the history is the data.

Q: "We're on Kinesis, we get exactly-once." True? No more than Kafka does. Kinesis gives ordered, at-least-once delivery per shard with replay; exactly-once is still an effect you engineer — KCL checkpoints can replay a batch after a worker failure, so your processing must be idempotent (dedup by sequence number, or idempotent upserts at the sink). The guarantee, as always, is re-earned at the external boundary, not granted by the transport.

References

  • Kafka: The Definitive Guide (Shapira, Palino, Sivaram, Petty) — the operational bible
  • Kafka docs — Design (delivery, ISR, transactions) and KRaft
  • Kreps, The Log (2013); Kleppmann, DDIA Ch. 11 (stream processing)
  • Amazon Kinesis Data Streams developer guide (shards, resharding, KCL, enhanced fan-out)
  • Apache Pulsar concepts (segments, tiered storage, multi-tenancy, geo-replication)
  • Confluent: Exactly-Once Semantics in Apache Kafka (the transactions design)

🛸 Hitchhiker's Guide — Phase 02: Event Ingestion & Messaging

Read this if: you want Kafka and Kinesis in your head fast — the anatomy, the durability knobs, the numbers, the gotchas — before the WARMUP derives them. Skim, then read the WARMUP, then build the broker.


0. The 30-second mental model

Kafka/Kinesis = a partitioned, replicated, retained log (P01's log, made real). You produce keyed records (key → partition → per-key order), they're replicated for durability (ISR + acks), consumed by groups that track offsets (so they can replay), and they age out by retention or fold to a table by compaction. The front door must survive bad input (DLQ) and overload (backpressure). One sentence: get ordering, durability, and schema right here — nothing downstream can fix ingestion.

1. Kafka anatomy in one diagram

Producer ──key→hash→ Partition (leader) ──replicate→ Followers (ISR)
                         │                               ▲
                  append (offset N)               acks=all waits for
                         │                          min.insync.replicas
Consumer GROUP ──assigned partitions──> poll(from committed offset) ──commit(next)
                         │
        retention(delete)  OR  compaction(latest-per-key + tombstones)
Controller (KRaft quorum) coordinates leaders & metadata

2. The durability knobs (memorise this combo)

RF=3  +  acks=all  +  min.insync.replicas=2
   → survive 1 broker loss, never lose an ACKed write   (P01: 2 + 2 > 3)
unclean.leader.election = false   → consistency over availability (turn ON only if you
                                    can tolerate data loss)
acks=1  → fast, loses data if leader dies pre-replication
acks=0  → fire & forget, may lose

3. The numbers

ThingNumber
Kafka orderingper-partition only
Group parallelism≤ partition count (extra consumers idle)
Kinesis shard in1 MB/s and 1000 rec/s
Kinesis shard out2 MB/s (shared) / 2 MB/s per consumer (enhanced fan-out)
Kinesis GetRecords5 calls/s/shard
Kinesis retention24h default → 365 days max
Partitions costfile handles, rebalance time, latency — don't over-shard

4. Producer truths

  • Key = ordering + skew. Low-cardinality key → hot partition (P01 skew, again). Fix: higher-cardinality key or key+bucket salt.
  • Idempotent producer (default now): PID + per-partition sequence → broker drops retried dups. Exactly-once append, not app-level dedup.
  • Transactions: atomic multi-partition write + offset commit → exactly-once inside Kafka (read_committed). Stops at the external sink.

5. Consumer-group truths

  • Each partition → exactly one consumer in the group.
  • Rebalance = reassign on join/leave/death. Eager = stop-the-world; cooperative-sticky = move only what must move (use it).
  • Frequent rebalances = a slow consumer blowing max.poll.interval.ms. Symptom: lag sawtooth + throughput drop.
  • Commit after processing = at-least-once (dedup downstream). Auto-commit = footgun.

6. Retention vs compaction (pick by intent)

events (clicks, payments)   → cleanup.policy=delete   (history IS the data)
state (config, CDC, profiles)→ cleanup.policy=compact  (latest-per-key + tombstone delete)

A compacted topic = a changelog that doubles as a table (KTable, __consumer_offsets).

7. Kafka vs Kinesis vs Pulsar (the one-liners)

  • Kinesis: lowest ops, AWS-native, shard-quota'd, AWS lock-in. "We're all-in on AWS, optimize ops."
  • Kafka/MSK: max control, richest ecosystem (Connect/Streams), portable. "We need throughput control / portability / the ecosystem."
  • Pulsar: broker/storage split → elastic storage, built-in multi-tenancy + geo. "We need elastic decoupled storage and tenants."

8. Ingestion safety in three moves

  1. Validate at ingress (P03 schema) → bad data never enters the lake.
  2. DLQ with reason → poison messages quarantined, pipeline flows, DLQ rate = alert.
  3. Backpressure / quotas → slow producers or shed load on purpose; never unbounded buffer.

9. Beginner mistakes that mark you

  1. acks=1 (or default-without-checking) for data you can't lose.
  2. Infinite in-place retry on a poison message → partition outage.
  3. Over-partitioning "for scale" → rebalance + latency + handle costs.
  4. Low-cardinality partition key → hot partition; then blaming Kafka.
  5. "Kinesis/Kafka gives exactly-once" → only inside; sink re-earns it.
  6. Replaying a processing-time pipeline → double counts (need event-time + idempotency).
  7. Auto-commit + crash → silent duplicates or loss.

10. War stories

  • "Lag is sawtoothing." → rebalance storm; a slow consumer keeps getting kicked. Tune max.poll.records/max.poll.interval.ms or speed the handler; switch to cooperative-sticky.
  • "Replay melted the brokers." → big backfill read cold (off page cache) → disk I/O storm. Throttle the replay / use a read replica / tiered storage.
  • "One country's events vanished into one partition."country as the key. Re-key.
  • "We can't reprocess last month." → retention was 7 days, no lake copy. The log wasn't kept; this is why you tee raw events to S3 (P08) on day one.

11. How this phase pays off later

  • DLQ + validate → schema registry & contracts (P03).
  • Replay + compaction → Flink reprocessing (P04), CDC/KTable (P05), serving snapshots (P11).
  • Partition/shard sizing → the capstone's global ingestion platform (P16).
  • acks/ISR quorum → Cassandra consistency (P11), multi-region (P14/P15).

Read the WARMUP, build the broker, then P03 puts a schema contract on the front door.

👨🏻 Brother Talk — Phase 02, Off the Record

Kafka and Kinesis, the stuff the docs won't tell you because the docs are written by people who want you to succeed and aren't on your pager.


Brother, ingestion is where I've seen the most careers quietly dented, and it's because of one brutal property: you cannot fix ingestion downstream. If you lose an event at the front door, it's gone — no Flink job, no Spark backfill, no clever SQL resurrects it. If you let a garbage schema through, it poisons every table that reads from it. So the stakes here are higher than they look, and the work is less glamorous than Flink or the lakehouse. Do it right anyway. The people who skip the boring durability config are the people who later write the postmortem titled "How We Lost Six Hours of Payments."

Let me give you the hard-won stuff.

Config the durability knobs on day one, in writing. RF=3, acks=all, min.insync.replicas=2, unclean.leader.election=false. Memorise that line like your phone number. The default acks in some clients has changed over the years, and "we used the default" is not a sentence you want in an incident review. And unclean.leader.election — that innocent-looking boolean — is literally the CAP theorem as a config flag. If it's true on a topic that matters, you've signed up to silently lose acknowledged data when the ISR empties. I've seen it default-on bite a team hard. Check it. Write down why it's set the way it is. That's a 10-minute ADR that saves a 6-hour outage.

The poison message will come for you. Not might. Will. Some upstream team will ship a malformed payload, or a null where you expected a value, or an emoji in a field that your deserializer chokes on, and if your consumer does the naive thing — try, fail, retry forever — that one record wedges the whole partition and everything behind it starves. I've watched lag climb into the millions because of one bad record nobody could see. The fix is a mindset: every consumer assumes its input is hostile. Validate, and on failure, shove it in the DLQ with the reason attached and keep moving. Then — and this is the part people forget — alert on the DLQ rate, because a sudden spike in quarantined records is the earliest, cheapest signal that an upstream producer broke their contract. You find out in minutes from a clean alert instead of in days from a confused executive asking why a dashboard looks wrong.

Respect the partition key like it's load-bearing, because it is. Choosing the key feels like a throwaway decision and it's actually one of the most consequential things you'll do. It decides your ordering guarantee (all of one key's events stay ordered) and your skew (pick a low-cardinality key like country or event_type and you've built yourself a hot partition that no amount of hardware fixes). I once inherited a pipeline keyed by event_type with about eight types — eight hot partitions doing all the work while the others idled. The fix wasn't more brokers; it was re-keying. When someone proposes a partition key, immediately ask: "how many distinct values, and is the distribution skewed?" That one question marks you as someone who's been burned.

"Exactly-once" will be on a vendor slide in a meeting you're in. Be ready. Someone will say "we don't need to worry about duplicates, Kafka/Kinesis gives us exactly-once." The junior move is to nod. The senior move is to ask, gently, "exactly-once into where?" Because the guarantee is real inside Kafka (transactions, read_committed) and evaporates the moment you write to S3 or Cassandra or a feature store — which is, you know, the entire point of the pipeline. You re-earn it at the sink with idempotency. Saying this calmly in a meeting, without making anyone feel dumb, is exactly the kind of thing that gets you promoted, because you just prevented a class of bug the whole room was about to wave away.

Keep a raw copy. Always. The single most valuable insurance policy in data engineering is teeing your raw events to cheap storage (S3) the instant they arrive, before any processing, with long retention. Kafka retention is finite (often 7 days), and the day you have a bug in your processing logic that's been running for two weeks, the only thing that saves you is being able to replay from a raw copy that goes back far enough. "We can't reproduce last month's data" is the most demoralizing sentence in this job, and it's almost always caused by someone trusting the stream's retention as their source of truth. The stream is transport. S3 is memory. Don't confuse them.

Now the career note. Ingestion is unsexy, and that is exactly why owning it is a power move. Everyone wants to build the cool Flink job; nobody wants to own the schema-validation gateway and the DLQ redrive tooling and the partition-key standards. So if you become the person who owns the front door — the one who can say "no event is lost, every bad payload is quarantined and visible, and we can replay anything" — you become infrastructure in the org's mind. You're not "the person who built feature X," you're "the person the platform depends on." That's a much harder position to be laid off from, and a much easier one to be promoted from. Boring, load-bearing, and owned by you. That's the play.

Build the broker by hand. When you make the replay test work — seek back to zero and watch the lag jump and the events flow again — you'll feel why replay is the platform's superpower. That feeling is worth more than reading about it ten times.

Next stop, P03: we put a real contract on this front door, because an unvalidated topic is just an outage with a delay timer.

— your brother 👨🏻

Lab 01 — Mini Broker (a faithful partitioned-log)

Phase: 02 — Event Ingestion & Messaging | Difficulty: ⭐⭐⭐⭐☆ | Time: 6–8 hours

You don't understand Kafka until you've built one. This lab implements the Kafka mental model in ~200 lines of pure Python: a partitioned, offset-addressed log with keyed partitioning, absolute monotonic offsets that survive retention, consumer-group offset tracking + replay, partition assignment + rebalancing, log compaction (the changelog→table duality), consumer lag, and a dead-letter-queue path that quarantines poison messages without blocking the pipeline.

What you build

  • partition_for_key — stable hashing → per-key ordering (P01 carried forward)
  • Partition — append-only log with absolute offsets; retain_count / retain_time advance start_offset (records age out) but offsets never renumber
  • compacted_view — fold a keyed log into the latest-value-per-key table; tombstones (value None) delete — the basis of KTables and CDC snapshots
  • assign_partitionsrange and roundrobin assignors; re-running with a different membership is a rebalance
  • ConsumerGroup — committed offsets, poll, commit_through, seek (replay), lag
  • ingest — validate-at-ingress; valid → main log, invalid → DLQ with the reason attached so the pipeline keeps flowing

Key concepts

ConceptWhat to understand
Per-key orderingthe only ordering that exists at scale; chosen by the partition key
Absolute offsetsretention deletes data but offsets stay stable (log_start_offset)
Consumer lagΣ(end − position) — the #1 streaming SLI
Replay = seekrewind offsets; the platform's most valuable capability
Compactiona keyed log is a table; tombstones delete
DLQpoison messages quarantined with a reason; the pipeline never blocks

Run

pip install -r requirements.txt
pytest test_lab.py -v
LAB_MODULE=solution pytest test_lab.py -v

Success criteria

  • All 15 tests pass.
  • You can explain why test_retention_by_count_advances_start_offset asserts end_offset == 10 even after dropping 6 records (offsets never renumber).
  • You can explain why seek(0, 0) makes lag jump back up — and why that's a feature.
  • You can state what the consumer of a compacted topic gets vs a regular topic.

Extensions

  • Add replication: each partition has RF replicas + an ISR; produce(acks="all") only returns once min.insync.replicas have it; kill a replica and watch the ISR shrink (P01 quorum math made concrete).
  • Add idempotent producer semantics (producer id + sequence number) so a retried produce doesn't duplicate (P01 exactly-once carried forward).
  • Add a DLQ replay/redrive: after fixing the validator, re-ingest the DLQ into main and assert the poison records now land.
  • Add sticky assignment and measure how many partitions move on rebalance vs range (the Kafka CooperativeStickyAssignor motivation).

Lab 02 — Partitioned-Log Ingestion in Scala

Phase: 02 — Event Ingestion & Messaging | Difficulty: ⭐⭐⭐⭐☆ | Time: 4–5 hours

The Scala twin of Lab 01's Python mini-broker — the ingestion substrate as a JVM library. Stable partitioning (per-key ordering), monotonic offsets, consumer-group offset tracking

  • replay, retention, log compaction, and a DLQ ingest gateway. Verified with ScalaTest.

What you build

  • Partitioner — 32-bit FNV-1a stable hash → partition (a key always maps to the same partition → per-key ordering survives restarts, unlike a salted runtime hash)
  • PartitionedLog — append (monotonic offsets), read-from-offset, retain (retention), compact (keep latest per key)
  • ConsumerGroup — poll / commit / seek (replay)
  • IngestGateway — validate → append, or route to the DLQ (poison-message safety)

Run

sbt test

Expected: 7 tests, all green in IngestionSpec — including per-key ordering, replay via seek, retention, compaction, and DLQ routing.

Key concepts (Scala expression)

ConceptScala
Per-key orderingpartitionFor(key) is stable → same partition, increasing offsets
Offsetsmonotonic per partition; survive retention
ReplayConsumerGroup.seek(p, offset) re-reads
CompactionLinkedHashMap keeps latest-per-key
DLQinvalid payloads diverted, main log never blocked

Success criteria

  • sbt test → all 7 specs pass.
  • You can explain why a stable hash (not the JVM's hashCode across processes) is required for per-key ordering.
  • You can explain compaction vs retention and when each applies.

Extensions

  • Add consumer rebalancing: N consumers share P partitions; reassign on join/leave.
  • Add idempotent producer semantics (dedup by a producer-supplied sequence number).
  • Add exactly-once commit: commit the consumer offset and the downstream write atomically (the 2-phase-commit idea — P04).

Phase 03 — Serialization, Schema & Data Contracts

Difficulty: ⭐⭐⭐⭐☆ Estimated Time: 2 weeks (28–36 hours) Prerequisites: Phase 02 (the topics these schemas govern)


Why This Phase Exists

The JD dedicates an entire core responsibility to this: "Own schema governance across event streams, lakehouse tables, and analytical datasets … manage Protobuf, Avro, and JSON schema evolution … build compatibility checks … integrate schema registry workflows into CI/CD … prevent breaking changes in event contracts." And the deliverables include a "data contract system integrated into CI/CD" and a "schema governance platform."

Here's the truth that makes this phase matter: the format on the wire is a contract, and an unversioned contract is an outage waiting for a deploy. When a producer and a consumer disagree about the bytes, you get silent corruption, deserialization storms, or a poisoned table — and the producer and consumer are always on different versions, because they're owned by different teams who deploy at different times. Schema governance is how you make that disagreement impossible to ship.

Concepts

  • Why encoding matters: text (JSON/CSV) vs binary (Avro/Protobuf); size, speed, type-safety, and self-description tradeoffs; why analytics wants columnar (Parquet/ORC, P08) but ingestion wants row-oriented schema-carrying formats.
  • Protobuf deeply: field numbers as wire identity, varint/zig-zag/length-delimited wire types, optional/repeated/oneof/map, unknown-field preservation, packed encoding, and the evolution rules (never reuse/repurpose a number; add, don't mutate).
  • Avro deeply: schema-travels-with-(or-near)-the-data, reader vs writer schema resolution, defaults, unions/nullability, and why Avro + a registry is the classic Kafka pairing.
  • JSON Schema: ubiquity and self-description vs cost and weak typing; when "just JSON" is fine and when it's a liability.
  • Compatibility: backward (new reader ↔ old data), forward (old reader ↔ new data), full, transitive variants; which one to pick based on deploy order.
  • Schema registry design: subjects, versions, IDs, the magic-byte wire envelope, and the registry as the source of truth.
  • Data contracts: schema + semantics + SLA + ownership + PII classification; contract testing; producer/consumer compatibility matrices; safe deprecation.
  • CI/CD integration: the registry compatibility check as a pre-merge gate (the lab).

Labs

Lab 01 — Schema Registry & Data-Contract Engine (flagship, implemented)

FieldValue
GoalBuild compatibility checking (backward/forward/full), Protobuf field-number rules, an error-accumulating Validated validator, and a registry that rejects breaking changes on register — the CI gate
ConceptsSchema evolution rules, Protobuf wire identity, applicative validation, registry-as-gate
How to Testpytest test_lab.py -v — 17 tests
Talking PointsWhy is "remove field" backward-OK but forward-broken? Why accumulate errors? Which mode for consumer-first vs producer-first deploys?
Resume bulletBuilt a schema-registry compatibility engine (backward/forward/full + Protobuf number rules) and an accumulating data-contract validator wired as a pre-merge CI gate

→ Lab folder: lab-01-schema-registry/

Lab 02 — Schema Compatibility & Contracts in Scala (implemented, sbt test)

FieldValue
GoalThe JVM-library form of Lab 01: backward/forward/full compatibility + Protobuf field-number rules, plus Cats Validated record validation that accumulates every violation
ConceptsCompatibility modes, Protobuf tag safety, applicative (accumulating) validation — in real Scala
How to Testsbt test — 11 ScalaTest specs (runs here)
Resume bulletBuilt a Scala schema-governance library: compatibility checks, Protobuf tag rules, and Cats-Validated accumulating record validation

→ Lab folder: lab-02-scala-contracts/

Extension Project A — Real Protobuf round-trip (spec)

Define a .proto, generate code, serialize/deserialize, then evolve it (add a field, change a number — observe the break) and verify unknown-field preservation across versions.

Extension Project B — Contract test in CI (spec)

Wire buf breaking (Protobuf) or the Confluent registry's compatibility check into a GitHub Action that fails a PR introducing an incompatible change. Produce the producer/consumer compatibility matrix as a build artifact.

Integrated-Scenario Hooks

  • This phase's validation is the ingress check the P02 DLQ routes failures from.
  • Its PII classification field-tags feed the governance platform (P14).
  • Its compatibility matrix is the centerpiece of the Hive/Flume → modern migration (P15).
  • Its Validated is the real Cats abstraction you implement in Scala (P12).

Guides in This Phase

Key Takeaways

  • The wire format is a contract; version it or ship outages on a delay timer.
  • Backward vs forward is decided by who deploys first, not by taste.
  • Protobuf identity is the number; Avro leans on names + defaults + reader/writer resolution.
  • Enforce contracts in CI, accumulate all violations, and tag PII at the schema.

Deliverables Checklist

  • Lab 01 implemented; all 17 tests pass
  • You can state the add/remove/type-change rules for backward, forward, and full
  • You can explain Protobuf number reuse danger and unknown-field preservation
  • Extension A round-trip or Extension B CI gate

Warmup Guide — Serialization, Schema & Data Contracts

Zero-to-principal primer for Phase 03: how bytes carry meaning (Protobuf, Avro, JSON), why field numbers and reader/writer resolution exist, the precise compatibility rules, the schema registry, and how a data contract becomes a CI gate that makes breaking changes un-shippable.

Table of Contents


Chapter 1: Encoding — Turning Structure into Bytes

A record in memory is a graph of typed values; the wire is a flat sequence of bytes. Encoding is the rule that maps between them, and the choice has consequences:

  • Text formats (JSON, CSV): self-describing (field names travel with every record), human-readable, ubiquitous — but large (field names repeated on every record), slow to parse, and weakly typed (is "42" a number? is the date a string?). Great for config and debugging; expensive at billions of events/day.
  • Binary, schema-on-write (Avro, Protobuf): compact, fast, strongly typed — but you need the schema to read the bytes. This is the right default for high-volume ingestion.
  • Columnar (Parquet, ORC — P08): a storage layout for analytics (read few columns over many rows), not an ingestion wire format. Different job.

The principal framing: ingestion wants row-oriented schema-carrying binary (Avro/Protobuf); analytics wants columnar (Parquet); JSON is the convenient default you grow out of. Picking the wrong one shows up as cost (JSON at scale) or pain (CSV's no-types nightmare).

Chapter 2: Protobuf in Working Detail

Protocol Buffers is the JD's preferred event payload, and its design is its evolution story. A message is a set of fields, each with a number (the tag), a type, and a name:

message Order {
  string order_id = 1;       // the "1" is the wire identity, not the name
  double amount   = 2;
  optional string currency = 3;
  repeated string tags = 4;
}

The wire format: each field is encoded as (field_number << 3 | wire_type) then the value. Wire types: varint (int32/64, bool, enum — 7 bits/byte, smaller numbers = fewer bytes), zig-zag (sint, so negatives don't cost 10 bytes), 64-bit/32-bit fixed, and length-delimited (strings, bytes, embedded messages, packed repeated). Consequences a principal knows:

  • The number is everything. Renaming a field is free (names aren't on the wire); changing or reusing a number re-interprets old bytes as the wrong field — silent corruption. The cardinal rule: never reuse or repurpose a field number; reserve retired ones.
  • Adding a field is safe if you give it a new number; old readers don't know it and skip it.
  • Unknown-field preservation: a Protobuf reader keeps fields it doesn't recognise and can re-serialize them intact — so an intermediary on an old schema doesn't drop data added by a newer producer. This is huge for multi-hop pipelines.
  • optional vs implicit (proto3): proto3 scalars have no "was it set?" by default (a missing int reads as 0, indistinguishable from an explicit 0); optional brings back presence tracking. repeated defaults to empty (never "missing"). oneof models mutually exclusive fields. map is sugar for repeated key/value.
  • Enums: add symbols safely (with a known default at 0); removing a symbol or changing a number is breaking. Always reserve a 0 = UNKNOWN.

The lab models the number-as-identity rule (protobuf_violations): a number whose type changed is flagged because it breaks every byte ever written with it.

Chapter 3: Avro and Reader/Writer Schema Resolution

Avro takes a different tack: the data is encoded against a writer schema, and the reader supplies its own reader schema; Avro resolves the two at read time:

  • Fields present in the writer but not the reader → ignored. Fields in the reader but not the writer → filled from the reader's default (so a reader field must have a default to be added safely). This resolution is exactly why Avro compatibility is defaults-driven.
  • Avro is fully self-describing if the schema travels with the file (object-container files embed it) — but in Kafka the schema is too big to ship per-record, so you ship a tiny schema ID and resolve it via a registry (Ch. 6).
  • Unions (["null", "string"]) model nullability; the order matters (the first is the default branch).

Avro vs Protobuf, the honest comparison: Avro's reader/writer resolution + defaults are elegant for data files and the Kafka+registry world; Protobuf's number-based identity + codegen + unknown-field preservation are excellent for RPC and multi-language services and multi-hop event pipelines. Both are correct choices; the JD names Protobuf as primary, so know it cold and know Avro well.

Chapter 4: JSON, JSON Schema, and When Text Is Fine

JSON's superpower is that everything speaks it and a human can read it. JSON Schema adds a validation contract (types, required, ranges, patterns, enums). It's the right call when: volume is modest, consumers are heterogeneous/external, debuggability matters more than bytes, or you're prototyping. It's a liability when: you're at billions of events/day (the repeated keys and parse cost dominate), you need strong cross-language typing, or you need compact binary. Principals don't sneer at JSON — they cost it out and switch to Avro/Protobuf when the numbers say so, keeping a JSON Schema contract either way.

Chapter 5: Compatibility — Backward, Forward, Full

The single most important operational idea in this phase, because producers and consumers are always on different versions. Definitions (precise, the way the lab implements them):

  • Backward compatible: a reader on the new schema can read data written with the old schema. → Consumers can upgrade first. You may add fields with defaults and remove fields; you may not add a required (no-default) field (old data lacks it) or change a field's type.
  • Forward compatible: a reader on the old schema can read data written with the new schema. → Producers can upgrade first. You may add fields (old readers ignore them) and remove optional fields; you may not remove a required field (old reader needs it) or change a type.
  • Full: both. → Only add or remove optional fields, never change types. The safest, and what you want for a contract many independent teams share.
  • Transitive variants check against all prior versions, not just the latest — stricter, prevents "compatible with N but not N−2" drift.

The decision rule: pick the mode from your deploy order. If consumers roll out before producers, you need backward; if producers roll first, forward; if you can't coordinate (the usual case at scale), full. Note the pleasing symmetry: remove is backward-safe/forward-dangerous; add-required is forward-safe/backward-dangerous. The lab's tests pin exactly this.

Chapter 6: The Schema Registry

A registry is a service that makes schemas first-class:

  • Subjects & versions: schemas are registered under a subject (often <topic>-value); each registration is a new version with a global ID.
  • The wire envelope: instead of shipping the schema per message, the producer ships a magic byte + the 4-byte schema ID, then the payload; the consumer fetches & caches the schema by ID. Tiny overhead, full resolution.
  • The compatibility gate: when you register a new version, the registry checks it against the subject's compatibility mode and rejects a breaking change. This is the lab's register() — and wired into CI, it's how "no breaking schema change reaches production without compatibility checks" (a JD SLO) becomes literally true.
  • Confluent Schema Registry, AWS Glue Schema Registry, and Buf Schema Registry are the market implementations; the concept is what you must own.

Chapter 7: Data Contracts — Beyond the Schema

A schema says what shape; a data contract says everything a consumer needs to depend on the producer safely:

  • Schema (types, fields) + compatibility policy.
  • Semantics: what the fields mean — units, ranges, invariants (an amount is in minor currency units and is non-negative; a status is one of these enum values). These are the semantic rules the lab's Validated enforces beyond mere types.
  • SLA / freshness: how often, how late, how complete (handoff to P13's SLOs).
  • Ownership: a named team, on a pager, accountable for the contract.
  • PII classification: each field tagged by sensitivity (none/pii/sensitive) to drive masking, access, and retention downstream (handoff to P14).
  • Lifecycle / deprecation: how a field or version is announced, deprecated, and removed over versions — never yanked.

The shift: a data product (P00) is a dataset with a contract. Contracts turn "that table broke and nobody knew who owned it" into "the contract test failed in the producer's CI before merge."

Chapter 8: Validation, Accumulation, and Contract Testing in CI

Two distinct enforcement points:

  • Schema-evolution checks (build time): does the new schema break the old one? → the registry/buf breaking gate (Ch. 5–6). Catches contract regressions before merge.
  • Record validation (runtime, at ingress — P02): does this record satisfy the schema + semantics? Failures → DLQ. Here the accumulation property matters: a fail-fast validator (Either/exceptions) reports the first error and stops, so the producer fixes one thing, re-runs, finds the next — a slow loop. An accumulating validator (Cats Validated, the lab's) reports all violations at once, so the producer fixes everything in one pass. For data contracts, accumulation is the correct shape, and it's why P12 builds Validated for real.

Contract testing closes the loop: the consumer publishes the contract it depends on; the producer's CI runs the consumer's expectations against the producer's output (Pact-style), so a breaking change fails the producer's build — shifting the failure left from prod to PR.

Lab Walkthrough Guidance

Lab 01 — Schema Registry & Contract Engine, suggested order:

  1. backward_violations — add-no-default breaks; remove is fine; type change breaks.
  2. forward_violations — remove-required breaks; add is fine; type change breaks (note the symmetry with backward).
  3. compatibility_violations — dispatch on mode; full = both.
  4. protobuf_violations — by number; type change on a shared number is wire-breaking.
  5. Validated.combine + _type_ok (bool is not int!) + validate_record — accumulate all errors; unknown fields allowed.
  6. SchemaRegistry.register — the gate: compatible → version bump; breaking → raise and don't store.

Success Criteria

You are ready for Phase 04 when you can, from memory:

  1. Contrast Protobuf, Avro, and JSON and say which you'd pick for ingestion vs analytics vs external APIs.
  2. State the Protobuf number rule and what unknown-field preservation buys you.
  3. Explain Avro reader/writer resolution and why defaults drive its compatibility.
  4. Give the add/remove/type-change rules for backward, forward, and full — and pick a mode from a deploy order.
  5. Describe the registry's wire envelope and the compatibility gate.
  6. List the parts of a data contract beyond the schema (semantics, SLA, ownership, PII).
  7. Explain why a contract validator should accumulate errors.

Interview Q&A

Q: You must add a mandatory tenant_id to an event consumed by 30 teams. How? Adding a required field is backward-incompatible — old data lacks it, so new consumers can't read history and the registry (rightly) rejects it under backward/full. The safe path: add it as optional with a default (or a sentinel like UNKNOWN), deploy producers to start populating it, let consumers adopt it at their pace, monitor coverage, and only later — if ever — tighten validation to "must be present going forward" via a new major version or a runtime contract rule rather than a schema-required flag. The principal point: "mandatory" is a rollout, not a flag flip, when 30 teams and historical data are involved.

Q: A producer renamed a Protobuf field and consumers broke. Why, if names aren't on the wire? They didn't just rename — almost certainly they reused or shifted a number, or regenerated code such that a number now maps to a different field/type. On the wire Protobuf is keyed by number; a rename alone is invisible and safe, but if currency (number 3) became region (number 3) with a different type, every old byte for "3" now deserializes as the wrong thing. The fix is the cardinal rule: never reuse a number — reserve the old one and give the new field a fresh number.

Q: Backward vs forward — how do you choose? By deploy order. If I roll consumers out before producers (so new readers must handle old data), I need backward. If producers roll first (old readers must tolerate new data), forward. At scale I usually can't coordinate the order across many teams, so I default to full (only add/remove optional fields, never change types) and use transitive mode to stop drift against older versions. The mnemonic: remove is backward-safe but forward-risky; add-required is forward-safe but backward-risky.

Q: Why should a data-contract validator accumulate errors instead of failing fast? Because the consumer of the result is a producer fixing their data, and the humane, fast loop is "here are all 6 things wrong, fix them and resubmit" — not "fix one, resubmit, find the next, repeat 6 times." Fail-fast (Either/exceptions) is right for dependent steps where step 2 is meaningless if step 1 failed; validation steps are independent, so the applicative Validated that accumulates is the correct abstraction (I build it in Scala in P12).

References

🛸 Hitchhiker's Guide — Phase 03: Serialization, Schema & Data Contracts

Read this if: you want Protobuf/Avro/JSON and the compatibility rules in your head fast. Skim, then read the WARMUP, then build the registry.


0. The 30-second mental model

The bytes on the wire are a contract between a producer and a consumer who are always on different versions. Schema governance makes a breaking change un-shippable: you encode in Protobuf/Avro (compact, typed), register the schema in a registry, and a compatibility gate rejects changes that would break the other side. One sentence: version the contract, pick the compatibility mode from your deploy order, and enforce it in CI.

1. Format cheat sheet

FormatShapeUse it forWatch out
Protobufbinary, number-keyed, codegenevents, RPC, multi-hopnever reuse a number
Avrobinary, reader/writer resolutionKafka+registry, data filesdefaults drive compat
JSON (+Schema)text, self-describingexternal APIs, prototypes, low volumebig & slow at scale
Parquet/ORCcolumnar storage (P08)analytics scansnot a wire format

2. Protobuf in 6 bullets

  • The number is the wire identity; the name is not. Rename freely; never reuse or change a number's type → silent corruption.
  • Add fields with new numbers → old readers skip them (safe).
  • Unknown-field preservation: old readers keep & re-emit fields they don't know → safe multi-hop.
  • proto3 scalars have no presence (missing == 0); use optional for presence; repeated defaults empty.
  • Enums: reserve 0 = UNKNOWN; add symbols safely; don't remove/renumber.
  • Wire types: varint (small ints cheap), zig-zag (sint for negatives), length-delimited (strings/messages/packed).

3. Avro in 4 bullets

  • Data is written with a writer schema, read with a reader schema; Avro resolves them.
  • Reader field not in writer → needs a default (so adds are safe only with defaults).
  • Writer field not in reader → ignored.
  • In Kafka you ship a schema ID (magic byte + 4 bytes), not the whole schema.

4. Compatibility — the symmetry to memorise

backward  = new READER reads OLD data   → consumers upgrade first
            ✅ add-with-default, remove        ❌ add-required, change type
forward   = old READER reads NEW data   → producers upgrade first
            ✅ add, remove-optional            ❌ remove-required, change type
full      = both → only add/remove OPTIONAL fields; never change types

Mnemonic: remove is backward-safe / forward-risky; add-required is forward-safe / backward-risky. Pick the mode from who deploys first (can't coordinate → full).

5. Registry = the CI gate

producer ships:  [magic byte][4-byte schema ID][payload]
consumer:        fetch+cache schema by ID → resolve
register(new):   check vs latest under mode → REJECT if breaking  ← the gate

Wired into CI, this makes "no breaking schema change reaches prod" literally true.

6. A data contract is more than a schema

schema (types) + compatibility policy
+ semantics (units, ranges, invariants)   ← the Validated semantic rules
+ SLA / freshness                          → P13 SLOs
+ ownership (a team on a pager)
+ PII classification (per field)           → P14 masking/access/retention
+ deprecation lifecycle (announce, deprecate, remove — never yank)

7. Validate by ACCUMULATING

Fail-fast (Either/exceptions) → "fix one error, resubmit, find the next." Accumulating (Validated) → "here are all 6, fix them at once." For contracts, accumulate. (You build Validated for real in Scala in P12.)

8. Beginner mistakes that mark you

  1. Reusing/renumbering a Protobuf field → silent corruption.
  2. Adding a required field and calling it backward-compatible.
  3. Thinking "rename broke it" when it was a number change.
  4. Shipping JSON at billions/day without costing it (key repetition + parse).
  5. No registry / no CI gate → breaking changes discovered in prod.
  6. A schema with no owner, no SLA, no PII tags → not a contract, just a shape.
  7. Fail-fast validation that makes producers fix errors one at a time.

9. War stories

  • "Half the events deserialize as garbage after a deploy." → a field number got reused. Reserve retired numbers; add buf breaking to CI.
  • "New consumer can't read last year's data." → someone added a required field (backward break) and backfilled nothing. Use optional-with-default.
  • "A PII column leaked into an open table." → no PII tags on the schema; governance had nothing to enforce on (P14).

10. How this phase pays off later

  • Validation = the P02 DLQ's reason-for-rejection.
  • Validated = real Cats code in P12.
  • PII tags = the input to P14 governance.
  • Compatibility matrix = the heart of the P15 migration ADR.

Read the WARMUP, build the registry gate, then P04: Flink turns these validated events into stateful, exactly-once results.

👨🏻 Brother Talk — Phase 03, Off the Record

Schemas and contracts — the least glamorous phase, the one that quietly prevents more 2 a.m. pages than any other. Let me tell you why to care.


Brother, I'm going to be honest: schema governance sounds like the most boring possible topic. "Compatibility modes." "Field numbers." Your eyes want to glaze. Mine did too. And then I lived through the alternative, and now I evangelize this stuff like a convert, because unversioned schemas are the single most common cause of self-inflicted data outages I've ever seen, and they're 100% preventable.

Here's the scene that converted me. A producer team — good engineers, well-meaning — renamed a field and shipped it on a Tuesday afternoon. They tested it. It worked for them. What they didn't know is that under the hood a field number got shuffled, and forty downstream consumers — analytics jobs, ML features, a fraud model, a finance report — started silently deserializing garbage. Not crashing. Silently wrong. The fraud model degraded. The finance numbers drifted. It took days to trace it back, because the failure was nowhere near the cause. That entire week of pain, across a dozen teams, would have been a single red X on a pull request if a compatibility check had been wired into CI. One red X versus a week of cross-team archaeology. That's the whole pitch for this phase.

So let me give you the stuff that matters.

"It worked when I tested it" is the most dangerous sentence in data engineering. A producer tests against the current consumer, in the current deploy, with fresh data. But the real world has consumers on six different versions, historical data written months ago, and replay jobs reading last year's bytes. The schema contract is the only thing that reasons about all of those at once. When you internalize that producer and consumer are never on the same version — that they literally cannot be, at any real org — the whole idea of compatibility modes stops being academic and becomes obviously necessary.

Make the breaking change impossible to merge, not merely discouraged. Here's the thing about humans: if a rule is a wiki page, it gets violated. If a rule is a failing CI check, it gets followed. The entire value of a schema registry isn't the storage — it's that register() rejects incompatible changes, and when you wire that into the producer's CI, breaking the contract becomes literally un-shippable. You're not asking people to be careful. You're making carelessness impossible. That's the principal move: don't write a guideline, build a guardrail.

Field numbers are sacred. Treat retiring one like retiring a jersey. In Protobuf, the number is the identity. When you remove a field, reserve its number so nobody ever reuses it. I cannot tell you how many corruptions trace to "oh, number 7 was free, I'll use it" when number 7 was actually a retired field still present in old data and replay streams. Reserve it. Comment it. Never reuse it. This one discipline prevents a whole category of silent corruption.

Tag PII at the schema, today, even if nobody's asked. This is the unglamorous favor you do your future self. When you define an event, mark which fields are PII right there in the contract. Why now, when there's no governance system yet to consume the tags? Because P14's governance platform — masking, access control, GDPR deletion — is infinitely easier to build when the PII is already labeled at the source than when you have to go back and classify ten thousand fields across a thousand schemas under a compliance deadline. The tag costs you ten seconds at definition time and saves a future team a quarter of misery. Do it.

Accumulate your errors, because the human on the other end is a person fixing data. When you validate a record and it has six problems, tell them all six. The fail-fast pattern — report one error, stop — is fine for a compiler but cruel for a data producer, who then fixes one thing, resubmits, finds the next, and slowly loses their mind. The Validated pattern in this lab (and for real in Scala in P12) exists because someone realized validation is about helping a human fix everything at once. Small thing. Huge difference in how much people hate or love your platform.

Now the career angle, because it's real. The person who owns schema governance becomes the person who prevents the org's most embarrassing outages, and that's a reputation worth having. It's not flashy — nobody throws a party because a breaking change got caught in CI instead of in prod — but the senior people notice. They notice that since you built the gate, the "mystery data corruption" incidents dropped to near zero. They notice that onboarding a new consumer is now safe because contracts are explicit. Quiet competence in the load-bearing, unglamorous places is exactly what gets you trusted with the architecture decisions. The flashy stuff gets you applause; the boring guardrails get you authority.

Build the registry gate. Make a breaking change fail the test. Feel how good it is to know — not hope — that the bad change can't ship. Then come to P04, where we take these clean, validated, contract-checked events and do the genuinely hard thing: stateful, exactly-once, event-time stream processing with Flink.

— your brother 👨🏻

Lab 01 — Schema Registry & Data-Contract Engine

Phase: 03 — Serialization, Schema & Data Contracts | Difficulty: ⭐⭐⭐⭐☆ | Time: 5–7 hours

"A topic without a schema is an outage on a delay timer." This lab builds the machinery that keeps a thousand producers from breaking a thousand consumers: compatibility checking (backward / forward / full), Protobuf field-number rules, an error-accumulating Validated validator, and a registry that rejects breaking changes on register() — i.e. the CI gate that the JD requires.

What you build

  • Schema / Field — fields with Protobuf-style numbers and Avro-style names/defaults/required (so you learn both evolution models in one model)
  • backward_violations / forward_violations / compatibility_violations — the exact rules: add-with-default is safe; add-required breaks backward; remove-required breaks forward; type change breaks both
  • protobuf_violations — wire identity is the number; a number whose type changed corrupts existing bytes
  • Validated — applicative, error-accumulating validation (collect every contract violation in one pass, not fail-fast) — the Cats Validated you build for real in P12
  • SchemaRegistry — versioned schemas + a compatibility mode; register is the pre-merge CI check

Key concepts

ConceptWhat to understand
Backwardnew readers read old data → can't add a no-default field
Forwardold readers read new data → can't remove a required field
Fullboth → only add/remove optional fields, never change types
Protobuf numbersrenaming is free; changing a number's type breaks the wire
Validatedaccumulate ALL errors so producers fix everything at once
Registry = CI gatebreaking changes are rejected before merge, not in prod

Run

pip install -r requirements.txt
pytest test_lab.py -v
LAB_MODULE=solution pytest test_lab.py -v

Success criteria

  • All 17 tests pass.
  • You can explain why "remove a field" is backward-compatible but forward-incompatible.
  • You can explain why Validated accumulates instead of failing on the first error, and why that's the right shape for a data contract.
  • You can state which compatibility mode you'd set for an event topic where consumers deploy before producers (forward) vs after (backward).

Extensions

  • Add transitive compatibility (check the new schema against all prior versions, not just the latest) — the stricter BACKWARD_TRANSITIVE mode.
  • Add enum evolution rules (adding a symbol is forward-safe with a default; removing one isn't) and nullable vs optional distinctions.
  • Generate a producer/consumer compatibility matrix (which writer versions which reader versions can read) — the artifact you put in a migration ADR (P15).
  • Wire it as a real pre-commit hook: fail the build if a .proto/.avsc change is incompatible with the registered latest.

Lab 02 — Schema Compatibility & Data Contracts in Scala

Phase: 03 — Serialization, Schema & Data Contracts | Difficulty: ⭐⭐⭐⭐☆ | Time: 4–5 hours

The Scala twin of Lab 01's Python registry — the schema-governance logic a JVM platform team ships as a library. Compatibility checking (backward/forward/full + Protobuf field-number rules) plus Cats Validated record validation that accumulates every violation in one pass. Verified with ScalaTest via sbt test.

What you build

  • Schema / Field — a schema model with Protobuf field numbers and the hasDefault rule that makes add/remove safety mechanical
  • CompatibilitybackwardViolations, forwardViolations, compatibilityViolations (none/backward/forward/full), and protobufViolations (reused tag + changed type = silent byte corruption)
  • Validation.validateRecord — Cats ValidatedNel validation accumulating missing-required, type-mismatch, and semantic-rule failures (a producer fixes them all at once)

Run

sbt test

First run downloads Scala 2.13 + Cats + ScalaTest, then compiles and runs. Expected: 11 tests, all green across CompatibilitySpec and ValidationSpec.

Key concepts (Scala expression)

ConceptScala
Backward compatnew reader, old data → added field needs a default
Forward compatold reader, new data → removed field needed a default
Protobuf tagsfield NUMBER is the wire identity; reuse+retype corrupts bytes
Accumulating validationValidated mapN/fold collects all errors (vs Either fail-fast)

Success criteria

  • sbt test → all 11 specs pass.
  • You can explain why a renamed field with the same number+type is Protobuf-safe but a retyped reused number is catastrophic.
  • You can explain why validateRecord uses Validated (accumulate) not Either (fail-fast).

Extensions

  • Add enum evolution rules (adding a symbol is backward-safe; removing isn't).
  • Add a CI gate function: assertCompatible(old, new, mode) that throws with all violations — the pre-merge check (P03 WARMUP Ch. 7).
  • Replace typ: String with a sealed FieldType ADT for exhaustive, typo-proof matching (P12's make-illegal-states-unrepresentable).

Phase 04 — Apache Flink

Difficulty: ⭐⭐⭐⭐⭐ Estimated Time: 2 weeks (35–45 hours) Prerequisites: Phase 01 (time, exactly-once), Phase 02 (the source), Phase 03 (validated events)


Why This Phase Exists

Flink is the JD's reference engine for "low-latency stream processing … stateful operators … checkpointing … savepoints … exactly-once … RocksDB state backend … rescaling." It is also the subject of an entire interview round (Round 2: "a Flink job with rising checkpoint duration, increasing RocksDB state size, delayed watermarks, and growing Kafka lag — diagnose it"). If you can reason about Flink's execution and state model, you can reason about every stream processor; the others (P05) are variations.

This is the hardest phase in the streaming half, because correct stateful streaming is genuinely hard: you must get event time, out-of-order arrival, state, fault tolerance, and exactly-once output all correct simultaneously. The lab makes you implement the core, so the words "checkpoint" and "exactly-once sink" stop being slogans and become code you've debugged.

Concepts

  • Execution model: job graph → operators → task slots & parallelism; the streaming dataflow; keyBy → keyed streams; chaining; the JobManager/TaskManager split.
  • Time: event time vs processing time vs ingestion time; watermarks (how generated, per-partition watermarks and the min-across-inputs rule, idle sources); allowed lateness; side outputs.
  • Windows: tumbling, sliding, session (merging), global; window assigners, triggers, evictors; the state cost of windows.
  • State: keyed state vs operator state; ValueState/ListState/MapState; state backends (heap vs RocksDB, incremental checkpoints); state TTL; why state size drives everything.
  • Fault tolerance: the Chandy-Lamport distributed snapshot; barriers & alignment (and unaligned checkpoints); checkpoints (engine-owned) vs savepoints (user-owned, portable); exactly-once vs at-least-once checkpointing.
  • Exactly-once sinks: idempotent sinks vs two-phase commit (pre_commit on barrier, commit on completion); transactional Kafka/file sinks; the Iceberg/Delta sink (P09).
  • Rescaling: key groups as the redistribution unit; max parallelism; restoring a savepoint at a new parallelism.
  • Backpressure (P01): credit-based flow control; the symptoms — rising checkpoint duration, alignment time, and lag — and their causes.
  • Flink SQL / Table API: declarative streaming; dynamic tables; the streaming-batch unification.

Labs

FieldValue
GoalBuild event-time windows + watermarks + allowed lateness + side output, keyed state with snapshot/restore (checkpoint recovery), key-group rescaling, and a two-phase-commit exactly-once sink
ConceptsWatermark-driven emission, checkpoint/savepoint, key groups, two-phase commit
How to Testpytest test_lab.py -v — 15 tests incl. restore-continuity and crash-recovery exactly-once
Talking PointsWhat makes a checkpoint consistent? Why is output invisible until commit? Why key groups for rescaling?
Resume bulletImplemented event-time windowing, checkpoint/restore state recovery, key-group rescaling, and a two-phase-commit exactly-once sink

→ Lab folder: lab-01-flink-engine/

Lab 02 — Event-Time Stream Processing in Scala (implemented, sbt test)

FieldValue
GoalReimplement the event-time core (tumbling windows, watermark, allowed lateness, side output, dedup) in idiomatic Scala, verified with ScalaTest — the JVM/Scala form of stream processing interviewers expect
ConceptsThe same Flink semantics, expressed in Scala (case classes, immutable Acc, keyed processor)
How to Testsbt test — 6 specs incl. order-independence under shuffle (real Scala, runs here)
Resume bulletImplemented event-time windowing with watermarks, allowed lateness, and exactly-once dedup in production-style Scala (ScalaTest-verified)

→ Lab folder: lab-02-scala-windows/

docker Flink cluster; a DataStream job consuming the P02 Kafka topic; keyed windows; a RocksDB state backend; take a savepoint, change the job, restore from the savepoint; observe the Flink UI's backpressure, watermark, and checkpoint metrics.

Extension Project B — Stateful fraud detector (spec; → capstone)

The JD's Problem 2: event-time sliding + session windows, broadcast rules state, Cassandra enrichment (P11), RocksDB, S3 checkpoints, savepoint-based rule upgrades, replay for validation. Sketch the topology and the state/keying plan.

Integrated-Scenario Hooks

  • This engine consumes P02's validated topic and writes P09's Iceberg tables via the 2PC sink.
  • Its checkpoints live in S3 (P08); its state backend is RocksDB (an LSM, P08/P11).
  • Its enrichment reads Cassandra (P11); its rules arrive as broadcast state.
  • It is the processing core of capstone Problems 1 and 2 (P16).

Guides in This Phase

Key Takeaways

  • Watermarks turn "when is a window done?" into an explicit, tunable answer.
  • A checkpoint is a consistent global snapshot; restore resumes with exact state — that's exactly-once state.
  • Exactly-once output needs idempotent or two-phase-commit sinks; the engine alone can't.
  • State size is the cost center; RocksDB, TTL, and key design exist to manage it.
  • Rescaling works because key groups (not keys) are the redistribution unit.

Deliverables Checklist

  • Lab 01 implemented; all 15 tests pass
  • You can explain checkpoint barriers, alignment, and what stalls them
  • You can explain the two-phase-commit sink protocol end to end
  • You can diagnose the Round-2 scenario (rising checkpoint duration + state + lag)
  • Extension A savepoint round-trip or Extension B topology sketch

Warmup Guide — Apache Flink

Zero-to-principal primer for Phase 04: Flink's execution and state model, event time and watermarks, the distributed-snapshot checkpoint algorithm, savepoints and rescaling, exactly-once sinks, and how to read the failure signatures interviewers love (rising checkpoint duration + growing state + delayed watermarks + lag).

Table of Contents


Flink runs a streaming dataflow: a directed graph of operators (sources → transforms → sinks) processing records one at a time (not micro-batches — that's Spark, P05). The runtime pieces:

  • JobManager (the coordinator): builds the execution graph, schedules tasks, triggers checkpoints, coordinates recovery. TaskManagers (the workers): run operator subtasks in task slots (a slot = a unit of resource isolation; one slot can run a pipeline of chained operators).
  • Parallelism: each operator has N parallel subtasks; records are distributed across them. keyBy(k) partitions the stream by key (hash) so all records for a key go to the same subtask — keyed streams are where keyed state lives, and the per-key ordering of P01 applies.
  • Operator chaining: Flink fuses adjacent operators (e.g. map→filter) into one task to avoid serialization/handoff — a real performance lever and a thing to know when reading the job graph.
  • Sources/sinks: connectors (Kafka, Kinesis, files, Iceberg). Modern sources implement a unified interface with split enumeration and per-split watermarks (Ch. 2).

Chapter 2: Event Time and Watermarks, Precisely

P01 established why event time (wall clocks lie). Flink's implementation:

  • Each event carries a timestamp (extracted at the source). A WatermarkStrategy generates watermarks — typically boundedOutOfOrderness(d): W = max_ts_seen − d. The watermark is a record-like marker flowing with the stream that asserts "no event with ts ≤ W is still coming."
  • Per-partition watermarks + the min rule: each source partition tracks its own watermark; an operator with multiple inputs takes the minimum across them (the slowest input gates progress). This is why a single idle partition can stall watermarks org-wide — Flink offers withIdleness(...) to let idle partitions be ignored. A classic prod incident: "watermarks stopped advancing" → one partition went silent.
  • Allowed lateness: after a window fires at the watermark, keep it for allowedLateness to absorb stragglers (re-emitting corrections); beyond that, route to a side output (OutputTag) — late data is information, never silently dropped.
  • The triangle (P01): larger d = more complete first results but higher latency and more retained state; you measure the real out-of-order distribution and choose d against it.

Chapter 3: Windows and Triggers

Windows turn an unbounded stream into bounded computations:

  • Tumbling (fixed, non-overlapping), sliding/hopping (fixed size, overlapping — one event in size/slide windows, multiplying state), session (gap-based, dynamic boundaries that merge when a late event bridges two sessions — the trickiest), and global (one window, you supply the trigger).
  • Assigner → trigger → (evictor) → function: the assigner places events in windows; the trigger decides when to fire (default: on watermark past window end; can fire early on processing time or counts); the function aggregates (reduce/aggregate are incremental and cheap; process buffers all elements and is expensive).
  • State cost: open windows × keys × accumulator size. This is the number behind "our RocksDB state is growing" — usually too many open windows (huge allowed lateness, sliding windows, or unbounded key cardinality). The lab keeps an explicit lifecycle (open→closeable→expired) for exactly this reason.

Chapter 4: State and State Backends

State is what makes streaming powerful and dangerous:

  • Keyed state (scoped to the current key): ValueState, ListState, MapState, ReducingState, AggregatingState. Operator state (scoped to a subtask): used by sources/sinks (e.g. Kafka offsets). Broadcast state: a low-throughput stream (rules, config) replicated to every subtask — the fraud-rules pattern.
  • State backends: HashMapStateBackend (state on the JVM heap — fast, but bounded by memory, full snapshots) vs EmbeddedRocksDBStateBackend (state in an embedded LSM tree on local disk — supports state far larger than memory, incremental checkpoints, at the cost of serialization + disk I/O per access). The choice is "does my state fit in heap?" — large keyed state ⇒ RocksDB.
  • RocksDB specifics that show up on-call: it's an LSM (P08/P11) so writes go to memtables then SST files with background compaction; state grows when compaction can't keep up or when TTL isn't set; read amplification from many SST levels. State TTL expires old entries so state doesn't grow unbounded (the bounded-dedup idea of P01, as a feature).
  • The lab's snapshot()/restore() is a state backend's job: serialize all keyed state to a durable structure and rebuild it.

Chapter 5: Checkpoints — The Distributed Snapshot

How do you snapshot a distributed, running dataflow consistently, without stopping it? The Chandy-Lamport asynchronous barrier snapshotting algorithm — Flink's crown jewel:

  1. The JobManager injects a checkpoint barrier (numbered N) into the source streams.
  2. Barriers flow with the data. When an operator receives barrier N on all its inputs (alignment), it snapshots its state (asynchronously, to durable storage — S3) and forwards barrier N downstream.
  3. When all operators (including sinks) confirm checkpoint N, the JobManager marks it complete. On failure, the whole job restarts from the last complete checkpoint — every operator restores its state and sources rewind to the offsets in that checkpoint.

Key subtleties:

  • Alignment is where exactly-once is bought and where it can hurt: an operator must wait for the barrier on every input, buffering the faster inputs. Under skew or backpressure, alignment time balloons → rising checkpoint duration (the Round-2 sym ptom). Unaligned checkpoints trade some of this by snapshotting in-flight data, helping under backpressure.
  • Exactly-once vs at-least-once checkpointing: at-least-once skips alignment (lower latency) but can double-count on recovery. Exactly-once aligns.
  • Because sources rewind to checkpointed offsets and operators restore exact state, the effect is exactly-once within the Flink job — output exactly-once still needs Ch. 7.
  • The lab's checkpoint-recovery test (restore-and-continue == one run) is this property in miniature.

Chapter 6: Savepoints, Upgrades, and Rescaling

  • Checkpoint vs savepoint: same mechanism, different ownership. Checkpoints are automatic, engine-owned, optimized for fast recovery, and may be cleaned up. Savepoints are manual, user-owned, portable, self-contained snapshots you take on purpose to upgrade code, change parallelism, migrate clusters, or A/B a new version. The deploy pattern: take a savepoint → stop the job → start the new job from the savepoint.
  • Stateful upgrades require operator UIDs (so Flink can map saved state to operators across code changes) and state-schema compatibility (evolving the state's types — P03 ideas applied to state). Forget a UID and your state won't restore.
  • Rescaling: changing parallelism means redistributing keyed state. Flink solves this with key groups: each key maps to one of maxParallelism key groups (fixed at job creation); key groups are assigned to subtasks in contiguous ranges; rescaling moves whole key-group ranges between subtasks. The lab implements key_group and task_for_key_group exactly. The catch interviewers probe: maxParallelism is fixed forever at first run — set it generously, because you can't exceed it later.

Chapter 7: Exactly-Once Sinks

Checkpoints give exactly-once state; the output to an external system needs more, because a checkpoint restore replays the records since the last checkpoint — so the sink must not re-emit them. Two patterns:

  • Idempotent sink: writes are naturally repeatable — upsert by key, set-union, overwrite-by-id. Replaying produces the same final state. Simplest; works when the sink supports it (Cassandra upsert, a keyed KV store).
  • Two-phase commit (Flink's TwoPhaseCommitSinkFunction): on each checkpoint barrier, pre-commit the buffered output as a pending transaction (durable, invisible); on the checkpoint-complete notification, commit it (visible). On recovery, re-commit pending transactions (they were part of the checkpoint) — idempotently. This is how transactional Kafka sinks, the file sink ("pending" → "finished" file rename), and the Iceberg/Delta sink (P09: stage data files, commit the snapshot) achieve exactly-once. The lab implements this protocol, including the crucial property: buffered-but-not-yet- pre-committed data is dropped on crash — and that's correct, because the source offset for it wasn't checkpointed either, so it'll be re-read.

Chapter 8: Backpressure and the Failure Signatures

Flink uses credit-based flow control: a downstream subtask grants the upstream credits (buffer space); when it's slow, credits dry up and the upstream slows — backpressure propagates cleanly to the source (which slows its Kafka consumption → lag). The Round-2 signature and how the symptoms connect:

slow operator / sink  →  backpressure  →  buffers fill  →  checkpoint barriers move slowly
                                                         →  alignment time ↑  →  checkpoint DURATION ↑
                                                         →  source slows  →  Kafka LAG ↑
late/out-of-order data + huge allowed lateness/sliding   →  many open windows  →  STATE size ↑
no state TTL / unbounded key cardinality                 →  RocksDB grows  →  STATE size ↑, GC ↑

The diagnostic discipline: find the backpressured operator (Flink UI colors it), then ask why it's slow — a slow sink (external system, the usual culprit), data skew on a hot key (P01), an expensive process function buffering elements, or under-parallelism. The fix matches the cause: scale the sink / async I/O, re-key or salt the hot key, switch to incremental aggregation, add state TTL, or raise parallelism (via savepoint rescale, Ch. 6). Saying that chain out loud is passing Round 2.

Not everything needs the DataStream API. Flink SQL treats streams as dynamic tables (a changelog) and lets you write windowed aggregations, joins, and pattern matching (MATCH_ RECOGNIZE) declaratively — with the same event-time/watermark/state semantics underneath. The streaming-batch unification: the same SQL can run over a bounded source (batch) or unbounded (streaming). Use SQL for the 80% of standard transforms (it's less code and optimizable); drop to DataStream for custom state, timers, and complex logic. Know that the state/exactly-once story is identical — SQL doesn't escape watermarks or checkpoints, it sits on them.

Lab Walkthrough Guidance

Lab 01 — Flink-Grade Stream Processor, suggested order:

  1. assign_window + KeyedWindowProcessor.process/flush (watermark, lateness, side output, dedup) — get the order-independence and lateness tests green first.
  2. snapshot()/restore() — then the headline test: process half, snapshot, restore into a fresh operator, process the rest → identical to one run (checkpoint recovery).
  3. key_group / task_for_key_group — deterministic, contiguous, covers all tasks (rescaling).
  4. TwoPhaseCommitSink — invisible until commit; crash before commit → recover exactly-once; double commit idempotent; uncheckpointed buffer dropped.

Success Criteria

You are ready for Phase 05 when you can, from memory:

  1. Draw Flink's execution model (JobManager/TaskManager/slots/parallelism/keyBy/chaining).
  2. Explain watermark generation, the min-across-inputs rule, and the idle-partition stall.
  3. Explain the barrier-snapshot algorithm and why alignment buys exactly-once and can stall.
  4. Contrast checkpoints and savepoints and describe a stateful upgrade (UIDs!).
  5. Explain key groups and why maxParallelism is fixed at job creation.
  6. Explain idempotent vs two-phase-commit sinks and where each fits.
  7. Walk the Round-2 failure chain (backpressure → checkpoint duration + state + lag) and give the cause-matched fix.

Interview Q&A

Q (Round 2): A Flink job has rising checkpoint duration, growing RocksDB state, delayed watermarks, and growing Kafka lag. Diagnose. These are one story, not four bugs. Something downstream is slow (most often the sink, or a hot-key skew, or an expensive process function). That backpressures upstream: buffers fill, so checkpoint barriers move slowly and alignment time climbs → checkpoint duration ↑; the source is throttled → Kafka lag ↑. Meanwhile delayed watermarks point at event-time/idle-partition issues or the same backpressure delaying barrier/marker flow, and growing RocksDB state points at too many open windows (huge allowed lateness / sliding windows / unbounded keys) or missing state TTL. I'd find the backpressured operator in the UI, confirm whether it's the sink or a skewed key, then fix the cause: async/scaled sink, re-key/salt the hot key, incremental aggregation, add state TTL, or rescale via savepoint — and consider unaligned checkpoints to relieve the alignment stall while I fix the root cause.

Q: Checkpoints vs savepoints — when do you use each? Checkpoints are the engine's automatic safety net for failure recovery — frequent, fast, engine-owned, possibly cleaned up. Savepoints are my tool for planned change — upgrade the job's code, change parallelism, migrate clusters, fork for A/B — taken on purpose, self-contained, portable. The upgrade ritual: savepoint → stop → start new version from savepoint, which only works if operators have stable UIDs and the state schema is compatible.

Q: How does Flink rescale stateful jobs, and what's the gotcha? Keyed state is partitioned by key group (hash(key) % maxParallelism), and key groups are assigned to subtasks in contiguous ranges, so changing parallelism just moves whole key-group ranges — no per-key reshuffle needed. The gotcha is that maxParallelism is fixed at job creation and caps your future parallelism; set it generously up front (changing it later requires a state migration), because you can scale up to it but never beyond it.

Q: You need exactly-once into S3/Iceberg. How? Checkpoints give exactly-once state inside Flink, but the S3 write needs a transactional sink: two-phase commit. On each checkpoint barrier the sink pre-commits — for files, it writes data to a temp/pending location; for Iceberg, it stages data files. On checkpoint-complete it commits — rename pending files to final, or commit the Iceberg snapshot atomically. On recovery it re-commits pending transactions idempotently. Crucially, data buffered since the last checkpoint is dropped on a crash, which is correct because the source offset wasn't advanced past it either — so it's re-read and re-processed, netting exactly-once.

References

🛸 Hitchhiker's Guide — Phase 04: Apache Flink

Read this if: you want Flink's model — execution, time, state, checkpoints, exactly-once — in your head fast, and to be ready for the "diagnose this Flink job" interview round. Skim, read the WARMUP, build the engine.


0. The 30-second mental model

Flink processes records one at a time over a graph of operators, keeps keyed state, orders by event time using watermarks, snapshots the whole running job consistently with barrier checkpoints (so a crash resumes with exact state), and gets exactly-once output via idempotent or two-phase-commit sinks. One sentence: Flink = stateful event-time streaming with consistent snapshots; correctness is checkpoints + the right sink.

1. Execution model in one diagram

JobManager (coordinates, triggers checkpoints)
  └─ TaskManagers → task slots → operator subtasks
Source → keyBy(k) → window/process (KEYED STATE) → sink
         hash-partition      checkpoint barriers flow with data
parallelism = N subtasks/operator; chaining fuses adjacent ops

2. Watermarks (the thing everything hangs on)

W = max_event_time_seen − bounded_delay
window fires when  W ≥ window.end
multi-input op watermark = MIN across inputs  ← one idle partition stalls everything
                                                (fix: withIdleness)
late-but-allowed → re-emit correction; too-late → side output (never drop)

Trade: bigger delay = more complete first results, more latency, more state.

3. State & backends

HashMap (heap)RocksDB (disk LSM)
size≤ memory≫ memory
checkpointfullincremental
speedfastestserialize + disk I/O
use whensmall statelarge keyed state

State grows from: too many open windows (sliding / huge allowed lateness), unbounded key cardinality, no state TTL. State size is the cost center.

4. Checkpoints = consistent distributed snapshot

JobManager injects BARRIER N into sources
→ each operator, on receiving N on ALL inputs (ALIGNMENT), snapshots state async → S3
→ all confirm → checkpoint N COMPLETE
crash → restart from last complete checkpoint: restore state + rewind source offsets

Alignment buys exactly-once and is where it stalls under backpressure → unaligned checkpoints help.

5. Checkpoint vs savepoint

checkpoint = automatic, engine-owned, fast recovery, may be GC'd
savepoint  = manual, user-owned, portable → upgrades, rescale, migrate
upgrade ritual: savepoint → stop → start new version from savepoint (need stable operator UIDs)

6. Rescaling via key groups

key → key_group = hash(key) % maxParallelism   (fixed at job creation!)
key_group → task = kg * parallelism // maxParallelism   (contiguous ranges)
rescale = move whole key-group ranges between subtasks
GOTCHA: maxParallelism caps you forever — set it generously

7. Exactly-once OUTPUT (state alone isn't enough)

idempotent sink     → upsert/set-union/overwrite-by-id (Cassandra, KV)   ← simplest
two-phase commit    → pre_commit on barrier (pending, invisible)
                      commit on checkpoint-complete (visible, idempotent)
                      recover → re-commit pending  (files rename / Iceberg snapshot commit)
buffered-not-precommitted data is DROPPED on crash → correct (offset not advanced either)

8. The Round-2 failure chain (memorize this)

slow sink / hot-key skew / expensive process()
   → backpressure → buffers fill
       → checkpoint barriers slow + alignment ↑ → CHECKPOINT DURATION ↑
       → source throttled → KAFKA LAG ↑
huge allowed-lateness / sliding / no TTL / unbounded keys → many open windows → STATE ↑

Fix the CAUSE: async/scale the sink, re-key/salt the hot key, incremental aggregation, state TTL, rescale; unaligned checkpoints relieve alignment while you fix root cause.

9. Beginner mistakes that mark you

  1. Ordering/aggregating by processing time → wrong on replay (P01 again).
  2. No withIdleness → one quiet partition freezes all watermarks.
  3. process() that buffers all elements instead of incremental aggregate.
  4. No state TTL + unbounded keys → RocksDB grows forever.
  5. Forgetting operator UIDs → savepoint won't restore after a code change.
  6. Setting maxParallelism too low (or defaulting it) → can't scale up later.
  7. Assuming checkpoints give exactly-once output (they give exactly-once state).

10. How this phase pays off later

  • 2PC sink → P09 Iceberg/Delta exactly-once writes.
  • Checkpoints in S3 / RocksDB LSM → P08 storage, P11 LSM internals.
  • Broadcast state + Cassandra enrichment → P11 + capstone fraud detector (P16).
  • The failure chain → P13 observability & incident response.

Read the WARMUP, build the engine, then P05: the other streaming engines (Kafka Streams, Spark SS, Akka/FS2) as variations — plus CDC and streaming joins.

👨🏻 Brother Talk — Phase 04, Off the Record

Flink is the phase that separates the people who talk about streaming from the people who've actually run it at 3 a.m. Let me get you to the second group.


Brother, this is the big one. Flink is where a lot of data engineers hit a wall, and it's not because the API is hard — it's because stateful, fault-tolerant, exactly-once, event-time streaming requires you to hold five hard things in your head at once, and if any one of them slips, you get silent wrongness. So let me give you the mental scaffolding that kept me sane.

First: respect that streaming is harder than batch, and stop fighting it. In batch, you have all the data, you process it, you're done — if something's wrong you just re-run. In streaming, the data never stops, it arrives out of order, some of it is late, your job will crash mid-flight, and you still have to be correct and not lose or duplicate anything. That's genuinely a harder problem, and Flink's apparent complexity is the irreducible complexity of that problem, not accidental complexity someone could have designed away. Once I accepted that — "Flink is complicated because the problem is complicated" — I stopped resenting the watermarks and checkpoints and started appreciating them as the minimum machinery that makes the impossible possible.

Second: watermarks are a confidence statement, and the idle-partition bug will get you. A watermark says "I'm now confident nothing older than this is coming." Beautiful. But here's the trap that's bitten me and everyone I know: Flink takes the minimum watermark across all input partitions, so if one partition goes quiet — a low-traffic region, a sensor that stopped, a test topic with no data — its watermark freezes, the minimum freezes, and your entire job's windows stop firing. Output just... stops. And you stare at it for an hour before you realize one idle partition is holding the whole thing hostage. Learn withIdleness now, and when someone says "the Flink job stopped emitting but isn't crashing," your first thought should be "which partition went quiet?" That instinct alone is worth this whole phase.

Third: state is where the bodies are buried. Every production Flink horror story I know is ultimately a state story. State grew unbounded because nobody set a TTL. State exploded because someone used sliding windows with a huge allowed-lateness and suddenly there were millions of open windows. State got hot because one key (the "whale" from P01 — it's always P01) carried 80% of the traffic onto one subtask. RocksDB compaction couldn't keep up and the job slowed to a crawl. When you design a Flink job, the question that matters most is not "what's the logic" — it's "how big does the state get, and what bounds it?" If you can't answer that, you haven't designed it yet, you've prototyped it.

Fourth: "exactly-once" has a specific, narrow meaning, and the slide deck lies about it (again). Flink genuinely gives you exactly-once state — the checkpoint algorithm is real and beautiful, restore resumes with exact state. But the output to your database or S3? That's exactly-once only if your sink is idempotent or does two-phase commit, because on recovery Flink replays everything since the last checkpoint. I've seen teams assume "we're on Flink, so exactly-once" and then wonder why their database has duplicates after every job restart. The answer is always: the sink wasn't transactional or idempotent. Build the two-phase-commit sink in this lab and feel the protocol — pre-commit on the barrier, commit on completion, recover re-commits — and you'll never make that assumption again.

Fifth, and this is the interview gold: the Round-2 question — "checkpoint duration is rising, state is growing, watermarks are delayed, lag is climbing" — looks like four problems and it's one. It's almost always backpressure from a slow sink, cascading into all four symptoms. The junior tries to fix four things. The principal says "these are one story" and traces the backpressure to its source. When you can stand at a whiteboard and draw the chain — slow sink → backpressure → buffers fill → barriers slow → checkpoint duration up + source throttled → lag up — calmly, while everyone else is spooked by the four red graphs, you have just demonstrated exactly the "escalation point for the hardest failures" the JD is hiring for. That's not memorization; it's understanding the causal model. Build the lab and the model becomes yours.

Now, the encouragement, because Flink can feel demoralizing. You will be confused the first time you try to reason about barrier alignment. Everyone is. The concepts are genuinely deep — the checkpoint algorithm is a published academic paper (Chandy-Lamport, then the Flink team's async version). You're not slow; you're learning load-bearing distributed-systems theory that most "senior" engineers wave their hands at. The payoff is enormous: Flink expertise is rare and valuable precisely because it's hard, and the person who can actually debug a stateful streaming job — not just write one in the happy path, but debug it when state is exploding and checkpoints are timing out — is the person who gets the principal title and the pager that comes with it.

Build the engine. Make the checkpoint-recovery test pass — process half a stream, snapshot, restore into a fresh operator, finish, and watch it match the uninterrupted run exactly. That moment, when you see exact state survive a simulated crash, is when Flink stops being scary and starts being yours.

Next stop, P05: the other streaming engines, so you can say why Flink and not Kafka Streams or Spark Structured Streaming — and handle CDC and streaming joins.

— your brother 👨🏻

Lab 01 — Flink-Grade Stream Processor

Phase: 04 — Apache Flink | Difficulty: ⭐⭐⭐⭐⭐ | Time: 8–10 hours

This is the flagship of the whole streaming half of the track. You build the core of Flink's event-time machinery by hand — windows, watermarks, allowed lateness, keyed state — and then the two things that make Flink Flink: checkpoint/restore (so a crash resumes with exact state) and a two-phase-commit sink (so output is exactly-once). Plus key-group rescaling, so you can change parallelism safely.

What you build

  • KeyedWindowProcessor — tumbling event-time sum/count per key with watermark-driven emission, allowed-lateness re-emission, and a late-event side output (never dropped)
  • snapshot() / restore() — capture ALL operator state into a portable structure and rebuild from it: checkpoint recovery. The test proves restore-and-continue == one uninterrupted run.
  • key_group / task_for_key_group — Flink's mechanism: keys → fixed key groups → contiguous task ranges, so rescaling redistributes whole key-group ranges
  • TwoPhaseCommitSinkwrite → pre_commit(barrier) → commit(complete); nothing is visible until its checkpoint completes, a crash mid-way recovers exactly-once, and a double commit is idempotent

Key concepts

ConceptWhat to understand
Watermarkmax_ts − delay; emission is watermark-driven, not arrival-driven
Allowed latenessemitted ≠ done; windows linger and re-fire corrections
Checkpointa consistent snapshot of ALL state; restore resumes exactly
Savepointa portable, user-owned checkpoint (upgrades/migrations)
Key groupthe unit of state redistribution; why maxParallelism is fixed
Two-phase commitexactly-once output: commit only on checkpoint complete, idempotently

Run

pip install -r requirements.txt
pytest test_lab.py -v
LAB_MODULE=solution pytest test_lab.py -v

Success criteria

  • All 15 tests pass — test_checkpoint_restore_continues_identically and test_crash_between_precommit_and_commit_recovers_exactly_once are the ones that certify you understand Flink's correctness model.
  • You can explain why output is invisible until commit, and what happens to buffered data on a crash (dropped — and why that's still exactly-once).
  • You can explain why key groups (not keys) are the rescaling unit and why that fixes maxParallelism at job creation.

Extensions

  • Add sliding and session windows (session needs merge-on-late-bridge — genuinely hard; do it to feel why state is the cost center).
  • Make seen_ids bounded by a TTL behind the watermark (real dedup state can't grow forever — P01's bounded-dedup tradeoff, now in a real operator).
  • Add broadcast state: a second "rules" stream that every key sees (the fraud-rules pattern from the JD's Problem 2).
  • Add a RocksDB-style spilling state backend behind an interface and measure the heap-vs-disk tradeoff — the abstraction real Flink exposes.

Lab 02 — Event-Time Stream Processing in Scala

Phase: 04 — Apache Flink | Difficulty: ⭐⭐⭐⭐☆ | Time: 5–6 hours

Stream processing on the JVM is the in-demand Scala data-engineering skill — Flink, Kafka Streams, and Spark are Scala/JVM systems, and their operators are written in this style. This lab implements Flink's event-time core in idiomatic Scala (case classes, immutable accumulators, a keyed processor), verified with ScalaTest via sbt test. It mirrors Lab 01's Python flink-engine so you can compare the language to the semantics.

What you build

  • Windowing.assignWindow — tumbling [start, end) arithmetic (boundary → next window)
  • StreamProcessor — per-key event-time aggregation with watermark-driven emission, allowed-lateness corrected re-emission, late-event side output, and dedup for exactly-once effect
  • Windowing.finalResults — fold emissions+updates to the consumer's latest-per-window view

Run

sbt test

First run downloads Scala 2.13 + ScalaTest, then compiles and runs. Expected: 6 tests, all green, including the property that certifies the design — order independence under bounded shuffle (shuffled delivery == in-order delivery).

Key concepts (same as Lab 01, in Scala)

ConceptScala expression
WatermarkmaxTs - watermarkDelay; emission is watermark-driven
Allowed latenessemitted windows linger and re-fire isUpdate = true corrections
Side outputtoo-late events appended to lateEvents (never dropped)
Exactly-once effectseen set deduplicates by eventId before accumulation
ImmutabilityAcc is a case class; updates are .copy(...)

Success criteria

  • sbt test → all 6 specs pass.
  • You can read the Scala and map every line to the Flink concept (WARMUP).
  • You can explain why the shuffle test is the one that proves correctness.

Extensions

  • Make the processor purely functional: thread state through an immutable fold (no var/ mutable), returning (newState, emitted) — the FS2/Cats way (P12).
  • Add sliding and session windows (session needs merge-on-late-bridge).
  • Bound the seen dedup set by a TTL behind the watermark (P01's bounded-dedup tradeoff).
  • Wire it to real Flink: drop this logic into a KeyedProcessFunction[String, Event, ...] with ValueState/timers (the production form — see WARMUP Ch. 4).

Phase 05 — Streaming Alternatives & CDC

Difficulty: ⭐⭐⭐⭐☆ Estimated Time: 1.5 weeks (24–32 hours) Prerequisites: Phase 04 (Flink — the reference engine these are compared against)


Why This Phase Exists

Flink is not the only stream processor, and a principal must be able to say why Flink and not Kafka Streams here, or Spark Structured Streaming there, or Akka Streams for that service. The JD lists them all — "Kafka Streams, Flink DataStream API, Flink SQL, Spark Structured Streaming, Akka Streams" — and names CDC platforms, streaming joins, enrichment, and sessionization as required patterns. This phase rounds out the streaming half: the engine landscape, the FP/JVM streaming libraries (Akka Streams, FS2 — foreshadowing P12), and the join/CDC patterns that every engine implements and that you build here once, engine-agnostically.

Concepts

  • Kafka Streams: a library, not a cluster — embeds in your app; KStream vs KTable (the stream/table duality, P02 compaction made API); local state in RocksDB + changelog topics for fault tolerance; interactive queries; when "no cluster to run" wins.
  • Spark Structured Streaming: micro-batch (and continuous) on the Spark SQL engine; checkpointing to a durable store; the same DataFrame API as batch (unification); higher latency than Flink but trivial if you're already on Spark.
  • Akka Streams: Reactive-Streams back-pressured graphs on the actor runtime; typed, composable, in-process — the right tool for streaming inside a service (P12).
  • FS2 (Functional Streams for Scala): pure, back-pressured streaming on Cats Effect — resource-safe, referentially transparent (P12).
  • The engine decision matrix: latency, state size, exactly-once needs, ops model, language, ecosystem — Flink vs Kafka Streams vs Spark SS vs Akka/FS2.
  • CDC: turning a DB's binlog/WAL into events (Debezium); snapshot + streaming bootstrap; applying CDC to a table idempotently (the lab); the OLTP→lakehouse bridge.
  • Join patterns: stream↔table (temporal / as-of), stream↔stream (interval/windowed), enrichment from an external store (P11); the leakage and state-size traps.
  • Sessionization and other stateful patterns (dedup, top-N, running aggregates).

Labs

Lab 01 — Streaming Joins & CDC Apply (flagship, implemented)

FieldValue
GoalImplement CDC materialization (idempotent by LSN, tombstone memory), the as-of stream↔table join (point-in-time correct), the interval stream↔stream join, and gap-based sessionization
ConceptsChange-stream → table, temporal leakage, bounded-interval joins, session windows
How to Testpytest test_lab.py -v — 14 tests incl. CDC idempotence under shuffle
Talking PointsWhy must the dimension lookup be event-time-valid? Why keep version memory after a delete? Why bound a stream-stream join?
Resume bulletBuilt engine-agnostic streaming join + CDC primitives: idempotent change-stream materialization, point-in-time stream-table enrichment, interval joins, sessionization

→ Lab folder: lab-01-joins-cdc/

Extension Project A — Engine bake-off memo (spec)

One page: pick a real use case (clickstream sessionization; CDC replication; in-service enrichment) and choose Flink vs Kafka Streams vs Spark SS vs Akka/FS2 — with the deciding dial (P00) and an ADR.

Extension Project B — Debezium CDC pipeline (spec)

docker Postgres + Debezium + Kafka; capture row changes; materialize a table with your apply_cdc; demonstrate snapshot→streaming handover and idempotent re-apply.

Integrated-Scenario Hooks

  • This phase's CDC apply feeds the lakehouse upsert tables (P09) and Cassandra serving (P11).
  • Its as-of join is the enrichment step of the capstone fraud detector (P16, JD Problem 2).
  • Its Akka/FS2 preview is built for real in Scala in P12.

Guides in This Phase

Key Takeaways

  • The engines differ in latency, ops model, and language — not in the patterns they must implement.
  • Kafka Streams = a library (no cluster); Flink = lowest latency + richest state; Spark SS = free if you're on Spark; Akka/FS2 = in-service streaming.
  • CDC materialization is idempotent-by-LSN; the stream/table duality is everywhere.
  • Joins are where leakage (as-of) and unbounded state (intervals) bite — bound both.

Deliverables Checklist

  • Lab 01 implemented; all 14 tests pass
  • You can choose an engine for a stated use case with the deciding dial
  • You can explain KStream vs KTable and the changelog-topic fault-tolerance model
  • Extension A memo or Extension B CDC pipeline

Warmup Guide — Streaming Alternatives & CDC

Zero-to-principal primer for Phase 05: the stream-processing engine landscape (Kafka Streams, Spark Structured Streaming, Akka Streams, FS2) and how to choose between them, Change Data Capture, and the join/sessionization patterns every engine must implement.

Table of Contents


Chapter 1: The Engine Landscape

P04 taught Flink as the reference. The others trade Flink's power for different operational shapes:

  • Flink: dedicated cluster, record-at-a-time, lowest latency, richest state & exactly-once. Most powerful, most to operate.
  • Kafka Streams: a library you embed in your service — no cluster — using Kafka itself for state durability. Lowest ops if you're already on Kafka.
  • Spark Structured Streaming: micro-batch on the Spark engine; same DataFrame API as batch; higher latency, but "free" if your org already runs Spark.
  • Akka Streams / FS2: in-process, back-pressured streaming inside a single service — for app-level pipelines, not cluster-scale analytics.

The principal point: these are not "better/worse," they're different operational commitments. The skill is matching the engine to the use case (Ch. 5).

Chapter 2: Kafka Streams — Streaming as a Library

Kafka Streams flips the model: instead of submitting a job to a cluster, you write a normal JVM application that links the Streams library and scales by running more instances (in the same consumer group). The model:

  • KStream = an unbounded stream of records (events). KTable = a changelog folded into the latest-value-per-key table (P02 compaction, as a first-class type). The two interconvert (toTable, toStream) — the stream/table duality made API. The lab's apply_cdc is a KStream→KTable fold.
  • Local state + changelog topics: stateful operations keep state in a local RocksDB store; for fault tolerance, every state change is also written to a changelog topic in Kafka, so a failed instance's state is rebuilt by replaying the changelog. Exactly-once is available via Kafka transactions (processing.guarantee=exactly_once_v2).
  • Interactive queries: because state is local, you can query it directly (a lightweight serving path).
  • When it wins: you're already on Kafka, you want no separate cluster to operate, and your scale fits "run more app instances." When it doesn't: cross-cluster sources, the very largest state, or sub-millisecond latency needs (Flink territory).

Chapter 3: Spark Structured Streaming — Micro-Batch on the SQL Engine

Spark Structured Streaming models a stream as an unbounded table that grows; each micro-batch processes the new rows with the same DataFrame/SQL API as batch Spark (P06). Properties:

  • Micro-batch means latency is bounded by the batch interval (typically hundreds of ms to seconds) — higher than Flink's record-at-a-time, but plenty for most analytics. A continuous mode exists for lower latency with weaker guarantees.
  • Checkpointing to a durable store (HDFS/S3) plus idempotent/transactional sinks gives exactly-once (the offsets processed and the output are tracked together).
  • Watermarks & windows exist (similar semantics to P04) but with micro-batch granularity.
  • When it wins: you already run Spark, your latency budget is seconds not milliseconds, and you want one engine and one API for batch and streaming (the unification). When it doesn't: hard low-latency or very large keyed-state, fine-grained timers — Flink.

Chapter 4: Akka Streams and FS2 — Functional, In-Service Streaming

These are not cluster engines; they're in-process streaming libraries for building back-pressured pipelines inside a service (an ingestion gateway, an enrichment microservice, a connector):

  • Akka Streams: a Reactive Streams implementation on the Akka actor runtime. You build a typed graph of Source → Flow → Sink with built-in backpressure (demand flows upstream — P01 Ch. 8 made concrete). Great for bounded-resource, composable in-service streaming with strong typing.
  • FS2 (Functional Streams for Scala): pure, referentially-transparent streams on Cats Effect (P12), with resource safety (a stream that opens a file/connection releases it even on failure or cancellation) and backpressure by construction. The functional-purist's choice.
  • These matter for this role because the JD explicitly lists Akka/Cats/ZIO/FS2 and asks you to build internal SDKs (P12). You'll often build the platform's in-service components (a validating ingestion sidecar, a CDC connector) with exactly these libraries, while the cluster-scale processing runs on Flink/Spark.

Chapter 5: Choosing an Engine

The decision matrix (and the dial each turns — P00):

NeedFlinkKafka StreamsSpark SSAkka/FS2
Latencymsms–ss (micro-batch)ms (in-proc)
Ops modelclusterlibrary (no cluster)cluster (yours already?)in-service
State sizehuge (RocksDB)large (RocksDB+changelog)mediumsmall (in-proc)
Exactly-oncestrongestyes (Kafka txns)yes (ckpt+sink)DIY
Batch+stream one APIvia Table APInoyesno
Best whenlow-latency stateful at scalealready-on-Kafka, no clusteralready-on-Sparkin-service pipelines

The principal answer is always contextual: "low-latency stateful analytics at scale → Flink; we're all-Kafka and want no cluster → Kafka Streams; we already run Spark and need seconds-latency with one API → Spark SS; it's a single service's internal pipeline → Akka/ FS2." Then the ADR.

Chapter 6: Change Data Capture

CDC turns a database's commit log (MySQL binlog, Postgres WAL) into a stream of row-level change events — the bridge from OLTP systems to the lakehouse and to event-driven consumers, without the dual-write problem (you don't write to both the DB and Kafka; you capture what the DB already committed).

  • Debezium is the dominant connector: it reads the binlog/WAL and emits change events (op = c/u/d, before/after images, source LSN) to Kafka.
  • Snapshot + streaming: on first run, CDC takes a consistent snapshot of the table (the current rows), then switches to streaming the log from the snapshot's position — so consumers get the full state and all subsequent changes, with no gap. (Extension B.)
  • Applying CDC (the lab): fold the change stream into a materialized table. The correctness keys: apply changes in LSN order, make it idempotent by version (a re-delivered or out-of-order change is a no-op — P01 again), and keep tombstone version memory so a stale insert can't resurrect a deleted row. This is exactly how a CDC sink keeps a lakehouse upsert table (P09) or a Cassandra table (P11) correct.
  • Ordering & keys: CDC events for a row must stay ordered → key the Kafka topic by the primary key (P02 per-key ordering). Schema changes in the source flow through as schema evolution (P03).

Chapter 7: Join Patterns

Joins are where streaming gets subtle, because at least one side is unbounded:

  • Stream ↔ Table (temporal / "as-of" join): enrich each event with a dimension value — but the value valid at the event's time, not the latest. Using the latest is temporal leakage: you'd attribute an event to a dimension state that didn't exist yet (a user's current tier applied to last month's event). The lab's temporal_join does the point-in-time-correct lookup; this is the streaming twin of the feature-store point-in-time-join leakage bug.
  • Stream ↔ Stream (interval / windowed join): join two streams where the records fall within a bounded time interval of each other (click→purchase within 30 min). The bound is what makes it feasible — each side's state need only be retained for the interval width (Flink's interval join). Unbounded "join everything to everything" is unimplementable at scale; the bound is the design.
  • Enrichment from an external store: look up a key in Cassandra/DynamoDB (P11) per event — use async I/O (don't block the operator) and a cache; mind the consistency (the store may lag the stream).

Chapter 8: Sessionization and Stateful Patterns

  • Sessionization (the lab): group a key's events into sessions separated by an inactivity gap (a 30-min silence ends the session). Boundaries are dynamic (unlike fixed windows), and a late event can bridge two sessions into one (the merge logic Flink session windows implement). The natural shape for "web session," "trip," "viewing session" features.
  • Other stateful patterns you'll implement on these engines: dedup (P01's seen-ids, with bounded TTL state), top-N / leaderboards (keyed sorted state), running aggregates (incremental, not buffer-all), pattern detection (CEP / MATCH_RECOGNIZE — the fraud-rule sequences). All share the lesson: bound the state (TTL, window, gap), or it grows forever (P04 Ch. 4).

Lab Walkthrough Guidance

Lab 01 — Streaming Joins & CDC Apply, suggested order:

  1. apply_cdc — version-gated apply; delete with tombstone memory; then the idempotent-under-shuffle and stale-insert-after-delete tests (the correctness core).
  2. temporal_join — sort dim versions per key, bisect for the latest valid_from ≤ ts; None before the first version (no leakage).
  3. interval_join — per-key, the [ts+lower, ts+upper] window; validate lower ≤ upper.
  4. sessionize — per-key, split when ts − last > gap (gap exclusive — the boundary test).

Success Criteria

You are ready for Phase 06 when you can, from memory:

  1. Contrast Flink, Kafka Streams, Spark SS, and Akka/FS2 and pick one for a stated use case.
  2. Explain KStream vs KTable and Kafka Streams' changelog-topic fault tolerance.
  3. Explain CDC, the snapshot+streaming bootstrap, and the idempotent-by-LSN apply.
  4. Explain the as-of join and the temporal-leakage bug it prevents.
  5. Explain why stream-stream joins must be bounded and how sessionization differs from fixed windows.

Interview Q&A

Q: When would you NOT use Flink? When its operational weight isn't justified. If we're already all-in on Kafka and the use case fits "run more app instances," Kafka Streams removes a whole cluster to operate. If we already run Spark and our latency budget is seconds with batch+stream sharing one API, Spark Structured Streaming is the lower-friction choice. If it's a single service's internal back-pressured pipeline, Akka Streams or FS2 keeps it in-process. Flink earns its operational cost when you need low-latency, large keyed-state, fine-grained timers, and the strongest exactly-once — otherwise a lighter engine is the principal choice.

Q: How does CDC stay correct under retries and reordering? Each change carries a monotonic source LSN/version, and the apply is version-gated: a change is applied only if its version exceeds the last applied version for that key, so duplicates and out-of-order delivery are no-ops — exactly-once effect on the materialized table. Deletes keep tombstone version memory so a stale insert that arrives after a delete (with a lower version) can't resurrect the row. Order is preserved per row by keying the CDC topic on the primary key. The snapshot+streaming handover ensures no gap between the initial state and the change stream.

Q: A teammate enriched events with users.current_tier. What's wrong? Temporal leakage. They joined each event to the user's current tier, but the correct value is the tier that was valid at the event's timestamp — an as-of join. Using the current value means a user who upgraded last week has all their old events mislabeled as "pro," which corrupts any time-based analysis or training data (it's the same point-in-time bug as a feature store). The fix is a temporal join keyed by event time against the dimension's validity intervals.

References

🛸 Hitchhiker's Guide — Phase 05: Streaming Alternatives & CDC

Read this if: you know Flink (P04) and need the rest of the streaming world fast — the other engines, when to pick each, CDC, and the join patterns. Skim, read WARMUP, build the lab.


0. The 30-second mental model

Flink isn't the only stream processor — it's the heaviest. Kafka Streams is a library (no cluster), Spark Structured Streaming is micro-batch on the Spark engine, Akka Streams / FS2 are in-service back-pressured pipelines. They differ in ops and latency, not in the patterns: CDC (change stream → table), as-of joins, interval joins, sessionization. One sentence: pick the engine by operational fit; the patterns are the same everywhere.

1. Engine picker (one line each)

Flink           → low-latency, large state, strongest exactly-once; a cluster to run
Kafka Streams   → a LIBRARY in your app, state in RocksDB + changelog topics; no cluster
Spark Str. Str. → micro-batch (seconds), same API as batch Spark; free if you're on Spark
Akka Streams    → in-service, typed, back-pressured graphs (Reactive Streams)
FS2             → in-service, pure/resource-safe streams on Cats Effect  (P12)

2. KStream vs KTable (the duality, again)

KStream = events (append)        KTable = latest-value-per-key (compacted log → table)
toTable() / toStream() interconvert     state durability = changelog topic in Kafka

This is P02 compaction as a typed API; your apply_cdc is a KStream→KTable fold.

3. CDC in 5 bullets

  • DB commit log (binlog/WAL) → change events (Debezium): op=c/u/d, before/after, LSN.
  • No dual-write: you capture what the DB already committed.
  • Snapshot + streaming: full table snapshot, then stream from that position (no gap).
  • Apply idempotently by version/LSN: dup/out-of-order = no-op (P01).
  • Tombstone version memory: keep the version after delete so a stale insert can't resurrect the row. Key the topic by PK for per-row ordering.

4. Join patterns (and their traps)

stream↔table (AS-OF)   : value valid AT EVENT TIME, not latest → else TEMPORAL LEAKAGE
stream↔stream (INTERVAL): r.ts ∈ [l.ts+lower, l.ts+upper] → bound = bounded state
enrichment (external)   : async I/O + cache; the store may lag the stream

5. Sessionization

new session when (ts - last) > gap   (gap exclusive)
boundaries are DYNAMIC; a late event can BRIDGE two sessions into one (merge)

The shape for web sessions, trips, viewing sessions.

6. Beginner mistakes that mark you

  1. "Always Flink" — can't justify it; ignoring Kafka Streams / Spark SS fit.
  2. Enriching with the current dimension value → temporal leakage.
  3. Unbounded stream-stream joins → unbounded state.
  4. Blocking external lookups in an operator (no async I/O) → backpressure.
  5. CDC apply without LSN gating → duplicates/resurrected rows.
  6. Forgetting CDC needs PK-keyed topics for per-row ordering.

7. War stories

  • "Old events all show users as 'pro'." → enriched with current tier; needed as-of join.
  • "The CDC table has rows we deleted." → no tombstone version memory; a stale insert resurrected them.
  • "Our enrichment service fell over under load." → synchronous external lookups; switch to async I/O + cache.
  • "Kafka Streams app lost its state on redeploy." → no/short changelog retention; the changelog is the durability.

8. How this phase pays off later

  • CDC apply → P09 lakehouse upserts, P11 Cassandra/DynamoDB sinks.
  • As-of join → capstone fraud detector enrichment (P16).
  • Akka/FS2 → built for real in Scala (P12).
  • Engine picker → ADRs in P15.

Read WARMUP, build the lab, then P06: we cross from streaming into batch — Spark internals, the other half of the platform.

👨🏻 Brother Talk — Phase 05, Off the Record

The engine wars, CDC, and joins — where I'll save you from two expensive mistakes: over-engineering with Flink, and the leakage bug that quietly poisons your data.


Brother, let me start with a confession that'll save you grief: for a long time I reached for Flink for everything, because it was the powerful, impressive choice. And I paid for it — in operational toil, in 3 a.m. pages for a cluster that was overkill for the job, in onboarding pain for teammates who just wanted to enrich a stream. The lesson took too long: the most powerful tool is not the best tool; the best tool is the one that fits the job with the least operational weight. A principal who reaches for Flink reflexively is signaling the same immaturity as a junior who reaches for microservices reflexively.

So here's the engine maturity ladder, the way I think about it now. If you're already on Kafka and the job fits "run more app instances," Kafka Streams lets you delete an entire cluster from your life — your streaming logic lives in a normal app you deploy normally. If you already run Spark and your latency budget is seconds, Spark Structured Streaming means one engine, one API, one set of skills for batch and stream. If it's a single service's internal pipeline, Akka Streams or FS2 keeps it in-process where it belongs. And then, when you genuinely need millisecond latency with huge keyed state and the strongest exactly-once — then Flink earns its keep. Being able to say "we don't need Flink here, and here's the lighter thing that fits" is a more senior move than knowing every Flink knob.

Now, the bug. I'm going to plant this so deep you'll never commit it: temporal leakage in joins. It's the most innocent-looking, most damaging mistake in stream enrichment. You have events, you have a dimension table (users, products, accounts), you join them — and the obvious thing, the thing every tutorial shows, is to look up the dimension's current value. And it's wrong. Because the event happened in the past, and the dimension's value then may differ from now. A user who's "pro" today was "free" when they generated last month's events. If you label those old events "pro," every time-based analysis is corrupted, every model trained on that data learns a lie, and — the cruel part — nothing crashes. The dashboard looks fine. The model trains fine. It's just subtly, silently wrong, and you find out months later when a number doesn't reconcile. The fix is the as-of join: look up the value that was valid at the event's timestamp. Build it in this lab, feel why the point-in-time lookup matters, and carry that paranoia forever. (It's the same bug as feature-store leakage — the data world keeps re-discovering it under different names.)

Third thing: CDC is magic, and like all magic it has rules. Change Data Capture is one of the most powerful patterns you'll wield — it turns any database into an event stream without the dual-write problem, because you're capturing what the DB already committed rather than asking the app to write twice. But the apply side has to be correct or you get corruption that's miserable to debug. The rules: apply in LSN order, gate by version so duplicates and reordering are no-ops, and — the one people forget — keep tombstone memory so that after you delete a row, a stale older insert can't wander in and resurrect it. I've seen "deleted" customers reappear in a downstream table because someone's CDC apply forgot that last rule. It looks like a ghost. It's an ordering bug. Build the idempotent apply in the lab and you'll never ship the ghost.

A word on the functional libraries — Akka Streams and FS2. They'll feel out of place next to Flink and Spark, and I want you to not skip them, because they're a preview of the most intellectually rewarding phase in this whole track (P12). When you build the platform's in-service pieces — a validating ingestion sidecar, a CDC connector, an enrichment service — you'll often reach for exactly these, and they bring something the cluster engines don't: resource safety by construction. A stream that opens a connection cannot forget to close it, even on failure, because the type system and the runtime guarantee it. That's a different kind of correctness than checkpoints — it's correctness you get at compile time — and once you taste it, sloppy resource handling in other languages starts to feel barbaric. Hold that feeling; P12 turns it into a superpower.

Career note: the person who can look at a problem and say "this doesn't need a cluster" saves the org real money and real toil, and that gets noticed by the people who pay the cloud bill and carry the pager. Restraint reads as seniority. The flashy move is building the impressive distributed thing; the principal move is building the right-sized thing and being able to defend why it's right-sized with the workload's numbers. Practice saying no to Flink as confidently as you say yes.

Build the lab. Make CDC idempotent under a shuffled, duplicated stream. Make the as-of join refuse to leak. Then come to P06, where we cross the bridge from streaming to batch and take apart Spark — the engine that, when it misbehaves, generates the most expensive bills and the longest job runtimes in the business.

— your brother 👨🏻

Lab 01 — Streaming Joins & CDC Apply

Phase: 05 — Streaming Alternatives & CDC | Difficulty: ⭐⭐⭐⭐☆ | Time: 5–7 hours

Flink, Kafka Streams, and Spark Structured Streaming differ in API and runtime, but they all have to get the same four patterns correct. This lab implements them engine-agnostically: CDC materialization (change stream → table, idempotent by LSN), the as-of join (point-in-time correct stream↔dimension enrichment — the leakage killer), the interval join (stream↔stream within a time window), and sessionization.

What you build

  • apply_cdc — fold a Debezium-style change stream into a materialized table; apply only newer versions so duplicates and out-of-order delivery are no-ops (exactly-once effect on the table); deletes leave tombstone version memory so a stale insert can't resurrect a deleted key
  • temporal_join — enrich each event with the dimension value valid at the event's time (not the latest) — using a future value is temporal leakage
  • interval_join — pair two streams within [ts+lower, ts+upper]; bounded intervals bound the state
  • sessionize — gap-based session windows per key (the merge logic of Flink session windows, distilled)

Key concepts

ConceptWhat to understand
CDC → tablea change log materializes a table; idempotent by LSN/version
Tombstone memorykeep the version after delete so stale inserts can't resurrect
As-of joinpoint-in-time correctness; using a later dim value is leakage
Interval joinbounded window = bounded state for stream-stream joins
Sessionizationdynamic, gap-defined windows; the user-behavior shape

Run

pip install -r requirements.txt
pytest test_lab.py -v
LAB_MODULE=solution pytest test_lab.py -v

Success criteria

  • All 14 tests pass — test_cdc_idempotent_under_duplicate_and_shuffle and test_cdc_stale_insert_after_delete_rejected certify the CDC correctness model.
  • You can explain why the as-of join must use the event-time-valid dimension value, and what bug you get if you use the latest.
  • You can explain why a stream-stream join needs a bounded interval to be feasible.

Extensions

  • Add late-data handling to the as-of join (a dimension update that arrives after the event — do you re-emit? this is P04's allowed-lateness on the dimension side).
  • Implement the CDC snapshot + streaming bootstrap (initial full-table snapshot then switch to the change stream — Debezium's actual lifecycle).
  • Add a windowed stream-stream join with watermark-driven state cleanup (combine this lab with P04's watermark machinery).

Phase 06 — Apache Spark Internals

Difficulty: ⭐⭐⭐⭐⭐ Estimated Time: 2 weeks (35–45 hours) Prerequisites: Phase 01 (partitioning, skew), Phase 08 helps but isn't required yet


Why This Phase Exists

Spark is the workhorse of batch (and a serious streaming engine, P05), and it is the subject of interview Round 3: "a Spark job on EMR runs for 9 hours, spills heavily, produces millions of small Parquet files, and causes Athena queries to scan excessive data." The JD demands you "understand Spark execution internals: DAGs, stages, tasks, shuffles, broadcast joins, adaptive query execution, partitioning, memory management, spill behavior, executor sizing, speculative execution, and failure recovery" and "debug slow Spark jobs, shuffle explosions, data skew, executor OOMs, excessive spill, bad partitioning, small-file problems."

This phase makes those words mechanical. The performance of a Spark job is almost entirely determined by shuffle, skew, partitioning, memory, and file layout — and once you can reason about those five, a "9-hour job" becomes a checklist, not a mystery.

Concepts

  • The execution hierarchy: application → jobs (per action) → stages (split at shuffle) → tasks (one per partition) → executors/cores; the DAG scheduler.
  • Narrow vs wide dependencies: narrow (map/filter — pipelined) vs wide (groupBy/join/ repartition — shuffle); why the shuffle is the cost center.
  • The shuffle: map-side write (sorted/spilled to disk) → reduce-side fetch; shuffle service; why shuffles dominate runtime, network, and disk; shuffle explosions.
  • Data skew: a few hot keys → fat partitions → straggler tasks; detection (median multiple) and fixes (salting, AQE skew-join, broadcast).
  • Joins: broadcast-hash vs sort-merge vs shuffle-hash; the broadcast threshold; AQE runtime join switching and skew-join splitting; dynamic partition pruning.
  • Adaptive Query Execution (AQE): coalescing shuffle partitions, switching join strategies, optimizing skew — using runtime statistics.
  • Memory & spill: unified memory (spark.memory.fraction), execution vs storage, Tungsten off-heap, spill to disk, executor OOM, executor sizing (cores × memory).
  • File layout: the small-file problem, target file sizes, repartition/coalesce before write, output partitioning (and over-partitioning), Parquet (P08).
  • Reliability: task retries, speculative execution, stage recomputation, idempotent writes (staging + atomic commit / table formats), safe backfills and reprocessing.
  • Spark SQL & Catalyst: logical → optimized → physical plan; predicate pushdown; cost-based optimization (P10); reading explain().

Labs

Lab 01 — Spark Execution Simulator (flagship, implemented)

FieldValue
GoalModel stage planning at shuffle boundaries, skew detection, AQE coalescing, broadcast decisions, shuffle sizing, spill estimation, and small-file reporting
ConceptsStages/shuffle, skew, AQE, broadcast joins, executor memory/spill, small files
How to Testpytest test_lab.py -v — 16 tests
Talking PointsWhy does a shuffle create a stage? When broadcast vs sort-merge? Why is spill a partition-count problem?
Resume bulletBuilt a Spark execution model (stage planning, skew detection, AQE coalescing, broadcast/shuffle sizing, spill + small-file diagnostics) for tuning batch jobs

→ Lab folder: lab-01-spark-simulator/

Lab 02 — Spark Execution Model in Scala (implemented, sbt test)

FieldValue
GoalThe Scala twin of Lab 01 (Spark is a Scala/JVM system): stage planning, skew detection, AQE coalesce, broadcast/shuffle sizing, spill, small-file report — verified with ScalaTest
ConceptsSame Spark internals, in idiomatic Scala (a real SparkSession needs JDK 17/21; the model runs anywhere)
How to Testsbt test — 6 ScalaTest specs (runs here); real-Spark path is the extension
Resume bulletModeled Spark's execution/tuning logic (stages, skew, AQE, spill, file sizing) in Scala with ScalaTest coverage

→ Lab folder: lab-02-scala-spark-model/

Extension Project A — Tune a real Spark job (spec)

Generate a skewed dataset; run a groupBy/join locally (PySpark); read the Spark UI; fix it with AQE, repartitioning, broadcast, and salting; measure the runtime and small-file count before/after.

Extension Project B — Safe backfill framework (spec; → P13)

Design (and sketch in code) a backfill that is idempotent (staging + atomic commit or a table-format write), partitioned by date, resumable, and reproducible — the JD's "reproducible and auditable backfills."

Integrated-Scenario Hooks

  • This phase's small-file report is the cause of the Athena scan-cost explosion (P10) and the motivation for compaction (P09).
  • Its idempotent write pattern is realized by table-format commits (P09).
  • Its skew is P01's skew, fourth costume; its shuffle sizing reappears for EMR cost (P07).

Guides in This Phase

Key Takeaways

  • Spark performance = shuffle + skew + partitioning + memory + file layout. Master those five.
  • A stage boundary is a shuffle; tasks = partitions; the shuffle is where time and money go.
  • Spill is a partition-count problem as much as a memory one; AQE coalesces small partitions.
  • Small files are the silent killer that turns into Athena's bill (P10).
  • Idempotent writes (staging + atomic commit / table formats) make backfills safe.

Deliverables Checklist

  • Lab 01 implemented; all 16 tests pass
  • You can narrate the Round-3 9-hour-job diagnosis end to end
  • You can explain AQE's three tricks (coalesce, join switch, skew-join)
  • Extension A real-tune or Extension B backfill design

Warmup Guide — Apache Spark Internals

Zero-to-principal primer for Phase 06: how Spark turns your code into jobs/stages/tasks, why the shuffle dominates everything, how skew/spill/small-files actually arise, what AQE does, and how to read and fix a 9-hour job.

Table of Contents


Chapter 1: The Execution Hierarchy

Spark turns a program into a hierarchy you must be able to name to debug:

  • Application → one driver + executors. Job → triggered by each action (count, write, collect). Stage → a set of tasks that can run without a shuffle; stages are split at shuffle boundaries (Ch. 2). Task → the unit of execution, one per partition, run on an executor core.
  • So the parallelism of a stage = its partition count, and the wall-clock time of a stage ≈ the slowest task (the straggler — Ch. 4). Two numbers explain most performance: how many partitions, and how evenly the data is spread across them.
  • The DAG scheduler builds the stage graph; the task scheduler places tasks on executors honoring locality. The lab's plan_stages is the DAG scheduler's split-at-shuffle in miniature.

Chapter 2: Narrow vs Wide — Why Shuffles Make Stages

A transformation's dependency determines whether it needs a shuffle:

  • Narrow: each output partition depends on one input partition (map, filter, union). These pipeline within a stage — no data movement across the network.
  • Wide: each output partition depends on many input partitions (groupByKey, reduceByKey, join, repartition, distinct, sort). These require a shuffle: redistribute data by key across the cluster. A shuffle materializes intermediate data (written to disk) and ends a stage — the next stage reads the shuffle output.

So #stages = #shuffles + 1, and every wide op is a place where data crosses the network and hits disk. This is why the principal's first instinct on a slow job is "how many shuffles, and can I remove one?" (e.g. replace a shuffle-join with a broadcast-join, Ch. 5).

Chapter 3: The Shuffle in Detail

The shuffle is the most expensive operation in Spark and the source of most pathologies:

  • Map side: each task partitions its output by the target partition (hash of key), sorts/buffers it, and spills to disk when buffers fill, producing shuffle files.
  • Reduce side: each task in the next stage fetches its partition's data from every map task (an all-to-all transfer) — M × R connections in the worst case. The external shuffle service serves these so executors can die without losing shuffle data.
  • Shuffle explosion: too many partitions (tiny tasks, scheduler overhead, many small files) or too few (huge tasks, spill, OOM). spark.sql.shuffle.partitions defaults to 200 — almost always wrong for your data; size it by bytes (the lab's shuffle_partitions: ceil(total / 128MB)).
  • The cost levers: fewer shuffles (broadcast, pre-partitioning/bucketing), right-sized partitions (128–256 MB), and AQE (Ch. 6).

Chapter 4: Data Skew

Skew is P01's hot-partition problem, now killing a batch job: a few hot keys (a "whale" account, a NULL join key, a Zipfian distribution) send disproportionate data to a few reduce partitions, so a handful of tasks run for hours while the rest finish in seconds — and the stage can't complete until the straggler does.

  • Detect: per-partition (per-task) input sizes in the Spark UI; a few partitions far above the median (the lab's detect_skew, median-multiple). Also: one task at 100% while others idle; massive spill on a few tasks.
  • Fix:
    • Broadcast the other side if it's small (no shuffle at all — Ch. 5).
    • Salting: append a random bucket to the hot key (key#0..key#n), aggregate in two stages (per-bucket then combine) — spreads the whale across partitions (P01's salt).
    • AQE skew-join (Ch. 6): Spark automatically splits skewed partitions into sub-partitions at runtime.
    • Filter/handle the NULL-key case explicitly (a shockingly common cause).

Chapter 5: Join Strategies

Spark picks a join physical plan; knowing them lets you force the right one:

  • Broadcast-hash join: ship the small side to every executor and hash-join locally — no shuffle. The fastest when one side fits spark.sql.autoBroadcastJoinThreshold (default 10 MB; raise it deliberately for medium dims). The lab's should_broadcast.
  • Sort-merge join: shuffle both sides by key, sort, merge. The default for two large tables; robust but pays two shuffles + sorts.
  • Shuffle-hash join: shuffle both, build a hash table on one side; used in narrower cases.
  • Dynamic partition pruning: at runtime, use the dimension's join keys to prune the fact-table partitions scanned — a big win for star schemas.

The principal move: confirm the small side is actually small (stats can be stale → ANALYZE TABLE), broadcast it, and you've turned a two-shuffle sort-merge into a no-shuffle broadcast — often the single biggest speedup.

Chapter 6: Adaptive Query Execution

AQE re-optimizes the plan at runtime using actual shuffle statistics (instead of trusting stale compile-time estimates). Its three tricks, all interview-relevant:

  1. Coalesce shuffle partitions: after a shuffle, merge small adjacent partitions up to a target size — so you can set shuffle.partitions high without ending up with thousands of tiny tasks (the lab's coalesce_partitions). Note it never splits a big partition — that's the skew path.
  2. Switch join strategy: a planned sort-merge join becomes a broadcast join once runtime stats reveal one side is actually small.
  3. Skew-join handling: detect skewed partitions and split them into sub-partitions that run in parallel — automating Ch. 4's salting.

Turn AQE on (spark.sql.adaptive.enabled=true, default in modern Spark) and a lot of manual tuning evaporates — but you still must understand what it's doing to debug when it doesn't help (e.g. it can't fix skew it can't see, or a UDF that blocks pushdown).

Chapter 7: Memory, Spill, and Executor Sizing

  • Unified memory: an executor's heap is split (after reserved memory) into a region governed by spark.memory.fraction (default 0.6), shared between execution (shuffle, join, sort, aggregation buffers) and storage (cached data), which borrow from each other. The rest is user memory.
  • Spill: when a task's working set (e.g. a partition being sorted/aggregated) exceeds the available execution memory, Spark spills to disk — correct but slow (the lab's estimate_spill). The fix is frequently more partitions (smaller per-partition working set), not just more memory — a key insight juniors miss.
  • Executor OOM: too few partitions (giant tasks), a huge collect/broadcast, skew piling data on one executor, or too many cores per executor (each core's task competes for the same heap). Sizing: a common rule is ~4–5 cores/executor and memory sized so target-size partitions fit with headroom; more, smaller executors often beat few giant ones.
  • Off-heap / Tungsten: Spark's binary memory format + code generation reduce GC and object overhead — why Spark SQL is faster than RDD-of-objects.

Chapter 8: File Layout and the Small-File Problem

How Spark writes determines downstream cost (P08–P10):

  • A write produces one file per output partition per task, so 200 shuffle partitions × N tasks can emit millions of tiny files — the small-file problem. Tiny files mean huge metadata, slow listing, poor scan performance, and (on S3) request-cost and throttling pain. The lab's small_file_report quantifies the fix.
  • Target file size: 128 MB – 1 GB. Too small = the tax above; too large = poor read parallelism and memory pressure.
  • Fixes: repartition(n) / coalesce(n) to target the right file count before write (repartition shuffles to balance; coalesce avoids a shuffle but can't increase partitions); partition the output by a sensible column (date) without over-partitioning (which recreates the small-file problem per partition); run compaction (P09) on table formats.

Chapter 9: Reliability, Idempotent Writes, and Backfills

  • Fault tolerance: tasks retry on failure; stages recompute from shuffle data (or recompute the lineage if shuffle data is lost). Speculative execution re-launches slow ("straggler") tasks on other executors and takes whichever finishes first — helps with bad nodes, wastes resources on genuine skew (where the fix is Ch. 4, not speculation).
  • Idempotent writes: a naive job that fails mid-write can leave partial output; a retry then duplicates. The fixes: write to a staging path then atomically rename/commit (the FileOutputCommitter v2 caveats matter on S3 — no atomic rename!), or use a table format (P09) whose commit is atomic (the safe modern answer). Overwrite a partition atomically (INSERT OVERWRITE dynamic partition) for partition-scoped backfills.
  • Safe backfills (a JD deliverable): partitioned by date, resumable (reprocess only failed partitions), reproducible (deterministic logic — event-time, P01 — so a rerun yields identical output), and auditable (record what was reprocessed). The test of a good backfill is the same as a good replay: rerun it and get the same answer.

Lab Walkthrough Guidance

Lab 01 — Spark Execution Simulator, suggested order:

  1. plan_stages/num_stages — new stage at each wide op; #stages = #wide + 1.
  2. detect_skew — median-multiple; the fat partition pops out.
  3. coalesce_partitions — greedy merge to target; the big partition stands alone.
  4. should_broadcast + shuffle_partitions — the join + sizing decisions.
  5. estimate_spill — partition vs execution-memory share.
  6. small_file_report — the compaction pitch (reduction ratio).

Success Criteria

You are ready for Phase 07 when you can, from memory:

  1. Map application → job → stage → task and say what creates a stage.
  2. Explain the shuffle (map write/spill → reduce fetch) and M×R cost.
  3. Diagnose skew (detection + four fixes) and explain salting.
  4. Choose broadcast vs sort-merge and state the threshold.
  5. Explain AQE's three tricks.
  6. Explain spill and why more partitions can fix it; size executors sanely.
  7. Explain the small-file problem and how to fix it on write.
  8. Describe an idempotent, reproducible backfill.

Interview Q&A

Q (Round 3): A Spark job runs 9 hours, spills heavily, writes millions of small Parquet files, and Athena then scans too much. Walk me through it. Four linked symptoms. The 9 hours + heavy spill point at shuffle + skew: I'd check the Spark UI for a stage where one task runs far longer than the rest (skew) and for partitions exceeding execution memory (spill). Fix skew by broadcasting the small side if applicable, salting the hot key, or enabling AQE skew-join; fix spill by raising the partition count (smaller working sets) rather than just memory. The millions of small files come from too many output partitions × tasks — I'd repartition/coalesce to target 128 MB–1 GB files before write (or write to a table format and compact). That same small-file problem is why Athena scans too much and costs too much (P10): few large, well-partitioned, columnar files let Athena prune and skip. So the through-line is: tame the shuffle/skew to fix runtime, fix file sizing to fix both the write and the downstream scan cost.

Q: When does adding executor memory NOT fix spill? When the real problem is too few partitions. Spill happens when a single task's working set exceeds its execution-memory share; if you have 200 partitions over a 1 TB shuffle, each task handles ~5 GB and will spill regardless of reasonable heap sizes. Doubling memory delays it; doubling the partition count halves each task's working set and often eliminates it. So my first move on spill is "are partitions sized to ~128–256 MB?" before "give it more RAM" — more memory is the expensive fix for a free one.

Q: Broadcast vs sort-merge join — how do you decide, and what's the trap? If one side fits the broadcast threshold (default 10 MB, often worth raising for medium dimensions), broadcast it — no shuffle, dramatically faster. Otherwise sort-merge (shuffle both, sort, merge). The trap is stale statistics: Spark might not broadcast because it thinks a side is bigger than it is (or AQE hasn't kicked in), so I ANALYZE TABLE to refresh stats or hint the broadcast explicitly. The bigger trap is broadcasting something that's not actually small and OOMing the executors — confirm the size, don't guess.

Q: How do you make a backfill safe? Idempotent, reproducible, resumable, auditable. Idempotent: write to a staging path with an atomic commit, or use a table format (Iceberg/Delta) whose commit is atomic and overwrite by partition — so a retry can't duplicate. Reproducible: deterministic logic keyed on event time (P01), so a rerun produces identical output. Resumable: partition by date and reprocess only the failed partitions. Auditable: record which partitions were rewritten and from what input version. The acceptance test is the replay test — run it twice, diff the output, expect zero.

References

  • Spark: The Definitive Guide (Chambers & Zaharia); Learning Spark, 2e
  • High Performance Spark (Karau & Warren) — skew, shuffle, memory in depth
  • Spark SQL performance tuning & AQE
  • Spark tuning guide — memory, serialization
  • Zaharia et al., Resilient Distributed Datasets (NSDI 2012) — the RDD/lineage paper
  • PMC track Phase 07 WARMUP — partitioning/shuffle foundations

🛸 Hitchhiker's Guide — Phase 06: Apache Spark Internals

Read this if: you want to debug a slow/expensive Spark job fast and ace the "9-hour-job" interview round. Skim, read WARMUP, build the simulator.


0. The 30-second mental model

Spark splits your code into stages at every shuffle; each stage runs one task per partition; a stage finishes when its slowest task does. So performance = shuffle + skew + partitioning + memory + file layout. One sentence: fewer shuffles, evenly-sized partitions (128–256 MB), broadcast the small side, let AQE coalesce, and don't write a million tiny files.

1. The hierarchy

app → job (per action) → STAGE (split at shuffle) → TASK (1 per partition) → core/executor
#stages = #shuffles + 1        stage time ≈ slowest task (the straggler)

2. Narrow vs wide

narrow (map/filter)         → pipeline in-stage, no network
wide (groupBy/join/repartition/distinct/sort) → SHUFFLE → new stage, hits disk+network
first instinct on a slow job: "how many shuffles, can I remove one (broadcast)?"

3. The five performance levers

LeverSymptom if wrongFix
Shuffle countlong runtime, network heavybroadcast, bucketing, fewer wide ops
Skew1 task runs for hourssalt, AQE skew-join, broadcast, fix NULL keys
Partition sizespill / OOM / tiny taskssize by bytes: ceil(total/128MB)
Memoryspill, executor OOMmore partitions first, then memory; ~4–5 cores/exec
File layoutmillions of small files, Athena costrepartition/coalesce to 128MB–1GB; compact

4. The numbers

spark.sql.shuffle.partitions default = 200   ← almost always wrong; size by bytes
target partition / file size = 128 MB – 1 GB
autoBroadcastJoinThreshold default = 10 MB   ← raise deliberately for medium dims
spark.memory.fraction = 0.6 (exec+storage share of heap)
spill ≈ max(0, partition_bytes − heap×0.6)   ← more partitions shrinks this

5. Join picker

small side ≤ threshold → BROADCAST hash join (no shuffle)   ← biggest single speedup
two big tables         → SORT-MERGE (shuffle both, sort, merge)
stale stats hiding a small side → ANALYZE TABLE / broadcast hint
star schema            → dynamic partition pruning

6. AQE = three runtime tricks

1. coalesce small shuffle partitions (never splits a big one)
2. switch sort-merge → broadcast when a side is actually small
3. skew-join: split skewed partitions into parallel sub-partitions
turn it on (default modern Spark); still understand it to debug when it can't help

7. Spill: the counterintuitive one

spill happens when a TASK's working set > its execution-memory share
fix = MORE PARTITIONS (smaller working set) at least as often as more RAM

8. Small files = the silent bill

write emits 1 file / output-partition / task → millions of tiny files
→ huge metadata, slow listing, bad scans, S3 request cost (P08/P10)
fix: repartition/coalesce to target BEFORE write; compact table formats (P09)

9. Beginner mistakes that mark you

  1. Leaving shuffle.partitions=200 for any data size.
  2. "Add more memory" for spill instead of more partitions.
  3. Sort-merge joining when one side fits a broadcast.
  4. Writing without controlling file count → small-file explosion.
  5. Ignoring the NULL-join-key skew bomb.
  6. Non-idempotent writes → duplicate data on retry/backfill.
  7. Trusting stale table stats (no ANALYZE) so AQE/broadcast misfire.

10. War stories

  • "One task at 100%, 199 idle." → skew. Salt the hot key or AQE skew-join.
  • "OOM on a join." → broadcasting a not-actually-small side, or too few partitions.
  • "Athena bill 20×'d after the new pipeline." → the Spark job writes millions of small files; compact + partition (P09/P10).
  • "Backfill double-counted." → non-idempotent write; staging+atomic commit or table format.

11. How this phase pays off later

  • Small-file report → P08 (Parquet sizing), P09 (compaction), P10 (Athena cost).
  • Idempotent writes → P09 table-format commits.
  • Shuffle/executor sizing → P07 EMR cost & YARN.
  • Skew → P01, P02, P11 — same enemy, fifth costume.

Read WARMUP, build the simulator, then P07: where Spark runs — Hive/Tez/MapReduce lineage and EMR operations.

👨🏻 Brother Talk — Phase 06, Off the Record

Spark is where the big cloud bills and the all-night job-watching live. Let me give you the debugging instincts that took me years and a lot of wasted compute to earn.


Brother, here's a number that'll focus your attention: a misbehaving Spark job is often the single most expensive thing in a data platform. A streaming bug loses some events; a Spark bug burns a 200-node cluster for nine hours and produces a million files that then make every downstream query cost ten times more. So getting good at Spark isn't just a skill — it's directly money. The people who can look at a 9-hour job and make it a 40-minute job are paid well precisely because that swing is worth real dollars every single day.

So let me give you the debugging instinct, the one that separates the people who flail at Spark from the people who diagnose it. When a Spark job is slow, do not start changing configs. Open the Spark UI and look at the stages. Find the stage that's taking the time. Look at its tasks. And ask one question: is the time spread evenly across tasks, or is one task taking forever while the rest finished? That single observation forks your entire diagnosis:

  • If one task is the straggler → it's skew. A hot key is dumping most of the data onto one partition. No amount of memory or nodes fixes skew — you fix the distribution (salt the key, broadcast the other side, enable AQE skew-join, or kill the NULL-key bomb).
  • If all tasks are slow and spilling → it's partition sizing / memory. Your partitions are too big, each task's working set exceeds memory, everything spills to disk. Fix: more partitions, smaller working sets.
  • If the job is fast but the output is a zillion files → it's a write/layout problem that becomes someone else's query bill. Fix: coalesce/repartition before write.

That fork — even vs uneven task times — is 80% of Spark debugging, and almost nobody teaches it. They teach you knobs. The knobs are useless until you've read the UI and know which problem you have.

Now, the counterintuitive truth I want to drill into you, because it's the most common expensive mistake: "the job is spilling, give it more memory" is usually wrong. Spill happens when a single task's chunk of data is bigger than the memory that task gets. If you have a terabyte split into 200 partitions, each task is wrestling 5 GB and will spill no matter how fat you make the executors. The fix isn't a bigger executor — it's more partitions, so each task handles 256 MB instead of 5 GB. More memory is the expensive band-aid; more partitions is the free cure. I watched teams 3× their cluster cost fighting spill that a one-line repartition would have fixed. Don't be that team.

The small-file thing deserves its own rant, because it's the gift that keeps on taking. When Spark writes, it makes roughly one file per partition per task, and if you're not paying attention you get millions of tiny files. And here's the cruelty: the Spark job that creates them might run fine. The pain lands downstream — on the Athena queries (P10) that now scan and pay for all that fragmentation, on the metastore that chokes listing them, on the S3 bill for all those requests. So the engineer who created the mess often isn't the one who feels it, which is why it keeps happening. Be the person who controls file count on write. It's a coalesce away, and it saves the org a downstream tax they'd otherwise pay forever.

A word on AQE: turn it on, love it, but don't worship it. Adaptive Query Execution will fix a lot of your partition-sizing and even some skew automatically, and it's genuinely great. But it can only fix what it can see in the runtime stats, and it can't fix a UDF that blocks predicate pushdown, or a fundamentally bad join, or skew it doesn't detect. So understand what it does — coalesce, join-switch, skew-split — so that when it doesn't help, you know why and what to do by hand. AQE is an autopilot; you still need to know how to fly.

The reliability stuff — idempotent writes, safe backfills — feels boring next to performance, but it's where the trust is won. The first time you have to backfill three months of data because of an upstream bug, and you can do it knowing that a retry won't duplicate and a rerun produces byte-identical output, you'll understand why we obsess over it. The teams that fear backfills are the teams whose writes aren't idempotent. The teams that backfill casually, like it's nothing, built on table formats (P09) with atomic commits. Be the second team. "Can you safely reprocess the last quarter?" should make you shrug, not sweat.

Career-wise: Spark expertise is one of the most legible forms of value in data engineering, because the wins are measurable. "I took the nightly job from 9 hours to 45 minutes and cut the cluster cost 70%" is a sentence that gets you promoted, and it's the kind of win this phase teaches you to produce. Performance work has a scoreboard, and a scoreboard is a beautiful thing for a career.

Build the simulator. Internalize the even-vs-uneven fork. Then come to P07, where we look at where Spark actually runs — Hive and Tez and the Hadoop lineage, and EMR operations, where spot instances and cluster sizing turn all this into the cloud bill.

— your brother 👨🏻

Lab 01 — Spark Execution Simulator

Phase: 06 — Apache Spark Internals | Difficulty: ⭐⭐⭐⭐☆ | Time: 5–7 hours

Round 3 of the interview hands you "a Spark job on EMR runs for 9 hours, spills heavily, produces millions of small Parquet files, and makes Athena scan too much." This lab builds the calculator and diagnostics behind every sentence of that answer: stage planning at shuffle boundaries, skew detection, AQE coalescing, broadcast decisions, shuffle sizing, spill estimation, and a small-file report.

What you build

  • plan_stages / num_stages — split a logical plan into stages at shuffle (wide) boundaries (the thing that makes Spark Spark)
  • detect_skew — partitions far above the median → the "one task runs forever" bug
  • coalesce_partitions — AQE's merge of small post-shuffle partitions (and why a fat partition stands alone — coalesce never splits)
  • should_broadcast — broadcast-hash-join decision (skip the shuffle for a small side)
  • shuffle_partitions — size parallelism by bytes (the fix for the wrong default 200)
  • estimate_spill — when a partition exceeds execution memory → spill (a partition-count problem, not just a memory one)
  • small_file_report — the compaction pitch ("1M files → 8k") that fixes Athena cost (P10)

Key concepts

ConceptWhat to understand
Stage boundarya shuffle materializes data; narrow ops pipeline within a stage
Skewa few partitions >> median dominate stage time; fix by salt/AQE/broadcast
AQE coalescemerge small partitions post-shuffle; never split (skew-join does that)
Broadcast joinship the small side everywhere; avoid the shuffle
Spillpartition > execution memory → disk; more partitions = less spill
Small filesthe #1 cause of scan-cost and metadata blowups (P08–P10)

Run

pip install -r requirements.txt
pytest test_lab.py -v
LAB_MODULE=solution pytest test_lab.py -v

Success criteria

  • All 16 tests pass.
  • You can narrate the Round-3 answer end to end using these functions.
  • You can explain why spill is fixed by more partitions as often as by more memory.
  • You can explain why a >target partition is left alone by coalesce but split by skew-join.

Extensions

  • Add salting: split a hot key into key#0..key#n, re-aggregate, and show the skew factor (P01) drop — the manual version of AQE skew-join.
  • Model AQE dynamic join switching (a sort-merge join that becomes a broadcast join once runtime stats show one side is small).
  • Add an idempotent write planner: stage-dir → atomic commit (foreshadows P09 table-format commits) so a retried job doesn't double-write.

Lab 02 — Spark Execution Model in Scala

Phase: 06 — Apache Spark Internals | Difficulty: ⭐⭐⭐⭐☆ | Time: 4–5 hours

Spark is a Scala/JVM system, so its tuning logic is naturally Scala. This lab is the Scala twin of Lab 01's Python simulator — stage planning at shuffle boundaries, skew detection, AQE coalescing, broadcast/shuffle sizing, spill, and the small-file report — as pure functions verified with ScalaTest (sbt test).

Why a model, not a real SparkSession? Spark 3.5 targets JDK 8/11/17/21; this environment runs JDK 26, where Spark's heavy sun.misc.Unsafe/JDK-internal reflection won't start. The execution model captures the reasoning the interview tests (Round 3) without the runtime — and runs anywhere. The "run it on real Spark" path is the extension.

What you build (SparkModel)

  • planStages / numStages — split a plan into stages at wide (shuffle) ops
  • detectSkew — partitions > factor × median (the straggler detector)
  • coalescePartitions — AQE greedy merge to target (big partition stands alone)
  • shouldBroadcast / shufflePartitions — join + parallelism sizing
  • estimateSpill — partition vs execution-memory share
  • smallFileReport — the compaction pitch (reduction ratio)

Run

sbt test

Expected: 6 tests, all green in SparkModelSpec.

Success criteria

  • sbt test → all 6 specs pass.
  • You can narrate the Round-3 "9-hour job" diagnosis using these functions (WARMUP).
  • You can explain why spill is a partition-count problem and why coalesce never splits a fat partition (that's the skew-join path).

Extensions

  • Run it on real Spark (JDK 17/21): add "org.apache.spark" %% "spark-sql" % "3.5.1", build a skewed DataFrame, groupBy/join, read explain(), and confirm AQE coalesces and switches the join — then compare to this model's predictions.
  • Add salting (split a hot key k#0..k#n, re-aggregate) and show the skew factor drop.
  • Model AQE dynamic join switching (sort-merge → broadcast once runtime stats arrive).

Phase 07 — Hive, Tez, MapReduce & EMR

Difficulty: ⭐⭐⭐⭐☆ Estimated Time: 1.5 weeks (24–32 hours) Prerequisites: Phase 06 (Spark — runs on the same YARN/EMR substrate)


Why This Phase Exists

The JD lists Hive, Tez, MapReduce legacy systems, EMR, EMR Serverless, EMR on EKS, YARN and demands you "understand Hive and Tez execution … metastore behavior … legacy warehouse migration" and "EMR operations: cluster lifecycle, managed scaling, spot interruptions, bootstrap actions, instance fleets, YARN tuning, Spark-on-YARN, EMR Serverless, EMR on EKS, IAM, logging, and cost controls." It also lists migrating off Hive/Tez (Round 5). So you must understand the Hadoop lineage you're modernizing and operate the EMR clusters your Spark/Flink jobs run on — including the cost arithmetic that makes you the "cost efficiency" person.

Concepts

  • The Hadoop lineage: HDFS (P08) + MapReduce (map → shuffle/sort → reduce, every stage to disk) → Tez (DAG execution, pipelined, no disk between steps) → why Spark won (in-memory DAGs). Knowing this is knowing what you're migrating from.
  • Hive: the metastore (table → files + partitions + stats), the query compiler (HiveQL → MapReduce/Tez DAG), partitions & partition pruning, file formats (ORC native), statistics, bucketing, and the SerDe model. The ancestor of the lakehouse catalog (P10).
  • Tez: DAG vertices/edges, container reuse, dynamic parallelism — the bridge between MapReduce and Spark-style execution.
  • YARN: ResourceManager/NodeManager, containers (memory + vcores), the ApplicationMaster, schedulers (capacity/fair), and resource binding (the lab) — the cluster OS under Spark/ Hive/Tez on EMR.
  • EMR operations: cluster lifecycle (provision → bootstrap → run → terminate), instance fleets/groups, managed scaling, core vs task nodes, spot interruptions & safe placement, bootstrap actions, logging to S3, IAM roles (EMRFS), and cost controls.
  • EMR flavours: EMR-on-EC2 (you manage the cluster/YARN), EMR Serverless (no cluster, pay per vCPU/GB-hour), EMR on EKS (Spark on your Kubernetes) — and when each wins.
  • Cost: spot economics, right-sizing, the small-file/partition-pruning levers (P06/P10), cost attribution by team (a JD deliverable).

Labs

Lab 01 — EMR/YARN Cost & Scheduling Model (flagship, implemented)

FieldValue
GoalYARN container packing + binding resource, EMR-on-EC2 cost with spot mix, EMR Serverless cost, spot-interruption probability, core/task split, and Hive partition pruning
ConceptsYARN scheduling, spot economics, EMR cost models, partition pruning
How to Testpytest test_lab.py -v — 14 tests
Talking PointsWhat binds container count? When EMR Serverless vs EC2 vs EKS? Why core nodes on-demand?
Resume bulletBuilt an EMR/YARN capacity + cost model (container packing, spot risk, serverless vs cluster cost, partition pruning) for cost-efficient batch operations

→ Lab folder: lab-01-emr-cost-model/

Extension Project A — Hive → Spark/Iceberg migration plan (spec; → P15)

Map a legacy Hive/Tez warehouse to a modern lakehouse: preserve history, convert ORC layouts, keep the metastore working through the transition, eliminate small files, and decommission MapReduce/Tez — the JD's Problem 3 and Round 5.

Extension Project B — EMR cost review (spec)

Take a (synthetic) month of EMR usage; find waste (idle clusters, on-demand where spot is safe, over-provisioned nodes, full scans from missing partition filters) and produce a prioritized savings list with dollar estimates and cost attribution by team.

Integrated-Scenario Hooks

  • This phase's partition pruning is the same lever as Athena cost (P10) and Iceberg partitioning (P09).
  • Its spot/core-task model is how the capstone's batch tier (P16) runs cheaply.
  • Its migration plan is the spine of P15's Round-5 design review.

Guides in This Phase

Key Takeaways

  • MapReduce → Tez → Spark is a story of less disk between steps; know it to migrate well.
  • Hive's metastore + partitions are the ancestor of the lakehouse catalog (P10).
  • YARN packs containers by the binding resource (memory or vcores) — size tasks to fit.
  • Spot is cheap but interruptible: core nodes on-demand, task nodes on spot.
  • EMR Serverless wins for spiky/short jobs; EC2/EKS for steady or K8s-native ones.

Deliverables Checklist

  • Lab 01 implemented; all 14 tests pass
  • You can explain the MapReduce → Tez → Spark progression and why each step was faster
  • You can choose EMR-on-EC2 vs Serverless vs EKS with the deciding dial
  • Extension A migration plan or Extension B cost review

Warmup Guide — Hive, Tez, MapReduce & EMR

Zero-to-principal primer for Phase 07: the Hadoop lineage you're modernizing (MapReduce → Tez → Spark), Hive's metastore and partition model, YARN as the cluster OS, and EMR operations + cost — the substrate your batch jobs run on and the bills you must control.

Table of Contents


Chapter 1: The Hadoop Lineage — MapReduce → Tez → Spark

You will spend part of this role migrating off the Hadoop stack, so you must understand it:

  • MapReduce (2004): the original model. A job is map → shuffle/sort → reduce, and crucially every stage boundary materializes to HDFS (disk). Multi-step jobs chain many MR jobs, each writing/reading disk — robust but slow. It's the conceptual root of everything (P06's stages are MR's map/reduce generalized).
  • Tez (2013): replaced MR under Hive/Pig with a DAG engine — vertices and edges, pipelined, no forced disk write between every step, container reuse, dynamic parallelism. A big speedup for Hive without rewriting queries.
  • Spark (2014): in-memory DAGs with lineage-based recovery (P06) — caches intermediate data in memory, generalizes beyond map/reduce, unifies batch + SQL + streaming + ML.

The through-line: each step removed disk I/O between stages. When you justify a Hive/Tez → Spark/Iceberg migration (Round 5), this is the performance story.

Chapter 2: Hive — Metastore, Partitions, Formats

Hive is "SQL over files," and its lasting contribution is the metastore:

  • Hive Metastore (HMS): a relational DB mapping logical tables → physical locations (HDFS/S3 paths), partitions, schemas, SerDes, and statistics. Every engine — Spark, Trino, Presto, Flink — can use the HMS (or the Glue Data Catalog, its AWS-managed equivalent, P10) as the source of truth for "what tables exist and where." The lakehouse catalog (P09/P10) is the metastore's descendant.
  • Partitions: a Hive table is partitioned by columns (e.g. dt=2026-06-14/region=us), which become directory paths. A query with a predicate on a partition column prunes to the matching directories (the lab's partitions_scanned) — the single biggest cost/perf lever. The classic failure: too many partitions (high-cardinality partition column → millions of tiny directories → metastore meltdown) or the small-file problem within partitions (P06/P08).
  • Statistics: ANALYZE TABLE populates row counts and column stats the optimizer needs (P10 CBO); stale stats = bad plans.
  • Formats: Hive's native columnar format is ORC (P08), with built-in indexes and bloom filters; Parquet is equally supported. The SerDe abstracts row encoding.
  • External vs managed tables: managed = Hive owns the data (drop = delete); external = Hive only catalogs it (drop = metadata only). Get this wrong and you delete the lake.

Chapter 3: Tez Execution

Tez is the DAG engine that made Hive fast before Spark took over. Worth knowing because legacy warehouses still run on it and the JD lists "Tez container failures" as a debugging target:

  • A Tez job is a DAG of vertices (each like a map or reduce stage) connected by edges (data movement: one-to-one, broadcast, scatter-gather/shuffle). It pipelines vertices and reuses containers across them (avoiding MR's per-stage JVM startup).
  • Dynamic parallelism: Tez can adjust the number of reducer tasks at runtime based on data size (a precursor to Spark's AQE, P06).
  • Common failures: container OOM (vertex memory misconfigured), shuffle/skew (same as P06), and metastore bottlenecks under many concurrent queries.

Chapter 4: YARN — The Cluster OS

YARN (Yet Another Resource Negotiator) is the resource manager under MapReduce/Tez/Hive/Spark on EMR-on-EC2:

  • ResourceManager (cluster-wide scheduler) + NodeManagers (per-node agents) + per-job ApplicationMaster (negotiates resources, tracks the job). Spark-on-YARN runs the Spark driver in (or alongside) the AM and executors in YARN containers.
  • A container = a slice of a node's memory + vcores. How many containers fit on a node = min(node_mem // container_mem, node_vcores // container_vcores) — and the binding resource (whichever runs out first) is what you tune (the lab's yarn_plan). The classic surprise: "this node has 64 cores but only fits 8 containers" because memory bound first.
  • Schedulers: Capacity (queues with guaranteed shares) and Fair (equalize over time) — how you isolate tenants on a shared cluster.
  • Tuning: container sizing, yarn.nodemanager.resource.memory-mb, overhead for off-heap, and matching Spark executor memory to container memory (with overhead headroom, P06 Ch. 7).

Chapter 5: EMR Operations

EMR is AWS's managed Hadoop/Spark/Flink/Hive. What you operate:

  • Cluster lifecycle: provision → bootstrap actions (install deps, configure) → run steps → terminate (transient clusters) or stay (long-running). Transient per-job clusters are a cost pattern; long-running shared clusters are a multi-tenancy pattern.
  • Instance fleets / groups: define node roles — master (1, runs the RM/AM), core (run NodeManagers and HDFS DataNodes — losing one risks data + the job), task (compute only, no HDFS — safe to lose). Fleets let you mix instance types and spot/on-demand with target capacities.
  • Managed scaling: EMR auto-adds/removes nodes based on YARN demand within min/max bounds — elastic cost without manual resizing.
  • Storage: EMRFS lets Hadoop read/write S3 as if it were HDFS (the lake lives in S3, P08); local HDFS is for shuffle/temp. IAM roles (instance profile + EMRFS role) govern S3 access (P14).
  • Logging: push YARN/Spark logs to S3 so they survive cluster termination — essential for post-mortems (P13).

Chapter 6: EMR Flavours — EC2 vs Serverless vs EKS

EMR on EC2EMR ServerlessEMR on EKS
You managethe cluster + YARNnothing (just submit)a K8s cluster
Scalingmanaged scalingautomatic per-jobK8s autoscaling
Cost shapenode-hours (idle costs!)per vCPU/GB-hour while runningnode-hours on EKS
Best forsteady, large, HDFS-heavyspiky/short/intermittent jobsK8s-native orgs, shared platform
Spin-upminutes (cluster boot)secondspod scheduling

The decision (lab's cost models): steady heavy batch → EC2; bursty/unpredictable → Serverless (no idle cost); already running everything on Kubernetes → EKS (one platform, one scheduler). The hidden factor is idle + bootstrap cost: a cluster that boots for 5 minutes and sits idle between hourly jobs is pure waste — Serverless eliminates it.

Chapter 7: Spot, Cost, and Cost Attribution

The "cost efficiency" mandate is arithmetic:

  • Spot instances: up to ~70–90% cheaper than on-demand, but reclaimable with a 2-minute warning. The probability that at least one of your spot nodes is interrupted is 1 − (1−p)^n (the lab) — which grows fast with node count, so at 50 nodes you should expect interruptions and design for them.
  • Safe spot placement: put core nodes (HDFS + AM — losing them kills the job/data) on on-demand, and task nodes (stateless compute) on spot (the lab's safe_spot_plan). Checkpoint long jobs so a reclaimed task node just reschedules its work.
  • Other levers (cross-phase): partition pruning (this lab) + columnar formats (P08) + compaction/no-small-files (P06/P09) cut scan and storage cost; right-sized containers cut compute; managed scaling + transient clusters cut idle.
  • Cost attribution (a JD deliverable): tag every cluster/job/bucket with an owner and a cost-allocation tag, so the bill is a map by team — the thing that actually changes behavior (teams optimize what they're charged for).

Chapter 8: Migrating Off the Legacy Stack

The JD's Problem 3 / Round 5: migrate Hive/Tez/Flume → Kafka/Flink/Spark/Iceberg/Athena. The principal pattern (full treatment in P15):

  1. Preserve history: keep the metastore working; the lakehouse can read the same S3 data via the catalog during transition.
  2. Convert layouts: ORC/Parquet conversion as needed; fix broken partitions; eliminate small files (compaction).
  3. Introduce the table format (Iceberg/Delta, P09): register existing data, then add ACID/time-travel/streaming writes.
  4. Dual-run & validate: run old (Hive/Tez) and new (Spark/Iceberg) in parallel; diff outputs (P13 data-diffing) until parity holds.
  5. Cut over queries (Athena/Trino, P10) and writers (Flink/Spark); decommission MapReduce/Tez/Flume only after a soak period with rollback available.

The non-negotiables: keep history reproducible, validate parity, and always have a rollback — the same migration discipline as schema evolution (P03) and backfills (P06).

Lab Walkthrough Guidance

Lab 01 — EMR/YARN Cost Model, suggested order:

  1. yarn_planmin(mem//task, vcores//task); report the binding resource (the lesson).
  2. cluster_cost_usd — on-demand + spot mix.
  3. emr_serverless_cost_usd — per vCPU/GB-hour; scales linearly with time.
  4. spot_interruption_probability1−(1−p)^n; grows with node count.
  5. safe_spot_plan — core on-demand, task spot.
  6. partitions_scanned — equality-filter pruning; no filter = full scan.

Success Criteria

You are ready for Phase 08 when you can, from memory:

  1. Explain MapReduce → Tez → Spark as a disk-I/O-reduction story.
  2. Explain the Hive metastore, partitions, pruning, and managed-vs-external tables.
  3. Explain YARN containers and the binding-resource computation.
  4. Explain EMR node roles (master/core/task) and why spot goes on task nodes.
  5. Choose EMR-on-EC2 vs Serverless vs EKS for a workload, with the deciding factor.
  6. Compute spot interruption probability and a safe core/task split.
  7. Outline a Hive/Tez → lakehouse migration with validation and rollback.

Interview Q&A

Q: You're asked to cut the EMR bill 40%. Where do you look first? Attribution first — tag clusters/jobs by team so the bill is a map; you can't cut what you can't see. Then the mechanical levers in order of usual impact: (1) idle/bootstrap waste — move spiky jobs to EMR Serverless or transient clusters so you stop paying for warm idle clusters; (2) spot — put task nodes on spot (core nodes stay on-demand) for ~70% off the compute that's safe to interrupt; (3) scan cost — partition pruning + columnar + no small files (P06/P08/P10) often cuts the Athena/Spark scan bill 10×; (4) right-sizing — containers and node types matched to the binding resource. Each is a number I can estimate, so I'd present a prioritized list with dollar figures, not a vibe.

Q: A core node on spot got reclaimed and the job died. What went wrong? Core nodes run HDFS DataNodes and (often) the ApplicationMaster — losing one can lose shuffle/temp data and kill the application, so they must be on-demand. Spot belongs on task nodes, which are stateless compute: a reclaimed task node just reschedules its work elsewhere. The fix is the core/task split, plus checkpointing long jobs so a reclamation is a reschedule, not a restart. At any real node count, spot interruptions are a certainty (1−(1−p)^n), so the architecture has to assume them.

Q: When is EMR Serverless the right call over a cluster? When the workload is spiky, short, or intermittent, because a managed cluster charges you for boot time and idle minutes between jobs, while Serverless bills only the vCPU/GB-hours you actually consume and starts in seconds. For a steady, all-day, HDFS-heavy pipeline a right-sized EC2 cluster with managed scaling can be cheaper and gives more control; for "a few jobs an hour" Serverless usually wins on the idle-cost elimination alone. If the org already runs everything on Kubernetes, EMR-on-EKS unifies the platform and scheduler.

References

🛸 Hitchhiker's Guide — Phase 07: Hive, Tez, MapReduce & EMR

Read this if: you need the Hadoop lineage you're modernizing and the EMR ops + cost arithmetic, fast. Skim, read WARMUP, build the cost model.


0. The 30-second mental model

MapReduce → Tez → Spark is a story of removing disk between stages. Hive gave us the metastore (table→files+partitions, the lakehouse catalog's ancestor). YARN is the cluster OS that packs jobs into memory+vcore containers. EMR runs all of it on AWS — and your job there is mostly cost: spot, right-sizing, partition pruning, no idle clusters. One sentence: know the legacy to migrate it; operate EMR to make it cheap and reliable.

1. The lineage

MapReduce: map → shuffle/sort → reduce, DISK between every stage (slow, robust)
Tez:       DAG, pipelined, container reuse, dynamic parallelism (Hive got fast)
Spark:     in-memory DAG + lineage recovery (P06)  ← what you migrate TO
through-line: each step removed disk I/O between stages

2. Hive = the metastore

Hive Metastore (or Glue Catalog) = table → S3/HDFS path + partitions + schema + stats
partitions = directories (dt=2026-06-14/region=us); predicate → PRUNE to matching dirs
traps: too-many-partitions (high-cardinality col → metastore meltdown), small files
managed table (drop=delete data!) vs external (drop=metadata only)
ANALYZE TABLE for stats (CBO needs them, P10); native columnar = ORC (P08)

3. YARN containers

container = memory + vcores slice of a node
per node = min(node_mem // task_mem, node_vcores // task_vcores)
the BINDING resource (runs out first) is what you tune
"64 cores but only 8 containers" = memory bound

4. EMR node roles (memorize)

master : 1, runs ResourceManager / AM
core   : NodeManager + HDFS DataNode → losing one risks DATA + the job → ON-DEMAND
task   : compute only, no HDFS → safe to lose → SPOT

5. EMR flavours

EMR on EC2     → you run the cluster+YARN; steady heavy batch; idle costs money
EMR Serverless → no cluster, pay per vCPU/GB-hour WHILE running; spiky/short jobs win
EMR on EKS     → Spark on your Kubernetes; K8s-native orgs, one platform
hidden cost: cluster boot + idle minutes → Serverless eliminates it

6. The cost numbers

spot savings ≈ 70–90%, 2-min reclaim warning
P(≥1 interruption) = 1 − (1−p)^n   ← 50 nodes @2% ≈ 0.64 → expect it
Athena/Trino scan = $5/TB → partition prune + columnar + no small files = the bill
EMR Serverless ≈ $0.052/vCPU-hr + $0.0058/GB-hr

7. Cost-cut playbook (in order of usual impact)

1. attribute (tag by team) — can't cut what you can't see
2. kill idle/bootstrap waste (Serverless / transient clusters)
3. spot the task nodes (~70% off safe compute)
4. scan cost: partition prune + columnar + compaction (often 10×)
5. right-size containers/nodes to the binding resource

8. Beginner mistakes that mark you

  1. Core nodes on spot → reclaim kills HDFS + the job.
  2. High-cardinality partition column → millions of dirs, metastore meltdown.
  3. Dropping a managed table thinking it's external → data gone.
  4. Paying for warm idle clusters between hourly jobs (use Serverless).
  5. Sizing containers without checking which resource binds.
  6. No partition filter → full-table scan → the cost explosion.

9. War stories

  • "Spot reclaim killed the cluster." → core node was on spot. Core=on-demand always.
  • "Metastore is timing out." → someone partitioned by user_id (millions of dirs).
  • "The bill doubled and nobody knows whose job." → no cost-allocation tags.
  • "DROP TABLE deleted the lake." → it was a managed table, not external.

10. How this phase pays off later

  • Partition pruning → P09 (Iceberg partitioning), P10 (Athena cost).
  • Metastore → P10 (Glue Catalog, Lake Formation).
  • Migration plan → P15 Round-5 design review.
  • Cost arithmetic → P13 cost reviews, P00 tradeoff dials.

Read WARMUP, build the cost model, then P08: where the data actually lives — S3, HDFS, and the columnar formats (Parquet/ORC) that make scans cheap.

👨🏻 Brother Talk — Phase 07, Off the Record

The "legacy and ops" phase. Less glamorous than Flink, but it's where the cloud bill lives and where migrations are won or lost. Let me tell you what actually matters.


Brother, I know how this phase feels. After the intellectual high of Flink and Spark internals, "Hive metastore and EMR operations" sounds like a step backward — like you're being asked to learn the old stuff. And partly you are. But here's the reframe that made me take it seriously: you cannot migrate off a system you don't understand, and migrating legacy warehouses to the lakehouse is one of the highest-value things a principal does. The Round-5 interview is literally "lead the migration from Hive/Tez/Flume to Kafka/Flink/Iceberg." You can't lead that migration if Hive is a black box to you. So this phase isn't about loving the old stack — it's about understanding it well enough to retire it safely.

Let me give you the operational truths.

The EMR bill is your reputation. I mean it. In a lot of orgs, the data platform is one of the biggest line items in the cloud bill, and EMR clusters are a huge chunk of that. The engineer who can walk into a cost review and say "I found $40k/month: these clusters sit idle between hourly jobs so I moved them to Serverless, these task nodes can be spot for 70% off, and this query does a full scan because someone dropped the partition filter" — that engineer gets listened to, because they just found real money. Cost optimization has a scoreboard, and a scoreboard makes you visible. Learn the arithmetic in this lab; it's not academic, it's your next performance review.

The spot/core/task thing will bite someone on your team. Make sure it's not you. Here's the trap: spot instances are gloriously cheap, so the temptation is to put everything on spot. And then a reclamation takes out a core node — which was running HDFS and the application master — and the whole job dies, maybe with data loss. The rule is simple and you must never violate it: core nodes on-demand, task nodes on spot. Core nodes hold state (HDFS) and coordination (AM); losing them is catastrophic. Task nodes are disposable compute; losing one just reschedules work. And do the math from the lab — at 50 spot nodes with a 2% interruption rate, the probability of losing at least one is about 64%. You're not gambling on whether you'll get interrupted; you're guaranteed to. Design for it.

Partitioning is a double-edged sword, and people cut themselves on both edges. Partition your tables and queries can prune to the directories they need — massive cost and speed win. But partition by a high-cardinality column (user_id, session_id, anything with millions of values) and you create millions of tiny directories, the metastore chokes listing them, and you've built a slower system than no partitioning at all. The art is partitioning by something with the right cardinality — usually date, sometimes date + a low-cardinality category — that matches how queries actually filter. When you design a table, the question is "how will people filter this?" and you partition to match. Get this right and you're a hero; get it wrong and you're the cause of the metastore incident.

The DROP TABLE footgun. This one's almost a rite of passage, so let me inoculate you: managed tables, when dropped, delete the underlying data. External tables only drop the metadata. Someone, somewhere, on every team, eventually runs DROP TABLE on a managed table thinking it just removes the catalog entry, and deletes a chunk of the lake. Know which kind your tables are. For anything important, prefer external tables (or table formats with proper governance, P09/P14) so a careless drop is recoverable. The day you prevent this for your team is a day you've earned trust.

Now the migration wisdom, because it's the real prize of this phase. Migrating off Hive/Tez is not a rewrite-and-pray — it's a dual-run-and-validate. You stand the new lakehouse up alongside the old warehouse (they can even read the same S3 data through the catalog), you run both in parallel, you diff the outputs until they match for a soak period, then you cut over, and you keep the old path warm for rollback until you're sure. The engineers who botch migrations are the ones who flip the switch and hope. The ones who nail them treat it like schema evolution (P03) and backfills (P06): always reversible, always validated, never a big-bang. That patience is what makes a migration boring — and boring migrations are successful migrations. Nobody writes a postmortem about the migration that went smoothly because someone validated parity for two weeks first.

Career note: "I migrated us off the legacy Hadoop stack and cut the bill 40% while improving freshness" is a principal-defining sentence. It touches cost, reliability, and modernization all at once, and it requires exactly the cross-phase fluency this whole track is building. This unglamorous phase is where that sentence becomes possible.

Build the cost model. Internalize the spot/core/task rule and the partition-cardinality tradeoff. Then come to P08, where we go under all of this to the bytes themselves — S3 and the columnar formats (Parquet/ORC) whose internals decide whether a scan is cheap or ruinous.

— your brother 👨🏻

Lab 01 — EMR/YARN Cost & Scheduling Model

Phase: 07 — Hive, Tez, MapReduce & EMR | Difficulty: ⭐⭐⭐☆☆ | Time: 4–5 hours

Owning EMR means owning the cluster's cost and placement arithmetic. This lab builds it: YARN container packing (and which resource binds), EMR-on-EC2 cost with a spot mix, EMR Serverless pay-per-use, spot-interruption probability, the core/task node split that makes spot safe, and Hive partition pruning.

What you build

  • yarn_plan — bin-pack tasks into containers across nodes; report whether memory or vcores is the binding resource (the "64 cores but memory caps me" lesson)
  • cluster_cost_usd — EMR-on-EC2 cost with a spot fraction
  • emr_serverless_cost_usd — pay per vCPU-hour + GB-hour (the spiky-workload win)
  • spot_interruption_probability1 − (1−p)^n (why 50 spot nodes ≈ guaranteed loss)
  • safe_spot_plan — on-demand core nodes (HDFS/AM) + spot task nodes
  • partitions_scanned — Hive partition pruning (the cost/perf lever; full scan when no filter)

Key concepts

ConceptWhat to understand
YARN binding resourcea node fits min(mem//task, vcores//task) containers
Spot economicsup to ~70–90% off, but interruptible (2-min warning)
Core vs task nodescore (HDFS/AM) on-demand; task (compute) on spot
EMR Serverlessno idle cluster; pay only while running — great for spiky jobs
Partition pruningno filter = full scan = the cost explosion

Run

pip install -r requirements.txt
pytest test_lab.py -v
LAB_MODULE=solution pytest test_lab.py -v

Success criteria

  • All 14 tests pass.
  • You can explain why spot_interruption_probability(50, 0.02) ≈ 0.64 and why that means "don't put HDFS or the driver on spot."
  • You can decide EMR-on-EC2 vs EMR Serverless vs EMR-on-EKS for a stated workload.

Extensions

  • Add an EMR-on-EKS vs EMR-on-EC2 vs Serverless total-cost comparator for a given job shape (steady vs spiky) and recommend with the deciding dial (P00).
  • Model managed scaling: given a job's task curve over time, compute the node count and cost vs a fixed cluster.
  • Add bootstrap + idle cost: include cluster spin-up time and idle minutes — the hidden cost that makes Serverless win for short, frequent jobs.

Phase 08 — Storage & Columnar Formats

Difficulty: ⭐⭐⭐⭐☆ Estimated Time: 1.5 weeks (24–32 hours) Prerequisites: Phase 06 (small files), Phase 07 (partitions, EMRFS)


Why This Phase Exists

This is the byte level the JD demands you debug at: "Parquet internals: row groups, column chunks, pages, encodings, compression, statistics, predicate pushdown, dictionary encoding, and file sizing … Protobuf schema evolution … compare Avro, Protobuf, JSON, Parquet, and ORC … S3 consistency, object layout, request costs, prefix scaling, lifecycle policies, encryption, and cross-region replication." Everything above storage — query speed, scan cost, lakehouse performance — is determined by how the bytes are laid out here. The single most leveraged skill of the whole track is understanding why a columnar file with good stats and partitioning turns a 1 TB scan into a 10 GB one (a 100× cost difference).

Concepts

  • S3 as the lake substrate: object storage (not a filesystem); strong read-after-write consistency (since 2020); prefix-based request scaling (~3,500 PUT/s, 5,500 GET/s per prefix); request costs; multipart upload; lifecycle policies (tiering to IA/Glacier); encryption (SSE-S3/KMS, P14); cross-region replication; the small-file & listing cost.
  • HDFS (the on-prem ancestor): NameNode (metadata) + DataNodes (blocks), block size, replication factor — and why S3-as-lake replaced it (decoupled storage/compute, elasticity).
  • Parquet internals: file → row groupscolumn chunkspages; per-chunk statistics (min/max/null) enabling predicate pushdown; dictionary + RLE/ bit-packing encodings; compression (snappy/zstd/gzip); file sizing (128 MB–1 GB).
  • ORC: stripes → row index → streams; built-in indexes + bloom filters; Hive-native; Parquet vs ORC tradeoffs.
  • Avro (P03 revisited as storage): row-oriented, schema-carrying — great for ingestion/ write-heavy and schema evolution, poor for column-pruned analytics.
  • Format selection: row vs columnar; Parquet/ORC (analytics) vs Avro (ingestion) vs JSON/ CSV (interchange) — costed, not by taste.
  • Layout for skipping: sorting/clustering on filter columns to tighten row-group stats (the basis of Z-ordering, P09); partitioning vs the small-file/over-partitioning trap.

Labs

Lab 01 — Parquet Reader & Predicate Pushdown (flagship, implemented)

FieldValue
GoalBuild a columnar format: row groups with min/max/null stats, predicate pushdown that skips groups, column projection, and dictionary/RLE encoding
ConceptsColumnar layout, row-group stats, pushdown asymmetry (proves absence), encodings
How to Testpytest test_lab.py -v — 14 tests
Talking PointsWhy do stats only prove absence? Why does sorting boost pushdown? Why is projection a cost feature?
Resume bulletImplemented a columnar reader with row-group statistics, predicate pushdown, projection, and dictionary/RLE encoding

→ Lab folder: lab-01-parquet-reader/

Extension Project A — Real Parquet inspection (spec)

Write Parquet with pyarrow; inspect the footer (parquet-tools/pyarrow): row-group count, column stats, encodings, sizes. Sort on a column, rewrite, and observe tighter stats and better skipping. Measure bytes read for a projected, filtered query.

Extension Project B — Format & layout bake-off (spec)

Same dataset as Parquet/ORC/Avro/JSON: compare file size, write time, and a column-projected filtered read time. Vary row-group size and file size; chart the small-file penalty.

Integrated-Scenario Hooks

  • This phase's pushdown + sorting is what Iceberg/Delta exploit (P09) and what makes Athena/Trino cheap (P10).
  • Its small-file lens closes the loop with P06 (Spark writes) and P07 (Hive partitions).
  • Its S3 layout (prefixes, lifecycle, encryption) feeds P14 governance and P16 capstone.

Guides in This Phase

Key Takeaways

  • The bytes' layout decides every downstream cost; this is the highest-leverage phase.
  • Columnar = read only the columns and row groups you need; stats enable skipping.
  • Stats prove absence, so sorting/clustering on filter columns is what makes skipping work.
  • S3 is object storage with prefix scaling and request costs — small files hurt even idle.
  • Pick formats by cost: Parquet/ORC for analytics, Avro for ingestion, JSON/CSV for interchange.

Deliverables Checklist

  • Lab 01 implemented; all 14 tests pass
  • You can draw Parquet's file→row-group→column-chunk→page hierarchy
  • You can explain predicate pushdown and why sorting amplifies it
  • You can state S3 prefix scaling limits and the small-file cost
  • Extension A inspection or Extension B bake-off

Warmup Guide — Storage & Columnar Formats

Zero-to-principal primer for Phase 08: S3/HDFS as the lake substrate, Parquet and ORC internals (row groups, pages, stats, encodings), Avro vs columnar, and the data-layout decisions that determine every downstream query's speed and cost.

Table of Contents


Chapter 1: S3 — Object Storage as a Lake

S3 is the substrate of the modern lake, and it is object storage, not a filesystem — a distinction that explains many gotchas:

  • Objects, not files: a flat key→bytes store. "Directories" are a fiction (key prefixes); there's no cheap rename (a rename is a copy + delete — which is why naive Spark/Hadoop commit protocols are slow/unsafe on S3 and why table formats, P09, exist).
  • Consistency: since Dec 2020, S3 has strong read-after-write consistency for all operations — the old "eventual consistency" footguns (S3Guard, etc.) are gone. Know this is current; interviewers test whether you're up to date.
  • Request scaling: throughput scales per prefix — roughly 3,500 PUT/s and 5,500 GET/s per prefix, and S3 auto-scales as you spread keys across prefixes. A million tiny files under one prefix → throttling; spreading prefixes (and fewer, larger files) → speed.
  • Costs: storage ($/GB-month by tier), requests (PUT/GET per 1,000 — why small files cost money even idle), and data transfer (cross-region/egress). Lifecycle policies tier cold data to IA/Glacier automatically.
  • Security (P14): SSE-S3 / SSE-KMS encryption at rest, bucket policies, VPC endpoints, cross-region replication for DR.

Chapter 2: HDFS and Why S3 Won

HDFS was the original lake storage: a NameNode holds the filesystem metadata (the directory tree, block→DataNode map) and DataNodes store fixed-size blocks (default 128 MB), replicated (default 3×) for durability. It's fast for colocated compute (data locality) but has two fatal-at-cloud-scale problems: the NameNode is a memory-bound single point (millions of small files = NameNode heap death — the small-file problem's origin) and storage is coupled to compute (scaling one means scaling the other).

S3 won the lake because it decouples storage from compute (scale independently; spin clusters up/down against durable storage), is effectively infinite and 11-nines durable, and is cheaper. The cost: no data locality (compute reads over the network) and no atomic rename (→ table formats). Know HDFS to understand what EMR/Spark inherited and what you're migrating from (P07/P15).

Chapter 3: Row vs Columnar

The fundamental storage choice:

  • Row-oriented (CSV, JSON, Avro): all of row 1, then all of row 2… Great for writing whole records and reading whole rows (OLTP, ingestion). Bad for analytics: to sum one column you read every column of every row.
  • Columnar (Parquet, ORC): all of column A, then all of column B… Great for analytics:
    • Projection: reading 2 of 50 columns reads ~4% of the bytes.
    • Compression: a column is homogeneous (all ints, all of one enum) → compresses far better than mixed rows.
    • Skipping: per-chunk stats let you skip data that can't match (Ch. 6). Analytics is overwhelmingly "aggregate a few columns over many rows," so columnar wins — which is why the lakehouse is built on Parquet/ORC.

Chapter 4: Parquet Internals

Parquet's hierarchy (the lab builds it):

File
└── Row Group (≈128 MB of rows — the unit of parallelism & skipping)
    └── Column Chunk (one column's data for this row group)
        └── Page (≈1 MB — the unit of encoding/compression/IO)
    + Column statistics per chunk: min, max, null_count, (distinct_count)
File footer: schema + row-group metadata + column stats + offsets
  • A reader reads the footer first (metadata), then reads only the column chunks it needs (projection) from only the row groups whose stats can match (pushdown, Ch. 6) — often a tiny fraction of the file.
  • Row-group size trades parallelism vs skipping granularity vs memory; ~128 MB is typical.
  • Repetition/definition levels encode nested/optional fields (Dremel encoding) — how Parquet stores arrays/maps/structs and nulls compactly.

Chapter 5: Encodings and Compression

Two layers, and the order matters:

  1. Encoding (logical, type-aware): dictionary encoding maps distinct values → small integer codes (huge win for low-cardinality columns — the lab's dictionary_encode), then RLE + bit-packing on the codes (huge win for runs/sorted data — the lab's rle_encode). Delta encoding for sorted integers/timestamps.
  2. Compression (byte-level): snappy (fast, default), zstd (better ratio, now often preferred), gzip (smaller, slower). Applied to the already-encoded pages.

The lesson: columnar + dictionary + RLE is why a column compresses 10× where the same data as JSON rows barely compresses — and why sorting the data first (so runs form) multiplies the win. This is also a cost lever (fewer bytes = cheaper scans, P10).

Chapter 6: Predicate Pushdown and Why Sorting Matters

Predicate pushdown = the reader uses per-row-group statistics to skip groups that can't contain a matching row. For WHERE id = 55, a group with min=60, max=69 is skipped without reading its data. The lab's row_group_can_match is exactly this.

The crucial asymmetry: statistics prove ABSENCE, not presence. A group with min=50, max=59 might contain id=55, so it's scanned and row-filtered; a group with min=60 can't, so it's skipped. This is why pushdown's effectiveness depends entirely on how tight the per-group min/max ranges are — and that depends on data layout:

  • If the data is sorted by id, each row group covers a tight, disjoint id range, so a point/range query skips almost everything. Massive win.
  • If id is randomly distributed, every group's [min, max] spans the whole range, so nothing can be skipped. Pushdown does nothing.

This is the entire motivation for sorting / clustering / Z-ordering (P09): you lay data out so the columns people filter on have tight per-group stats. "Why is my Parquet query still slow despite pushdown?" is almost always "your data isn't sorted on the filter column."

Chapter 7: ORC, Avro, JSON, CSV — Choosing a Format

FormatOrientationBest forAvoid for
Parquetcolumnaranalytics on the lake; Spark/Trino/Athenatiny row-by-row writes
ORCcolumnarHive-centric analytics; has bloom filters + strong indexesnon-Hadoop ecosystems sometimes
Avrorowingestion/streaming, schema evolution, write-heavycolumn-pruned analytics
JSONrow, textinterchange, debugging, external APIshigh-volume analytics (cost)
CSVrow, texthuman/legacy interchangeanything typed or large
  • Parquet vs ORC: both excellent columnar formats; ORC has historically stronger built-in indexes/bloom filters and is Hive-native; Parquet has the broader ecosystem. Pick by ecosystem; don't agonize.
  • Avro's niche: row-oriented + schema-carrying makes it ideal for the ingestion side (Kafka, write-heavy, evolving schemas — P03) where you write whole records and rarely column-prune. The pattern: Avro on the wire/landing, Parquet on the lake.

Chapter 8: File Sizing and the Small-File Problem

The recurring villain, now at its source:

  • Target file size: 128 MB – 1 GB. Too small → the small-file tax (metadata blowup, S3 request costs, listing slowness, poor scan throughput, HDFS NameNode pressure). Too large → poor read parallelism, memory pressure, slow single-file operations.
  • Where small files come from: Spark writing one file per partition per task (P06), over-partitioning a Hive table (P07), streaming sinks committing frequently (P04), and per-record/per-minute micro-writes.
  • Fixes: repartition/coalesce to target before writing (P06); compaction jobs (P09 table formats automate this); right-sized partitioning; batching streaming writes.
  • The cost framing: small files inflate scan cost (Athena reads/opens more, $/TB up), request cost (more GETs), and operational cost (slow listing, metastore pressure) — so compaction is a cost program (a JD deliverable), not just tidiness.

Lab Walkthrough Guidance

Lab 01 — Parquet Reader & Predicate Pushdown, suggested order:

  1. dictionary_encode / rle_encode / rle_decode — the encodings (and roundtrip).
  2. _column_stats + build_parquet — chunk into row groups, compute min/max/null.
  3. row_group_can_match — the pushdown logic; remember stats prove absence; all-null group skipped; != not prunable.
  4. scan — skip via pushdown, then row-filter + project; report scanned/skipped.
  5. Then the extension: sort the data on the predicate column, rebuild, and watch row_groups_skipped jump — Ch. 6 made tangible.

Success Criteria

You are ready for Phase 09 when you can, from memory:

  1. Explain S3 as object storage: prefix scaling, request costs, no atomic rename, strong consistency.
  2. Explain why S3 (decoupled storage/compute) replaced HDFS.
  3. Contrast row vs columnar and why analytics wants columnar (projection, compression, skipping).
  4. Draw Parquet's file→row-group→column-chunk→page hierarchy and the role of stats.
  5. Explain dictionary + RLE encoding and why sorting amplifies compression and pushdown.
  6. Explain why pushdown proves absence and why data layout (sorting) determines its value.
  7. Pick a format for ingestion vs analytics vs interchange, and state the file-size target.

Interview Q&A

Q: Your Athena query has a WHERE on an indexed column but still scans the whole table. Why? Almost certainly the data isn't sorted/clustered on that column, so every Parquet row group's [min,max] for it spans the full range and predicate pushdown can skip nothing — stats only prove absence, and with overlapping ranges they can't. Confirm by inspecting row-group stats; the fix is to rewrite the data sorted (or Z-ordered, P09) on the filter column so each group covers a tight, disjoint range, plus partition on a coarse column (date) for directory-level pruning. Also check it's not a small-file problem inflating per-file overhead. The point: columnar skipping is a layout property, not an automatic one.

Q: When would you store data as Avro instead of Parquet? On the ingestion/write side. Avro is row-oriented and schema-carrying, which suits whole- record writes, streaming, and schema evolution (P03) — you rarely column-prune a Kafka topic. Parquet is columnar and shines on the read/analytics side where you project a few columns over many rows. The common architecture is Avro on the wire and at landing, then a job rewrites to Parquet (sorted, right-sized) for the analytical lake. Using Parquet for high-frequency row-by-row ingestion, or Avro for big analytical scans, is using each against its grain.

Q: A pipeline writes millions of 1 MB Parquet files. What's wrong and what does it cost? That's the small-file problem: each file carries footer/metadata overhead, S3 charges per GET/PUT (so even idle they cost money), listing is slow, and scans can't reach good throughput — and downstream Athena/Trino pay more per query ($/TB plus per-file overhead). It usually comes from too many Spark output partitions/tasks or over-partitioning. The fix is to target 128 MB–1 GB files (coalesce/repartition before write), partition sensibly (date, not user_id), and run compaction — ideally via a table format (P09) that automates it. It's a cost program, not housekeeping.

References

🛸 Hitchhiker's Guide — Phase 08: Storage & Columnar Formats

Read this if: you want S3 + Parquet internals fast, and to understand why one query scans 1 TB and another scans 10 GB of the same data. Skim, read WARMUP, build the reader.


0. The 30-second mental model

The lake lives in S3 (object store: prefix scaling, request costs, no atomic rename). Data is stored columnar (Parquet/ORC): read only the columns you need, and skip row groups whose min/max stats prove they can't match your filter. One sentence: the byte layout — columnar + tight stats from sorting + right file sizes — decides every downstream query's speed and cost.

1. S3 truths

object store, NOT a filesystem → no cheap rename (copy+delete) → table formats exist (P09)
strong read-after-write consistency since 2020 (the old "eventual" footguns are gone)
scaling ≈ 3,500 PUT/s + 5,500 GET/s PER PREFIX → spread prefixes, few large files
costs: storage $/GB-mo + REQUESTS per 1000 (small files cost even idle) + egress
lifecycle policies → auto-tier cold data to IA/Glacier

2. Why S3 beat HDFS

HDFS: NameNode (metadata SPOF, dies on millions of small files) + DataNodes; storage+compute COUPLED
S3:   decoupled storage/compute → scale independently, spin clusters up/down, ~infinite, 11 nines
trade: no data locality (network reads), no atomic rename

3. Parquet hierarchy

File → Row Group (~128MB) → Column Chunk → Page (~1MB)
+ per-chunk stats: min, max, null_count   ← the skip metadata
footer = schema + row-group metadata + stats + offsets
reader: read footer → project columns → skip row groups via stats → decode pages

4. Why columnar wins analytics

projection: read 2 of 50 columns ≈ 4% of bytes
compression: a homogeneous column compresses 10× (mixed rows barely)
skipping: per-group stats skip non-matching data

5. Encodings (the compression magic)

dictionary encode (low-cardinality → int codes) → RLE + bit-packing (runs/sorted) → snappy/zstd
SORTING the data first makes runs form → multiplies compression AND pushdown

6. Pushdown — the asymmetry that matters

stats prove ABSENCE, never presence:
  group min=60,max=69, WHERE id=55 → SKIP (proven absent)
  group min=50,max=59, WHERE id=55 → SCAN + row-filter (can't rule out)
effectiveness ∝ how TIGHT per-group min/max are ∝ DATA LAYOUT (sorting!)
random data → every group spans full range → pushdown skips NOTHING

7. Format picker

Parquet  → analytics lake (Spark/Trino/Athena)
ORC      → Hive-centric analytics (bloom filters, strong indexes)
Avro     → ingestion/streaming, schema evolution (row, schema-carrying)
JSON/CSV → interchange/debug/external; costly at analytics scale
pattern: Avro on the wire/landing → Parquet (sorted, right-sized) on the lake

8. File sizing

target 128 MB – 1 GB
too small → small-file tax (metadata, S3 requests, listing, NameNode, scan cost)
too big   → poor parallelism, memory pressure

9. Beginner mistakes that mark you

  1. Expecting pushdown to work on randomly-ordered data (it can't skip).
  2. Millions of tiny files under one prefix → throttling + request cost.
  3. JSON/CSV for big analytics → 10× the scan bill.
  4. Avro for column-pruned analytics / Parquet for row-by-row ingestion (against the grain).
  5. Assuming S3 rename is cheap (it's copy+delete → use table formats).
  6. Citing "S3 is eventually consistent" (it's strongly consistent since 2020).

10. War stories

  • "Filter on indexed column, full scan anyway." → data not sorted on it; stats overlap.
  • "Athena bill huge for a 'small' query." → millions of small files + no partitioning.
  • "Spark commit corrupted on S3." → no atomic rename; use a table format (P09).

11. How this phase pays off later

  • Sorting → tight stats → Z-order/clustering (P09), Athena cost (P10).
  • No atomic rename → why Iceberg/Delta commits exist (P09).
  • Small files → compaction (P09), cost program (P13).
  • Encryption/prefixes/lifecycle → governance (P14).

Read WARMUP, build the reader, sort the data and watch skips jump, then P09: table formats that add ACID, snapshots, and time travel on top of these files.

👨🏻 Brother Talk — Phase 08, Off the Record

The bytes. The least sexy phase, the highest leverage. Let me convince you why this is where the real money and the real speed live.


Brother, I'm going to make a claim that sounds like exaggeration and isn't: this phase — the boring one about file formats and storage — is the single highest-leverage thing in the entire track. Here's why. Everything above it (Spark, Trino, Athena, the lakehouse) is just reading these bytes. If the bytes are laid out well, every query above is fast and cheap. If they're laid out badly, no amount of cluster tuning saves you. I've watched teams throw bigger warehouses and more nodes at slow queries for months, when the actual fix was "your data isn't sorted and you have a million tiny files." Layout beats hardware. Every time.

Let me give you the one insight that, if you truly get it, makes you dangerous: predicate pushdown only works if the data is sorted on what you filter. Everyone learns "Parquet has statistics and does pushdown" and assumes that's automatic magic. It isn't. The statistics are per-row-group min/max, and they can only help you skip a group if that group's range doesn't overlap your filter. If your data is randomly ordered, every single row group spans the entire range of values, every group's min is near the global min and max near the global max, and so nothing can ever be skipped. Pushdown does literally nothing. The exact same data sorted on the filter column gives each group a tight, disjoint range, and suddenly a point query skips 99% of the file. Same data. Same format. Same query. 100× difference, purely from sort order. When you internalize this — when "why is this query slow?" reflexively makes you ask "is the data sorted on the predicate?" — you've gained an instinct most senior engineers don't have.

The small-file thing I keep hammering across phases lives here, at its source, so let me say the quiet part: small files are a tax you pay forever, levied by past-you on future-everyone. A Spark job spits out a million 1 MB files because nobody set the output partitioning, the job finishes fine, everyone moves on — and then for months, every Athena query against that table pays extra (more files to open, more requests, $/TB inflated by overhead), the metastore struggles to list them, and the S3 request bill quietly accumulates. The person who created the mess felt nothing; everyone downstream pays rent. Be the person who controls file size on write, and be the person who notices a small-file problem and schedules compaction. It's unglamorous and it saves real money.

Here's a thing that'll date you in an interview if you get it wrong: S3 is strongly consistent now. Has been since late 2020. For years we built elaborate workarounds (S3Guard, consistency layers) because S3 was eventually consistent for listings, and a lot of older engineers still carry that mental model. If you say "well, S3 is eventually consistent so we need to be careful about..." in 2026, you've just signaled your knowledge is five years stale. It's strongly read-after-write consistent. Know it. (And conversely: S3 still has no atomic rename — that hasn't changed, and it's the reason table formats exist, which is the whole next phase.)

The format choice — Avro vs Parquet — trips people up because they think one must be "better." They're not competitors; they're for different sides of the pipeline. Avro on the wire and at landing (row-oriented, schema-carrying, write-whole-records, evolves gracefully — perfect for Kafka and ingestion). Parquet on the lake (columnar, projects and skips, compresses hugely — perfect for analytics). The architecture that confuses these — Parquet for high-frequency row writes, or Avro for big column-pruned scans — fights the grain of each format and pays for it. The clean pattern is: ingest as Avro, then a job rewrites to sorted, right-sized Parquet for the analytical lake. Say that in a design review and you sound like you've built this before.

One more, because it's a quiet superpower: learn to read a Parquet footer. pyarrow or parquet-tools will show you row-group count, per-column min/max stats, encodings, and sizes. When a query is slow, open the file and look. Are the row groups huge or tiny? Do the stats for the filter column have tight ranges or do they all span the full range (= not sorted = no skipping)? Is it dictionary-encoded? Most engineers debug query performance by staring at the query plan and guessing; the ones who open the actual files find the real problem in minutes. That habit — go to the bytes — is what "debug at the byte and file level" (the JD's exact words) means in practice.

Career note: storage and format expertise is invisible until it isn't. Nobody throws a parade for "good file layout." But the engineer who can cut a query from 90 seconds to 3 by rewriting a table sorted and compacted, and explain exactly why with the footer stats, is doing something most people treat as black magic. Being the person who demystifies the bytes makes you the person other engineers bring their performance problems to — and that's authority.

Build the reader. Sort the data and watch the skipped-row-group count jump in the test. Feel the layout lesson in your hands. Then come to P09, where we put ACID, snapshots, and time travel on top of these files — the lakehouse, the thing that makes the lake a real database.

— your brother 👨🏻

Lab 01 — Parquet Reader & Predicate Pushdown

Phase: 08 — Storage & Columnar Formats | Difficulty: ⭐⭐⭐⭐☆ | Time: 5–7 hours

Parquet's magic — "it only reads the data it needs" — is just row-group statistics + predicate pushdown + column projection. This lab builds a working columnar format so those words become code: a file of row groups, each with per-column min/max/null stats, a reader that skips groups whose stats prove no match, and dictionary/RLE encoding.

What you build

  • dictionary_encode / rle_encode / rle_decode — the encodings that shrink columnar data (dictionary for low-cardinality, RLE for runs)
  • build_parquet — rows → row groups → columnar chunks with min/max/null stats (the footer metadata real Parquet keeps)
  • row_group_can_match — the pushdown predicate: skip a group whose stats prove no row matches (stats prove absence, never presence)
  • scan — projection + pushdown + row-level filter, reporting row_groups_scanned vs row_groups_skipped so you see the I/O saved

Key concepts

ConceptWhat to understand
Columnar layoutfile → row groups → column chunks → pages; read only needed columns
Row-group statsmin/max/null per chunk → skip chunks that can't match
Pushdown asymmetrystats prove absence, not presence → passing groups still filter
Dictionary/RLElow-cardinality + runs compress hugely; the columnar advantage
Projectionreading 2 of 50 columns reads ~4% of the bytes

Run

pip install -r requirements.txt
pytest test_lab.py -v
LAB_MODULE=solution pytest test_lab.py -v

Success criteria

  • All 14 tests pass — test_scan_skips_groups_via_pushdown is the headline (9 of 10 groups skipped for id == 55).
  • You can explain why a group passing row_group_can_match still needs row-level filtering.
  • You can explain why sorting data on the filter column makes pushdown dramatically more effective (tight min/max per group).

Extensions

  • Add bloom filters per row group for high-cardinality equality (where min/max can't prune) — ORC's trick.
  • Add page-level stats (a finer skip granularity) and measure the extra pruning.
  • Sort the data on the predicate column before build_parquet and show row_groups_skipped jump — the data-layout lesson behind Z-ordering/clustering (P09).
  • Estimate bytes read (projected columns × scanned groups) vs a row-store full read — the columnar cost win (P10 Athena $/TB).

Phase 09 — Lakehouse Table Formats

Difficulty: ⭐⭐⭐⭐⭐ Estimated Time: 2 weeks (30–40 hours) Prerequisites: Phase 08 (the files these formats sit on), Phase 01 (snapshot isolation)


Why This Phase Exists

The lakehouse is the JD's center of gravity for storage: "Iceberg snapshots … partition evolution and hidden partitioning … compact small files … table layouts for Athena and Trino … streaming writes and batch reads safely … schema evolution … time travel … recover from bad writes … retention and deletion policies." Table formats — Iceberg, Delta Lake, Hudi — are what turn a pile of Parquet files (P08) into a real database: ACID transactions, MVCC snapshots, time travel, safe concurrent writers, and the maintenance (compaction, expiry) that keeps it fast. This is also the destination of the Round-5 migration (Hive → lakehouse).

Concepts

  • Why table formats exist: S3 has no atomic rename (P08), so "which files are the table right now?" needs an explicit, atomically-swapped metadata pointer. Table formats provide it → ACID on object storage.
  • Iceberg architecture: metadata.jsonmanifest listmanifests → data files; every commit writes a new metadata file and atomically swaps the catalog pointer (a new snapshot). Hidden partitioning, partition evolution, time travel, snapshot isolation via optimistic concurrency.
  • Delta Lake: a transaction log (_delta_log/ ordered JSON commits + periodic Parquet checkpoints); ACID, time travel, MERGE, OPTIMIZE/Z-ORDER, VACUUM.
  • Apache Hudi: Copy-on-Write (rewrite files on update — read-optimized) vs Merge-on-Read (write delta logs, merge at read — write-optimized); a timeline of actions; record-level indexes for fast upserts; the upsert/incremental specialist.
  • Snapshot isolation & optimistic concurrency: readers see a consistent snapshot; writers commit optimistically and retry on conflict — how Spark and Flink write the same table without a lock (the lab).
  • Maintenance: compaction (rewrite_data_files / OPTIMIZE) for small files; expire_snapshots / VACUUM to reclaim storage; remove_orphan_files.
  • Streaming + batch on one table: exactly-once streaming writes (the P04 two-phase-commit sink commits an Iceberg snapshot) coexisting with batch reads/backfills.
  • Schema & partition evolution: add/rename/reorder columns safely; change partitioning without rewriting history (hidden partitioning).
  • Deletes & GDPR: row-level deletes (copy-on-write vs merge-on-read), and the time-travel-vs-deletion tension (a deleted row can be resurrected by time travel until snapshots expire — a real privacy gotcha, P14).

Labs

Lab 01 — Iceberg-Style Table Format (flagship, implemented)

FieldValue
GoalBuild snapshots (MVCC), optimistic-concurrency commits, time travel, partition pruning, compaction, and snapshot expiry
ConceptsSnapshot isolation, optimistic concurrency, time travel, compaction, expiry
How to Testpytest test_lab.py -v — 12 tests
Talking PointsHow do DELETE and time travel coexist? Why optimistic concurrency over locking? What does expiry cost you?
Resume bulletImplemented an Iceberg-style table format: MVCC snapshots, optimistic-concurrency commits, time travel, compaction, and snapshot expiry

→ Lab folder: lab-01-iceberg-table/

Lab 02 — Iceberg-Style Table Commits in Scala (implemented, sbt test)

FieldValue
GoalThe Scala twin of Lab 01: immutable snapshots, optimistic-concurrency commits (CAS on current snapshot → conflict + retry), time travel, compaction, and snapshot expiry (the GDPR-erasure caveat)
ConceptsSnapshot isolation, OCC, time travel, compaction, compliant deletion — in real Scala
How to Testsbt test — 6 ScalaTest specs (runs here)
Resume bulletBuilt an Iceberg-style table-commit library in Scala (optimistic concurrency, time travel, compaction, snapshot expiry) with ScalaTest coverage

→ Lab folder: lab-02-scala-iceberg/

Extension Project A — Real Iceberg/Delta hands-on (spec)

With Spark + Iceberg (or Delta): create a table, append, MERGE (upsert), time-travel query, OPTIMIZE/compact, expire_snapshots/VACUUM; inspect the metadata/manifests (or _delta_log); evolve the schema and partition spec.

Extension Project B — Format selection memo (spec)

Iceberg vs Delta vs Hudi for a stated workload (append-heavy analytics vs upsert-heavy CDC vs multi-engine openness) with the deciding dial and an ADR (P15).

Integrated-Scenario Hooks

  • This table format is the sink of the P04 Flink job (two-phase-commit → snapshot) and the P05 CDC apply (upserts).
  • Its partition pruning + P08 layout is what makes Athena/Trino cheap (P10).
  • Its time-travel-vs-deletion tension is a P14 privacy problem.
  • It is the storage core of capstone Problems 1 and 3 (P16).

Guides in This Phase

Key Takeaways

  • Table formats add ACID + snapshots + time travel over Parquet because S3 has no atomic rename.
  • Snapshots are immutable (MVCC); writers use optimistic concurrency and retry on conflict.
  • Compaction and snapshot expiry are mandatory maintenance, not optional tidiness.
  • Iceberg = open multi-engine; Delta = Spark-native rich; Hudi = upsert/incremental specialist.
  • Time travel and deletion are in tension — expiry is how you actually delete (P14).

Deliverables Checklist

  • Lab 01 implemented; all 12 tests pass
  • You can draw Iceberg's metadata→manifest list→manifest→data-file hierarchy
  • You can explain optimistic concurrency and the conflict-retry loop
  • You can choose Iceberg vs Delta vs Hudi for a workload
  • Extension A hands-on or Extension B selection memo

Warmup Guide — Lakehouse Table Formats

Zero-to-principal primer for Phase 09: why table formats exist, Iceberg/Delta/Hudi internals, snapshot isolation and optimistic concurrency, time travel, compaction and expiry, streaming+batch on one table, and the deletion-vs-time-travel tension.

Table of Contents


Chapter 1: Why Table Formats Exist

Recall P08: S3 is an object store with no atomic rename and "directories" that are just key prefixes. So "what files make up this table right now?" has no safe answer if you just ls a prefix — a concurrent writer mid-commit leaves you reading half-written state, and the old Hive convention ("the table is all files under this path") can't do atomic multi-file commits, row-level deletes, or time travel.

A table format fixes this with an explicit, atomically-swapped metadata pointer: the table is "whatever the current metadata file says," and a commit is "write new metadata, then atomically swap the pointer." That single atomic swap is what gives you ACID on a data lake — the defining lakehouse capability. Everything else (time travel, safe concurrent writers, schema evolution) follows from "the table is a versioned, atomically-updated pointer to a set of files," which the lab implements as a list of immutable snapshots.

Chapter 2: Iceberg Architecture

Iceberg's metadata hierarchy (the lab is a simplification of this):

catalog → table metadata.json (current snapshot id, schema, partition spec, snapshot list)
            └── snapshot → manifest list → manifest(s) → data files (+ per-file stats)
  • A snapshot is the table's state at a point in time: which data files are live. Each commit produces a new snapshot (immutable) and atomically updates the metadata pointer.
  • Manifests list data files with per-file stats (partition values, record counts, column min/max — P08's stats lifted to the table level), so the planner prunes files without listing S3 (a huge speedup over Hive's directory listing).
  • The catalog (Glue, Hive, Nessie, REST) holds the current-metadata pointer and provides the atomic swap.
  • Because snapshots are immutable and additive, you get time travel for free (just read an old snapshot's manifests).

Chapter 3: Snapshot Isolation and Optimistic Concurrency

How do Spark and Flink and a compaction job write the same table without corrupting it, and without a global lock?

  • Snapshot isolation (MVCC): every reader pins a snapshot and sees a consistent view; a concurrent writer creating a new snapshot doesn't affect in-flight reads (P01's snapshot isolation, applied).
  • Optimistic concurrency: a writer reads the current snapshot as its base, prepares its changes, and at commit checks "is the current snapshot still my base?" If yes, it atomically swaps in the new snapshot. If no (someone else committed first), it gets a conflict and must re-read the new snapshot and retry its changes against it. The lab's commit raises ConflictException on a stale base — exactly this.
  • Why optimistic, not locking? Locks across distributed writers on object storage are slow and fragile; optimistic concurrency is lock-free and works because most commits don't actually conflict (different partitions/files). Conflicts that do matter (two writers deleting the same file) are caught and retried. This is the mechanism behind "multiple engines, one table."

Chapter 4: Time Travel and Recovery

Because snapshots are immutable:

  • Time travel: query the table AS OF a snapshot id or timestamp — read that snapshot's manifests. Used for reproducibility ("the exact data the model trained on"), auditing, and debugging ("what did this table look like before the bad job?").
  • Recovery from bad writes: if a job corrupts the table, you roll back to the previous snapshot (atomic pointer swap to the old metadata) — instant recovery, no restore-from-backup. This is one of the lakehouse's best operational properties (the JD's "recover from bad writes").
  • The cost: snapshots and their data files accumulate → storage grows → you must expire old snapshots (Ch. 6), which is what bounds time travel (and enables real deletion, Ch. 8).

Chapter 5: Partitioning, Hidden Partitioning, and Evolution

  • Hidden partitioning (Iceberg): you partition by a transform of a column (days(ts), bucket(16, id)) and Iceberg records the partition value in metadata — queries filter on the column (WHERE ts > ...) and Iceberg derives the partition prune automatically. No more "you forgot to also filter on the partition column" (Hive's classic footgun) and no extra partition column to maintain.
  • Partition evolution: change the partition spec (e.g. dayshours) for new data without rewriting old data — old files keep their old spec, new files use the new one, and the planner handles the mix. Hive couldn't do this; it required a full rewrite.
  • Schema evolution: add/drop/rename/reorder columns by field id (not name/position), so renames and reorders are safe and old data reads correctly (P03's evolution discipline, applied to tables).

Chapter 6: Compaction, Clustering, and Expiry

The maintenance that keeps a lakehouse fast (and the JD's explicit asks):

  • Compaction (rewrite_data_files / Delta OPTIMIZE): merge many small files into fewer target-sized ones (P08's small-file fix), producing a new snapshot with the same data. Essential for streaming-written tables (many tiny commits) and for keeping scan cost down (P10). The lab's compact collapses per partition.
  • Clustering / Z-ordering / sorting: lay data out so the columns people filter on have tight per-file stats (P08 Ch. 6) — so pushdown/file-pruning actually skips. Z-order multi-dimensionally clusters several columns at once.
  • Expire snapshots (expire_snapshots / Delta VACUUM): delete old snapshots and the data files only they referenced — reclaim storage. This bounds time travel (you can't travel to an expired snapshot — the lab tests this) and is the mechanism that makes deletion real (Ch. 8).
  • Remove orphan files: delete files no snapshot references (e.g. from failed commits).

The principal point: a lakehouse table is not "set and forget" — it needs a maintenance schedule (compaction + expiry + clustering), usually automated (P13). A table nobody compacts becomes a small-file swamp; a table nobody expires grows storage and can't truly delete.

Chapter 7: Delta Lake and Hudi

The other two major formats — know the differences:

  • Delta Lake: the table state is a transaction log — an ordered series of JSON commit files in _delta_log/ (each adds/removes files), with periodic Parquet checkpoints to avoid replaying the whole log. Rich Spark-native features: MERGE (upsert), OPTIMIZE + Z-ORDER, VACUUM, time travel, change data feed. Historically Databricks/Spark-centric; now open with growing multi-engine support.
  • Apache Hudi: built for upserts/incremental from day one.
    • Copy-on-Write (CoW): an update rewrites the whole data file → reads are fast (read-optimized), writes are heavier.
    • Merge-on-Read (MoR): an update writes a small delta log file → writes are fast, reads merge base + deltas (slower reads until compaction). The write-optimized choice for high-frequency CDC.
    • A timeline of actions (commits, compactions, cleans) + record-level indexes for fast upserts.

The selection heuristic (Extension B):

  • Iceberg → open, multi-engine (Spark/Flink/Trino/Athena all first-class), big append-heavy analytical tables, partition/schema evolution — the safe "open lakehouse" default.
  • Delta → you're Spark/Databricks-centric and want its mature MERGE/OPTIMIZE/Z-ORDER.
  • Hudi → upsert/CDC-heavy with low-latency write needs (MoR), record-level updates.

All three give ACID + time travel + compaction; the differences are ecosystem, upsert model, and engineering culture — not "which is correct."

Chapter 8: Streaming + Batch, and Deletion vs Time Travel

  • Streaming + batch on one table: a Flink/Spark streaming job writes via an exactly-once sink (P04's two-phase commit → commit an Iceberg/Delta snapshot atomically), while batch jobs read consistent snapshots and backfill — one table, no Lambda dual-system. This is the lakehouse's headline promise: end the streaming/batch storage split.
  • Deletion vs time travel — the privacy gotcha: a DELETE (or GDPR erasure) removes a row from the current snapshot, but old snapshots still reference the data files containing it, so time travel can resurrect it. The deletion is only real once you expire the snapshots (and remove the orphaned files) that still point at it. So a compliant "delete this user's data" is: delete from current + ensure no retained snapshot references it + run expiry/vacuum. Forgetting the second half is a genuine compliance bug (P14). The same immutability that gives you time travel is the thing that makes deletion a two-step process.

Lab Walkthrough Guidance

Lab 01 — Iceberg-Style Table Format, suggested order:

  1. _commit + append — snapshots accumulate the live file set; parent links.
  2. snapshot + scan + read_records — time travel + partition pruning.
  3. delete_where — current loses the files; an old snapshot still sees them (MVCC).
  4. commit (optimistic) — stale base → ConflictException; retry against current succeeds.
  5. compact — one file per partition, records preserved.
  6. expire_snapshots — drop old (keep current); time travel to expired raises.

Success Criteria

You are ready for Phase 10 when you can, from memory:

  1. Explain why table formats exist (no atomic rename → atomic metadata swap → ACID).
  2. Draw Iceberg's metadata→manifest-list→manifest→data-file hierarchy.
  3. Explain snapshot isolation + optimistic concurrency + the conflict-retry loop.
  4. Explain time travel and snapshot-based rollback recovery.
  5. Explain hidden partitioning and partition/schema evolution without rewrite.
  6. Explain compaction, clustering, and snapshot expiry — and why they're mandatory.
  7. Contrast Iceberg/Delta/Hudi (incl. Hudi CoW vs MoR) and pick one.
  8. Explain the deletion-vs-time-travel tension and a compliant delete.

Interview Q&A

Q: How do Spark and Flink write the same table at the same time without corruption? Optimistic concurrency over immutable snapshots. Each writer reads the current snapshot as its base, stages new data files, and at commit checks that the current snapshot is still its base; if so it atomically swaps in a new snapshot, if not it gets a conflict and re-reads + retries against the new snapshot. Readers always pin a consistent snapshot (MVCC), so they never see a half-written commit. There's no global lock — it works because most commits touch different files and genuine conflicts are caught and retried. That's the whole "multiple engines, one table" story.

Q: A user invokes GDPR erasure. You ran DELETE. Are you compliant? Not yet. DELETE removes the row from the current snapshot, but the data files containing it are still referenced by older snapshots, so time travel can resurrect the data. Real erasure requires the second step: ensure no retained snapshot references those files and run snapshot expiry + orphan-file removal so the underlying files are actually deleted. So a compliant flow is delete-from-current → expire the snapshots that still reference the data → vacuum. The same immutability that gives time travel makes deletion a two-phase operation — and forgetting phase two is a real compliance bug.

Q: Iceberg vs Delta vs Hudi — how do you choose? By ecosystem and write pattern, since all three give ACID + time travel + compaction. Iceberg is the open, multi-engine default — Spark, Flink, Trino, Athena are all first-class, and its hidden partitioning + partition evolution are excellent for big analytical tables. Delta if we're Spark/Databricks-centric and want its mature MERGE/OPTIMIZE/Z-ORDER. Hudi if the workload is upsert/CDC-heavy with low write latency — its Merge-on-Read writes deltas fast and merges at read, and record-level indexes make upserts cheap. I'd write the ADR on those axes, not on "which is best."

Q: Your streaming job writes a tiny file every few seconds and queries got slow. Fix? That's the small-file problem from frequent streaming commits. Two parts: (1) batch the writes more (larger commit intervals / buffering) so each commit is fewer, bigger files; (2) run compaction (rewrite_data_files / OPTIMIZE) on a schedule to merge the small files into target-sized ones — a new snapshot with the same data. Also expire old snapshots so the rewritten-away files get reclaimed. Pair compaction with clustering/Z-order on the filter columns so the compacted files also prune well (P08). Compaction + expiry on a schedule is mandatory maintenance for any streaming-written lakehouse table.

References

  • Apache Iceberg spec and Apache Iceberg: The Definitive Guide (Aghajanyan et al.)
  • Delta Lake docs and the Delta Lake VLDB paper (Armbrust et al., 2020)
  • Apache Hudi docs — CoW vs MoR, timeline, indexes
  • Armbrust et al., Lakehouse: A New Generation of Open Platforms (CIDR 2021)
  • Kleppmann, DDIA Ch. 7 (snapshot isolation, MVCC)

🛸 Hitchhiker's Guide — Phase 09: Lakehouse Table Formats

Read this if: you want Iceberg/Delta/Hudi in your head fast — why they exist, how commits and time travel work, and how to choose. Skim, read WARMUP, build the table.


0. The 30-second mental model

A table format turns the Parquet files of P08 into a real database: ACID, snapshots, time travel. The trick — since S3 has no atomic rename — is an atomically-swapped metadata pointer: the table is "whatever the current metadata says," and a commit writes new metadata

  • swaps the pointer (a new immutable snapshot). One sentence: the lakehouse = atomic metadata swap over immutable file snapshots → ACID + MVCC + time travel on a data lake.

1. Why table formats exist

S3: no atomic rename, "directory" = prefix → can't safely say "the table is these files now"
fix: explicit metadata pointer, atomically swapped on commit → ACID
everything else (time travel, concurrent writers, evolution) follows from immutable snapshots

2. Iceberg hierarchy

catalog → metadata.json (current snapshot, schema, partition spec)
            └── snapshot → manifest list → manifests → data files (+ per-file stats)
manifests carry partition values + min/max → prune files WITHOUT listing S3 (vs Hive)

3. Concurrency = optimistic, not locked

writer: read base snapshot → stage files → commit IF current == base (atomic swap)
        else CONFLICT → re-read new snapshot, RETRY
readers: pin a snapshot (MVCC), never see half-written commits
→ Spark AND Flink AND compaction write one table, no global lock

4. Time travel + rollback

snapshots are immutable → query AS OF snapshot/timestamp (reproducibility, audit)
bad write? roll back = swap pointer to previous metadata (instant, no restore)
cost: snapshots accumulate → must EXPIRE them (bounds time travel, enables real delete)

5. Partitioning that doesn't betray you

HIDDEN partitioning: partition by transform days(ts)/bucket(id); query filters the COLUMN,
  Iceberg derives the prune → no "forgot the partition filter" footgun
partition EVOLUTION: change spec for NEW data, no rewrite of old
schema evolution by FIELD ID → safe rename/reorder

6. Maintenance is mandatory

compaction (rewrite_data_files / OPTIMIZE) → fix small files (P08), same data, new snapshot
clustering / Z-ORDER → tight per-file stats → pushdown actually skips (P08)
expire_snapshots / VACUUM → reclaim storage, bound time travel, enable real deletes
remove_orphan_files → clean failed commits
a table nobody compacts/expires becomes a swamp

7. Iceberg vs Delta vs Hudi

Iceberg → open, multi-engine (Spark/Flink/Trino/Athena), append-heavy analytics, evolution → default
Delta   → Spark/Databricks-centric; mature MERGE / OPTIMIZE / Z-ORDER
Hudi    → upsert/CDC-heavy; CoW (read-fast) vs MoR (write-fast, merge at read); record indexes
all three: ACID + time travel + compaction. Differences = ecosystem + upsert model.

8. Deletion vs time travel (the privacy trap)

DELETE removes a row from CURRENT snapshot, but old snapshots still reference its files
→ time travel can RESURRECT it → not GDPR-compliant until you EXPIRE those snapshots + vacuum
compliant erase = delete current → expire referencing snapshots → remove orphan files

9. Beginner mistakes that mark you

  1. Treating a lakehouse table as set-and-forget (no compaction/expiry schedule).
  2. Thinking DELETE = erased (time travel resurrects until expiry).
  3. Using a global lock instead of optimistic concurrency for concurrent writers.
  4. Hive-style "all files under the path" thinking (no atomic commit).
  5. Picking a format by hype instead of ecosystem + upsert pattern.
  6. Streaming tiny commits with no compaction → small-file swamp.

10. How this phase pays off later

  • 2PC → snapshot commit ← P04 exactly-once sink.
  • Partition pruning + clustering → P10 Athena/Trino cost.
  • Deletion vs time travel → P14 privacy/GDPR.
  • Compaction/expiry schedule → P13 automated maintenance.
  • Storage core of capstone Problems 1 & 3 (P16).

Read WARMUP, build the table, make the conflict-retry test pass, then P10: the query engines (Trino/Athena) that read all this — and how to optimize them.

👨🏻 Brother Talk — Phase 09, Off the Record

The lakehouse — the thing that made the data lake a real database. Let me tell you what's genuinely transformative here and what'll bite you.


Brother, I've been doing this long enough to remember the before. Before table formats, a data lake was a swamp of Parquet files under S3 prefixes, and "the table" was a gentleman's agreement about which files counted. You couldn't safely have two jobs write at once. You couldn't delete a row without rewriting whole partitions and praying nothing read mid-rewrite. You couldn't time-travel. A failed job could leave the table in a half-written, corrupt state that someone discovered three queries downstream. It was stressful in a way that's hard to convey now. So when I tell you Iceberg/Delta/Hudi are transformative, I mean it from scar tissue: they took the most anxiety-inducing part of data engineering — "is the table even in a valid state right now?" — and made it a solved problem. Appreciate that. You're learning the thing that fixed the swamp.

Here's the mental unlock that makes all of it click: the table is a pointer, and a commit is swapping the pointer. That's it. That's the whole magic. The table isn't "the files in this directory" — it's "whatever the current metadata file says the files are," and committing is atomically swapping which metadata file is current. Once you see it that way, everything follows effortlessly: time travel is just keeping the old metadata files around; rollback is swapping the pointer back; concurrent writes are two people racing to swap the pointer, and the loser retries. The lab makes you build this pointer-swap, and when you do, the entire lakehouse stops being a collection of features to memorize and becomes one simple idea you understand.

Now the two things that'll bite you, because they bite everyone.

First: maintenance is not optional, and the lakehouse won't nag you. A table format will happily let you stream a tiny file every five seconds forever, accumulating a million little files and a thousand snapshots, getting slower and more expensive every day, and it will never complain. The ACID and time travel work perfectly right up until your queries are crawling and your storage bill is absurd. The fix — compaction and snapshot expiry — has to be scheduled, by you, as a deliberate maintenance job (P13). The teams that love their lakehouse run compaction nightly and expire snapshots on a retention policy. The teams that hate it forgot, and now have a swamp with extra metadata. Set up the maintenance the day you create the table, not the day it's already slow.

Second — and this one is a genuine compliance landmine: time travel and "delete" are in tension. A junior runs DELETE FROM users WHERE id = 123, sees the row gone from a SELECT, and reports "done, GDPR-compliant." They're wrong, and it's a serious wrong. The row is gone from the current snapshot, but every old snapshot still points at the data file that contains it, so anyone with time-travel access can read it right back. The data is not actually gone until you expire every snapshot that references it and vacuum the orphaned files. So a real erasure is: delete from current, then expire the referencing snapshots, then remove orphan files. The very immutability that gives you beautiful time travel is the thing that makes deletion a two-step dance — and the person who forgets step two has a privacy incident waiting (P14). Internalize this now; it's the kind of subtle correctness bug that doesn't show up in any test but shows up in an audit.

On the Iceberg-vs-Delta-vs-Hudi religious war: don't enlist. They all do ACID, time travel, and compaction. The honest differences are ecosystem and write pattern. Iceberg is the open, multi-engine bet — if you want Spark and Flink and Trino and Athena all reading and writing the same table without one vendor's gravity, Iceberg. Delta if you're living in Spark/Databricks and want its mature MERGE and OPTIMIZE. Hudi if your life is upserts and CDC and you need fast writes (Merge-on-Read). When someone tries to drag you into "X is objectively best," the senior move is "best for what workload and what ecosystem?" and an ADR on those axes. Tribalism about formats is a junior tell.

The genuinely exciting thing — the reason this phase matters strategically — is one table for streaming and batch. For a decade we maintained the Lambda architecture: a fast, approximate streaming path and a slow, correct batch path, two codebases computing the same thing, forever drifting apart. The lakehouse kills that. A Flink job writes the table with exactly-once commits (your P04 two-phase-commit sink committing a snapshot), batch jobs read consistent snapshots and backfill, and it's one table, one source of truth. When you can architect that — end the streaming/batch split — you're delivering exactly the "unified platform" the JD's mission statement asks for. That's a principal-level outcome, and it rests on understanding this phase deeply.

Career note: lakehouse expertise is the hot, marketable skill in data infrastructure right now, precisely because it's the convergence point of everything — storage (P08), streaming (P04), batch (P06), query (P10), governance (P14). The engineer who can stand up an Iceberg platform with proper maintenance, exactly-once streaming writes, and compliant deletion is doing the most strategically central work on the team. Build the table format by hand. Make the optimistic-concurrency conflict-and-retry work. Then come to P10, where we make the queries that read all of this fast and cheap.

— your brother 👨🏻

Lab 01 — Iceberg-Style Table Format

Phase: 09 — Lakehouse Table Formats | Difficulty: ⭐⭐⭐⭐⭐ | Time: 6–8 hours

A "lakehouse" is just ACID + snapshots + time travel layered over the Parquet files of P08. This lab builds that layer: immutable snapshots (MVCC), optimistic-concurrency commits (so two writers don't corrupt the table), time travel, partition pruning, compaction, and snapshot expiry. After this, Iceberg/Delta/Hudi stop being magic.

What you build

  • IcebergTable with append / delete_where — every write creates a new immutable snapshot pointing at the live file set
  • commitoptimistic concurrency: a commit against a stale base snapshot raises ConflictException; the writer must re-read and retry (how concurrent engines stay correct without a lock)
  • snapshot / scan / read_recordstime travel (read as of any snapshot) and manifest-based partition pruning
  • compact — rewrite many small files into one per partition, preserving the data (a new snapshot)
  • expire_snapshots — reclaim old snapshots (and break time travel to them)

Key concepts

ConceptWhat to understand
Snapshot / MVCCeach commit is immutable; readers see a consistent snapshot
Optimistic concurrencyconflicting writers detected at commit → retry (no lock)
Time travelquery AS OF a snapshot; delete doesn't erase history
Partition pruningskip files via manifest partition values (P08 at table level)
Compactionfix the small-file problem without changing the data
Snapshot expiryreclaim storage; the cost of unbounded time travel

Run

pip install -r requirements.txt
pytest test_lab.py -v
LAB_MODULE=solution pytest test_lab.py -v

Success criteria

  • All 12 tests pass — test_commit_conflict_on_stale_base and test_delete_where_removes_then_time_travel_keeps certify the ACID/MVCC model.
  • You can explain how a DELETE coexists with time travel (old snapshots still reference the removed files).
  • You can explain why optimistic concurrency (not locking) is how Iceberg lets Spark and Flink write the same table.

Extensions

  • Add partition evolution: change the partition spec for new snapshots without rewriting old data (Iceberg's hidden partitioning) — scan must handle mixed specs.
  • Add merge-on-read vs copy-on-write deletes (Hudi/Delta): position/equality delete files vs rewriting data files — and the read-time merge cost tradeoff.
  • Add remove_orphan_files (files no snapshot references) and a MERGE (upsert) operation.
  • Track manifest stats (per-file min/max like P08) and prune at file level inside scan.

Lab 02 — Iceberg-Style Table Commits in Scala

Phase: 09 — Lakehouse Table Formats | Difficulty: ⭐⭐⭐⭐⭐ | Time: 5–6 hours

The Scala twin of Lab 01's Python table — the lakehouse commit protocol as a JVM library. Immutable snapshots, optimistic-concurrency commits (compare-and-swap on the current snapshot → serializable, atomic, conflict-detecting), time travel, compaction, and snapshot expiry (the GDPR-erasure caveat). ScalaTest-verified.

What you build (IcebergTable)

  • commit / append / overwrite — each produces a new immutable Snapshot; OCC via expectedBase (stale base → ConcurrentCommitException, caller refreshes and retries)
  • scan / scanAsOf — read current, or time-travel to any snapshot
  • compact — rewrite many data files into one (same rows) → fixes small files
  • expireSnapshots — prune history; required for a compliant delete (else time travel resurrects deleted rows — P14)

Run

sbt test

Expected: 6 tests, all green in IcebergTableSpec — including the OCC conflict+retry and the overwrite→time-travel→expiry erasure sequence.

Key concepts (Scala expression)

ConceptScala
Snapshot isolationa table is a list of immutable Snapshots
Optimistic concurrencycommit(expectedBase, ...) CAS; conflict → retry
Time travelscanAsOf(snapshotId)
Compactionmerge currentFiles → one DataFile, same rows
Compliant erasureoverwrite + expireSnapshots (delete alone isn't enough)

Success criteria

  • sbt test → all 6 specs pass.
  • You can explain why two writers off the same base can't both commit (OCC), and the retry.
  • You can explain why a DELETE/overwrite isn't a complete GDPR erasure until snapshots expire.

Extensions

  • Add partition/hidden-partitioning metadata to DataFile and prune in scan by predicate.
  • Add manifest files (a metadata layer between snapshot and data files) — Iceberg's real structure — and stats-based file skipping (P08 pushdown).
  • Add MERGE/upsert (key-based) producing position/equality deletes.

Phase 10 — Query & Analytics Engines

Difficulty: ⭐⭐⭐⭐☆ Estimated Time: 1.5 weeks (24–32 hours) Prerequisites: Phase 08 (file layout), Phase 09 (table formats), Phase 06 (Spark SQL/joins)


Why This Phase Exists

This is the query layer the JD demands you "optimize at the plan level … join strategies, predicate pushdown, partition pruning, statistics, CTE materialization, broadcast joins, dynamic filtering, and cost-based optimization … diagnose slow Athena/Trino/Hive/Spark SQL queries … design lakehouse tables for high-concurrency analytics … know when Athena is sufficient and when a dedicated Trino/Spark/Flink architecture is needed." The query engine is where users feel the platform, and where the cost shows up on the bill ($5/TB). Get the layout (P08) and table format (P09) right and the optimizer can be brilliant; get them wrong and no engine can save the query.

Concepts

  • The engines: Trino/Presto (distributed MPP, federates over many sources, no storage of its own), Athena (serverless managed Trino over S3 + Glue, pay-per-TB), Spark SQL (Catalyst optimizer, batch+interactive), Hive (legacy, P07), Flink SQL (streaming, P04). Same SQL, different runtimes and cost models.
  • The optimizer pipeline: parse → logical plan → rule-based rewrites (predicate/ projection pushdown, constant folding, CTE handling) → cost-based choices (join order & strategy) using statistics → physical plan.
  • Pushdown & pruning: predicate pushdown (filter at the scan), projection pushdown (read only needed columns), partition pruning (skip partitions), file/row-group skipping (P08 stats), dynamic filtering (prune the fact side from the dimension side at runtime).
  • Joins: broadcast vs partitioned (shuffle) hash join vs sort-merge; join reordering (small tables first); the role of statistics (ANALYZE) and stale-stats failures.
  • Cost on Athena/Trino: $5/TB scanned → layout (partitioning, columnar, compaction, sorting) is the cost program; workgroup controls and per-query data limits.
  • Concurrency & resource management: Trino's resource groups, memory limits, spill; designing tables for high-concurrency BI.
  • Catalog & governance: Glue Data Catalog (the metastore, P07), Lake Formation (fine-grained table/column/row access + masking — handoff to P14), workgroups.
  • When Athena vs dedicated Trino/Spark/Flink: serverless ad-hoc vs always-on tuned cluster vs streaming — the architecture decision (P00 dials).

Labs

Lab 01 — Query Optimizer & Cost Estimator (flagship, implemented)

FieldValue
GoalBuild projection + partition pruning → bytes scanned → Athena cost, broadcast vs partitioned join selection, and cost-based join reordering
ConceptsPushdown, pruning, $/TB, join strategy, CBO reordering
How to Testpytest test_lab.py -v — 14 tests
Talking PointsWhy doesn't a non-partition filter prune? Why does join order matter if final size is fixed? When Athena vs Trino cluster?
Resume bulletBuilt a cost-based query optimizer model (pushdown, partition pruning, join strategy + reordering, $/TB scan cost) for diagnosing slow/expensive analytics

→ Lab folder: lab-01-query-optimizer/

Extension Project A — Diagnose a real slow query (spec)

On Athena/Trino over an Iceberg/Parquet table: EXPLAIN a slow query, find the full scan or bad join, fix it with partitioning/sorting (P08/P09), ANALYZE for stats, and measure bytes-scanned + cost before/after.

Extension Project B — Athena-vs-Trino decision memo (spec)

For a stated workload (ad-hoc BI vs steady high-concurrency dashboards vs federated joins), choose Athena vs a dedicated Trino cluster vs Spark SQL with the deciding dial and an ADR.

Integrated-Scenario Hooks

  • This phase's scan cost is paid down by P08 layout + P09 compaction/clustering — the through-line of "layout is a cost feature."
  • Its Lake Formation masking is built in P14.
  • It is the analytics access layer of capstone Problems 1 and 5 (P16).

Guides in This Phase

Key Takeaways

  • The optimizer is rule-based rewrites (pushdown/pruning) + cost-based choices (join order/ strategy) over statistics.
  • Athena/Trino cost is $5/TB — partition, project, sort, and compact to cut it 10–100×.
  • Broadcast the small side; reorder joins small-first; keep stats fresh (ANALYZE).
  • Athena for serverless ad-hoc; dedicated Trino for steady high-concurrency; Flink SQL for streaming.
  • The engine can only exploit a good layout — P08/P09 decide what's possible here.

Deliverables Checklist

  • Lab 01 implemented; all 14 tests pass
  • You can read an EXPLAIN and spot a full scan / bad join
  • You can quantify a query's bytes scanned and $ cost, and the fix
  • You can choose Athena vs Trino vs Spark SQL for a workload
  • Extension A diagnosis or Extension B decision memo

Warmup Guide — Query & Analytics Engines

Zero-to-principal primer for Phase 10: how Trino/Athena/Spark-SQL optimize a query, the pushdown/pruning/join decisions that determine speed and the $/TB bill, and when to pick serverless Athena vs a dedicated Trino cluster.

Table of Contents


Chapter 1: The Engine Landscape

A query engine is compute that reads storage — separate from the storage itself (the lakehouse split). The players:

  • Trino / Presto: a distributed MPP SQL engine with no storage of its own — it federates queries over S3/Iceberg/Hive/Cassandra/MySQL/… via connectors. Coordinator plans; workers execute in parallel, streaming data between stages (in memory, spilling if needed). Trino is the actively-developed fork; PrestoDB is the original.
  • Athena: AWS's serverless, managed Trino over S3 + Glue Catalog. No cluster to run; you pay per TB scanned. Great for ad-hoc and intermittent analytics.
  • Spark SQL: the Catalyst optimizer + Tungsten execution on Spark (P06) — batch and interactive; same engine as your ETL, good when you want one stack.
  • Hive (P07): the legacy SQL-on-Hadoop; mostly what you migrate from.
  • Flink SQL (P04): the streaming SQL engine — same dynamic-table semantics.

Same ANSI SQL, different runtimes, cost models, and latency. The principal skill is choosing (Ch. 8) and optimizing (the rest).

Chapter 2: The Optimizer Pipeline

Every engine turns SQL into an execution plan through the same stages:

  1. Parse → an abstract syntax tree → an unoptimized logical plan (a tree of relational operators: scan, filter, project, join, aggregate).
  2. Rule-based optimization (heuristics that are always good): predicate pushdown, projection pushdown, partition pruning, constant folding, CTE handling, subquery decorrelation. These rewrite the logical plan into an equivalent, cheaper one.
  3. Cost-based optimization (CBO): choices where the right answer depends on the datajoin order and join strategy — decided using statistics (Ch. 5).
  4. Physical planning: pick operators (broadcast vs hash join, hash vs sort aggregate), add exchanges (shuffles), and produce the executable plan.

EXPLAIN shows you this plan — reading it (where's the scan? how big? which join? a full table scan?) is the core debugging skill (Ch. 6 Q&A).

Chapter 3: Pushdown and Pruning

The rewrites that determine how much data is read (and thus the bill):

  • Predicate pushdown: push WHERE filters down to the scan so the engine reads fewer files/row-groups/pages (P08 stats do the skipping). WHERE country='US' evaluated at the scan, not after reading everything.
  • Projection pushdown: read only the columns the query needs (columnar, P08). Selecting 3 of 50 columns reads ~6% of the bytes.
  • Partition pruning: a filter on a partition column skips entire partitions/directories (P07/P09). The lab's bytes_scanned shows the combined projection × pruning effect — and the trap: a filter on a non-partition column can't prune partitions (it still reads all, then filters), which is why partition-column choice matters so much.
  • Dynamic filtering: in a join, build a filter from the (small) dimension side at runtime and push it into the (huge) fact scan — e.g. only scan fact partitions for the 10 dates the filtered dimension selected. Massive for star schemas.

The unifying idea: the cheapest data to process is the data you never read. Pushdown + pruning is the engine not reading what it doesn't need — and how effective it is depends entirely on the layout you built in P08/P09.

Chapter 4: Join Strategies and Reordering

Joins are the optimizer's hardest decisions:

  • Broadcast (replicated) join: ship the small side to every worker and hash-join locally — no shuffle. Fastest when one side fits memory/threshold (the lab's choose_join_strategy). The risk: broadcasting something not-actually-small → OOM.
  • Partitioned (shuffle) hash join: hash-partition both sides by the join key across workers, then join locally. The default for two large tables; pays a shuffle (P06).
  • Sort-merge join: shuffle + sort both, merge — for very large sorted joins.
  • Join reordering (CBO): for a multi-table join, the order changes the size of intermediate results. Joining small tables first keeps intermediates small (the lab's estimate_join_cost). The final result size is order-independent, but the work to get there is not — which is why a CBO with good stats can make a 10-table join tractable.

Chapter 5: Statistics and Cost-Based Optimization

CBO is only as good as its statistics:

  • Table/column stats: row counts, column min/max/distinct/null (P08's per-file stats aggregated). The optimizer uses these to estimate selectivity (how many rows a predicate keeps) and cardinality (rows out of a join), which drive join order and strategy.
  • ANALYZE TABLE populates/refreshes stats. Stale stats are the #1 cause of bad plans: the optimizer thinks a table is small and broadcasts it (OOM), or picks a bad join order. A shockingly common "the query got slow" cause is "stats are stale after a big load."
  • Modern engines also use runtime adaptivity (Spark AQE, P06; Trino's adaptive features) to correct compile-time misestimates with actual data — but fresh stats still matter.

Chapter 6: The $/TB Cost Model

On Athena/Trino-over-S3 the cost is brutally simple and brutally important: $5 per TB scanned (the lab's athena_cost_usd). Consequences:

  • A query scanning 1 TB costs 100× one scanning 10 GB of the same data — and the difference is layout, not hardware: partitioning + columnar projection + sorting + compaction (P06/P08/P09) cut bytes scanned by 10–100×.
  • This makes the query engine the place where bad upstream decisions (small files, no partitioning, JSON instead of Parquet) finally cost money — visibly, per query, forever.
  • Controls: Athena workgroups with per-query/per-workgroup data-scanned limits stop a runaway SELECT * from scanning a petabyte; cost-allocation tags attribute spend by team (P07/P13).
  • The principal framing: a slow query is usually an expensive query, and the fix is almost always layout, not a bigger cluster. "Diagnose the slow query" = "find the full scan and give it something to prune."

Chapter 7: Concurrency, Resource Management, and Table Design

  • Trino concurrency: a dedicated cluster serves many concurrent queries; resource groups allocate memory/CPU between tenants/queues; per-query memory limits + spill prevent one query from starving others. Athena handles concurrency for you (with service limits) but you pay per scan.
  • Table design for high-concurrency BI: partition on the common filter (date), sort/cluster on secondary filters (P08/P09), compact to target file sizes, pre-aggregate hot rollups into data marts (without duplicating business logic — keep one source of truth and lineage, P13). The goal: every dashboard query prunes hard and scans little.

Chapter 8: Catalog, Lake Formation, and When to Use What

  • Glue Data Catalog (P07): the shared metastore Athena/Trino/Spark/EMR read — "what tables exist, where, what schema/partitions/stats." One catalog, many engines.
  • Lake Formation (P14): fine-grained governance over the catalog + S3 — table/column/row level permissions and column masking for a principal. The query engine enforces these at scan time (a restricted user reads fewer columns/rows). Built in P14.
  • The Athena vs dedicated-Trino vs Spark-SQL decision (P00 dials):
    • Athena → serverless, intermittent/ad-hoc, no ops, pay-per-scan; perfect until concurrency/ cost at scale argues otherwise.
    • Dedicated Trino cluster → steady, high-concurrency BI/dashboards where always-on tuned compute + resource groups beat per-scan billing, and you want federation.
    • Spark SQL → you're already on Spark and want one engine for ETL + heavy transformations; higher latency for interactive.
    • Flink SQL → streaming/continuous (P04). Write the ADR on concurrency, latency, ops, cost shape, and federation needs — not fashion.

Lab Walkthrough Guidance

Lab 01 — Query Optimizer, suggested order:

  1. bytes_scanned — projection width × scanned-partition rows; non-partition filter = full scan.
  2. athena_cost_usd$5/TB, 10 MB minimum.
  3. pushdown_savings — optimized vs naive full scan ratio.
  4. choose_join_strategy — broadcast the smaller side under threshold, else partitioned.
  5. join_order (ascending) + estimate_join_cost — small-first beats large-first on the sum of intermediates.

Success Criteria

You are ready for Phase 11 when you can, from memory:

  1. Contrast Trino/Athena/Spark-SQL/Flink-SQL and their cost models.
  2. Walk the optimizer pipeline (rule-based rewrites → CBO → physical plan).
  3. Explain predicate/projection/partition pushdown and dynamic filtering.
  4. Choose a join strategy and explain why join order changes cost.
  5. Explain why stale stats cause bad plans, and what ANALYZE fixes.
  6. Compute a query's bytes scanned and $ cost and name the layout fix.
  7. Choose Athena vs dedicated Trino vs Spark SQL for a workload.

Interview Q&A

Q: An Athena query costs $50 and takes 3 minutes. How do you make it cheap and fast? $50 means it scanned ~10 TB ($5/TB), so the fix is "read less." I'd EXPLAIN and check: is it a full scan because the filter isn't on a partition column (add/align partitioning — date is typical), or SELECT * reading every column (project only what's needed), or scanning a swamp of small files (compact, P09), or unsorted data so row-group pruning can't skip (sort/Z-order on the filter column, P08)? Usually it's several of these. The point is the fix is layout, not a bigger engine — Athena has no "bigger cluster" knob anyway; the only lever is bytes scanned, and bytes scanned is decided upstream in P08/P09.

Q: The optimizer chose a bad join and OOM'd. Why? Almost always stale or missing statistics: the optimizer estimated a table as small and broadcast it to every worker, but it was actually huge → OOM; or it picked a poor join order because cardinality estimates were wrong. The fix is ANALYZE TABLE to refresh stats so the CBO sees reality, and (belt and suspenders) rely on runtime adaptivity (Spark AQE / Trino's adaptive features) to correct misestimates mid-flight. If a specific table is reliably small, a broadcast hint encodes the intent. Bad plans are usually bad inputs (stats), not a bad optimizer.

Q: When do you move off Athena to a dedicated Trino cluster? When the workload becomes steady and high-concurrency — lots of dashboards hitting it all day. Athena's serverless per-scan model is perfect for intermittent/ad-hoc use (no ops, pay only when you query), but at sustained high concurrency a tuned always-on Trino cluster with resource groups can be cheaper and give predictable latency and isolation between tenants. I'd decide on the numbers: query concurrency, daily scan volume (× $5/TB vs cluster cost), latency SLOs, and whether we need cross-source federation. It's a cost-and-control crossover, captured in an ADR — not a one-size answer.

References

🛸 Hitchhiker's Guide — Phase 10: Query & Analytics Engines

Read this if: you want Trino/Athena optimization and the $/TB cost model fast, and to ace "diagnose this slow/expensive query." Skim, read WARMUP, build the optimizer.


0. The 30-second mental model

Trino/Athena/Spark-SQL are compute that reads storage. The optimizer does rule-based rewrites (pushdown, pruning — always good) + cost-based choices (join order/strategy — need stats). On Athena you pay $5/TB scanned, so the whole game is read less. One sentence: the cheapest data is the data you never read — and what you can skip is decided by the layout you built in P08/P09.

1. Engine picker

Trino/Presto → MPP, federates many sources, no own storage; dedicated cluster
Athena       → serverless managed Trino over S3+Glue; pay $5/TB; ad-hoc/intermittent
Spark SQL    → Catalyst optimizer; same stack as your ETL; higher interactive latency
Flink SQL    → streaming (P04)

2. Optimizer pipeline

SQL → logical plan → RULE-BASED (pushdown, pruning, fold) → COST-BASED (join order/strategy, stats)
    → physical plan (broadcast vs hash, exchanges)
EXPLAIN shows it. Debugging = read the plan: where's the scan? how big? full scan? which join?

3. Read less (pushdown + pruning)

predicate pushdown  → filter at the scan (P08 stats skip)
projection pushdown → only needed columns (3 of 50 ≈ 6% bytes)
partition pruning   → filter on PARTITION col skips partitions
                      (filter on NON-partition col → NO pruning → full scan!)
dynamic filtering    → small dim side builds a filter that prunes the big fact scan

4. Joins

broadcast      → small side to all workers, no shuffle (risk: not-small → OOM)
partitioned    → hash both sides by key, shuffle (default for 2 big tables)
join ORDER     → small tables first → small intermediates (final size is order-independent)
stats (ANALYZE) drive CBO; stale stats → bad plans (broadcast a huge table → OOM)

5. The $/TB law

Athena/Trino-on-S3 = $5 / TB scanned
1 TB scan vs 10 GB scan of the SAME data = 100× cost → fix is LAYOUT not hardware
levers: partition + project + sort/Z-order + compact (P06/P08/P09)
guardrails: workgroup per-query scan limits; cost-allocation tags

6. Concurrency & table design

Trino resource groups + memory limits + spill → isolate tenants
high-concurrency BI: partition on date, sort on secondary filter, compact, pre-agg marts
keep ONE source of truth + lineage (don't fork business logic into every mart)

7. Catalog & governance

Glue Data Catalog = shared metastore (Athena/Trino/Spark/EMR all read it)
Lake Formation = table/column/row permissions + column masking, enforced at scan (P14)

8. Beginner mistakes that mark you

  1. Filtering on a non-partition column and expecting pruning.
  2. SELECT * (reads every column → full width → big bill).
  3. Querying a small-file swamp (compact first, P09).
  4. Unsorted data so row-group stats can't skip (sort/Z-order, P08).
  5. Stale stats → optimizer broadcasts a huge table → OOM.
  6. Throwing a bigger cluster at a layout problem (Athena has no such knob anyway).

9. War stories

  • "$50 query." → ~10 TB scanned: full scan + SELECT * + small files. Partition, project, compact → cents.
  • "Optimizer OOM'd on a join." → stale stats; broadcast a big table. ANALYZE.
  • "Dashboards slow at peak." → no resource groups / unpartitioned marts; isolate + pre-agg.

10. How this phase pays off later

  • Layout → cost closes the loop with P06/P08/P09.
  • Lake Formation masking → built in P14.
  • Athena-vs-Trino ADR → P15 decisions.
  • Analytics layer of capstone Problems 1 & 5 (P16).

Read WARMUP, build the optimizer, then P11: the other read path — low-latency serving (Cassandra/DynamoDB) for single-digit-ms lookups, not seconds-scale scans.

👨🏻 Brother Talk — Phase 10, Off the Record

The query layer — where users feel the platform and where the bill arrives. Let me give you the one mantra that makes you the person who fixes "the query is slow."


Brother, here's a mantra to carry forever, and it'll make you look like a wizard in this phase: the cheapest data to process is the data you never read. Every query optimization, every cost saving, every "make this faster" boils down to making the engine read less. Internalize that and you'll stop reaching for "bigger cluster" and start reaching for "why is this reading so much?" — which is the actual fix 90% of the time.

Let me tell you the thing that reframed query performance for me. On Athena, you pay $5 per terabyte scanned. That's it. Not per query, not per second — per byte read. Which means performance and cost are the same problem: a fast query is a cheap query, because both come from reading less data. The first time a panicked PM showed me a "$50 query" and I realized $50 = 10 terabytes scanned, the whole thing clicked: this query is reading ten terabytes to answer a question that probably touches a few gigabytes. The fix isn't a faster engine. The fix is making it not read the 10 TB. And that fix lives upstream — in the partitioning, the columnar projection, the sorting, the compaction (everything you learned in P08/P09). The query engine is where bad layout decisions finally send you the invoice.

So here's the debugging ritual, and it's almost mechanical once you know it. Query slow or expensive? Run EXPLAIN and find the scan. How many bytes is it reading? Then ask the four questions:

  1. Is it scanning all partitions because the filter isn't on the partition column? → fix the partitioning or the query.
  2. Is it SELECT * reading every column when it needs three? → project.
  3. Is it scanning a million tiny files? → compact (P09).
  4. Is the data unsorted so row-group stats can't skip anything? → sort/Z-order on the filter column (P08).

It's almost always one or more of those, and none of them is "buy a bigger warehouse." When you can walk a room through that ritual calmly while everyone else is debating cluster sizes, you're demonstrating exactly the "diagnose slow Athena/Trino/Spark SQL queries" skill the JD lists.

Now, the stats thing, because it causes the weirdest, most maddening bugs. The cost-based optimizer makes its biggest decisions — which join, what order — based on statistics about your tables. And statistics go stale after a big load. So you get the classic ghost story: "the query was fast yesterday and slow today and nothing changed." Something changed — the data grew, the stats didn't, and now the optimizer thinks a 500 GB table is 5 MB and tries to broadcast it to every worker, and everything falls over. ANALYZE TABLE. Refresh the stats. It's the "did you turn it off and on again" of query optimization, and it fixes a shocking fraction of "mysteriously slow" queries. Add it to your reflexes.

A subtle one that separates senior from principal: don't fork your business logic into every data mart. It's tempting, when dashboards are slow, to build pre-aggregated marts everywhere — and you should, for performance. But if each mart re-implements "what counts as an active user" slightly differently, you've created a future where three dashboards show three numbers and nobody knows which is right, and lineage is a swamp. The discipline is: pre-aggregate for speed, but keep one canonical definition of each metric (one source of truth), and derive the marts from it. Performance and a single version of the truth. The teams that skip the second half end up in "why don't these two reports match" hell, which is a credibility-destroying place to be.

On the Athena-vs-Trino question: don't overthink it early. Athena is gloriously low-effort — serverless, no cluster, pay only when you query — and for ad-hoc and intermittent analytics it's almost always the right starting point. You move to a dedicated Trino cluster when the usage becomes steady and high-concurrency (lots of dashboards all day), where an always-on, tuned, resource-grouped cluster gives predictable latency and can be cheaper than per-scan billing, and where you need to federate across sources. It's a crossover decision you make with numbers (concurrency, daily scan TB × $5, latency SLOs), not a religious one. The junior move is picking based on what's trendy; the principal move is the crossover math in an ADR.

Career note: being the person who can make queries fast and cheap is visible value — the cost shows up on a dashboard the finance team watches, and the latency shows up in every analyst's day. "I cut our Athena bill 60% and made the exec dashboard load in 2 seconds instead of 40" is a sentence that travels. And the beautiful part is that the skill is mostly understanding layout (P08/P09) and reading EXPLAIN — not arcane engine internals. You already have the hard parts from the previous phases; this one teaches you to cash them in at the query layer.

Build the optimizer. Watch the savings ratio hit 100× when you project and prune. Then come to P11, where we leave the analytics world entirely for the serving world — single-digit- millisecond lookups from Cassandra and DynamoDB, a completely different beast with completely different rules.

— your brother 👨🏻

Lab 01 — Query Optimizer & Cost Estimator

Phase: 10 — Query & Analytics Engines | Difficulty: ⭐⭐⭐⭐☆ | Time: 5–6 hours

Trino, Athena, and Spark SQL all live or die by the same optimizer decisions. This lab builds them: projection + partition pruning → bytes scanned → Athena $/TB cost, broadcast vs partitioned join selection, and cost-based join reordering (small tables first). After this, "diagnose the slow/expensive query" is arithmetic.

What you build

  • bytes_scanned — combine columnar projection and partition pruning into the bytes the engine actually reads
  • athena_cost_usd$5/TB (the law that makes layout a cost feature)
  • pushdown_savings — optimized vs naive full scan (the 10–100× ratio)
  • choose_join_strategy — broadcast the small side vs partitioned (shuffle) hash join
  • join_order / estimate_join_cost — CBO: small tables first minimizes the sum of intermediate results (even though the final size is order-independent)

Key concepts

ConceptWhat to understand
Projectionread only needed columns → fewer bytes (P08)
Partition pruningfilter on partition col → skip partitions; else full scan
$/TBscan cost is the bill; layout is the lever
Broadcast vs partitionedsmall side broadcast = no shuffle
Join ordersmall-first keeps intermediates small (the CBO win)

Run

pip install -r requirements.txt
pytest test_lab.py -v
LAB_MODULE=solution pytest test_lab.py -v

Success criteria

  • All 14 tests pass.
  • You can explain why a filter on a non-partition column doesn't prune (and what would).
  • You can explain why join order changes cost even when the final result size doesn't.
  • You can turn a "this query is expensive" complaint into a bytes-scanned + $ estimate.

Extensions

  • Add dynamic filtering (build a filter from the dimension side of a join to prune the fact scan at runtime) and show the bytes saved.
  • Add a CBO that uses column min/max stats (P08) to estimate selectivity per predicate instead of a global selectivity.
  • Add Lake Formation column/row masking to the scan (P14): a restricted principal reads fewer columns → different bytes + a governance check.
  • Cost a CTE materialized once vs recomputed and pick based on reuse count.

Phase 11 — Serving: Cassandra & DynamoDB

Difficulty: ⭐⭐⭐⭐☆ Estimated Time: 1.5 weeks (24–32 hours) Prerequisites: Phase 01 (quorums, partitioning), Phase 04/05 (the streams that feed it)


Why This Phase Exists

The lakehouse (P09/P10) answers any query in seconds; a serving store answers one query shape in single-digit milliseconds — and you need both. The JD's Problem 4 is exactly this: "a low-latency serving store for derived real-time features … Cassandra schema design, partition key design, hot partition avoidance, TTL policy, compaction strategy, consistency-level selection, write idempotency, streaming upserts from Flink, backfill from Spark, read-path latency SLOs, tombstone monitoring, repair strategy, multi-region replication." This phase teaches the masterless, query-first serving model — and the ways it bites you (large partitions, tombstones, hot partitions) that have no analog in the analytics world.

Concepts

  • Cassandra architecture: masterless (Dynamo-style), consistent hashing token ring, replication factor + NetworkTopologyStrategy, tunable consistency (ONE/QUORUM/ALL, LOCAL_QUORUM), hinted handoff, read repair, anti-entropy repair, gossip.
  • Query-first data modeling: model around the queries, not the entities; denormalize; one table per query pattern; partition key (distribution) + clustering keys (sort within partition).
  • The partition pitfalls: large partitions (cells & bytes limits — GC, slow reads, repair pain), hot partitions (skew — P01's enemy again), and the fixes (better key, bucketing, salting).
  • Writes & LSM: writes are appends to a commitlog + memtable → flushed to immutable SSTablescompaction merges them. Last-write-wins by timestamp; idempotent upserts (P01) are natural.
  • Tombstones & TTL: deletes and TTL expiry create tombstones; reads must scan past them until compaction removes them; too many per read → slow → failed queries. TTL-heavy / queue-like tables are the classic disaster.
  • Compaction strategies: STCS (size-tiered, write-heavy), LCS (leveled, read-heavy), TWCS (time-window, time-series/TTL) — and their write/read/space amplification tradeoffs.
  • Consistency-level selection: R + W > RF for strong; LOCAL_QUORUM for multi-region; the latency/durability/availability dial (P01) per query.
  • DynamoDB: partition key + sort key, RCU/WCU (or on-demand) capacity, adaptive capacity + hot-partition limits, GSIs/LSIs, DynamoDB Streams, single-table design.
  • Connecting streams → serving: idempotent streaming upserts from Flink (P04 sink), Spark backfills (P06) rate-limited so they don't overwhelm the cluster, read-path SLOs, and multi-region replication.

Labs

Lab 01 — Cassandra Data Modeler & Serving Calculator (flagship, implemented)

FieldValue
GoalBuild consistency/durability math, partition health, hot-partition detection, tombstone thresholds, compaction-strategy selection, and time-series bucketing
ConceptsR+W>RF, large/hot partitions, tombstones, compaction strategies, bucketing
How to Testpytest test_lab.py -v — 14 tests
Talking PointsWhy model around queries? Why are large partitions dangerous? When TWCS vs LCS vs STCS?
Resume bulletBuilt a Cassandra data-modeling calculator (consistency, partition health, tombstone & compaction strategy, time-series bucketing) for a low-latency serving layer

→ Lab folder: lab-01-cassandra-modeler/

Extension Project A — Real Cassandra/Scylla hands-on (spec)

docker Cassandra; model a "features by user" and a "events by sensor by day" table; insert with TTL; force flush + compaction; observe tombstones (nodetool); test QUORUM reads with a node down.

Extension Project B — Stream → serving pipeline (spec; → capstone)

Flink (P04) idempotent upserts into Cassandra + a rate-limited Spark (P06) backfill; define read-path p99 SLOs and tombstone alerts; sketch multi-region with LOCAL_QUORUM (JD Problem 4).

Integrated-Scenario Hooks

  • This serving layer is fed by P04 (Flink upserts) and P06 (Spark backfill), enriched into in P05 (as-of joins read it), and is the serving tier of capstone Problems 2 and 4 (P16).
  • Its consistency math is P01's quorums; its hot partition is P01's skew, sixth costume.

Guides in This Phase

Key Takeaways

  • Serving stores answer one query shape in ms; model around the query, denormalize.
  • Large partitions and hot partitions are the killers — model cells/bytes and skew up front.
  • Tombstones from deletes/TTL can fail reads; pick the compaction strategy for the workload.
  • Tunable consistency (R+W>RF, LOCAL_QUORUM) is P01's quorum dial, per query.
  • Streams feed serving via idempotent upserts; backfills must be rate-limited.

Deliverables Checklist

  • Lab 01 implemented; all 14 tests pass
  • You can design a partition key avoiding large and hot partitions
  • You can choose a consistency level and compaction strategy for a workload
  • You can explain the tombstone problem and its fixes
  • Extension A hands-on or Extension B stream→serving pipeline

Warmup Guide — Serving: Cassandra & DynamoDB

Zero-to-principal primer for Phase 11: the masterless, query-first serving model, partition design and its killers (large + hot partitions), the LSM write path, tombstones & TTL, compaction strategies, tunable consistency, DynamoDB, and wiring streams to serving safely.

Table of Contents


Chapter 1: Serving vs Analytics

The lakehouse (P09/P10) answers any query over all the data in seconds; a serving store answers one query shape over one key in single-digit milliseconds. They are different jobs with opposite designs, and a real platform needs both (the JD's Problem 4 wires a streaming pipeline into Cassandra serving):

Lakehouse (P09/P10)Serving (Cassandra/DynamoDB)
Queryany, ad-hoc, scansone known key/shape
Latencysecondssingle-digit ms
Modelnormalized-ish, columnardenormalized, query-shaped
Scale axisdata volumeread/write QPS

The mistake is using one for the other: scanning Cassandra like a warehouse (it can't), or point-looking-up the lakehouse per request (too slow). This phase is the serving side, and its rules will feel wrong if you bring relational/analytics instincts.

Chapter 2: Cassandra Architecture

Cassandra is a masterless (Dynamo-style) distributed database — every node is equal, there's no single leader:

  • Token ring + consistent hashing: the partition key hashes to a token; the ring assigns token ranges to nodes (with vnodes for smoother distribution). This is P01's consistent hashing — adding a node moves only ~1/N of the data, which is why Cassandra scales horizontally without resharding pain.
  • Replication: each partition is replicated to RF nodes; NetworkTopologyStrategy places replicas across racks/datacenters (the basis of multi-region, P14).
  • Coordinator + gossip: any node can coordinate a request (route to replicas); nodes share state via gossip; failures are handled by hinted handoff (store writes for a down node) and read repair (fix stale replicas on read) + scheduled anti-entropy repair.
  • No joins, no foreign keys, no ad-hoc WHERE — you query by the partition key (and clustering keys). That constraint is the design (Ch. 3).

Chapter 3: Query-First Data Modeling

The cardinal rule, and the one that breaks relational brains: model around your queries, not your entities. You start from "what exact lookups must I serve?" and build a table per query shape, denormalizing freely (writing the same data several ways is normal and correct):

  • Partition key: decides distribution (which node) and is required in every query — it's how you look the row up. All rows for a partition key live together on one replica set.
  • Clustering keys: sort rows within a partition (e.g. events by time within a user), enabling efficient range scans inside a partition.
  • Denormalization: no joins means you pre-join at write time — store the data in the shape each query needs. A "messages by user" table and a "messages by channel" table may hold the same messages twice. That's the trade: write amplification + storage for read speed.

If you find yourself wanting a join or an ad-hoc filter on a non-key column, you've modeled it like a relational DB — step back and design the table from the read.

Chapter 4: The Partition Killers — Large and Hot

Two partition pathologies cause most Cassandra incidents (the lab models both):

  • Large partitions: a partition's storage is cells = rows × columns; keep it under ~100k cells and ~100 MB (the hard limit is ~2 billion cells, but you die long before that). Oversized partitions cause GC pressure, slow reads, painful repair, and node instability. The trap is an unbounded partition — "all events for a user" grows forever. Fix: bucket the partition key (e.g. user + day), so each partition is bounded (the lab's bucket_key). You must reason about partition growth over time at design.
  • Hot partitions: a celebrity key (a viral product, a whale account) takes disproportionate traffic and melts its replica set — P01's skew, sixth costume (the lab's hot_partition_factor). Fix: a higher-cardinality key, bucketing, or salting (and reconcile downstream).

Model both before shipping — Cassandra won't warn you; it just degrades until it falls over.

Chapter 5: The Write Path, LSM, and Compaction

Cassandra (like RocksDB, P04/P08) is an LSM-tree store, which is why writes are fast and why tombstones (Ch. 6) exist:

  • A write appends to the commit log (durability) and a memtable (in-memory); when the memtable fills it flushes to an immutable SSTable on disk. Writes never do random I/O (P08's sequential-I/O lesson) — that's the speed.
  • Because SSTables are immutable, an "update" is just a new write with a later timestamp; reads merge across SSTables + memtable, newest-timestamp-wins (last-write-wins). This makes upserts naturally idempotent (a replayed write with the same data is a no-op effect) — perfect for streaming sinks (Ch. 8, P04).
  • Compaction merges SSTables (and drops superseded/expired data). The strategy matters (the lab's choose_compaction_strategy):
    • STCS (Size-Tiered): merge similar-sized SSTables — low write amplification; the write-heavy / general default.
    • LCS (Leveled): keep SSTables in non-overlapping levels — fewer SSTables per read (better read latency) at higher write amplification; read-heavy workloads.
    • TWCS (Time-Window): group by time window and drop whole expired windows — **time-series
      • TTL** (avoids per-row tombstones, Ch. 6).

Chapter 6: Tombstones and TTL

The LSM's dark side, and a top cause of mysterious Cassandra outages:

  • A delete doesn't remove data — it writes a tombstone (a "this is gone" marker with a timestamp). TTL expiry does the same. The real data + tombstone linger until compaction removes them (after gc_grace_seconds, to ensure deletes propagate).
  • On read, Cassandra must scan past tombstones to find live data. Too many tombstones per read = slow; past a threshold (warn ~1,000, fail ~100,000 — the lab's tombstone_scan_ok) Cassandra aborts the query. The symptom is baffling: "reads were fine, now they error," with no obvious cause unless you check tombstone counts.
  • The classic disasters: queue-like tables (constant write+delete) and TTL-heavy tables on STCS. Fixes: avoid the queue anti-pattern; use TWCS for time-series-with-TTL (drops whole expired SSTables without generating per-row tombstones); model to avoid range scans over deleted data; monitor tombstones-per-read (P13).

Chapter 7: Tunable Consistency

Cassandra exposes P01's quorum dial per query:

  • Consistency levels: ONE (fast, tolerates RF−1 down, may read stale), QUORUM (majority = RF//2 + 1), ALL (strongest, tolerates 0 down — one node down = unavailable), LOCAL_QUORUM (quorum within the local DC — the multi-region default, avoids cross-DC latency).
  • Strong (read-your-writes) iff R + W > RF (the lab's consistency_is_strong) — the read and write replica sets must overlap. RF=3 with QUORUM reads + QUORUM writes (2+2>3) is the canonical strong-but-available setting, tolerating one node down (tolerated_failures).
  • The dial is per data product: a balance lookup wants R+W>RF; a "recently viewed" list is fine at ONE. Reaching for ALL "to be safe" is the rookie move — it's the least available setting.

Chapter 8: DynamoDB and Connecting Streams to Serving

  • DynamoDB is AWS's managed serving store: a partition key (+ optional sort key), RCU/WCU provisioned capacity or on-demand, adaptive capacity (smooths mild skew, but a single hot partition still has a throughput ceiling), GSIs/LSIs (secondary query shapes — GSIs cost extra capacity), DynamoDB Streams (CDC-out), and single-table design (model multiple entities in one table by key design). It trades Cassandra's open-source/tuning control for zero-ops (no compaction/repair to run) at a capacity price.
  • Connecting streams → serving (the JD's Problem 4): a Flink job (P04) computes a real-time feature and upserts it into Cassandra/DynamoDB — and because the store is last-write-wins and upserts are idempotent, the exactly-once sink (P04) and the serving store compose perfectly (a replayed upsert rewrites the same value). A Spark job (P06) backfills history into the same table — but rate-limited, because an unthrottled backfill hammers the cluster and blows the live read-path p99 SLO. That tension (backfill throughput vs serving latency) is a real principal design problem: throttle the backfill, schedule it off-peak, and protect live reads.

Lab Walkthrough Guidance

Lab 01 — Cassandra Data Modeler, suggested order:

  1. quorum / consistency_is_strong / level_nodes / tolerated_failures — the consistency dial (R+W>RF; what each level survives).
  2. partition_cells / partition_health — ok/warn/danger by cells + bytes.
  3. hot_partition_factor — skew detection (P01, again).
  4. tombstone_scan_ok — the 100k fail threshold.
  5. choose_compaction_strategy — TWCS (time-series) / LCS (read) / STCS (write).
  6. bucket_key — bound time-series partition growth.

Success Criteria

You are ready for Phase 12 when you can, from memory:

  1. Contrast serving vs analytics and say why each needs the other.
  2. Explain Cassandra's masterless ring, RF, and tunable consistency.
  3. State the query-first modeling rule and why there are no joins.
  4. Diagnose large vs hot partitions and give the fixes (bucket, salt, re-key).
  5. Explain the LSM write path, tombstones, and the TTL/queue tombstone trap.
  6. Pick a compaction strategy for write-heavy / read-heavy / time-series workloads.
  7. Choose a consistency level (incl. LOCAL_QUORUM multi-region) for a query.
  8. Wire a Flink upsert + rate-limited Spark backfill without blowing the read SLO.

Interview Q&A

Q: How do you model a "messages" feature in Cassandra? By the reads, not the entity. If the app needs "latest messages for a user" and "messages in a channel," that's two tables: one partitioned by user_id (clustered by time desc) and one by channel_id (clustered by time), each storing the message — denormalized, written twice. No joins, no ad-hoc filters. I'd also bound partition growth: if a user or channel can accumulate unbounded messages, bucket the partition key by time (e.g. channel_id + month) so no partition exceeds the size limits. The whole design starts from the lookup and works backward to the partition key.

Q: Reads were fine, then started failing with no deploy. What happened? Classic tombstone threshold. A delete- or TTL-heavy table (often a queue-like access pattern) accumulated tombstones faster than compaction removed them, and a read had to scan past more than ~100,000 of them, so Cassandra aborted the query. The fix depends on the cause: switch time-series-with-TTL tables to TWCS (drops whole expired SSTables without per-row tombstones), redesign away from the queue/range-over-deleted-data pattern, and add a tombstones-per-read alert (P13) so you see it building before it fails. The maddening part is that nothing in the app changed — the data shape crossed a threshold.

Q: When do you use QUORUM vs ONE vs ALL? Per the data's needs. QUORUM (with R+W>RF, e.g. RF=3) is the default for data that needs read-your-writes while tolerating a node down — strong and available. ONE for data where staleness is fine and latency/availability matter most (a "recently viewed" list). ALL almost never — it's the least available (one node down = the operation fails). Multi-region uses LOCAL_QUORUM to get quorum within the local DC without paying cross-region round-trips. The point: consistency is a per-query dial (P01), not a global "safe" setting.

Q: Stream computes features → Cassandra, and you also need to backfill history. Risks? Two. First, idempotency: the Flink upserts must be safe to replay (they are, since Cassandra is last-write-wins — a re-delivered upsert rewrites the same value), so the exactly-once sink and the store compose cleanly. Second, and the real trap: the Spark backfill must be rate-limited. An unthrottled backfill writing history at full cluster speed will saturate Cassandra and blow the live read-path p99 SLO — you'd take down serving to load history. So I throttle the backfill, run it off-peak, possibly to a separate token-aware writer, and watch the read latency the whole time. Backfill throughput vs serving latency is the tension to design around.

References

  • Jeff Carpenter & Eben Hewitt, Cassandra: The Definitive Guide — modeling, consistency, ops
  • Apache Cassandra docs — data modeling, compaction, consistency levels, tombstones
  • DataStax data-modeling guides (query-first methodology, chebotko diagrams)
  • Amazon DynamoDB developer guide — capacity, adaptive capacity, single-table design, Streams
  • Kleppmann, DDIA Ch. 3 (LSM-trees / SSTables) & Ch. 5–6 (replication, partitioning) — P01/P08

🛸 Hitchhiker's Guide — Phase 11: Serving (Cassandra & DynamoDB)

Read this if: you want the low-latency serving model fast — and to avoid the large/hot partition and tombstone traps that have no analog in analytics. Skim, read WARMUP, build the modeler.


0. The 30-second mental model

A serving store (Cassandra/DynamoDB) answers one query shape in single-digit ms — the opposite of the lakehouse's "any query in seconds." You model around the query, denormalize, and the partition key is everything: it decides distribution (avoid hot), size (avoid large), and ordering. One sentence: design the table from the read you must serve, and protect the partition from getting hot, large, or tombstone-ridden.

1. Cassandra in one breath

masterless (Dynamo-style) token ring; RF replicas; tunable consistency (ONE/QUORUM/ALL)
writes: commitlog + memtable → immutable SSTables → COMPACTION merges them (LSM)
last-write-wins by timestamp; upserts are natural & idempotent (P01)
reads: merge SSTables + memtable, skip tombstones, read-repair

2. Consistency = P01's quorum, tunable

strong (read-your-writes): R + W > RF   e.g. RF=3, QUORUM(2)+QUORUM(2) > 3 ✓
ONE → fast, tolerates RF-1 down, may read stale
QUORUM → balanced, tolerates RF-quorum down
ALL → strongest, tolerates 0 down (one node down = unavailable)
multi-region → LOCAL_QUORUM (don't pay cross-region latency per read)

3. Model around the query

NOT: normalize entities, join later (there are no joins!)
YES: one table per query pattern; denormalize; partition key = how you look it up
partition key → distribution + ordering; clustering keys → sort within partition

4. The two partition killers

LARGE partition: cells = rows × cols; keep < ~100k cells & < ~100MB (hard limit ~2B cells)
  → GC pressure, slow reads, repair pain. Bucket time series; cap unbounded growth.
HOT partition: a celebrity key takes all the traffic (P01 skew, again)
  → melts one replica set. Fix: higher-cardinality key, bucket, or salt.

5. Tombstones (the TTL trap)

delete / TTL expiry → TOMBSTONE (a "this is gone" marker), lingers until compaction
reads scan past tombstones; > ~1000/read = warn, ≥ 100,000/read = FAILED query
queue-like / high-churn / TTL-heavy tables are the classic disaster
fix: TWCS for time-series TTL, model to avoid range scans over deleted data

6. Compaction strategy picker

STCS (size-tiered) → write-heavy / general default; low write amplification
LCS  (leveled)     → read-heavy; fewer SSTables/read, higher write amplification
TWCS (time-window) → time-series + TTL; drops whole expired SSTables cheaply

7. DynamoDB quick contrast

partition key (+ optional sort key); RCU/WCU or on-demand; adaptive capacity for mild skew
hot partition still real (per-partition throughput cap); GSIs cost extra capacity
single-table design; DynamoDB Streams for CDC-out
managed: no compaction/repair to run, but you pay per capacity + less tuning control

8. Streams → serving

Flink (P04) idempotent UPSERTS into Cassandra (last-write-wins; dedup by key)
Spark (P06) backfill RATE-LIMITED so it doesn't overwhelm the cluster
read-path p99 SLOs + tombstone alerts + repair schedule

9. Beginner mistakes that mark you

  1. Modeling like a relational DB (normalize + join) — there are no joins.
  2. A partition key that grows unbounded (every event for a user in one partition).
  3. A low-cardinality/celebrity key → hot partition.
  4. TTL-heavy table on STCS → tombstone storm → failed reads (use TWCS).
  5. ALL consistency "to be safe" → one node down = outage.
  6. Unthrottled Spark backfill → overwhelms the serving cluster.

10. War stories

  • "p99 spiked then reads started failing." → tombstone threshold on a TTL/queue table.
  • "One node is hot, others idle." → celebrity partition key; re-key/bucket/salt.
  • "Repair takes forever / OOMs." → a giant partition. Model the size first.

11. How this phase pays off later

  • Idempotent upserts ← P04 exactly-once sink; rate-limited backfill ← P06.
  • Consistency math = P01 quorums; hot partition = P01 skew.
  • Serving tier of capstone Problems 2 & 4 (P16); multi-region → P14/P15.

Read WARMUP, build the modeler, then P12: Scala & functional programming — the language and SDK layer that ties the whole platform together with type-safe, resource-safe code.

👨🏻 Brother Talk — Phase 11, Off the Record

Serving stores — where your relational instincts will actively hurt you. Let me reprogram them before Cassandra punishes you.


Brother, the hardest thing about Cassandra isn't Cassandra — it's unlearning the relational database in your head. Everything you know about good schema design from Postgres is, here, exactly wrong. Normalize? No — denormalize aggressively. Model your entities cleanly and join them at query time? There are no joins. One flexible table you query lots of ways? No — one table per query shape, even if that means writing the same data five times. The single mindset shift that unlocks Cassandra is: you don't model your data, you model your queries. You start from "what exact lookup must I serve in 5 milliseconds?" and build a table whose partition key is that lookup. Get this and Cassandra is a rocket; fight it with relational instincts and it's a minefield.

Let me give you the traps, because they're vicious and they don't exist in the analytics world you just came from.

Trap one: the partition that grows forever. This one's seductive because it feels natural. "All events for a user? Partition by user_id." Clean, right? And then your heaviest user accumulates ten million events in one partition, and that partition becomes a monster — reads against it crawl, garbage collection thrashes, repair chokes, and one day a node falls over trying to compact it. The fix is bucketing: partition by user_id + time_bucket (say, per day), so each partition is bounded. You have to think about partition growth over time at design time, because Cassandra won't warn you — it'll just get slower and slower until it falls over. Model the size. Ask "how big is the biggest partition in a year?" before you ship.

Trap two: tombstones, the silent assassin. This is the one that pages people at 3 a.m. with the most confusing symptoms. In Cassandra, a delete doesn't remove data — it writes a tombstone, a marker saying "this is gone," and the real data lingers until compaction cleans it up. Same for TTL expiry. Now imagine a queue-like table where you constantly write and delete, or a TTL-heavy table — tombstones pile up, and every read has to scan past all of them to find live data. Past about a thousand tombstones per read you get warnings; past a hundred thousand, Cassandra fails the query outright. And the failure looks like nothing you'd expect — "reads were fine, now they're erroring" — and the cause is invisible unless you know to look at tombstone counts. The lesson: deletes and TTLs are not free in Cassandra, queue patterns are an anti-pattern, and time-series-with-TTL wants TWCS compaction (which drops whole expired SSTables without generating per-row tombstones). Burn this into your memory now; it'll save you a brutal on-call.

Trap three: the consistency level you reach for "to be safe." Nervous engineers pick ALL consistency because it sounds the safest. It's the least available setting — if even one replica is down, every operation fails. The sweet spot is almost always QUORUM (or LOCAL_QUORUM in multi-region), which gives you read-your-writes (R+W>RF) and tolerates a node down. This is just P01's quorum math wearing Cassandra's clothes, and being able to derive "RF=3, QUORUM reads + QUORUM writes = strong, survives one failure" live is exactly the kind of thing that signals you actually understand distributed systems rather than reciting settings.

Now the thing that connects this phase to the rest of the platform, because it's where it gets beautiful: streams feed serving. Your Flink job (P04) computes a real-time feature and upserts it into Cassandra — and because Cassandra is last-write-wins and upserts are idempotent, your exactly-once sink and your serving store fit together perfectly: a replayed or duplicated upsert just rewrites the same value. Meanwhile a Spark job (P06) backfills history into the same table — but you must rate-limit it, because an unthrottled backfill will hammer the serving cluster and blow your read-path SLOs for live traffic. That tension — backfill throughput vs serving latency — is a real principal-level design problem (it's literally JD Problem 4), and handling it gracefully (throttled backfill, separate load, off-peak scheduling) is the kind of thing that separates "built a serving table" from "operated a serving platform under live load."

Career-wise: the serving layer is where the product touches your platform — it's the single-digit-millisecond lookups behind the app, the fraud check, the personalization. When it's slow or down, customers feel it immediately, unlike a late analytics dashboard. So owning the serving tier means owning real user-facing reliability, which is high-stakes and high-visibility. The engineer who can design a Cassandra table that stays healthy under years of growth, deletes, and traffic skew — and wire it to the streaming platform safely — is doing genuinely hard, genuinely valued work.

Build the modeler. Internalize "model the query, not the data," and the three traps (growth, tombstones, consistency). Then come to P12 — my favorite phase — where we finally get to the language and abstractions (Scala, Cats, ZIO, FS2) that let you build all of this as type-safe, resource-safe platform code other engineers can't misuse.

— your brother 👨🏻

Lab 01 — Cassandra Data Modeler & Serving Calculator

Phase: 11 — Serving: Cassandra & DynamoDB | Difficulty: ⭐⭐⭐⭐☆ | Time: 5–6 hours

A serving store answers one query shape in single-digit milliseconds — and punishes you for modeling it like a relational DB. This lab builds the arithmetic that keeps a Cassandra table healthy: consistency/durability (R+W>N), partition health (large partitions are the #1 footgun), hot-partition detection, tombstone thresholds, compaction-strategy selection, and time-series bucketing.

What you build

  • quorum / consistency_is_strong / level_nodes / tolerated_failures — tunable consistency as P01's quorum math (R+W>RF; what each level survives)
  • partition_cells / partition_health — cells & bytes vs the limits → ok/warn/danger
  • hot_partition_factor — skew detection (the serving costume of P01's hot key)
  • tombstone_scan_ok — the delete/TTL-heavy read that blows the 100k tombstone limit
  • choose_compaction_strategy — STCS (write) / LCS (read) / TWCS (time-series/TTL)
  • bucket_key — time-series bucketing so a per-entity series doesn't grow unbounded

Key concepts

ConceptWhat to understand
R + W > RFtunable strong consistency; ONE/QUORUM/ALL trade latency vs durability
Large partitionsGC pressure, slow reads, repair pain — model before shipping
Hot partitiona celebrity key melts one replica set; fix the partition key
Tombstonesdeletes/TTL create them; too many per read = failed query
CompactionSTCS/LCS/TWCS match write/read/time-series workloads
Bucketingbound partition growth for time series

Run

pip install -r requirements.txt
pytest test_lab.py -v
LAB_MODULE=solution pytest test_lab.py -v

Success criteria

  • All 14 tests pass.
  • You can explain why "model around the query, not the entities" is Cassandra's first rule.
  • You can explain why a TTL-heavy queue table is a tombstone disaster and what compaction strategy fixes it.
  • You can pick a partition key that avoids both large and hot partitions.

Extensions

  • Add a DynamoDB capacity model: RCU/WCU (or on-demand) sizing for a read/write rate, plus the hot-partition / adaptive-capacity caveat and a GSI cost.
  • Add streaming upserts from Flink (P04 idempotent sink) + a Spark backfill rate limiter (don't overwhelm the cluster) — the JD's Problem 4.
  • Model multi-region (NetworkTopologyStrategy, LOCAL_QUORUM) and the consistency/latency tradeoff across regions (P14/P15).

Phase 12 — Scala & Functional Data Engineering

Difficulty: ⭐⭐⭐⭐⭐ Estimated Time: 3 weeks (40–50 hours) Prerequisites: Phase 03 (Validated/contracts), Phase 01 (the concurrency this models)


Why This Phase Exists

The JD is emphatic that this is a Scala/JVM role: "build production-grade Scala services and libraries … Cats abstractions such as Functor, Applicative, Monad, Traverse, Validated, EitherT, OptionT, Resource … effect systems such as Cats Effect or ZIO … Akka or Akka Streams … JVM memory, GC, thread pools … build internal SDKs." And: "This role expects more than basic Scala syntax. You should be able to build reusable frameworks and platform libraries that prevent entire classes of bugs." Round 4 is exactly "design a type-safe Scala SDK."

This phase teaches the abstractions that let you build a platform library other engineers cannot misuse — type safety that makes illegal states uncompilable, resource safety that makes leaks impossible, and effect systems that make concurrency composable and testable.

Concepts

  • Scala for data infra: case classes, ADTs (sealed traits), pattern matching, immutability, type parameters, given/implicits (typeclasses); why Spark/Flink/Kafka-Streams internals are Scala/JVM.
  • Type-level modeling: make illegal states unrepresentable — newtypes/opaque types (an OrderId is not a String), smart constructors returning Validated, ADTs for exhaustive error/event modeling. The bug-class-eliminating discipline.
  • Cats typeclasses: Functor (map), Applicative (independent combine — Validated accumulates), Monad (dependent sequence — fail-fast), Traverse (List[F[A]] → F[List[A]]), Validated, Either/EitherT, OptionT, Semigroup/Monoid, Resource.
  • Effect systems (Cats Effect IO, ZIO ZIO[R,E,A]): referential transparency (describe vs run), composition, fibers/structured concurrency, cancellation, Ref/Deferred, resource safety, error channels — the modern way to write correct concurrent code.
  • FS2: pure, back-pressured streaming on Cats Effect; resource-safe streams; parEvalMap for bounded concurrency — the in-service pipeline tool (P05).
  • Akka / Akka Streams: the actor model (state isolation, supervision) and Reactive-Streams back-pressured graphs — the older but widespread alternative.
  • JVM performance: heap/GC (G1/ZGC), thread pools, blocking vs async boundaries, serialization (Kryo/Java), off-heap, profiling — what "JVM tuning" means in practice.
  • SDK/library design: type-safe APIs, error modeling, retry semantics, binary compatibility, versioning, developer ergonomics — building the platform's internal libraries.

Labs

Lab 01 — Functional Data SDK (flagship, implemented)

FieldValue
GoalBuild the core FP abstractions: Validated (accumulating), Result/Either (fail-fast), Resource/bracket (LIFO release on failure), IO (lazy effect), retry — plus authentic Scala/Cats reference source
ConceptsApplicative vs monad, resource safety, referential transparency, effect combinators
How to Testpytest test_lab.py -v — 19 tests (Python companion); reference.scala for the real Scala
Talking PointsWhy is Validated applicative and Either monadic? Why does IO do nothing until run? How does Resource guarantee release?
Resume bulletBuilt a functional data SDK (accumulating validation, resource-safe brackets, a lazy effect type, retry combinators) with an authentic Cats/Cats-Effect/FS2 reference implementation

→ Lab folder: lab-01-functional-sdk/

Lab 02 — Functional Data SDK in real Scala (implemented, sbt test)

FieldValue
GoalThe real Scala version of Lab 01: an idiomatic Cats + Cats Effect SDK — typed domain (value classes + smart constructors), ValidatedNel accumulating contracts (mapN), Resource LIFO-safe lifecycles, IO effects + retry — verified with ScalaTest
ConceptsMake-illegal-states-unrepresentable, applicative validation, resource safety, effects-as-values — in production Scala
How to Testsbt test — 8 specs across ContractSpec/ResourceSpec/EffectsSpec (real Scala, runs here)
Resume bulletBuilt a type-safe Scala data SDK on Cats/Cats-Effect (accumulating validation, resource-safe lifecycles, effectful retry) with ScalaTest coverage

→ Lab folder: lab-02-scala-sdk/ (the reference.scala from Lab 01 is the extended Cats/FS2 tour)

Extension Project A — Real Scala SDK (spec)

Port reference.scala into an sbt project; add munit-cats-effect tests; publish a tiny event-contract library (schema + Validated decode) and consume it from a second module — exercise binary compatibility (don't break the API).

Extension Project B — JVM tuning memo (spec)

Profile a (synthetic) Spark/Flink JVM: GC pauses, heap pressure, serialization hotspots; recommend GC (G1 vs ZGC), heap sizing, off-heap, and Kryo registration — the JD's "JVM performance tuning."

Integrated-Scenario Hooks

  • This phase's Validated is P03's contract validator, for real.
  • Its Resource/IO/FS2 are how you build P02's ingestion sidecar and P05's CDC connector.
  • Its type-safe SDK design is the "internal developer platform" of P13/P15 and the JD's deliverable #2 (a Flink stream-processing framework with templates).

Guides in This Phase

Key Takeaways

  • This is a Scala/JVM role; you build platform libraries, not just pipelines.
  • Make illegal states unrepresentable: types eliminate whole bug classes at compile time.
  • Validated accumulates (applicative); Either/Monad fail-fast (dependent) — use each correctly.
  • Resource/bracket guarantee release even on failure; IO/ZIO make effects lazy and composable.
  • A great SDK prevents misuse by construction; that's the JD's bar ("prevent entire classes of bugs").

Deliverables Checklist

  • Lab 01 implemented; all 19 tests pass; you've read reference.scala
  • You can explain applicative vs monad and when each is right
  • You can explain resource safety and referential transparency
  • You can sketch a type-safe event-contract SDK API
  • Extension A real Scala SDK or Extension B JVM tuning memo

Warmup Guide — Scala & Functional Data Engineering

Zero-to-principal primer for Phase 12: Scala for platform code, the Cats abstractions (Functor→Monad, Validated, Traverse, Resource), effect systems (Cats Effect/ZIO), FS2 and Akka, JVM tuning, and how to build an SDK other engineers can't misuse.

Table of Contents


Chapter 1: Why Scala/JVM for Data Infrastructure

The data world runs on the JVM: Spark, Flink, Kafka, Kafka Streams are written in Scala/ Java, and their performance-critical extension points (UDFs, custom sinks, state functions, serializers) are JVM code. Scala specifically because it combines:

  • Immutability + ADTs for safe modeling of events/contracts.
  • A powerful type system (generics, typeclasses, higher-kinded types) for building reusable abstractions that prevent bugs rather than catch them.
  • Functional + OO so you can be pragmatic.

The JD wants "more than basic Scala syntax" — it wants you to build platform libraries. That requires the abstractions in this phase. (Java is the interop floor; you'll read it and sometimes write it, but the leverage is in Scala's type system.)

Chapter 2: Type-Level Modeling — Make Illegal States Unrepresentable

The principal's favorite phrase. The idea: encode constraints in types so violating them doesn't compile — moving whole bug classes from runtime to compile time:

  • Newtypes / opaque types: OrderId(String) and UserId(String) are different types, so you can't pass a user id where an order id is expected. No more "I swapped two string args" bugs.
  • Smart constructors: a value's constructor is private; the only way to make one is a factory that validates and returns Validated/Either — so an Amount literally cannot hold a negative number anywhere in the program.
  • ADTs (sealed traits): model your events and errors as a closed set of cases; pattern matches are exhaustive (the compiler warns if you miss a case) — so adding a new event type forces every handler to address it.

This is the deepest form of the JD's "prevent entire classes of bugs": the bug can't be written. The lab's typed OrderId/Amount (in reference.scala) show it.

Chapter 3: The Typeclass Ladder — Functor, Applicative, Monad

Cats gives a vocabulary of abstractions, ordered by power. You must know the ladder because which one you reach for changes behavior:

  • Functormap: transform the value inside a context (F[A] => (A=>B) => F[B]). "I have an Option[Int], make it an Option[String]."
  • ApplicativemapN/ap: combine independent computations (F[A], F[B] => F[(A,B)]). Crucially, applicatives can accumulate (Ch. 4). "Validate three fields independently and collect all errors."
  • MonadflatMap: sequence dependent computations where each step depends on the previous (F[A] => (A => F[B]) => F[B]). Monads short-circuit (fail-fast). "Load config, then use it to fetch, then …"
  • Traversetraverse/sequence: turn List[F[A]] into F[List[A]] (e.g. validate a whole list, collecting results). The workhorse for "apply an effectful function over a collection."

The interview-grade distinction: applicative = independent + can accumulate; monad = dependent + short-circuits. Choosing the wrong one (a monad for validation) is why you get fail-fast error reporting when you wanted all errors (P03).

Chapter 4: Validated, Either, and Error Modeling

The concrete payoff of Ch. 3:

  • Either[E, A] (and its monad) — fail-fast: the first Left stops the chain. Right for dependent steps (step 2 is meaningless if step 1 failed). EitherT[F, E, A] stacks it on an effect F (e.g. EitherT[IO, Error, A]) for effectful fail-fast pipelines.
  • Validated[E, A] (applicative) — accumulating: ValidatedNel collects a non-empty list of errors via mapN. Right for independent validations (a data contract: report every violation at once — the lab + P03).
  • Error ADTs: model errors as a sealed trait (NotFound | Downstream(msg) | ...) not strings/exceptions, so handling is exhaustive and typed.
  • OptionT[F, A] similarly stacks Option on an effect.

Rule of thumb: typed errors over exceptions; Validated for parallel validation; Either/ EitherT for sequential business logic.

Chapter 5: Resource Safety and Bracket

A leaked connection/file/cursor is a production incident. bracket (acquire → use → release, release guaranteed even on failure or cancellation) is the primitive; Resource is its composable form:

  • Resource.make(acquire)(release) packages the lifecycle; .use(f) runs f with the resource and always releases it.
  • Resource composes monadically (for { a <- r1; b <- r2 } yield (a,b)), releasing in LIFO order (open Kafka, open Cassandra, … close Cassandra, close Kafka) — even if use throws or the fiber is cancelled. The lab tests exactly this LIFO-on-exception property.
  • This is compile-time-enforced discipline: if you have a Resource, the only way to get its value is .use, which guarantees release. You can't forget to close it.

This is correctness most languages get via try/finally discipline (easily forgotten); Cats makes it a type you can't misuse.

Chapter 6: Effect Systems — Cats Effect and ZIO

The modern way to write correct concurrent, effectful code:

  • Referential transparency: an IO[A] (Cats Effect) or ZIO[R,E,A] (ZIO) is a value describing an effect, not the effect itself. Building it does nothing; running it (unsafeRunSync/the runtime) performs it — and running it twice performs it twice. This is the lab's IO. Why it matters: effects become values you can pass around, retry, combine, and test without performing them.
  • ZIO's ZIO[R, E, A]: three type params — R (environment/dependencies it needs), E (typed error channel), A (success). Encodes dependency injection and typed errors in the type. Cats Effect's IO[A] keeps it simpler (errors via Throwable, deps via constructor/Resource).
  • Structured concurrency: fibers (lightweight green threads) with parMapN/race/ start; cancellation is safe and propagates; Ref (atomic state), Deferred (promise), Queue, Semaphore for coordination. You get massive concurrency without thread-pool juggling or callback hell.
  • Error handling: handleErrorWith, retry/recover; the lab's retry combinator is a miniature (Cats Effect's real one does exponential backoff + jitter — see reference.scala).

The principal value: effect systems make concurrency composable, cancelable, resource-safe, and testable — the opposite of ad-hoc threads and futures.

Chapter 7: FS2 and Akka Streams

Two ways to build streaming inside a service (vs cluster engines, P05):

  • FS2 (Functional Streams for Scala): pure, lazy, back-pressured streams on Cats Effect. Stream[IO, A] — compose with map/evalMap/parEvalMap(n) (bounded concurrency = backpressure), and resource safety is built in (Stream.resource releases even mid- stream). The functional choice for connectors, ingestion sidecars, CDC consumers.
  • Akka Streams: a Reactive-Streams implementation on the Akka actor runtime — Source → Flow → Sink graphs with backpressure via demand signaling. Plus actors (isolated state, message-passing, supervision hierarchies) for stateful concurrent components. Widespread, battle-tested; a different (effects-as-actors) philosophy than FS2's pure effects. (Note Akka's licensing change pushed some orgs to Pekko, the Apache fork.)

Both give the backpressure of P01 Ch. 8 as a first-class, typed construct.

Chapter 8: JVM Performance and SDK Design

  • JVM tuning (the JD's "JVM memory, GC, thread pools, serialization, profiling"):
    • GC: G1 (default, balanced) vs ZGC/Shenandoah (low-pause, large heaps — good for big Flink/Spark state); watch GC pause time and frequency (a top cause of latency spikes and Flink checkpoint stalls, P04).
    • Heap & off-heap: size the heap, but large state belongs off-heap/RocksDB (P04) to avoid GC; tune spark.memory.fraction (P06).
    • Serialization: Java serialization is slow/large; Kryo (with registered classes) is the Spark standard; Flink has its own serializers. Serialization is a real shuffle/ checkpoint bottleneck.
    • Thread pools & blocking: never block the compute pool with I/O — Cats Effect/ZIO give you a separate blocking pool; mixing them is a classic deadlock/starvation bug.
  • SDK design (Round 4): a platform library is judged by whether people can't misuse it. Type-safe APIs (Ch. 2), error modeling (Ch. 4), resource safety (Ch. 5), sensible defaults, binary compatibility (don't break the ABI across minor versions — MiMa checks this), semantic versioning, and ergonomics (good error messages, discoverable API). The JD's deliverable #2 — a Flink framework with templates + observability baked in — is this.

Lab Walkthrough Guidance

Lab 01 — Functional Data SDK (Python companion), suggested order:

  1. Validated.map/map2map2 accumulates on the invalid path (applicative).
  2. Result.flat_mapfail-fast (the second step doesn't run after an error).
  3. bracket then Resource.make/flat_map — release on exception, then LIFO composition.
  4. IO.map/flat_map/run — lazy: building does nothing; the lazy-until-run + runs-each-time tests are the point.
  5. retry — a new lazy IO with bounded attempts.
  6. Read reference.scala to see each in real Cats/Cats-Effect/FS2.

Success Criteria

You are ready for Phase 13 when you can, from memory:

  1. Explain "make illegal states unrepresentable" with newtypes, smart constructors, ADTs.
  2. Place Functor/Applicative/Monad/Traverse on the ladder and say what each does.
  3. Explain why Validated accumulates and Either short-circuits, and when to use each.
  4. Explain Resource/bracket and the LIFO-release-on-failure guarantee.
  5. Explain referential transparency and why effects-as-values are composable/testable.
  6. Contrast Cats Effect IO and ZIO ZIO[R,E,A]; contrast FS2 and Akka Streams.
  7. Name the JVM tuning levers (GC, off-heap, Kryo, blocking pools) and SDK design principles.

Interview Q&A

Q (Round 4): Design a Scala SDK so product teams define event schemas, validate contracts, build stream processors, and write to the lakehouse — safely. The spine is make misuse uncompilable. Events are typed ADTs with newtype ids and smart constructors (illegal states unrepresentable, Ch. 2). Decoding returns ValidatedNel so a bad payload yields all contract violations at once (Ch. 4), routed to a DLQ (P02/P03). Processing is expressed as effects (IO/ZIO) so it's lazy, composable, cancelable, and testable without side effects (Ch. 6); resources (Kafka, Cassandra, the Iceberg writer) are Resources so they cannot leak (Ch. 5). The lakehouse write is an exactly-once two-phase-commit sink (P04) exposed as a single safe API. Cross-cutting: typed errors (not exceptions), a retry combinator with backoff, observability baked into the templates, and binary compatibility + semver so upgrading the SDK never breaks consumers. The win is a library where the happy path is the only path that compiles.

Q: Why Validated instead of Either for validation? Because validation steps are independent and you want all the errors, while Either is a monad that short-circuits at the first Left. Validated is an applicative: mapN runs each validation and accumulates failures into a NonEmptyList, so a user submitting a bad record gets "name required, age must be ≥ 0, currency invalid" in one pass instead of fixing them one at a time. The flip side: use Either/EitherT for dependent business logic where step 2 is meaningless if step 1 failed — there short-circuiting is correct. Same shape (E and A), opposite combine semantics; pick by independence.

Q: What does an effect system buy you over Futures/threads? Referential transparency and structured concurrency. An IO/ZIO is a value describing an effect, so nothing runs until the edge of the program — which makes effectful code composable, retryable, and testable without performing side effects, and eliminates the "Future starts running the moment you create it" footgun. On top, fibers give cheap structured concurrency with safe cancellation that propagates, Resource guarantees cleanup even under cancellation, and typed error channels (especially ZIO's E) make failure explicit. Net: you write highly concurrent, resource-safe code that you can reason about and test, instead of juggling thread pools and callbacks.

Q: A Flink/Spark job has latency spikes and checkpoint stalls. JVM angle? Prime suspect is GC pauses: large on-heap state with G1 can produce long stop-the-world pauses that stall the operator and delay checkpoint barriers (P04). Fixes: move big state off-heap/RocksDB so the heap stays small, switch to a low-pause collector (ZGC/Shenandoah) for large heaps, and size the heap to avoid constant pressure. Also check serialization (use Kryo with registered classes, or Flink's serializers — Java serialization is a hidden tax on every shuffle/checkpoint) and blocking calls on the compute pool (offload I/O to a blocking pool). These JVM levers are exactly the "debug at the byte and GC level" the role expects.

References

  • Functional Programming in Scala (Chiusano & Bjarnason) — the red book; the ladder & laws
  • Cats and Cats Effect docs
  • ZIO docs — ZIO[R,E,A], fibers, layers
  • FS2 and Akka/Pekko Streams
  • Practical FP in Scala (Gabriel Volpe) — building a real app with these tools
  • JVM: Optimizing Java (Evans, Gough, Newland); ZGC/G1 docs for GC tuning

🛸 Hitchhiker's Guide — Phase 12: Scala & Functional Data Engineering

Read this if: you want the Scala/Cats/ZIO/FS2 toolkit fast and to pass Round 4 ("design a type-safe Scala SDK"). Skim, read WARMUP, build the SDK.


0. The 30-second mental model

This is a Scala/JVM role, and the leverage is building platform libraries other engineers can't misuse. You do that with: types that make illegal states uncompilable, Cats abstractions (Validated/Either/Resource), and effect systems (Cats Effect/ZIO/FS2) that make effects values you can compose, retry, and test. One sentence: make the happy path the only path that compiles.

1. Type-level modeling

newtype: OrderId(String) ≠ UserId(String) → can't swap args (won't compile)
smart constructor: Amount.of(cents): Validated → an Amount can NEVER be negative
ADT (sealed trait): closed set of cases → exhaustive pattern match (compiler enforces)
→ whole bug classes moved from runtime to COMPILE time

2. The typeclass ladder

Functor   map      F[A] => (A=>B) => F[B]            transform inside a context
Applicative mapN   F[A], F[B] => F[(A,B)]            INDEPENDENT combine → can ACCUMULATE
Monad     flatMap  F[A] => (A=>F[B]) => F[B]         DEPENDENT sequence → SHORT-CIRCUITS
Traverse  traverse List[F[A]] => F[List[A]]          effectful map over a collection

Applicative = independent + accumulate. Monad = dependent + fail-fast. Pick by independence.

3. Error modeling

Validated (applicative) → collect ALL errors (data contracts, P03)  [ValidatedNel + mapN]
Either / EitherT (monad) → fail-fast (dependent business logic)
errors as a sealed-trait ADT, NOT strings/exceptions → typed, exhaustive

4. Resource safety

bracket(acquire)(use)(release): release ALWAYS runs (even on failure/cancel)
Resource composes monadically → releases LIFO (open A,B → close B,A)
if you HAVE a Resource, the only way to its value is .use → you CAN'T forget to close

5. Effect systems (the big idea)

IO[A] / ZIO[R,E,A] = a VALUE describing an effect; building it does NOTHING; run() performs it
referential transparency → effects are values: pass around, retry, combine, TEST without running
ZIO[R,E,A]: R=deps, E=typed error, A=value. Cats Effect IO[A]: simpler (Throwable errors)
structured concurrency: fibers, parMapN, race, cancellation-safe; Ref/Deferred/Queue/Semaphore

6. FS2 vs Akka Streams (in-service streaming)

FS2     → pure, lazy, back-pressured streams on Cats Effect; parEvalMap(n) = bounded concurrency;
          resource-safe by construction
Akka    → actor model (isolated state, supervision) + Reactive-Streams graphs (Source→Flow→Sink)
          (note: Akka relicensed → Pekko is the Apache fork)
both give P01's backpressure as a typed first-class construct

7. JVM tuning levers

GC: G1 (default) vs ZGC/Shenandoah (low-pause, big heaps) ← GC pauses stall Flink checkpoints (P04)
big state → off-heap/RocksDB (keep heap small) ; spark.memory.fraction (P06)
serialization: Kryo (registered classes) ≫ Java serialization ; a hidden shuffle/checkpoint tax
never block the compute pool with I/O → use a separate blocking pool

8. SDK design (what Round 4 grades)

type-safe API + typed errors + Resource safety + sensible defaults
binary compatibility (MiMa) + semver → upgrading never breaks consumers
ergonomics: good errors, discoverable API, templates with observability baked in
goal: people CANNOT misuse it (the JD's "prevent entire classes of bugs")

9. Beginner mistakes that mark you

  1. Either/monad for validation → fail-fast when you wanted all errors (use Validated).
  2. Stringly-typed everything → swapped-arg bugs; no newtypes/ADTs.
  3. Eager side effects / Futures-that-run-on-creation instead of lazy IO.
  4. try/finally resource handling that someone eventually forgets (use Resource).
  5. Blocking I/O on the compute thread pool → starvation/deadlock.
  6. Breaking binary compatibility in a "minor" SDK release → downstream breakage.
  7. Java serialization in Spark → slow shuffles (register Kryo).

10. How this phase pays off later

  • Validated = P03 contract validator (real).
  • Resource/IO/FS2 = P02 ingestion sidecar, P05 CDC connector.
  • Type-safe SDK = the "internal developer platform" of P13/P15 + JD deliverable #2.
  • JVM/GC tuning = P04 checkpoint stalls, P06 spill.

Read WARMUP, build the SDK, read reference.scala, then P13: orchestration, reliability, and observability — running all of this in production with SLOs.

👨🏻 Brother Talk — Phase 12, Off the Record

My favorite phase. Scala and functional programming scare people off, and that's exactly why being good at it is a moat. Let me get you over the hump.


Brother, I'll be straight: this is the phase people avoid, and that's precisely why it's worth your time. Functional Scala has a reputation — monads, "category theory," cryptic operators — that makes a lot of competent engineers quietly decide it's not for them. So the pool of data engineers who can actually build a clean, type-safe, resource-safe Scala platform library is small, and the JD is explicitly hunting for them ("more than basic Scala syntax… build reusable frameworks… prevent entire classes of bugs"). Push through the intimidation and you've got a skill most of your competition skipped. The moat is real.

Here's the reframe that dissolves the fear: you don't need category theory; you need a few practical patterns. Forget the scary words. The whole thing reduces to a handful of moves you can actually feel:

  • "I want to combine validations and see all the errors" → that's Validated, and the reason it accumulates is the only thing you need to know about "applicative."
  • "I want to chain steps where each needs the last, and stop on the first failure" → that's flatMap/Either, and "it short-circuits" is the only thing you need to know about "monad."
  • "I want this connection to always close, even if everything explodes" → that's Resource.
  • "I want to describe an effect now and run it later, and be able to test it without running it" → that's IO.

That's it. That's 90% of functional data engineering in practice. The lab makes you build each one, and once you've built Resource and watched it release in LIFO order even when an exception is thrown, the magic evaporates and it's just a clever, useful pattern. Build them. The understanding-by-building is the whole point.

Now the single most powerful idea in this phase, the one I want tattooed on your brain: make illegal states unrepresentable. Most engineering is "write code, then write tests/checks to catch the bad cases." Functional type-driven design flips it: make the bad cases impossible to even write. An OrderId that's a distinct type from UserId means you can never swap them — not "you'll catch it in review," but it won't compile. An Amount with a private constructor and a validating factory means a negative amount cannot exist anywhere in your program — ever, by construction. When you build a platform library this way, you're not asking the hundreds of engineers who use it to be careful. You're making carelessness impossible. That is the highest form of the "prevent entire classes of bugs" mandate, and it's what separates a senior who writes correct code from a principal who makes everyone's code correct by design.

The effect-system thing — IO/ZIO — trips people up, so let me give you the click. The confusion is "why would I want my code to do nothing when I call it?" Because an effect that's a value instead of an action is something you can hold, pass around, retry, combine, and test. When database.write() runs immediately, you can't test the logic around it without hitting a database. When database.write() returns an IO describing the write, you can build the whole program as a value, inspect it, wrap it in retry, run it in tests with a fake, and only actually perform it at the very edge. The laziness isn't a quirk — it's what makes effectful code as composable and testable as pure code. The lab's "build an IO, prove it does nothing until run, prove it runs each time" is that click in miniature. Feel it.

A practical warning that'll save you real pain: resource leaks and blocking-the-wrong-pool are the two FP-in-production bugs that bite hardest. Use Resource for everything with a lifecycle (connections, files, clients, the Iceberg writer) so cleanup is guaranteed — the day you have a "we ran out of connections in prod" incident, it's because someone didn't. And never block the compute thread pool with I/O; effect systems give you a separate blocking pool for a reason, and mixing them up gives you the world's most confusing deadlock. These aren't theory; they're the 2 a.m. pages of functional Scala.

On JVM tuning: don't glaze over it because it's not as elegant as the FP. The day a Flink job's checkpoints start timing out and you realize it's GC pauses from too much on-heap state — and you fix it by moving state off-heap and switching to ZGC — you'll have done something most "Scala developers" can't, because they treat the JVM as a black box. The role wants someone who debugs down to the GC and serialization layer. Knowing that Kryo-with-registered-classes is 10× faster than Java serialization on a shuffle, or that a low-pause collector saves your checkpoint SLA, is the kind of byte-level fluency that defines the seniority bar.

Career truth: Scala/FP fluency is rare and sticky. Rare because most people avoid it; sticky because once a company builds its platform on Cats/ZIO, they desperately need people who can maintain and extend it. Being the person who can build the SDK and explain it to the team so they can use it is a genuinely defensible, well-paid position. The intimidation factor that keeps others out is your advantage — walk through the door they won't.

Build the SDK. Make Resource release LIFO under an exception. Make Validated collect every error. Make IO stay lazy. Read reference.scala and see the real Cats. Then come to P13, where we make all of this observable and reliable in production — SLOs, lineage, data quality, incident response.

— your brother 👨🏻

Lab 01 — Functional Data SDK (Validated, Resource, IO)

Phase: 12 — Scala & Functional Data Engineering | Difficulty: ⭐⭐⭐⭐⭐ | Time: 6–8 hours

The JD's Round 4 is "design a Scala library with Cats abstractions, resource safety, effect systems, error modeling, and retry semantics." This lab makes you build those abstractions so you understand them from the inside — Validated (accumulating), Result (fail-fast monad), Resource/bracket (guaranteed LIFO release), IO (lazy effect / referential transparency), and retry (an effect combinator).

Two files, one set of ideas

  • lab.py / solution.py / test_lab.py — a Python companion that encodes the exact semantics so the tests run anywhere (no JVM/sbt needed). This is what you implement.
  • reference.scala — the authentic Cats / Cats-Effect / FS2 version an interviewer expects: ValidatedNel + mapN, EitherT, Resource.make, IO, FS2 Stream with parEvalMap backpressure, retry with exponential backoff. Read it; run it with sbt if you have a JVM. The Python tests verify the properties; the Scala is for fluency.

What you build (in Python)

AbstractionCats/ZIO equivalentProperty the test pins
Validated.map2Validated / mapNaccumulates all errors
Result.flat_mapEither / EitherTfail-fast (stops at first error)
bracket / ResourceResource / bracketrelease even on exception, LIFO
IOcats.effect.IO / ZIOlazy: describe ≠ run; runs each time
retryretry combinatorbounded attempts, still lazy

Run

pip install -r requirements.txt
pytest test_lab.py -v
LAB_MODULE=solution pytest test_lab.py -v

Success criteria

  • All 19 tests pass — test_validated_map2_accumulates_errors (applicative vs monad), test_resource_releases_lifo_on_exception (resource safety), and test_io_is_lazy_until_run (referential transparency) are the ones that certify understanding.
  • You can explain why Validated is an applicative (accumulates) and Result/Either is a monad (fail-fast), and when each is correct.
  • You can explain why a description-of-an-effect (IO) that does nothing until run() is more composable and testable than eager side effects.

Extensions

  • Add a Ref-style atomic mutable cell and a tiny parMapN to model concurrency (Cats Effect Ref / parMapN).
  • Add an FS2-style lazy Stream (pull-based) with take, map, evalMap, and a bounded parEvalMap — and show resource safety across the stream.
  • Port one function to real Scala in reference.scala and run it with sbt.
  • Build a typed EventContract[A] that combines a schema (P03) + Validated decoding into a reusable SDK type — the seed of the platform library the JD asks for.

Lab 02 — Functional Data SDK in real Scala (Cats + Cats Effect)

Phase: 12 — Scala & Functional Data Engineering | Difficulty: ⭐⭐⭐⭐⭐ | Time: 6–8 hours

Lab 01's Python companion taught the semantics. This is the real thing — an idiomatic Scala SDK built on Cats and Cats Effect, the exact stack Spark/Flink/Kafka-Streams platform teams use, with ScalaTest specs that compile and pass via sbt test. This is what Round 4 ("design a type-safe Scala SDK") actually wants to see.

What it demonstrates

FileConceptCats/CE feature
Domain.scalamake illegal states unrepresentablevalue classes + private smart constructor + ValidatedNel
Contract.scalaaccumulating data-contract validationmapN (applicative) collects all errors
Resources.scalaresource safetyResource.make → guaranteed LIFO release on failure
Effects.scalaeffects-as-values + retryIO, handleErrorWith, exponential backoff

The specs prove the principal-grade properties: validation accumulates (not fail-fast), resources release in LIFO order even when use throws, retry recovers after transient failures, and IO is lazy (building ≠ running).

Run

sbt test

First run downloads Scala 2.13, Cats, Cats Effect, and ScalaTest (a minute or two), then compiles and runs all specs. Expected: 8 tests, all green, across ContractSpec, ResourceSpec, EffectsSpec.

sbt compile      # just typecheck
sbt console      # a REPL with the SDK + Cats on the classpath

Layout

build.sbt                         # Cats + Cats Effect + ScalaTest
project/build.properties          # sbt version
src/main/scala/datasdk/           # the SDK (Domain, Contract, Resources, Effects)
src/test/scala/datasdk/           # ScalaTest specs (the proof)

See also ../lab-01-functional-sdk/reference.scala for an extended Cats/FS2 reference (FS2 streaming, EitherT, an IOApp) — read it for breadth.

Success criteria

  • sbt test → all 8 specs pass.
  • You can explain why Order.validate uses mapN (accumulate) and not a for-comprehension (flatMap, fail-fast), and when each is correct.
  • You can explain why ResourceSpec sees release cassandra before release kafka even though use raised — and why that guarantee is impossible to forget with Resource.
  • You can explain why building ref.update(_ + 1) changes nothing until the IO is run.

Extensions

  • Add an FS2 Stream that reads a Resource, processes with parEvalMap(n) (bounded backpressure), and prove resource safety across the stream.
  • Add a EventContract[A] typeclass (a schema + a ValidatedNel decoder) and derive instances — the seed of the platform library the JD asks for.
  • Port Effects.retry to add jitter, and write a deterministic test using Cats Effect's TestControl (virtual time) instead of real IO.sleep.
  • Wire a fake "lakehouse writer" as a Resource[IO, Writer] with an idempotent commit — the exactly-once sink (P04) as a typed, leak-proof API.

Phase 13 — Orchestration, Reliability & Observability

Difficulty: ⭐⭐⭐⭐☆ Estimated Time: 2 weeks (30–38 hours) Prerequisites: Phases 04/06 (the jobs being orchestrated), Phase 01 (failure domains)


Why This Phase Exists

This phase makes the platform operable — the force (P00) juniors ignore and principals obsess over. The JD demands orchestration (Airflow/Dagster, Step Functions, backfill & replay frameworks), reliability (SLOs, SLIs, error budgets, incident response, runbooks, capacity & cost), data quality (freshness, volume, uniqueness, referential integrity, anomaly detection, dataset certification), observability (streaming + batch dashboards, lineage, audit), and detecting silent data corruption. Its deliverables include a "streaming observability dashboard," a "data quality framework," and "incident response playbooks." This is where "it works in the notebook" becomes "it has run reliably for a year."

Concepts

  • Orchestration: DAG scheduling (topological execution), Airflow (operators, sensors, scheduler, backfill) vs Dagster (software-defined assets, types, content-addressed incrementality) vs Step Functions (serverless state machines); idempotent tasks, retries, SLAs.
  • Backfill & replay frameworks: partition-scoped, idempotent, reproducible, resumable, auditable recomputation (P06) + log replay (P02) — the platform's recovery superpower.
  • SLOs / SLIs / error budgets: define measurable objectives ("99.9% of critical streams process within 60s"), measure indicators, spend the error budget to gate change velocity, multi-window burn-rate alerting.
  • Streaming observability: lag, throughput, watermark delay, checkpoint health/duration, backpressure, DLQ rate, failure rate (P02/P04) — the streaming dashboard.
  • Batch observability: job duration, shuffle/spill, skew, small-file counts, cost per run (P06/P07).
  • Data quality & correctness: freshness, volume anomaly, uniqueness, referential integrity, duplicate detection, late-data reconciliation, data diffing, audit tables, dataset certification, and detecting silent data corruption (drift detectors aimed at inputs).
  • Lineage & auditability: field/dataset-level lineage graph, blast-radius (impact) analysis, audit logging.
  • Incident response: runbooks, on-call, the "what's the shared failure domain?" discipline (P01), postmortems, and the runbooks for Kafka/Flink/Spark/EMR/Athena/Cassandra/S3 failures the JD enumerates.
  • Capacity & cost: capacity planning (Little's Law, P01), cost reviews, cost attribution.

Labs

Lab 01 — Orchestration & Reliability Core (flagship, implemented)

FieldValue
GoalBuild a DAG scheduler with content-hash caching, SLO/error-budget math, data-quality checks, and a lineage/impact graph
ConceptsTopological execution, incremental caching, error budgets, data quality, lineage
How to Testpytest test_lab.py -v — 18 tests
Talking PointsWhy does content-hash caching make backfills cheap+reproducible? When does burn rate page you? What's blast radius?
Resume bulletBuilt an orchestration + reliability core: content-addressed DAG caching, SLO/error-budget tracking, data-quality checks, and lineage/impact analysis

→ Lab folder: lab-01-orchestration-core/

Extension Project A — Streaming observability dashboard (spec)

Define the metrics + alerts for a Flink pipeline (lag, watermark delay, checkpoint duration, backpressure, DLQ rate, failure rate) and the multi-window burn-rate alert rules — the JD's deliverable #8.

Extension Project B — Incident runbook + postmortem (spec)

Write a runbook for one failure (e.g. "Flink checkpoint duration rising + lag climbing", P04) and a blameless postmortem template — the JD's deliverable #12.

Integrated-Scenario Hooks

  • This phase's DAG + caching orchestrates the P06 backfills and P09 compaction jobs.
  • Its data-quality checks consume P03's contracts and feed the P14 governance certification.
  • Its lineage/impact is the blast-radius reasoning of P01, productized.

Guides in This Phase

Key Takeaways

  • Operability is a first-class force; SLOs/error budgets make reliability measurable.
  • Content-hash caching = cheap, reproducible backfills (re-run only what changed).
  • Data quality is monitored across freshness/volume/uniqueness/integrity — catch corruption before customers do.
  • Lineage turns an incident into one root cause via blast-radius reasoning.
  • Streaming observability = lag + watermark delay + checkpoint health + backpressure + DLQ rate.

Deliverables Checklist

  • Lab 01 implemented; all 18 tests pass
  • You can define an SLO/SLI/error budget for a data product
  • You can list the streaming + batch + quality metrics to monitor
  • You can use lineage to compute the blast radius of a change
  • Extension A dashboard spec or Extension B runbook+postmortem

Warmup Guide — Orchestration, Reliability & Observability

Zero-to-principal primer for Phase 13: orchestration (Airflow/Dagster/Step Functions), backfills/replay, SLOs/SLIs/error budgets, streaming & batch observability, data quality & silent-corruption detection, lineage, and incident response.

Table of Contents


Chapter 1: Orchestration — DAGs, Airflow, Dagster, Step Functions

An orchestrator runs a DAG of tasks in dependency order, with retries, scheduling, and observability. The three you'll meet:

  • Airflow: the incumbent. Python DAGs of operators (do work) and sensors (wait for a condition); a scheduler; rich backfill. Task-centric (you orchestrate operations), with external data dependencies you wire by hand. Battle-tested, huge ecosystem.
  • Dagster: asset-centric — you declare software-defined assets (the datasets), and Dagster tracks their dependencies, types, and freshness, with content-addressed incrementality (re-materialize only what changed — the lab's caching idea). More data-aware; strong typing and testing story.
  • Step Functions: AWS serverless state machines (JSON/ASL) that orchestrate Lambda/ Glue/EMR/ECS steps with retries and branching — great for serverless, event-driven workflows without running an orchestrator.

The unifying primitive is topological execution (the lab's topological_order): run a task only after its dependencies, never with a cycle. The principal-level choice is by ecosystem and data-awareness (Dagster's asset model vs Airflow's operator model vs Step Functions' serverless), captured in an ADR (P15).

Chapter 2: Incrementality, Backfills, and Replay

  • Content-hash caching (the lab's DagRunner): cache a task's output keyed by a hash of its code version + upstream output hashes, so re-running only re-executes what changed. This is what makes a pipeline cheap to re-run and is the basis of Dagster's incrementality and dbt's state comparison.
  • Backfills (P06): recompute history — partition-scoped (by date), idempotent (a retry doesn't duplicate — table-format commits, P09), reproducible (deterministic, event-time logic — P01), resumable (only failed partitions), auditable (what was rewritten, from what input version). The JD's "reproducible and auditable backfills."
  • Replay (P02): re-read the log from an older offset. Correct only with event-time + idempotency. Backfill (batch) and replay (stream) are the two recomputation tools; both rest on the same correctness requirements.
  • The acceptance test for all three: re-run it and get the identical result.

Chapter 3: SLOs, SLIs, and Error Budgets

The reliability vocabulary (Google SRE), applied to data:

  • SLI (indicator): a measured quantity — e.g. "% of events processed within 60s," "% of accepted events durably persisted," "table freshness."
  • SLO (objective): the target for an SLI — e.g. "99.9% within 60s" (the JD's examples).
  • Error budget: allowed_failures = total × (1 − SLO) (the lab). The budget is permission to fail — if you have budget left, ship faster; if you've burned it, freeze and stabilize. This turns reliability from an argument into a number that gates change velocity.
  • Burn rate (the lab): actual_error_rate / (1 − SLO). Burn rate > 1 means you'll exhaust the budget before the window ends; multi-window burn-rate alerts (fast window for acute, slow window for chronic) are the modern alerting recipe — they page on budget burn, not on every blip.

Every production dataset gets an SLO, an owner, lineage, and a quality policy (a JD success metric). "Reliable" without numbers is a wish.

Chapter 4: Streaming Observability

The metrics that tell you a streaming pipeline (P02/P04) is healthy — and the JD's deliverable #8 dashboard:

  • Consumer lag (P02): unconsumed records; rising = falling behind.
  • Throughput: events/s in and out.
  • Watermark delay (P04): how far behind real time the watermark is; rising = event-time trouble or an idle partition stalling watermarks.
  • Checkpoint health/duration (P04): rising duration = backpressure/alignment stalls.
  • Backpressure (P01/P04): the root signal; the UI shows the bottleneck operator.
  • DLQ rate (P02/P03): a step change = an upstream contract break — your earliest alert.
  • Failure/restart rate: job restarts, rebalances.

These connect: backpressure → checkpoint duration ↑ + lag ↑ (the P04 Round-2 chain). A good dashboard shows them together so the one root cause is obvious.

Chapter 5: Batch Observability

For Spark/EMR jobs (P06/P07): job duration trend, shuffle/spill volume, skew (max vs median task time), small-file counts on output (P08), cost per run (P07), partition freshness, and SLA-miss alerts. The batch analog of streaming's lag is "did the table land by its freshness SLO?"

Chapter 6: Data Quality and Silent Corruption

The JD's most-repeated theme: catch bad data before customers or executives do. The dimensions to monitor per dataset (the lab implements several):

  • Freshness: is the newest data recent enough? (event-time lag vs SLO)
  • Volume: is the row count a statistical anomaly vs history? (a sudden drop = upstream broke; a spike = duplication) — the lab's volume_anomaly.
  • Uniqueness: duplicate primary keys (the lab's uniqueness_violations).
  • Referential integrity: fact rows pointing at missing dimensions (the lab's orphans).
  • Schema/semantic validity (P03): contract conformance.
  • Distribution drift / cardinality drift: the values themselves changing shape — the detector aimed at inputs.

Silent data corruption is the worst failure: schema-valid but wrong data (a unit changed upstream, a join silently dropped rows, a processing-time bug double-counted). It passes basic checks and poisons everything downstream. You catch it with reconciliation (do batch and streaming outputs agree? — the lambda/kappa cross-check), data diffing (does the recomputed output match?), audit tables (counts that must balance), and distribution monitors. The principal builds the detection in, because by definition it won't announce itself.

Chapter 7: Lineage and Auditability

  • Lineage: the dataset/field-level graph of "what was computed from what" (the lab's Lineage). It answers the two questions of every incident: upstream ("this looks wrong — what feeds it?") and downstream/impact ("if I change/break this, what's the blast radius?" — P01). At scale this is a lineage browser (a JD platform component).
  • Auditability: who changed what, when; audit tables and logs that let you reconstruct and prove data history (and satisfy compliance, P14).

Chapter 8: Incident Response

When it breaks (it will), the principal is the escalation point (the JD's words). The discipline:

  • Ask "what's the shared failure domain?" first (P01): 12 jobs down that all read one Hive metastore is one incident. Lineage + failure-domain thinking collapses an alarm storm into a root cause.
  • Runbooks: pre-written diagnosis+mitigation for each failure class (Kafka broker instability, Flink checkpoint failures, Spark OOM, EMR spot loss, Cassandra tombstone storms, S3 throttling, Athena failures — the JD's list). A runbook turns a 2 a.m. panic into a checklist.
  • Mitigate, then fix: restore service (failover, rollback to a good snapshot — P09, DLQ the poison — P02) before root-causing.
  • Blameless postmortems: what happened, why, what we'll change (process + automation), so the same incident can't recur. Incidents decreasing in frequency and severity is a JD success metric.

Lab Walkthrough Guidance

Lab 01 — Orchestration & Reliability Core, suggested order:

  1. topological_order — Kahn; cycle detection.
  2. content_hash + DagRunner.run — caching: first run executes all, second none; bump a version → that task + downstream re-run (the incrementality property).
  3. error_budget + burn_rate — the SLO math.
  4. freshness_ok / volume_anomaly / uniqueness_violations / referential_integrity_orphans — the quality dimensions.
  5. Lineage.downstream/upstream/impact — blast radius.

Success Criteria

You are ready for Phase 14 when you can, from memory:

  1. Contrast Airflow / Dagster / Step Functions and the topological-execution primitive.
  2. Explain content-hash caching and the four properties of a safe backfill.
  3. Define SLI/SLO/error budget and burn-rate alerting.
  4. List the streaming and batch observability metrics and how they connect.
  5. Name the data-quality dimensions and how to detect silent corruption.
  6. Use lineage to reason about upstream cause and downstream blast radius.
  7. Run an incident: shared-failure-domain question, runbook, mitigate-then-fix, postmortem.

Interview Q&A

Q: How do you make backfills safe and cheap? Cheap via incrementality — content-hash caching (or partition-scoped reruns) so you only recompute what actually changed, not the whole history. Safe via four properties: idempotent writes (table-format atomic commits, P09, so a retry can't duplicate), reproducible logic (deterministic, event-time — P01, so a rerun yields identical output), resumable (reprocess only failed partitions), and auditable (record what was rewritten and from which input version). The acceptance test is the replay test: run it twice, diff the output, expect zero. That combination is the JD's "reproducible and auditable backfills."

Q: Define an SLO for a critical stream and the alert. SLI: percentage of accepted events processed and queryable within 60 seconds. SLO: 99.9% over a 28-day window. That implies an error budget of 0.1% — the number of late events I'm allowed. I alert on burn rate, not raw breaches: a fast window (e.g. 5 min) catches acute outages, a slow window (e.g. 1 hour) catches chronic degradation, and I page only when the burn rate says I'll exhaust the budget — so I'm not woken by a single slow event but I am for a real regression. When the budget's healthy we ship features; when it's burned we freeze and stabilize. The budget turns "is it reliable enough?" into a number that gates velocity.

Q: What is silent data corruption and how do you catch it? Data that's schema-valid but wrong — a unit changed upstream, a join silently dropped rows, a processing-time bug double-counted. It's the worst kind because it passes basic validation and quietly poisons everything downstream until an exec notices a number is off. You catch it by building detection in: reconciliation (batch vs streaming outputs must agree), data diffing (recomputed output must match), audit tables (counts that must balance), and distribution/ cardinality monitors on the values themselves — plus the volume and uniqueness checks. The principle: corruption won't announce itself, so reliability means monitoring for the absence of expected invariants, not just the presence of errors.

Q: 30 alerts just fired. What's your first move? Not to triage 30 things — to ask "what shared resource explains all of these at once?" Using lineage and failure-domain thinking (P01), a storm usually collapses to one root cause: 12 jobs failing that all read one metastore is a metastore incident; a whole rack of streaming jobs lagging is a Kafka/broker incident. I find the common upstream, mitigate to restore service (failover, rollback to a good snapshot, DLQ the poison), then root-cause and write a blameless postmortem so it can't recur. The calm comes from the model, not from heroics.

References

  • Google SRE Book & Workbook — SLOs, error budgets, multi-window burn-rate alerting
  • Apache Airflow and Dagster docs; AWS Step Functions
  • Data Quality Fundamentals (Moses, Gavish, Vorwerck) — observability for data
  • Great Expectations / dbt tests / Soda — data-quality frameworks (the lab in production)
  • SeniorMLEngineer Phase 12 WARMUP — drift detection, the input-side of these checks

🛸 Hitchhiker's Guide — Phase 13: Orchestration, Reliability & Observability

Read this if: you want the operability toolkit fast — orchestration, SLOs, data quality, lineage, incident response. Skim, read WARMUP, build the core.


0. The 30-second mental model

This is where "it ran" becomes "it's reliable." You orchestrate DAGs (with caching so re-runs are cheap), set SLOs/error budgets (reliability as a number), monitor data quality (catch corruption before execs do), keep lineage (blast radius), and respond to incidents by finding the shared failure domain. One sentence: make the platform observable, reproducible, and reliable — with numbers, not vibes.

1. Orchestrators

Airflow  → operators + sensors; task-centric; huge ecosystem; manual data deps
Dagster  → software-defined ASSETS; data-aware; content-addressed incrementality
Step Fns → AWS serverless state machines; event-driven; no orchestrator to run
primitive under all: TOPOLOGICAL execution (deps first, no cycles)

2. Incrementality + backfills

content-hash caching: re-run only what CHANGED (code version or upstream output)
safe backfill = idempotent (atomic commit P09) + reproducible (event-time P01)
              + resumable (failed partitions only) + auditable (what/when/from)
replay (stream, P02) + backfill (batch, P06): both need event-time + idempotency
acceptance test: re-run → identical result

3. SLO / error budget math

SLI = measured (% within 60s) ; SLO = target (99.9%)
error budget allowed = total × (1 − SLO)   ← permission to fail
burn rate = actual_error_rate / (1 − SLO)  ← > 1 means you'll blow the budget
alert on BURN RATE (fast + slow windows), not every blip
budget healthy → ship; budget burned → freeze & stabilize

4. Streaming dashboard (deliverable #8)

lag · throughput · WATERMARK DELAY · checkpoint health/duration · backpressure · DLQ rate · restarts
they connect: backpressure → checkpoint duration ↑ + lag ↑ (P04 Round-2 chain)

5. Data quality dimensions

freshness (event-time lag vs SLO) · volume (z-sigma anomaly) · uniqueness (dup keys)
referential integrity (orphans) · schema/semantic (P03) · distribution/cardinality drift

6. Silent corruption (the worst failure)

schema-VALID but WRONG (unit changed, join dropped rows, processing-time double-count)
catch with: reconciliation (batch vs stream agree), data DIFFING, audit tables (balances),
            distribution monitors
principle: it won't announce itself → monitor for ABSENCE of invariants

7. Lineage = the incident compass

upstream(node)  → "this looks wrong, what feeds it?"
downstream/impact(node) → blast radius of a change/incident (P01)
at scale: a lineage browser / data catalog

8. Incident response

FIRST question: "what shared failure domain explains ALL these alarms?" (P01)
runbooks per failure class (Kafka/Flink/Spark/EMR/Athena/Cassandra/S3)
mitigate (failover / rollback to good snapshot P09 / DLQ the poison P02) → THEN root-cause
blameless postmortem → incidents decrease in frequency & severity (a JD success metric)

9. Beginner mistakes that mark you

  1. "Reliable" with no SLO/number.
  2. Backfills that aren't idempotent → duplicate data on retry.
  3. Re-running the whole history when only one partition changed (no incrementality).
  4. Alerting on every error instead of burn rate → alert fatigue.
  5. Only monitoring for errors, not for silent corruption (absent invariants).
  6. Triaging 30 alarms as 30 problems instead of finding the shared domain.
  7. No lineage → can't compute blast radius before a change.

10. How this phase pays off later

  • DAG + caching orchestrates P06 backfills, P09 compaction.
  • Data quality consumes P03 contracts, feeds P14 certification.
  • Lineage/impact = P01 blast radius, productized; incident discipline → P15 leadership.

Read WARMUP, build the core, then P14: security, privacy & governance — locking the platform down (IAM, KMS, PII, Lake Formation, Terraform, DR).

👨🏻 Brother Talk — Phase 13, Off the Record

Operability — the unglamorous force that decides whether your platform survives. This is where you become the person they trust with the pager.


Brother, here's a truth that took me a while to accept: the cleverest pipeline that nobody can operate is worth less than the boring one that runs reliably for three years. Early in my career I optimized for elegant, impressive systems. Then I carried the pager for them, and I learned the lesson the hard way: a system is born in a sprint and lives for years, getting woken up, debugged, backfilled, and recovered thousands of times. Operability isn't a tax on the real work — it is the real work, amortized over the system's whole life. This phase is where you stop being someone who builds things and start being someone who can be trusted to build things that other people depend on at 3 a.m.

Let me give you the mindset shifts.

Make reliability a number, or it's just an argument. Without an SLO, "is it reliable enough?" is an endless, unwinnable debate — someone always wants more nines, someone always wants more features, and it's all vibes. The moment you say "99.9% of events within 60 seconds, measured over 28 days," the debate ends and the engineering begins. And the error budget is the most underrated management tool you'll ever wield: it's permission to fail. If you've got budget left, ship fast, take risks. If you've burned it, freeze and stabilize — and now that's not a judgment call, it's a number everyone agreed to in advance. The error budget turns the eternal "velocity vs reliability" fight into arithmetic. Bring that to a team and you've changed how they operate.

Alert on burn rate, not on blips, or you'll train everyone to ignore alerts. The fastest way to make on-call miserable and useless is to page on every single error. People learn to ignore the pager, and then they miss the real outage. The discipline is multi-window burn-rate alerting: page when the rate of budget consumption says you'll run out before the window ends, with a fast window for acute outages and a slow window for chronic bleed. Fewer, more meaningful pages. The health of your alerting is measured by how often a page is actionable — and getting that right is a gift to every human who carries the pager after you.

Silent corruption is the monster under the bed, and you have to build the night-light yourself. A pipeline that errors is easy — it tells you it broke. The nightmare is a pipeline that runs perfectly and produces wrong data: someone changed a unit upstream, a join quietly dropped 5% of rows, a processing-time bug double-counted during a replay. Schema-valid, no errors, completely wrong — and it poisons every dashboard and model downstream until, weeks later, an executive notices a number doesn't add up, and now you're doing forensic archaeology. The only defense is to build the detection in before you need it: reconcile batch against streaming, diff recomputed output against the original, keep audit tables whose counts must balance, monitor the distributions of your values, not just their presence. Corruption won't announce itself, so reliability means monitoring for the absence of invariants you expect. The principal builds this proactively; the senior adds it after the incident.

Lineage is your incident compass, so invest in it before the fire. When something looks wrong, the two questions are always "what feeds this?" (upstream — find the cause) and "what depends on this?" (downstream — find the blast radius). Without lineage you're guessing, and during an incident guessing is expensive. With it, a 30-alarm storm collapses into "oh, these all read the one metastore that's down — it's one incident." That collapse, from chaos to a single root cause, is the single most valuable thing in incident response, and it comes from lineage plus failure-domain thinking (P01). The calm senior in the war room isn't braver — they have a map.

Backfills separate the confident from the terrified. Here's a tell for how mature a data platform is: ask the team to reprocess last quarter and watch their faces. The terrified teams have non-idempotent writes and non-deterministic logic, so a backfill is a roll of the dice that might double-count or produce different numbers. The confident teams built on idempotent table-format commits (P09) and event-time determinism (P01), so a backfill is a shrug — re-run it, diff the output, it's identical. Be the team that shrugs. The whole "content-hash caching + idempotent + reproducible" discipline in this lab is what buys you that shrug, and the shrug is worth more than it looks: it means you can fix mistakes, which means you can take risks, which means you can move fast.

One more, on incidents and ego: mitigate before you understand. When prod is down, the instinct of smart engineers is to find the root cause first, because understanding feels like control. Resist it. Restore service first — fail over, roll back to a known-good snapshot (P09), DLQ the poison (P02) — then do the forensics calmly. Customers don't care why it broke while it's broken; they care that it's fixed. And do blameless postmortems religiously, because the goal isn't to find who to blame, it's to change the system so the same thing can't happen again. A team whose incidents decrease over time (a literal JD success metric) is a team that learns from each one instead of re-living it.

Career truth: the people who get promoted to principal are very often the people who are calm and effective in incidents and who make the whole team more reliable. It's not the flashiest skill, but it's the one that builds the deepest trust, because reliability is what the business actually feels. Own operability and you own the thing that matters most when it matters most.

Build the core. Make the cache skip unchanged work. Compute an error budget. Map a blast radius. Then come to P14, where we lock the whole thing down — security, privacy, and governance, the controls that keep a platform serving millions of users compliant and safe.

— your brother 👨🏻

Lab 01 — Orchestration & Reliability Core

Phase: 13 — Orchestration, Reliability & Observability | Difficulty: ⭐⭐⭐⭐☆ | Time: 6–7 hours

The control plane of a data platform in miniature: a DAG scheduler with content-hash caching (the incrementality behind Dagster/Airflow), SLO/error-budget math, data-quality checks, and a lineage graph for blast-radius reasoning. These are the systems that turn "it ran" into "it's reliable, observable, and reproducible."

What you build

  • topological_order — Kahn's algorithm; reject cycles
  • DagRunner + content_hash — execute tasks in order, caching by content hash so only changed work re-runs (bump a task version or change a seed → that task + downstream re-execute; everything else is reused)
  • error_budget / burn_rate — SLO/SLI reliability math (allowed failures, budget burn)
  • freshness_ok / volume_anomaly / uniqueness_violations / referential_integrity_orphans — the core data-quality dimensions
  • Lineagedownstream / upstream / impact (blast radius)

Key concepts

ConceptWhat to understand
Topological orderdependency-respecting execution; cycles are illegal
Content-hash cachingre-run only what changed → cheap, safe backfills
Error budgetallowed = total×(1−SLO); burn rate > 1 = page-worthy
Data quality dimsfreshness, volume, uniqueness, referential integrity
Lineage / impactthe blast radius of a change or incident

Run

pip install -r requirements.txt
pytest test_lab.py -v
LAB_MODULE=solution pytest test_lab.py -v

Success criteria

  • All 18 tests pass — test_version_bump_reexecutes_task_and_downstream and test_second_run_uses_cache certify the incremental-orchestration model.
  • You can explain why content-hash caching makes backfills reproducible and cheap.
  • You can compute an error budget and a burn rate and say when to page.

Extensions

  • Add retries with backoff and a task state machine (pending→running→retrying→ succeeded/failed) to the runner.
  • Add a partition-aware backfill: re-run only the date partitions whose inputs changed.
  • Add multi-window burn-rate alerting (fast 5-min + slow 1-hour windows) — Google SRE's alerting recipe.
  • Emit lineage + run metadata as events (the seed of a lineage browser / data catalog).

Phase 14 — Security, Privacy & Governance

Difficulty: ⭐⭐⭐⭐☆ Estimated Time: 2 weeks (28–36 hours) Prerequisites: Phase 03 (PII tags), Phase 09 (deletion vs time travel), Phase 10 (Lake Formation)


Why This Phase Exists

A platform serving millions must be compliant and secure by construction, not bolted on after an audit. The JD's Problem 5 is an enterprise governance platform, and its cloud responsibility lists "IAM least privilege, cross-account access, encryption at rest and in transit, KMS key policies, network isolation, private endpoints, S3 bucket policies, Lake Formation permissions, Glue catalog governance, Athena workgroup controls, … multi-region architecture, disaster recovery, infrastructure as code." Plus schema/contract governance (P03), PII classification, retention, audit, and breaking-change prevention. This phase builds the access-control, PII, audit, and IaC controls — and the multi-region/DR thinking — that make the platform trustworthy at scale.

Concepts

  • IAM: principals, policies, roles, the evaluation logic (explicit Deny > Allow > implicit Deny — the lab), least privilege, cross-account AssumeRole + trust policies, instance profiles, ABAC (tag-based access).
  • Encryption: at rest (SSE-S3 / SSE-KMS, envelope encryption, CMKs, key policies, grants) and in transit (TLS); who can decrypt is a key-policy question, not just an IAM one.
  • Network isolation: VPC, subnets, security groups, VPC endpoints / PrivateLink (keep traffic off the public internet), private subnets for data stores.
  • Lake Formation & Glue governance: fine-grained table/column/row permissions and column masking over the Glue Catalog + S3 (P10), enforced at query time; centralized grants; cross-account data sharing.
  • PII classification & data privacy: tag fields by sensitivity at the schema (P03), drive masking/tokenization, access, and retention; the right to erasure and the lakehouse deletion-vs-time-travel tension (P09).
  • Audit & lineage: CloudTrail-style audit of access and changes; who-accessed-what; dataset certification with an owner, SLA, lineage, and quality policy (P13).
  • Athena workgroup controls / cost guardrails: per-query data-scan limits, result encryption, cost attribution (P07/P10).
  • Terraform / IaC: declare infra as versioned, reviewed code; promotion across environments; drift detection; secrets management.
  • Multi-region & DR: cross-region replication (S3, Cassandra, Kafka MirrorMaker), RPO/RTO, failover, active-active vs active-passive (P15).

Labs

Lab 01 — Governance & Security Kit (flagship, implemented)

FieldValue
GoalBuild an IAM evaluator (Deny-wins), least-privilege analysis, PII column masking by clearance, row-level security, an audit log, and retention/deletion
ConceptsIAM precedence, least privilege, column/row security, audit, retention
How to Testpytest test_lab.py -v — 14 tests
Talking PointsWhy does Deny always win? Why tag PII at the schema? Why isn't DELETE a complete erasure on a lakehouse?
Resume bulletBuilt a governance kit: IAM policy evaluation, least-privilege analysis, PII column masking + row-level security, audit logging, and retention/erasure

→ Lab folder: lab-01-governance-kit/

Extension Project A — Terraform a governed data lake (spec)

Write (and terraform plan) an S3 bucket with SSE-KMS + bucket policy, a Glue database, a Lake Formation permission grant (column-masked), and an Athena workgroup with a scan limit — governance as code.

Extension Project B — Multi-region DR design (spec; → P15)

RPO/RTO for the capstone platform: S3 cross-region replication, Kafka MirrorMaker/MSK replication, Cassandra NetworkTopologyStrategy + LOCAL_QUORUM, failover runbook, and the cost/consistency tradeoffs.

Integrated-Scenario Hooks

  • This phase enforces P03's PII tags and completes P09's compliant deletion (snapshot expiry).
  • Its Lake Formation masking is the enforcement of P10's query layer.
  • Its multi-region/DR thinking feeds P15's architecture and the P16 capstone.

Guides in This Phase

Key Takeaways

  • IAM: explicit Deny always wins; grant least privilege and audit the unused gap.
  • Encryption is who-can-decrypt (KMS key policy), not just who-can-read (IAM).
  • PII is tagged at the schema (P03) and enforced as masking/row-security at query time.
  • Real erasure on a lakehouse = delete + expire snapshots (P09), or time travel resurrects it.
  • Govern as code (Terraform); design multi-region/DR with explicit RPO/RTO.

Deliverables Checklist

  • Lab 01 implemented; all 14 tests pass
  • You can explain IAM Deny precedence and least privilege
  • You can explain KMS envelope encryption and key policies
  • You can design column/row-level access + a compliant erasure
  • Extension A Terraform or Extension B DR design

Warmup Guide — Security, Privacy & Governance

Zero-to-principal primer for Phase 14: IAM and the Deny-wins evaluation, KMS encryption, network isolation, Lake Formation fine-grained access, PII/privacy/erasure, audit, Terraform/IaC, and multi-region/DR.

Table of Contents


Chapter 1: IAM and the Evaluation Logic

IAM controls who can do what to which resource. The pieces: principals (users, roles), policies (statements of effect + action + resource + optional condition), roles (assumable identities — services and cross-account access use these). The evaluation logic you must know cold (the lab implements it):

  1. Explicit Deny anywhere → denied, full stop. A targeted Deny cannot be overridden by any Allow, however broad.
  2. Otherwise, a matching Allowallowed.
  3. Otherwise → implicit Deny (default-deny).

So precedence is Deny > Allow > (default) Deny. The classic mistake is assuming a broad Allow s3:* grants access to a path a separate statement Denys — it doesn't. Least privilege is the governing principle: grant only the actions actually used (the lab's unused_grants finds the gap), scope resources tightly (no Resource: *), and prefer roles over long-lived keys. Cross-account access uses AssumeRole + a trust policy on the target role; ABAC grants by tags (principal attributes × resource tags) instead of static ARNs — scalable for many resources.

Chapter 2: Encryption and KMS

  • At rest: S3/EBS/RDS encrypt with a key. SSE-KMS uses envelope encryption — your data is encrypted with a per-object data key, which is itself encrypted by a CMK (customer master key) in KMS. The subtlety principals must hold: who can read the data is governed by the KMS key policy, not only S3/IAM. A principal with s3:GetObject but no kms:Decrypt on the CMK cannot read the object — a common "permissions look right but it's still denied" puzzle.
  • In transit: TLS everywhere; enforce it (bucket policy aws:SecureTransport, broker TLS).
  • Key management: key policies + grants, rotation, separate keys per data classification/ tenant (so revoking a key revokes access to a slice), and kms:ViaService conditions.

Chapter 3: Network Isolation

Defense in depth at the network layer:

  • VPC with public/private subnets; data stores (Cassandra, RDS, MSK, EMR core) live in private subnets — no public IPs.
  • Security groups (stateful instance firewalls) and NACLs (subnet-level) restrict who can reach what.
  • VPC endpoints / PrivateLink: reach AWS services (S3, KMS, Athena) and SaaS privately, so data-plane traffic never traverses the public internet — both a security and a data-exfiltration control (combine with bucket policies that require the endpoint). The rack-management OOB-network idea (separate planes) rhymes here.

Chapter 4: Lake Formation and Glue Governance

  • Glue Data Catalog (P07/P10) is the shared metastore. Lake Formation layers fine-grained access on top: grant table / column / row-level permissions and column masking to principals, centrally, enforced by the query engines (Athena/Trino/ Spark/EMR) at scan time. The lab's apply_column_policy + row_filter are this model in miniature.
  • This is how you let analysts query a table while never seeing the PII columns, and how a tenant sees only their rows — without copying data into separate restricted tables (which forks lineage and multiplies cost). Plus cross-account data sharing via Lake Formation grants, and tag-based (LF-Tags) policies for scale.

Chapter 5: PII, Privacy, and the Right to Erasure

  • Classification at the source: tag fields by sensitivity in the schema/contract (P03) — none / pii / sensitive. The tag then drives downstream masking (Ch. 4), access, and retention. Classifying ten thousand columns after the fact under a compliance deadline is misery; tagging at definition is ten seconds (P03's BROTHER-TALK plea).
  • Masking / tokenization: show masked or tokenized values to uncleared principals; keep the mapping in a separate, tightly-controlled store if reversible tokenization is needed.
  • Retention: delete data past its policy (the lab's records_to_delete); lifecycle policies (P08) automate tiering/deletion.
  • The right to erasure (GDPR/CCPA): deleting a user's data is subtle on a lakehouse — a DELETE removes it from the current snapshot, but old snapshots still reference the files, so time travel can resurrect it (P09). A compliant erasure = delete from current + expire the snapshots that reference it + remove orphan files. Forgetting the second half is a real compliance bug. (This is why retention + snapshot expiry are coupled.)

Chapter 6: Audit, Lineage, and Dataset Certification

  • Audit: log every access and change (CloudTrail for AWS APIs; the lab's AuditLog for data access) — who did what, when, and the decision. Required for compliance and for forensic incident response (P13).
  • Lineage (P13): the field/dataset graph proves where data came from — essential for privacy (where did this PII flow?) and impact analysis.
  • Dataset certification: every production dataset has an owner, SLA, lineage, PII classification, and quality policy (a JD success metric). Certification is the governance contract that makes a dataset safe to depend on (P00's "data product").

Chapter 7: Terraform and Infrastructure as Code

  • Why IaC: infrastructure as versioned, reviewed code — reproducible environments, peer review of security changes (a bucket made public shows up in a diff), and promotion across dev→staging→prod. The opposite of click-ops drift.
  • Terraform model: declarative resources, state (the source of truth of what exists), plan (preview the diff) → apply; modules for reuse; drift detection; remote state with locking. Secrets via a manager (not in code/state).
  • Governance-as-code: encode bucket policies, KMS keys, Lake Formation grants, IAM roles, and Athena workgroup limits in Terraform so the controls themselves are reviewed and versioned. Policy-as-code tools (OPA/Sentinel, tfsec/checkov) gate insecure changes in CI.

Chapter 8: Multi-Region and Disaster Recovery

  • Why: regional outages happen; some data must live in a region (residency, Ch. 5).
  • Replication: S3 cross-region replication; Kafka via MirrorMaker 2 / MSK replication; Cassandra via NetworkTopologyStrategy + per-DC replicas (read/write at LOCAL_QUORUM to avoid cross-region latency, P11); DynamoDB Global Tables.
  • RPO / RTO: Recovery Point Objective (how much data you can lose) and Recovery Time Objective (how fast you recover) — the two numbers that define your DR design and its cost.
  • Active-passive (failover to a standby — cheaper, slower) vs active-active (both serve — costlier, instant, but you must handle multi-region writes/conflicts, P01 vector clocks/LWW). The choice is a P00 dial (cost vs RTO vs consistency), written as an ADR (P15).
  • DR is only real if tested: game-day failovers, not a doc nobody's exercised.

Lab Walkthrough Guidance

Lab 01 — Governance & Security Kit, suggested order:

  1. iam_evaluate — explicit Deny short-circuits; matching Allow; else implicit Deny. Test that Deny wins regardless of statement order.
  2. unused_grants — least-privilege gap.
  3. apply_column_policy + mask_for — mask by class unless cleared.
  4. row_filter / region_filter — row-level security.
  5. AuditLog + audited_access — record every decision; query denials.
  6. retention_expired / records_to_delete — lifecycle/erasure.

Success Criteria

You are ready for Phase 15 when you can, from memory:

  1. State the IAM evaluation order and why a Deny beats any Allow; define least privilege.
  2. Explain KMS envelope encryption and why kms:Decrypt matters beyond s3:GetObject.
  3. Explain VPC endpoints/PrivateLink and private-subnet data stores.
  4. Explain Lake Formation column/row security and why it beats copying into restricted tables.
  5. Explain PII tagging at the schema and a compliant lakehouse erasure (snapshot expiry).
  6. Explain why governance should be Terraform/code and what policy-as-code gates.
  7. Design a multi-region/DR strategy with RPO/RTO and active-active vs active-passive.

Interview Q&A

Q: A principal has s3:GetObject on the bucket but still gets Access Denied. Why? Two usual causes. First, KMS: the object is SSE-KMS encrypted, and the principal lacks kms:Decrypt on the CMK — S3 lets them list/get the ciphertext request but KMS refuses to unwrap the data key, so the read fails. Who can decrypt is the key policy, not just IAM. Second, an explicit Deny: a separate statement (or an SCP/permission boundary, or a bucket policy) denies the path, and Deny beats the Allow. I'd check the KMS key policy and grants first, then look for an explicit Deny in the identity policy, bucket policy, SCP, or Lake Formation. "Permissions look right but it's denied" is almost always KMS or a Deny you didn't see.

Q: Analysts need to query the orders table but must never see the SSN column. How? Lake Formation column-level security (or the query-engine's masking), not a copy. I tag ssn as sensitive PII at the schema (P03), grant analysts table access with the SSN column masked/ excluded via a Lake Formation permission, and the query engine enforces it at scan time — so the analyst can SELECT * and simply never receives SSN, while a cleared role does. The anti-pattern is materializing a separate "orders_no_pii" table: it forks lineage, doubles storage, and drifts. One table, enforced fine-grained access — that's the lab's apply_column_policy.

Q: We got a GDPR erasure request for a user on our Iceberg lake. Walk me through it. DELETE the user's rows from the current table, but that's not sufficient: older Iceberg snapshots still reference the data files containing those rows, so time travel could resurrect them. The compliant flow is delete-from-current → expire the snapshots that still reference those files → remove orphan files (vacuum), so the underlying data is actually gone, then record the erasure in the audit log. This couples retention policy with snapshot expiry (P09). I'd also confirm via lineage (P13) that the PII didn't flow into derived tables or a serving store (P11) that need the same treatment — erasure follows the data's lineage, not just one table.

Q: Why manage security with Terraform instead of the console? Because security changes must be reviewed, versioned, and reproducible. In Terraform, a change that makes a bucket public, widens an IAM role, or alters a KMS key policy shows up as a diff in a pull request that a human (and a policy-as-code gate like tfsec/OPA) reviews before it ships — versus a console click nobody sees until the audit. You also get reproducible environments (dev mirrors prod), drift detection (someone clicked something → the plan shows it), and the ability to roll back infra like code. Governance-as-code turns "we hope it's configured right" into "the configuration is the reviewed, tested artifact."

References

🛸 Hitchhiker's Guide — Phase 14: Security, Privacy & Governance

Read this if: you want the security/governance controls fast — IAM, KMS, Lake Formation, PII, erasure, IaC, DR. Skim, read WARMUP, build the kit.


0. The 30-second mental model

A platform for millions must be compliant by construction. You control access with IAM (Deny wins), encrypt with KMS (who can decrypt ≠ who can read), isolate with VPC/ PrivateLink, enforce column/row access with Lake Formation, tag PII at the schema and mask it, audit everything, govern as Terraform, and design multi-region/DR with RPO/RTO. One sentence: least privilege + encryption + fine-grained masking + audit, all as reviewed code.

1. IAM precedence (memorize)

explicit DENY  >  matching ALLOW  >  implicit DENY (default)
a broad Allow CANNOT override a targeted Deny
least privilege: grant only what's used; scope resources (no Resource:*); roles > keys
cross-account: AssumeRole + trust policy ; ABAC: grant by tags

2. KMS (the "permissions look right but denied" trap)

SSE-KMS envelope: data key encrypts object; CMK encrypts the data key
who can READ = needs s3:GetObject AND kms:Decrypt (KEY POLICY, not just IAM)
separate keys per classification/tenant → revoking a key revokes a slice
in transit: TLS, enforce aws:SecureTransport

3. Network isolation

data stores in PRIVATE subnets (no public IP); security groups restrict reach
VPC endpoints / PrivateLink → AWS/SaaS traffic never hits the public internet
(combine with bucket policy requiring the endpoint = anti-exfiltration)

4. Lake Formation = fine-grained access

table / column / row permissions + COLUMN MASKING over Glue Catalog + S3
enforced by the query engine at scan time (Athena/Trino/Spark)
analyst queries the table but NEVER sees PII columns — no copy, no forked lineage

5. PII & erasure

tag PII at the SCHEMA (P03) → drives masking, access, retention downstream
masking/tokenization for uncleared principals
GDPR erasure on a lakehouse = DELETE current + EXPIRE snapshots + remove orphans (P09)
  (DELETE alone → time travel resurrects it = compliance bug)

6. Audit & certification

log who/what/when/decision (CloudTrail + data-access log)
dataset certification: owner + SLA + lineage + PII class + quality policy (a JD success metric)
lineage (P13) proves where PII flowed → erasure follows lineage, not one table

7. Terraform / IaC

infra as versioned, REVIEWED code → a public bucket shows up in a PR diff
plan (preview diff) → apply ; state = source of truth ; drift detection
governance-as-code: bucket policies, KMS, LF grants, IAM, Athena limits in TF
policy-as-code (tfsec/checkov/OPA) gates insecure changes in CI

8. Multi-region / DR

replicate: S3 CRR · Kafka MirrorMaker/MSK · Cassandra NetworkTopology + LOCAL_QUORUM · DDB Global
RPO (data you can lose) + RTO (time to recover) = the two numbers that define DR + its cost
active-passive (cheaper, slower) vs active-active (costly, instant, multi-write conflicts P01)
DR is only real if you GAME-DAY test the failover

9. Beginner mistakes that mark you

  1. Assuming a broad Allow overrides a Deny (it never does).
  2. Granting s3:GetObject and forgetting kms:Decrypt → mysterious denial.
  3. Copying data into "no-PII" tables instead of column masking (forks lineage).
  4. DELETE and calling GDPR erasure done (snapshots resurrect it, P09).
  5. Click-ops security (no review trail) instead of Terraform.
  6. Data stores with public IPs / no VPC endpoints.
  7. A DR plan never tested with a real failover.

10. How this phase pays off later

  • Enforces P03 PII tags; completes P09 compliant deletion; enforces P10 query masking.
  • Multi-region/DR → P15 architecture + P16 capstone.
  • Audit/certification → P13 observability + P00 data products.

Read WARMUP, build the kit, then P15: principal-level architecture & strategy — ADRs, migrations, build-vs-buy, design reviews, leadership.

👨🏻 Brother Talk — Phase 14, Off the Record

Security and governance — the phase that's boring until it's a breach or an audit. Let me make you the person who builds it in before either.


Brother, here's the uncomfortable truth about security and governance work: it's invisible when it's done right and career-ending when it's done wrong. Nobody throws a party because PII didn't leak this quarter. But the day a misconfigured bucket exposes customer SSNs, or an auditor asks "prove this analyst never saw payment data" and you can't, it's a very bad day that ends up in the news and sometimes in someone's exit interview. So the move is to build the controls in from the start, quietly, as part of how the platform works — not as a panicked retrofit after the incident. The engineers who do this don't get applause; they get trusted with the sensitive systems, which is its own kind of advancement.

Let me give you the things that actually bite.

The KMS gotcha will eat hours of your life until you internalize it. Someone will have perfect S3 IAM permissions and still get Access Denied, and they'll stare at the IAM policy convinced it's right, because it is. The catch: the object is KMS-encrypted, and who can decrypt is governed by the KMS key policy, separate from S3 IAM. No kms:Decrypt on the CMK, no read — full stop. The first time you debug this it's maddening; after that, "permissions look right but it's denied" should make you immediately check KMS. Knowing this one thing makes you look like a wizard when a teammate is melting down over a permissions mystery.

Deny always wins, and people forget it constantly. The number of times I've seen someone add a broad Allow s3:* and expect it to grant access to a path that a separate statement explicitly denies… it doesn't, and it never will. Explicit Deny beats any Allow, from any policy, in any order. This is good — it means you can put a guardrail Deny on pii/* and know no amount of over-broad granting downstream can punch through it. Use that. A targeted Deny is a seatbelt nobody can unbuckle by accident.

Tag PII at the schema, and I will keep saying this until you do it. I said it in P03 and I'll say it again here because it's that important: the difference between a sane governance program and a nightmare is whether PII is labeled at the source, when the field is defined. If it is, masking, access control, retention, and erasure all have something to hang off of — they're a matter of policy on a tag. If it isn't, you're doing forensic data archaeology across ten thousand columns under a regulatory deadline, and you will miss some, and the ones you miss are the breach. Ten seconds at definition time versus a quarter of misery and risk later. This is the single highest-leverage governance habit, and it costs almost nothing.

The GDPR-erasure-on-a-lakehouse trap is a genuine compliance landmine, and it's subtle. A junior runs DELETE FROM users WHERE id = X, confirms the row is gone, and reports compliance. Wrong — and seriously wrong. On Iceberg/Delta, old snapshots still reference the files containing that row, so time travel resurrects it. Real erasure requires deleting from current and expiring the snapshots that reference the data and removing orphan files — and following the lineage (P13) to wherever that PII flowed (derived tables, the serving store). The same immutability that gives you beautiful time travel makes deletion a multi-step, lineage-aware operation. The person who knows this prevents a compliance incident the rest of the team didn't even know was lurking.

Govern as code, or governance rots. Click-ops security is governance theater: someone makes a change in the console, nobody reviews it, and six months later you have no idea why a bucket is public or a role is over-broad. Terraform turns every security change into a diff in a pull request — a human and a policy-as-code gate (tfsec, OPA) review it before it ships. A bucket going public shows up as a red line someone has to approve. That review trail is worth more than any amount of after-the-fact auditing, because it stops the misconfiguration before it's live. Make the controls themselves code.

On DR and multi-region: the thing nobody tells you is that a disaster recovery plan you've never tested is fiction. Everyone has the doc. Few have actually failed over to the secondary region under realistic conditions and discovered the seventeen things that don't work — the DNS that didn't update, the replica that was hours behind (RPO!), the runbook step that assumed a human who's asleep. Game-day your failover. The teams that survive a regional outage are the ones who practiced, and being the person who runs those drills is how you find the gaps while they're cheap instead of during the actual fire.

Career truth: governance and security maturity is what lets a data platform handle regulated data — finance, health, anything with real PII — and that's where the high-stakes, high-budget work is. The engineer who can say "yes, we can put regulated data on this platform, here's how access, encryption, audit, and erasure are handled, and here's the Terraform that proves it" is the engineer who unlocks the most valuable use cases. It's not flashy, but it's trust, and trust is the currency of the principal level.

Build the kit. Make Deny win. Mask the PII. Audit the access. Then come to P15 — the leadership phase, where you stop building components and start setting the technical direction: ADRs, migrations, build-vs-buy, design reviews, and operating as a principal.

— your brother 👨🏻

Lab 01 — Governance & Security Kit

Phase: 14 — Security, Privacy & Governance | Difficulty: ⭐⭐⭐⭐☆ | Time: 5–6 hours

A platform serving millions must be compliant by construction. This lab builds the controls: an IAM policy evaluator (with the Deny-wins precedence everyone gets wrong), least-privilege analysis, PII column masking by clearance (Lake-Formation-style), row-level security, an audit log, and retention/deletion for GDPR.

What you build

  • iam_evaluate — explicit Deny > matching Allow > implicit Deny (wildcards via fnmatch) — the real IAM precedence
  • unused_grants — least-privilege analysis (granted but never used → remove)
  • apply_column_policy — mask each column by its PII class unless the principal's clearance covers it (column-level access control + masking)
  • row_filter / region_filter — row-level security / data residency
  • AuditLog / audited_access — every access leaves a who/what/decision trail
  • retention_expired / records_to_delete — lifecycle & erasure

Key concepts

ConceptWhat to understand
IAM precedencea targeted Deny beats any broad Allow (the classic gotcha)
Least privilegegrant only what's used; audit the gap
Column maskingPII visible only to cleared principals (P03 tags drive it)
Row-level securitya principal sees only its rows/region
Auditreconstruct who accessed what (compliance)
Retention vs deletepast retention → delete; in a lakehouse, also expire snapshots (P09)

Run

pip install -r requirements.txt
pytest test_lab.py -v
LAB_MODULE=solution pytest test_lab.py -v

Success criteria

  • All 14 tests pass — test_explicit_deny_overrides_allow and test_deny_wins_regardless_of_order certify you understand IAM precedence.
  • You can explain why PII tags belong on the schema (P03) and are enforced at the query engine (P10/Lake Formation).
  • You can explain why "DELETE the row" isn't a complete GDPR erasure on a lakehouse (P09 snapshots must expire too).

Extensions

  • Add cross-account AssumeRole evaluation (a principal in account A assuming a role in account B with a trust policy).
  • Add KMS envelope encryption modeling (data key wrapped by a CMK; a key-policy check).
  • Add tag-based access control (ABAC): grant by resource tags + principal attributes, not static ARNs.
  • Wire it to P09: a compliant erasure that deletes the row and expires the snapshots referencing it.

Phase 15 — Principal Architecture & Strategy

Difficulty: ⭐⭐⭐⭐⭐ (judgment, not code) Estimated Time: 2 weeks (24–32 hours) Prerequisites: all prior phases (you decide between the things you now understand)


Why This Phase Exists

Everything before this gave you the tools; this phase is the job. The JD's "Lead Technical Strategy and Architecture Reviews" responsibility: "create architecture decision records, review major designs, define streaming and lakehouse standards, decide when to use Spark/Flink/ Hive/Athena/Trino/Cassandra/Kinesis/Kafka/EMR, evaluate new technologies, lead migrations away from obsolete systems, mentor senior engineers, raise the bar, create reference architectures, present tradeoffs to leadership." Round 5 is exactly this: lead the Hive/Tez/Flume → Kafka/ Flink/Spark/Iceberg/Athena migration review. This phase teaches how a principal decides, migrates, and leads — and how to make those decisions durable and defensible.

Concepts

  • The decision discipline (P00, formalized): name the dial, quantify both ends, choose, write the ADR (context, decision, alternatives, consequences, what would reverse it).
  • Build vs buy: TCO over a horizon (Build = high upfront/low run; Buy = reverse; the crossover), plus the non-cost factors (control, lock-in, time-to-value, team skill, differentiation — don't build what isn't your differentiator).
  • Tool selection as ADRs: when Spark vs Flink vs Spark-SS; Kafka vs Kinesis vs Pulsar; Iceberg vs Delta vs Hudi; Athena vs Trino; Cassandra vs DynamoDB — each a weighted decision with a deciding factor, not a fashion.
  • Migrations (Round 5): the safe pattern — dual-write → validate parity → cutover → rollback-ready → decommission (the lab's gate checker); strangler-fig incrementalism; data validation/diffing (P13); never a big-bang; always reversible.
  • Multi-region architecture & DR: RPO/RTO, active-active vs active-passive, replication (P14), consistency tradeoffs (P01).
  • Reference architectures & standards: defining the org's blessed patterns (the streaming template, the lakehouse table standard, the contract policy) so teams don't reinvent — the platform-as-product mindset (P00).
  • Design reviews: running them (pressure-test the ADRs), and presenting tradeoffs to leadership in their language (cost, risk, time, reliability).
  • Technical leadership: mentoring senior engineers, raising the bar, influence without authority, simplifying without making fragile, operating as a principal.

Labs

Lab 01 — Architecture Decision Toolkit (flagship, implemented)

FieldValue
GoalBuild a weighted decision matrix with deciding factor, TCO, a safe-migration gate checker, RPO/RTO check, and an ADR renderer
ConceptsDecision matrices, TCO, safe-migration ordering, RPO/RTO, ADRs
How to Testpytest test_lab.py -v — 15 tests
Talking PointsWhy is "best architecture" meaningless without weights? What makes a migration safe? When build vs buy?
Resume bulletBuilt an architecture-decision toolkit (weighted matrices, TCO, safe-migration gating, RPO/RTO, ADR generation) to drive and document platform strategy

→ Lab folder: lab-01-decision-toolkit/

Extension Project A — Write 3 real ADRs (spec)

Pick three decisions from this track (table format, streaming engine, serving store) and write full ADRs with weighted matrices and the deciding factor — your portfolio of judgment.

Extension Project B — Lead the Round-5 migration review (spec)

Produce the full Hive/Tez/Flume → Kafka/Flink/Iceberg/Athena migration plan: phases, dual-write, validation strategy (data diffing, P13), cutover, rollback, cost, governance, team adoption, deprecation — the design-review deck.

Integrated-Scenario Hooks

  • This phase decides between everything in P02–P14, and leads the migration that composes them — the bridge to the P16 capstone.
  • Its ADR discipline is the through-line from P00's "name the dial."

Guides in This Phase

Key Takeaways

  • A principal's output is decisions, defended with numbers and made durable as ADRs.
  • "Best architecture" is meaningless without the workload's weights; name the deciding factor.
  • Build only your differentiator; buy/adopt the rest (TCO + lock-in + time-to-value).
  • Migrations are dual-write → validate → cutover → decommission, always reversible, never big-bang.
  • Leadership = standards, reference architectures, design reviews, mentoring, influence.

Deliverables Checklist

  • Lab 01 implemented; all 15 tests pass
  • You can produce a scored ADR for a real decision
  • You can lay out a safe migration with correct gates and rollback
  • You can present a tradeoff to leadership in cost/risk/time terms
  • Extension A three ADRs or Extension B migration review deck

Warmup Guide — Principal Architecture & Strategy

Zero-to-principal primer for Phase 15: how a principal decides (ADRs, decision matrices), build-vs-buy, tool selection as ADRs, safe migrations, multi-region/DR strategy, reference architectures, design reviews, and technical leadership.

Table of Contents


Chapter 1: The Decision Discipline and the ADR

A principal's primary output is decisions — and a decision isn't done when you've chosen, it's done when you've made the choice legible and durable. The discipline (P00, here formalized):

  1. Frame it as a dial (P00): name the conflicting forces (latency↔cost, exactly-once↔ simplicity, …).
  2. Quantify both ends with the workload's numbers (the five forces — P00).
  3. Score the options on weighted factors (the lab's decision matrix) and identify the deciding factor — the one that actually tipped it.
  4. Write the ADR: Context (the forces + numbers), Decision (one sentence), Alternatives (each, and the dial it turned the wrong way for us), Consequences (what gets better/worse, what to monitor, what would make us revisit).

Why the ADR matters: six months later "why did we do it this way?" is answered by a document, not by memory and ego; a new team member onboards on the reasoning, not just the result; and a decision can be reversed on purpose when its assumptions change (the "what would make us revisit" line). An architecture review (Ch. 7) is a room pressure-testing your ADRs.

Chapter 2: Build vs Buy

The recurring strategic question. The framework:

  • TCO over a horizon (the lab's tco): Build has high upfront (engineering time) and low run cost; Buy/adopt-OSS-managed has low upfront and higher recurring cost. There's a crossover month — below it Buy wins on cost, above it Build might. But cost is only half.
  • Non-cost factors: control (can you fix/extend it?), lock-in (cost to leave), time-to-value (Buy ships this quarter; Build ships next year), team skill (can you operate what you build?), and the decisive one — is it your differentiator?
  • The principal heuristic: build what differentiates you; buy/adopt everything else. Your org's edge is its product, not its message bus — so adopt Kafka, don't write one; adopt Iceberg, don't invent a table format. Build the thin platform layer on top that encodes your standards (the SDK, P12) — that's the differentiator worth owning.

Chapter 3: Tool Selection as ADRs

The JD asks you to "decide when to use Spark, Flink, Hive, Athena, Trino, Cassandra, Kinesis, Kafka, or EMR." Each is a weighted decision (the lab's recommend), and you've built the inputs across the track:

  • Kafka vs Kinesis vs Pulsar (P02): ops vs control vs elasticity/lock-in.
  • Flink vs Spark-SS vs Kafka-Streams vs Akka (P04/P05): latency vs ops vs ecosystem.
  • Iceberg vs Delta vs Hudi (P09): openness vs Spark-features vs upsert pattern.
  • Athena vs Trino vs Spark-SQL (P10): serverless/ad-hoc vs steady-concurrency vs one-stack.
  • Cassandra vs DynamoDB (P11): open/ops vs managed.
  • EMR-on-EC2 vs Serverless vs EKS (P07): steady vs spiky vs K8s-native.

The skill is not memorizing "use X" — it's running the matrix on this workload's weights and naming the deciding factor. The same two tools flip winners under different weights (the lab's test_weights_change_winner), which is why "best tool" is meaningless without the context.

Chapter 4: Migrations Done Safely

The JD's Round 5 and Problem 3: migrate Hive/Tez/Flume → Kafka/Flink/Spark/Iceberg/Athena. The only safe migration pattern (the lab's migration_gate_check):

  1. Dual-write: run old and new in parallel; the new path writes alongside the old without anyone depending on it yet.
  2. Validate parity: compare old vs new outputs continuously (data diffing, audit counts — P13) until they match for a soak period. Never cut over before parity holds.
  3. Cutover: move consumers to the new path — incrementally (a few first), with the old path still warm.
  4. Rollback-ready: at every step, you can revert to the old path instantly. A migration without a rollback is a gamble.
  5. Decommission: only after a soak period on the new path with no issues — and it's the last step, never before cutover is proven.

This is the strangler-fig pattern: grow the new system around the old, shift load gradually, retire the old once it carries nothing. It's the same discipline as schema evolution (P03), backfills (P06), and table-format adoption (P09): incremental, validated, reversible, never big-bang. The big-bang "flip the switch and pray" migration is how careers and quarters get lost.

Chapter 5: Multi-Region and DR Strategy

(Builds on P14 Ch. 8.) The strategic choices:

  • Why: regional outages, data residency, latency for global users.
  • RPO / RTO: the two numbers that define the design and its cost — how much data you can lose, how fast you recover. They're a P00 dial (more nines = more money).
  • Active-passive (standby region, failover): cheaper, simpler, slower RTO, possible data loss to RPO. Active-active (both serve): instant RTO, but you must handle multi-region writes (conflict resolution — P01 vector clocks/LWW, Cassandra LOCAL_QUORUM per DC) and pay for double capacity.
  • The decision is an ADR weighing cost vs RTO/RPO vs consistency vs operational complexity — and DR is only real if game-day tested (P14).

Chapter 6: Reference Architectures and Standards

A principal scales themselves by encoding decisions into standards so teams don't re-decide (or re-mistake) every time:

  • Reference architectures: the blessed shape for common needs (the standard ingestion pipeline, the lakehouse table layout, the streaming-job template) — documented, with the ADRs behind them.
  • Standards: streaming standards (event-time + idempotency + DLQ — P01/P02/P04), lakehouse standards (Iceberg + compaction + partitioning — P08/P09), contract standards (P03), table ownership/SLA/lineage (P13). These are the platform-as-product (P00): self-service with guardrails.
  • The payoff: "engineers can create new pipelines without reinventing architecture" (a JD success metric). Standards are how one principal's judgment reaches a thousand engineers.

Chapter 7: Design Reviews and Presenting to Leadership

  • Running a design review: it's pressure-testing ADRs. Ask for the dials and the numbers; probe the failure modes, the blast radius, the rollback, the cost; check the deciding factor holds under a weight change (sensitivity). The goal is a better, defensible decision, not a winner of an argument.
  • Presenting to leadership: translate to their language — cost, risk, time, and reliability, not Flink internals. "This migration saves $X/month and cuts incident rate Y%, takes Z quarters, with this rollback if it goes wrong" — a sentence an exec can decide on. The JD: "present tradeoffs to senior engineering leadership" and "translate business requirements into platform capabilities."

Chapter 8: Technical Leadership

The seniority-bar items (the JD's list):

  • Influence without authority: you rarely command; you persuade with ADRs, prototypes (build the risky bit to de-risk it), and earned trust.
  • Mentor senior engineers / raise the bar: review designs, teach the reasoning (not just the answer), set the example of writing things down.
  • Simplify without making fragile: the hardest skill — remove complexity while keeping the guarantees. (A simpler system you can operate beats a clever one you can't — P00 operability.)
  • Operate under ambiguity: turn vague fear ("will it scale?") into specific, numbered questions (P00) — calm comes from arithmetic.
  • Balance correctness, latency, cost, operability: the whole track, every day, as tradeoffs (P00).

Lab Walkthrough Guidance

Lab 01 — Architecture Decision Toolkit, suggested order:

  1. weighted_score / rank_options / recommend — and confirm test_weights_change_winner: the same options flip under different weights (the core lesson).
  2. tco — Build-vs-Buy economics.
  3. migration_gate_check — required gates + ordering (validate before cutover; decommission last).
  4. dr_meets_targets — RPO/RTO.
  5. render_adr — the durable artifact.

Success Criteria

You are ready for Phase 16 when you can, from memory:

  1. State the decision discipline and the ADR structure (incl. "what would reverse it").
  2. Apply build-vs-buy (TCO + non-cost factors + "build only your differentiator").
  3. Turn a tool choice into a weighted ADR with a deciding factor.
  4. Lay out a safe migration (dual-write → validate → cutover → decommission, reversible).
  5. Choose active-active vs active-passive with RPO/RTO.
  6. Explain how standards/reference architectures scale a principal's judgment.
  7. Present a tradeoff to leadership in cost/risk/time/reliability terms.

Interview Q&A

Q (Round 5): Lead the migration from Hive/Tez/Flume to Kafka/Flink/Spark/Iceberg/Athena. I'd run it as a strangler-fig, never a big-bang. Phase 0: stand up the new stack alongside the old, reading the same S3 data through the catalog where possible (P07/P09). Phase 1: dual-write — new producers (Kafka/Flink) write to Iceberg tables while Flume/Hive keep running and owning truth. Phase 2: validate parity continuously — data diffing and audit counts (P13) comparing new Iceberg outputs to the legacy warehouse until they match for a soak period; no cutover before parity. Phase 3: cut over consumers (Athena/Trino) incrementally to the new tables, old path still warm for instant rollback. Phase 4: decommission Flume/Hive/Tez only after a clean soak. Cross-cutting: governance (P14) and contracts (P03) ride along, cost is attributed (P07), and each phase has entry/exit criteria. The whole thing is reversible at every step — that's what makes a multi-quarter migration survivable.

Q: How do you decide build vs buy for a message bus? Buy/adopt — almost always. A message bus is not your differentiator; Kafka/Kinesis/Pulsar are mature, and the TCO of building and operating one dwarfs adopting one (the upfront engineering plus the perpetual operational burden). I'd reserve "build" for the thin layer that encodes our standards on top — the ingestion SDK with our contracts, DLQ, and observability baked in (P12) — because that is differentiating and cheap to own. The general heuristic: build what makes you uniquely better, adopt everything commoditized, and be honest that "we could build it" is rarely the same as "we should."

Q: Two senior engineers disagree on Flink vs Spark Structured Streaming. How do you resolve it? Not by authority or by who argues hardest — by the workload's numbers. I'd have us write the decision as a matrix: latency SLO, state size, exactly-once needs, existing stack/skills, ops budget, ecosystem. Score both honestly, weight by this use case, and the deciding factor falls out — and crucially, the same two engines flip winners under different weights, so the disagreement usually evaporates once we agree on the weights (which is the real question). Then we write the ADR with the deciding factor and what would make us revisit. The output isn't "I'm right," it's a documented decision the team owns and can revisit if the assumptions change.

Q: What makes a principal different from a staff engineer here? Scope of judgment and how it's encoded. A staff engineer makes excellent decisions for their systems; a principal sets the standards and reference architectures that make hundreds of engineers' decisions good by default, leads the cross-team migrations, and translates between business and platform. The artifacts are different too — a principal's work is ADRs, design reviews, mentoring, and the platform-as-product, not just shipped components. And the hardest principal skill is simplification: removing complexity while preserving the guarantees, because operability over a system's multi-year life dominates everything (P00).

References

  • Michael Nygard, Documenting Architecture Decisions (the ADR origin) ; adr.github.io
  • Martin Fowler, StranglerFigApplication and Patterns of Distributed Systems
  • Staff Engineer (Will Larson) and The Staff Engineer's Path (Tanya Reilly) — the role
  • Google SRE — DR, RPO/RTO ; AWS Well-Architected Framework — the decision pillars
  • Every prior phase's "decision" sections — this phase decides between them

🛸 Hitchhiker's Guide — Phase 15: Principal Architecture & Strategy

Read this if: you want the principal's decision/migration/leadership toolkit fast, and to pass Round 5 (lead the migration review). Skim, read WARMUP, build the toolkit.


0. The 30-second mental model

A principal's output is decisions, defended with numbers and made durable as ADRs. You choose tools by running a weighted matrix (not fashion), build only your differentiator, migrate via dual-write → validate → cutover → decommission (always reversible), and scale yourself through standards + reference architectures. One sentence: name the dial, quantify both ends, choose, write the ADR — and lead the migration without a big-bang.

1. The decision discipline

1. frame as a dial (P00 forces in conflict)
2. quantify both ends with the workload's numbers
3. score options on weighted factors → DECIDING FACTOR (the one that tipped it)
4. write the ADR: Context · Decision · Alternatives · Consequences · "what would reverse it"
ADR = decisions become legible, teachable, reversible-on-purpose

2. Build vs buy

TCO: Build = high upfront / low run ; Buy = low upfront / high run → find the CROSSOVER
non-cost: control, lock-in, time-to-value, can-you-operate-it, IS IT YOUR DIFFERENTIATOR?
heuristic: BUILD your differentiator; ADOPT everything commoditized (Kafka/Iceberg/…)
           build only the thin platform layer (SDK, P12) that encodes YOUR standards

3. Tool selection = run the matrix (you built the inputs)

Kafka/Kinesis/Pulsar (P02) · Flink/Spark-SS/Kafka-Streams/Akka (P04/05)
Iceberg/Delta/Hudi (P09) · Athena/Trino/Spark-SQL (P10) · Cassandra/DynamoDB (P11) · EMR×3 (P07)
"best tool" is meaningless without weights — the SAME two flip winners under different weights

4. Safe migration (Round 5)

dual_write → validate parity → cutover (incremental, old path warm) → decommission (LAST)
NEVER cut over before parity holds ; ALWAYS keep rollback ; never big-bang
= strangler-fig: grow new around old, shift load, retire old
same discipline as schema evolution (P03), backfills (P06), table adoption (P09)

5. Multi-region / DR

RPO (data you can lose) + RTO (time to recover) = the dial (more nines = more $)
active-passive (cheap, slower RTO) vs active-active (instant, costly, multi-write conflicts P01)
DR is only real if GAME-DAY tested (P14)

6. Scale yourself: standards + reference architectures

encode decisions as STANDARDS so teams don't re-decide (or re-mistake)
streaming standard (event-time+idempotency+DLQ), lakehouse standard (Iceberg+compaction), contracts
= platform-as-product: self-service WITH guardrails (P00)
→ "engineers create pipelines without reinventing architecture" (a JD success metric)

7. Design reviews & leadership

review = pressure-test the ADRs (dials? numbers? failure modes? rollback? sensitivity?)
to leadership: speak COST / RISK / TIME / RELIABILITY, not Flink internals
leadership: influence w/o authority, mentor, simplify-without-fragility, calm-from-numbers

8. Beginner mistakes that mark you

  1. "Best architecture" with no workload weights.
  2. Building a message bus / table format (not your differentiator).
  3. Big-bang migration with no parity validation or rollback.
  4. Decisions in chat, not ADRs → re-litigated every quarter.
  5. Presenting Flink internals to execs instead of cost/risk/time.
  6. A DR plan never game-day tested.
  7. Being the bottleneck (deciding everything) instead of setting standards.

9. War stories

  • "The migration broke prod." → big-bang cutover, no parity soak, no rollback.
  • "We re-argue Flink vs Spark every quarter." → no ADR captured the decision + revisit criteria.
  • "We built our own orchestrator." → not the differentiator; years of toil; adopt Dagster.
  • "Failover didn't work in the real outage." → never game-day tested.

10. How this phase pays off later

  • Decides between everything in P02–P14; leads the migration that composes them.
  • ADR discipline = P00's "name the dial," formalized; standards = the platform of P13.
  • Directly → the P16 capstone (compose it all) and your career trajectory.

Read WARMUP, build the toolkit, write some ADRs, then P16: compose the entire track into the five capstone systems — the JD's hard problems, end to end.

👨🏻 Brother Talk — Phase 15, Off the Record

The leadership phase. This is where you stop being measured by what you build and start being measured by what you decide and who you lift. Let me tell you what actually changes.


Brother, this is the phase where the job quietly transforms, and a lot of brilliant engineers miss the transition. Up to now, your value was your output — the pipeline you built, the bug you fixed, the system you optimized. At the principal level, your value becomes your judgment and your leverage: the decisions you make that hundreds of others build on, and the engineers you make better. That's a different game, and the skills that got you here (heads-down building) are not the skills that get you further. Let me give you the mindset shifts that took me too long.

Write it down. Always. This is the highest-leverage habit of the whole role. I cannot overstate this. When you make a real decision, write the ADR — context, decision, alternatives, consequences, and the magic line: what would make us revisit this. Do it even when nobody asks. Here's why it's transformative: a decision in someone's head is re-argued every quarter, drifts as memory fades, and dies when that person leaves. A decision in an ADR is legible — new people onboard on the reasoning, disagreements get resolved by pointing at the documented tradeoff instead of arguing personalities, and you can change your mind on purpose when the assumptions change. The engineers whose decisions are written down become the engineers people trust with bigger decisions, because their thinking is inspectable. The ones who decide in Slack threads stay stuck re-litigating. Be the first kind.

"Best" is a junior word. Banish it. There is no best database, no best streaming engine, no best table format — there's only best for this workload, with these weights, given this team. The moment you catch yourself or someone else saying "X is the best," the principal move is to ask "best for what?" and pull out the weights. The lab proves the point mechanically: the exact same two options flip winners when you change the weights. So the real work of a decision isn't picking the option — it's agreeing on the weights, and once the room agrees on those, the choice usually falls out and the argument evaporates. Most "technical disagreements" are actually unspoken disagreements about weights. Surface the weights and you resolve the fight.

Don't build what isn't your differentiator. This is the discipline that saves the most wasted years. Engineers love to build — it's fun, it's impressive, "we could write our own X" feels powerful. And it's usually a trap. Your org's edge is its product, not its message bus or its orchestrator or its table format. Every month spent building and operating commodity infrastructure is a month not spent on what makes you uniquely valuable. Adopt Kafka, adopt Iceberg, adopt Dagster — and build the thin layer on top that encodes your standards (the SDK, the contracts, the templates). That layer is your differentiator and it's cheap to own. "We could build it" is almost never the same as "we should." Saying no to building is a senior skill that feels like weakness and is actually strength.

Migrations: the cardinal sin is the big bang. I've watched migrations destroy quarters and bruise careers, and it's almost always the same mistake: flip the switch, pray, and discover in production all the things that didn't transfer. The safe way is boring and it works: dual-write so old and new run together, validate parity obsessively until the numbers match for a soak period, cut over a little at a time with the old path still warm, keep rollback at every step, and decommission the old thing last, only after the new one has proven itself. It takes longer. It's less exciting. And it's the difference between "we migrated to the lakehouse last year, it was smooth" and "the migration is the reason three people left." Patience and reversibility are the migration superpowers. Boring migrations are successful migrations.

Learn to speak to executives, because your best work dies if you can't sell it. Here's a painful truth: a brilliant architecture that leadership doesn't fund or approve is worth nothing. And leadership does not speak Flink. They speak cost, risk, time, and reliability. "We should adopt Iceberg because of snapshot isolation and hidden partitioning" is a sentence that gets you a blank stare. "This cuts our query bill 40%, reduces data incidents, and takes one quarter with a safe rollback" is a sentence that gets you a yes. Translating your deep technical judgment into the language of business outcomes isn't selling out — it's the skill that lets your judgment actually matter. Practice it.

Scale yourself, or become the bottleneck. The trap of being good at decisions is that everyone brings you every decision, and you become a human bottleneck who's in forty meetings and resentful. The escape is standards and reference architectures: encode your judgment once, as the blessed way to build an ingestion pipeline or a lakehouse table, with the ADRs behind it, so teams make good decisions without you in the room. That's how one principal's judgment reaches a thousand engineers — and it's literally a JD success metric ("engineers create pipelines without reinventing architecture"). The goal isn't to make every decision; it's to make most decisions unnecessary by setting good defaults.

The deepest principal skill, and the rarest: simplify without making fragile. Anyone can add complexity; anyone can cut corners. The hard, valuable thing is removing complexity while keeping the guarantees — making a system simpler to operate without making it break. Because a system lives for years and gets operated thousands of times, and the boring simple thing that any on-call can reason about beats the elegant complex thing that needs a PhD at 3 a.m. (P00's operability, one last time). When you can look at a sprawling design and find the simpler one that's just as correct, you're doing the thing that defines the level.

Career truth: the transition from "great engineer" to "principal" is the transition from answers to judgment, from building to deciding and lifting. It's uncomfortable because the feedback loop is slower and the work is less tangible. But it's where the leverage — and the title — lives. Write the ADRs. Surface the weights. Build only your differentiator. Migrate safely. Speak to the business. Set the standards. Lift the people. That's the job.

Build the toolkit. Write three real ADRs from this track. Then come to P16, where you compose everything — all sixteen phases — into the five capstone systems the JD asks for. The graduation.

— your brother 👨🏻

Lab 01 — Architecture Decision Toolkit

Phase: 15 — Principal Architecture & Strategy | Difficulty: ⭐⭐⭐☆☆ | Time: 4–5 hours

A principal's output is decisions, defended with numbers and made durable in writing. This lab builds the instruments: a weighted decision matrix with an explainable deciding factor (Build-vs-Buy, tool selection), TCO economics, a safe-migration gate checker (the Round-5 Hive→lakehouse discipline), an RPO/RTO check, and an ADR renderer.

What you build

  • weighted_score / rank_options / recommend — score N architectures on weighted factors and return the winner and the deciding factor (the ADR sentence)
  • tco — total cost of ownership over a horizon (the Build-vs-Buy crossover)
  • migration_gate_check — verify a migration has the safety gates in order (dual_write → validate → cutover → decommission, decommission last) — never cut over before validating parity
  • dr_meets_targets — RPO/RTO adequacy for multi-region DR
  • render_adr — produce an Architecture Decision Record

Key concepts

ConceptWhat to understand
Decision matrix"best" is meaningless without weights; the deciding factor is the ADR
TCOBuild = high upfront/low monthly; Buy = reverse; find the crossover
Safe migrationdual-write → validate parity → cutover → decommission (rollback always)
RPO/RTOthe two numbers that define DR and its cost
ADRdecisions become durable, reviewable, reversible-on-purpose

Run

pip install -r requirements.txt
pytest test_lab.py -v
LAB_MODULE=solution pytest test_lab.py -v

Success criteria

  • All 15 tests pass — test_weights_change_winner and test_cutover_before_validate_is_unsafe are the ones that certify the judgment model.
  • You can take any "build vs buy" or "tool X vs Y" decision and produce a scored ADR.
  • You can lay out a Hive/Tez/Flume → Kafka/Flink/Iceberg migration with correct, safe gates.

Extensions

  • Add a sensitivity analysis: how much would a weight have to change to flip the winner? (the robustness of the decision).
  • Add risk-adjusted TCO (probability-weighted cost scenarios) and a payback-period calculator.
  • Generate a full migration runbook from a gate plan (per-phase entry/exit criteria + rollback steps) — the artifact you present in a design review.

Phase 16 — Capstone Systems

Difficulty: ⭐⭐⭐⭐⭐ Estimated Time: 4–8 weeks (pick one capstone to build deeply; spec the rest) Prerequisites: Phases 00–15 (the capstones compose their labs)


Why This Phase Exists

The previous sixteen phases each built one component in isolation and proved it with tests. A principal's real test is the seams — composing components into a system that satisfies all five forces (P00) at once, under production constraints. This phase is the JD's five "Example Hard Problems" (and its "build a full production-grade data platform" deliverable), each laid out as an integration blueprint that wires together the labs you already built. Build one end to end (Docker-composed real engines for the extension); spec the others as architecture + ADRs (P15).

How to use this phase

For each capstone: (1) draw the architecture and the lifecycle (P00); (2) state the five-force numbers and the dials; (3) map each component to the phase/lab that implements it; (4) define the acceptance criteria (the correctness properties + SLOs); (5) write the key ADRs (P15). The "build" is gluing the verified miniatures into a flowing system; the "principal" is defending every seam.


Capstone 1 — Global Real-Time Event Ingestion Platform (JD Problem 1)

Billions of events/day from product/mobile/backend/partners, multi-region, Protobuf, schema-validated at ingress, DLQ, low-latency Flink, durable raw S3 + partitioned Iceberg, Athena/Trino query, reprocessing, regional failover, exactly-once-effect, end-to-end observability, cost attribution by producer team.

BUILT & RUNNABLE: capstone-01-ingestion-platform/ implements this end to end in real Scala — ingest → Cats-Validated contract + DLQ → event-time windowed aggregation → exactly-once lakehouse publish → query. sbt run drives the demo; sbt test proves the exactly-once effect (shuffled+duplicated input → identical table), DLQ safety, and query correctness (5 specs). The table below maps each concern to the phase/lab that deepens it.

ComponentPhase / Lab
Sizing (partitions/shards, latency budget, cost)P00 tradeoff-modeler, P02
Partitioning, consumer groups, retention, replay, DLQP02 mini-broker
Protobuf contracts + compatibility gate at ingressP03 schema-registry
Flink event-time processing + exactly-once Iceberg sinkP04 flink-engine
Raw S3 (tee on arrival) + Parquet layoutP08 parquet-reader
Iceberg tables, compaction, time travelP09 iceberg-table
Athena/Trino query + costP10 query-optimizer
Multi-region replication + DR (RPO/RTO)P14, P15
Lag/watermark/checkpoint/DLQ dashboards; SLOsP13 orchestration-core
Cost attribution by teamP07, P13

Acceptance: exactly-once-effect from ingress to Iceberg (replay/dup test, P01/P04); raw events recoverable for N days; 99.9% queryable within the freshness SLO; a poison message is quarantined without blocking; a region failover meets RPO/RTO; per-team cost is reported. Key dials: latency↔cost (Flink vs hourly batch), exactly-once↔simplicity, freshness↔replay.


Capstone 2 — Stateful Fraud / Risk Stream Processing (JD Problem 2)

Near-real-time suspicious-behavior detection: event-time, out-of-order, sliding + session windows, pattern detection, Cassandra enrichment, broadcast rules, RocksDB state, S3 checkpoints, savepoint-based deploys, rule versioning, low-latency alerts, replay for validation.

BUILT & RUNNABLE: capstone-02-fraud-detection/ — a Flink-style keyed, event-time stateful detector in Scala: amount/velocity/impossible-travel rules, hot-swappable broadcast ruleset, idempotent (deduped) alerts, and savepoint checkpoint/restore. sbt test proves savepoint-redeploy and replay correctness (8 specs).

ComponentPhase / Lab
Event-time windows, watermarks, lateness, state, checkpoint/savepointP04 flink-engine
Sessionization + interval joins + as-of enrichmentP05 joins-cdc
Cassandra enrichment lookups (async, P11 modeling)P11 cassandra-modeler
Broadcast rules state + rule versioning (savepoint upgrade)P04, P03 (rule schema)
Exactly-once alert sink (idempotent / 2PC)P04
Replay for rule/model validationP02, P04
RocksDB/JVM/GC tuning for stateP12

Acceptance: identical alerts under shuffled + duplicated delivery (P01/P04); a rule change deploys via savepoint with no state loss and no missed/duplicated alerts; late events handled per policy (corrected or side-output); replay reproduces historical alerts exactly. Key dials: detection latency↔completeness (watermark delay), state size↔cost, freshness↔ replay correctness.


Capstone 3 — Lakehouse Migration: Hive/Tez/Flume → Kafka/Flink/Iceberg/Athena (JD Problem 3, Round 5)

Migrate a legacy warehouse: preserve history, convert ORC/Parquet, keep schemas backward- compatible, introduce Iceberg, support Athena/Trino/Spark, eliminate small files, compact, fix broken partitions, replace batch-only with incremental + streaming writes, maintain lineage, reduce query cost, decommission Flume/Hive safely.

ComponentPhase / Lab
Legacy understanding (Hive metastore, Tez, EMR)P07 emr-cost-model
Schema compatibility through the transitionP03 schema-registry
Iceberg adoption, compaction, time travelP09 iceberg-table
Spark backfills (idempotent, reproducible)P06 spark-simulator
Flink streaming writes (exactly-once)P04 flink-engine
Query cutover + cost reductionP10 query-optimizer
Migration gates + ADRs (dual-write→validate→cutover→decommission)P15 decision-toolkit
Parity validation / data diffing + lineageP13 orchestration-core

Acceptance: parity (data diff = 0) between legacy and lakehouse for a soak period before cutover; rollback available at every step; small-file count and query $ both drop materially; Flume/Hive decommissioned only after the soak; lineage preserved end to end. Key dials: migration speed↔risk, cloud-native↔portable, cost↔freshness.


Capstone 4 — Cassandra Serving Layer for Real-Time Features (JD Problem 4)

Low-latency derived-feature serving: Cassandra schema + partition-key design, hot-partition avoidance, TTL, compaction strategy, consistency selection, write idempotency, streaming upserts from Flink, Spark backfills, read-path SLOs, tombstone monitoring, repair, multi-region replication.

BUILT & RUNNABLE: capstone-03-feature-serving/ — a Cassandra-style feature store in Scala: LWW upserts, TTL, tombstones + the read-fail threshold, tunable consistency (R+W>RF), median hot-partition detection, and a token-bucket rate-limited backfill that protects the read SLO. sbt test (8 specs).

ComponentPhase / Lab
Partition/clustering design, large+hot partition checksP11 cassandra-modeler
Consistency selection (R+W>RF, LOCAL_QUORUM)P11, P01
TTL + compaction strategy (TWCS/LCS/STCS) + tombstone alertsP11
Idempotent streaming upserts from FlinkP04 flink-engine
Rate-limited Spark backfillP06 spark-simulator
Read-path p99 SLOs + tombstone monitoringP13 orchestration-core
Multi-region (NetworkTopology + LOCAL_QUORUM) + DRP14, P15

Acceptance: read-path p99 within SLO under live load + concurrent backfill (backfill is rate-limited); upserts are idempotent (replay-safe); no partition exceeds size/hot thresholds; tombstones-per-read stays under the fail threshold; multi-region reads use LOCAL_QUORUM. Key dials: serving speed↔analytical flexibility, consistency↔latency, backfill throughput↔ read SLO.


Capstone 5 — Enterprise Data Governance Platform (JD Problem 5)

Governance for thousands of datasets/streams: dataset + schema registry, field-level ownership, PII classification, access-control integration, lineage graph, data-quality checks, contract testing, audit logging, retention, dataset certification, breaking-change prevention, integration with Lake Formation/Glue/Athena/IAM.

ComponentPhase / Lab
Schema/dataset registry + breaking-change prevention (CI gate)P03 schema-registry
PII classification + column/row masking + IAM + auditP14 governance-kit
Lineage graph + impact analysisP13 orchestration-core
Data-quality framework (freshness/volume/uniqueness/integrity)P13
Dataset certification (owner/SLA/lineage/quality)P13, P00 (data product)
Lake Formation / Glue / Athena integrationP10, P14
Retention + compliant erasure (delete + snapshot expiry)P09, P14

Acceptance: a breaking schema change is rejected in CI (never reaches prod); PII is masked for uncleared principals and access is audited; every dataset has owner/SLA/lineage/quality (certified); a GDPR erasure removes data including from time-travel snapshots; lineage answers "where did this PII flow?". Key dials: self-service↔governance, flexible schema↔safe contract.


Capstone 6 — The Full Production-Grade Platform (JD final deliverable)

Compose 1–5 into one platform: ingestion → contracts → streaming + batch → lakehouse → query + serving → governance → reliability → cost. This is the JD's mission statement realized — a unified (Kappa-style, P01) platform that ends the streaming/batch split (P09), serves both analytics (P10) and low-latency features (P11), is governed (P14) and observable (P13), and is defensible decision-by-decision in ADRs (P15). The deliverable is the architecture document + the working slice you built + the ADRs + the runbooks (P13) + the cost model (P00/P07).

BUILT (composition slice): capstone-04-unified-platform/ is a multi-module sbt build that reuses the real Phase 02 PartitionedLog and Phase 09 IcebergTable lab sources (no copy/paste) and wires them — ingest → DLQ → consume → aggregate → atomic Iceberg publish — proving the components compose at the seams. sbt test (7 specs); sbt "platform/runMain unified.UnifiedMain" runs it.

Runnable capstones in this folder

CapstoneMaps toVerify
capstone-01-ingestion-platformProblem 1 (ingest→window→lakehouse→query)sbt test (5) · sbt run
capstone-02-fraud-detectionProblem 2 (stateful fraud)sbt test (8)
capstone-03-feature-servingProblem 4 (Cassandra serving)sbt test (8)
capstone-04-unified-platformProblem 6 (compose the real labs)sbt test (7) · runMain

(Problems 3 and 5 — the Hive→lakehouse migration and the governance platform — remain architecture+ADR specs, as they are design/leadership exercises rather than single runnable systems.)

Guides in This Phase

Deliverables Checklist

  • Four capstones built and sbt test-verified (01 ingestion, 02 fraud, 03 serving, 04 unified multi-module) — 28 ScalaTest specs
  • Problems 3 & 5 specified as architecture + acceptance criteria + key ADRs
  • Each capstone's five-force numbers and dials stated
  • A runbook (P13) and a cost model (P00/P07) for a built one
  • Capstone 6 architecture document (the unified platform) — partial slice built in capstone-04

Capstone 01 — Global Real-Time Event Ingestion Platform (end-to-end, runnable)

Phase: 16 — Capstone Systems | Difficulty: ⭐⭐⭐⭐⭐ | Time: 1–2 weeks

The JD's Problem 1, built as ONE runnable Scala pipeline that composes four phases — not a diagram, a working system you can sbt run. Raw events → contract validation + DLQ (P02/P03) → event-time windowed aggregation (P04/P05) → exactly-once lakehouse sink (P09) → query (P10). The end-to-end correctness proofs run with sbt test.

The pipeline

RawEvent ─validate─►  Valid?  ──no──►  DLQ (reasons accumulated)        [Stage 1: P02/P03]
                          │yes
                          ▼
              event-time windowed aggregate   (dedup by eventId)        [Stage 2: P04/P05]
                          │
                          ▼
              publish: atomic overwrite snapshot                        [Stage 3: P09]
                          │
                          ▼
              query: totalByUser / bigSpenders                          [Stage 4: P10]

The whole Pipeline.run is deterministic in the set of input events: shuffling or duplicating the input produces the identical published table — the exactly-once effect, end to end (P01). That's the property PipelineSpec proves.

Run it

sbt run     # drive the demo: prints the DLQ, the published table, and two queries
sbt test    # the correctness proofs (5 specs)

sbt run output (abridged): 2 events rejected to the DLQ (one with 4 accumulated reasons), a duplicate event deduped, 3 aggregated rows published in 1 snapshot, queries answered.

What each file maps to

FileStagePhase
Events.scalaingestion + Cats Validated data contract + DLQP02, P03
Windowing.scalaevent-time tumbling aggregation, dedup by eventIdP04, P05
Lakehouse.scalaIceberg-style snapshot table, atomic publishP09
Pipeline.scalawires the stages + the query layerP00, P10
Main.scalasbt run driver
PipelineSpec.scalaend-to-end proofsP01, P13

The acceptance properties (what sbt test proves)

  • Exactly-once effect: shuffled + duplicated input → byte-identical table.
  • Idempotent sink: re-running republishes the same data (new snapshot, same rows).
  • DLQ safety: invalid events are rejected with all reasons and never reach the table.
  • Query correctness: totalByUser / bigSpenders over the published table.

How to extend toward production (the spec items)

  • Swap the in-memory log for the Phase 02 PartitionedLog (per-key ordering, replay) and the in-memory table for the Phase 09 IcebergTable (OCC commits, time travel, compaction) — both are in this repo as sibling Scala labs.
  • Add watermarks + allowed lateness from Phase 04 lab-02-scala-windows for true streaming emission instead of batch aggregation.
  • Add multi-region publish + RPO/RTO (P14/P15), cost attribution per producer (P07), and observability (lag/DLQ-rate/freshness SLOs — P13).
  • Replace the demo driver with an FS2 stream (P12) reading a real Kafka topic.

Success criteria

  • sbt test → 5 specs pass; sbt run → prints the four stages.
  • You can point at each line of the pipeline and name the phase + the dial it turns (P00).
  • You can explain why the pipeline is exactly-once in effect without exactly-once delivery.

Capstone 02 — Stateful Fraud / Risk Stream Processing (runnable Scala)

Phase: 16 — Capstone Systems | Difficulty: ⭐⭐⭐⭐⭐ | Time: 1–2 weeks

The JD's Problem 2, built as a Flink-style keyed, event-time stateful operator in Scala (P04/P05/P11) — verified with sbt test. Per-account state, a hot-swappable broadcast ruleset, three rule kinds, idempotent (deduped) alerts, and savepoint-style checkpoint/restore for stateful redeploys.

What it implements

CapabilityIn the codePhase
Keyed state per accountstate: Map[account, AccountState] (recent txns + last country)P04
Broadcast rules (hot-swap)updateRules(...) swaps the active rulesetP04
Amount rulesingle txn over a threshold
Velocity ruleN txns within a time window (event-time)P04
Impossible travelcountry switch faster than physically possibleP05 enrichment
Idempotent alertsdedup by (account, kind, eventTime) → exactly-once effectP01
Savepoint redeploysnapshotState / restoreP04
Replay validationre-run the stream → identical alert setP02/P13

Run

sbt test

Expected: 8 specs, all green — each rule fires correctly, alerts dedup, a broadcast rule update changes behavior, and the headline proofs:

  • savepoint/restore: processing [1,2,3] then snapshot→restore→process [4,5] yields the same alert set as processing [1..5] in one detector (no lost/duplicated alerts across a stateful redeploy).
  • replay: re-running the same stream produces the identical alert set.

Success criteria

  • sbt test → all 8 specs pass.
  • You can explain why alerts must be deduped (at-least-once delivery + idempotent effect = P01) and how a savepoint preserves the dedup set across a redeploy.
  • You can explain why velocity/impossible-travel are inherently event-time, not arrival-time.

How to extend toward production

  • Add watermarks + allowed lateness (Phase 04 lab-02-scala-windows) so late txns are handled, not silently misordered.
  • Enrich from the Cassandra serving layer (Capstone 03) for account risk profiles (the as-of join, P05/P11), via async lookups.
  • Emit alerts to the lakehouse (Phase 09 lab) for audit + replay, and to a low-latency sink for action.
  • Wrap the detector in an FS2 stream (P12) reading a real Kafka topic; checkpoint state to S3 (the real savepoint).

Capstone 03 — Cassandra Serving Layer for Real-Time Features (runnable Scala)

Phase: 16 — Capstone Systems | Difficulty: ⭐⭐⭐⭐⭐ | Time: 1–2 weeks

The JD's Problem 4, built as a Cassandra-style feature store in Scala (P11) — verified with sbt test. The parts that actually bite in production: LWW upserts, TTL, tombstones + the read-fail threshold, tunable consistency (R+W>RF), hot-partition detection, and a token-bucket rate-limited backfill that protects the read SLO.

What it implements

CapabilityIn the codeWhy it matters
LWW upsertupsert ignores stale writeTsidempotent → replay-safe streaming writes (P04)
TTLttlMillis; expired reads return Nonefeature freshness
Tombstonesexpiry/delete create them; scanPartition fails past thresholdthe #1 Cassandra outage (P11)
ConsistencyConsistency.isStrong(r,w,rf) = R+W>RFread-your-writes vs latency (P01)
Hot partitionhotPartitions (median-based, P06)skew/celebrity keys (P01)
Rate-limited backfillTokenBucket + Backfill.runbackfill must not blow the read SLO

Run

sbt test

Expected: 8 specs, all green — LWW idempotence, TTL→tombstone, tombstone-overflow read failure, the R+W>RF boundary (R+W==RF is not strong), median hot-partition detection, and a backfill that takes ceil(rows/rate) ticks while landing every row.

Success criteria

  • sbt test → all 8 specs pass.
  • You can explain why R + W = RF is not strong (no quorum overlap) and why LOCAL_QUORUM is the multi-region default.
  • You can explain the tombstone trap (delete/TTL → marker → read scans them → fails past a threshold) and why TWCS suits TTL-heavy time series.
  • You can explain why a backfill must be rate-limited (P06 throughput vs P11 read-path p99).

How to extend toward production

  • Wire streaming upserts from Capstone 02's detector or the Phase 04 windower (idempotent, LWW).
  • Add compaction strategies (STCS/LCS/TWCS) and model tombstone GC after gc_grace.
  • Add multi-region replication (NetworkTopologyStrategy + per-DC LOCAL_QUORUM) and an RPO/RTO story (P14/P15).
  • Replace the in-memory map with a real Cassandra driver session; keep these as the modeling/SLO layer that decides partition keys, TTLs, and consistency.

Capstone 04 — Unified Platform (multi-module sbt build reusing the real lab code)

Phase: 16 — Capstone Systems | Difficulty: ⭐⭐⭐⭐⭐ | Time: 1 week

Proof that the labs aren't toys: a multi-module sbt build whose ingestion and lakehouse modules compile the actual Scala sources from the Phase 02 and Phase 09 labs (no copy/paste — via unmanagedSourceDirectories), and a platform module that dependsOn both and wires them into one pipeline. Verified with sbt test and runnable with sbt "platform/runMain unified.UnifiedMain".

The module graph

root (aggregate)
├── ingestion   ← compiles ../../phase-02-.../lab-02-scala-ingestion/src/main/scala  (REAL PartitionedLog, ConsumerGroup, IngestGateway)
├── lakehouse   ← compiles ../../phase-09-.../lab-02-scala-iceberg/src/main/scala     (REAL IcebergTable, snapshots, OCC)
└── platform    ← dependsOn(ingestion, lakehouse); the glue + tests

The platform code imports ingestion.PartitionedLog and lakehouse.IcebergTable directly — if those didn't resolve to the lab modules, it wouldn't compile (the first test is that smoke check).

The pipeline

ingest "key"->"amount"  --validate--> DLQ (non-integers)          [REAL P02 IngestGateway]
                        \--ok--> PartitionedLog (per-key ordering) [REAL P02 PartitionedLog]
ConsumerGroup --read all partitions--> sum amount per key          [REAL P02 ConsumerGroup]
              --publish--> IcebergTable.append (atomic snapshot)   [REAL P09 IcebergTable]

Run

sbt test                                  # 7 specs across the composed modules
sbt "platform/runMain unified.UnifiedMain"  # the demo

runMain output: 1 event rejected to the DLQ, an Iceberg snapshot published, per-key sums (alice 20, bob 8).

What the tests prove

  • The real Phase 02 + Phase 09 types compile and compose (the smoke test).
  • End to end: ingest → aggregate → publish a snapshot; invalid payloads hit the DLQ only.
  • Order independence: shuffling the input yields identical published rows.
  • Per-key ordering via the real PartitionedLog; replay via the real ConsumerGroup.seek.
  • Publishing twice produces two Iceberg snapshots (real history).

Why this matters

The other capstones are self-contained miniatures; this one demonstrates the principal-level move of composing independently-built, independently-tested components into a platform without duplicating code — and that the seams (validation→DLQ, log→consumer, aggregate→commit) actually line up. The build itself is the artifact: unmanagedSourceDirectories is the pragmatic way to reuse sibling modules; in a real monorepo these would be published library modules the platform depends on by version.

Extend

  • Add a windowing module reusing Phase 04 lab-02-scala-windows for event-time aggregation instead of a plain sum.
  • Add a serving module reusing Capstone 03's FeatureStore as a second sink (lakehouse for analytics, Cassandra for low-latency serving — the dual-sink platform).
  • Publish the lab modules as real versioned artifacts and depend on them by coordinate.

🛸 Hitchhiker's Guide — Phase 16: Capstone Playbook

Read this if: you're ready to compose the whole track into a system and present it like a principal. This is the playbook for building and defending a capstone.


0. The 30-second mental model

The capstones are the JD's five hard problems, and each is a composition of labs you already built and verified. The work isn't new code — it's the seams: wiring components so all five forces (P00) hold at once, and defending every seam with numbers and ADRs. One sentence: you already built the parts; the capstone is proving you can make them flow correctly under production constraints.

1. The build recipe (every capstone)

1. Draw the lifecycle (P00): creation → ingestion → … → serving → governance → replay → deletion
2. State the FIVE-FORCE numbers: correctness guarantee, p99 latency, peak throughput, $/unit, who operates it
3. Map each box to a phase/lab (the README tables do this)
4. Define ACCEPTANCE = correctness properties (replay/dup tests) + SLOs
5. Write the key ADRs (P15): every tool choice = a dial named + deciding factor
6. Build a thin end-to-end SLICE (Docker-real engines), spec the rest

2. The five capstones, one line each

1. Global ingestion  : Kafka/Protobuf/Flink → Iceberg → Athena, multi-region, exactly-once, cost/team
2. Fraud streaming   : event-time + sessions + Cassandra enrichment + broadcast rules + savepoint deploys
3. Lakehouse migration: Hive/Tez/Flume → Iceberg/Flink/Athena via dual-write→validate→cutover→decommission
4. Cassandra serving : feature store, partition/TTL/consistency design, Flink upserts + rate-limited backfill
5. Governance        : registry + PII + lineage + quality + certification + compliant erasure
6. THE platform      : compose 1–5 into one unified (Kappa) platform

3. The seams that break (where principals earn it)

ingestion → processing : processing-time logic double-counts on replay (use event-time, P01)
processing → lakehouse : non-2PC sink duplicates on recovery (exactly-once sink, P04)
lakehouse → query      : small files / no sort → scan-cost explosion (compact + Z-order, P08/09)
stream → serving       : unthrottled backfill blows read SLO (rate-limit, P11)
delete → compliance    : DELETE without snapshot expiry resurrects PII (P09/P14)
schema change → consumers: no CI gate → breaks downstream (compatibility gate, P03)

4. Acceptance properties to always test

replay/dup test: shuffled + duplicated input → identical output (exactly-once effect, P01)
parity (migration): legacy vs new data-diff = 0 before cutover (P13/P15)
checkpoint recovery: restore mid-stream → continue identically (P04)
read SLO under load: p99 within target with concurrent backfill (P11)
breaking change blocked in CI (P03) ; PII masked + audited (P14)

5. Presenting it (Round 1 + Round 5)

to engineers: the architecture, the dials, the failure modes, the rollback
to leadership: cost / risk / time / reliability (NOT Flink internals) — P15
the deck: lifecycle diagram + five-force numbers + ADRs + acceptance criteria + runbook + cost model

6. Beginner mistakes that mark you

  1. Designing without numbers (no SLO, no peak, no $).
  2. Ignoring the seams (each component "works" but the system double-counts/leaks).
  3. Big-bang migration with no parity/rollback (P15).
  4. No exactly-once at the external sink (P04/P09/P11).
  5. No cost model / no per-team attribution.
  6. No rollback / no runbook (P13).

7. What "done" looks like

one capstone BUILT end-to-end (verified seams), four SPEC'd (architecture + ADRs + acceptance)
five-force numbers + dials per capstone
runbook + cost model for the built one
Capstone 6 architecture doc = the unified platform (the JD's mission)

Build one for real. Spec the rest. Then read the Brother Talk, and go interview — you've done the whole lifecycle.

👨🏻 Brother Talk — Phase 16, The Graduation

You made it to the end. Let me close the loop — what you've actually become, and how to carry it.


Brother, look back for a second. Sixteen phases ago, the job description read like an intimidating wall of technologies — EMR, Hive, Tez, Spark, Flume, Flink, Scala, Akka, Cats, Kafka, Kinesis, Cassandra, Parquet, Protobuf, Athena, S3, Iceberg, Delta, Hudi, Trino — and it probably felt like you'd need to be ten different experts. Now you've built a miniature of every one of them by hand, and proved each with tests. More importantly, you've seen the thing the wall of names hides: it's not twenty unrelated technologies, it's a handful of ideas wearing twenty costumes. Partitioning is the same problem in Kafka, Cassandra, and Spark. Exactly-once is the same dance at every sink. The log underlies Kafka and Iceberg both. Quorum math is one formula. Once you see the ideas under the tools, the wall isn't a wall — it's a map, and you have it.

That's the real graduation. Not "I know Flink" — anyone can list tools. It's "I understand the lifecycle of data and the forces that pull on every decision, so I can pick up any new tool and slot it into the model in an afternoon." The model is the asset. Tools will change — five years from now half these names will be replaced — but creation→ingestion→validation→transport→ processing→storage→query→serving→governance→replay→deletion will not, and neither will correctness/latency/throughput/cost/operability. You learned the durable thing.

Let me give you the closing truths.

You are now the person who turns fear into numbers. When a room is anxious about "will this scale? is this correct? what will it cost?", you're the one who decomposes it into a latency budget, a five-force scorecard, a $/TB estimate, an exactly-once proof. That calm — the calm that comes from arithmetic, not bravado — is what the principal title actually rewards. It's not that you're the smartest in the room; it's that you did the math everyone else was too anxious to do. Keep doing the math. Out loud. In ADRs.

Build the capstone for real, even just a thin slice. Reading about a system and wiring one together are different universes. The first time you push an event through Kafka → Flink → Iceberg → Athena and watch it come out queryable, exactly-once, with the replay test passing — something clicks that no amount of theory delivers. And it's the thing that makes you unbluffable in an interview: you're not reciting, you're remembering something you actually made work. Pick one capstone. Docker-compose the real engines. Make the seams flow. That's your portfolio centerpiece.

The seams are the job, forever. Every component in this track "works" in isolation — that's the easy 80%. The principal's 20% is the seams: the processing-time logic that double-counts on replay, the non-transactional sink that duplicates on recovery, the unthrottled backfill that blows the read SLO, the DELETE that resurrects PII through time travel. Those are where production breaks and where your value lives. You've now seen them all named. Carry that seam- radar into every design review.

Keep writing things down. I've said it every phase and I'll say it last: the ADR habit is the compounding interest of an engineering career. Decisions written down are decisions that teach, that survive your absence, that earn you the next, bigger decision. The engineers who write get trusted; the ones who don't re-argue forever. Be the one who writes.

And a word on the journey itself, brother to brother: this was a lot. Seventeen phases, dozens of labs, every technology in a brutal JD, taken to depth. If you worked through it honestly — built the labs, made the tests pass, read the warmups slowly, sat with the brother-talks — you didn't just prepare for a role. You changed the way you think about data systems. That's worth more than any single job, because it travels with you to all of them.

The JD called this "a top-tier expert data infrastructure role" and "intentionally hard." It is. And you're now equipped for it — not because you memorized it, but because you built it. Go take the interview. Design the 5M-events-per-second platform on the whiteboard (Round 1). Debug the Flink job with the rising checkpoints (Round 2). Fix the 9-hour Spark job (Round 3). Design the type-safe Scala SDK (Round 4). Lead the migration (Round 5). You've done every one of those, by hand, with tests that pass.

You're ready, brother. Go build the platform that serves the millions.

— your brother 👨🏻

Interview Prep — The Principal Data Infrastructure Loop

This track maps directly onto the JD's own five-round evaluation loop. Each round below links to the phases that prepare it, lists what interviewers actually probe, and gives the "signal" that separates a senior answer from a principal one. The meta-skill across all five (P00): turn vague prompts into numbers, name the dial, and defend the seam.

Use the per-phase WARMUP.md "Interview Q&A" sections as your question bank, and the CHEATSHEET.md for the numbers. This page is the strategy.


Round 1 — Distributed Data Systems Design

Prompt (JD): Design a real-time platform ingesting 5M events/sec, validating Protobuf, processing with Flink, storing raw + curated in S3/Iceberg, serving Athena queries and Cassandra features.

Prepares it: P00 (tradeoffs), P01 (distributed systems), P02 (Kafka/Kinesis), P03 (Protobuf/contracts), P04 (Flink), P08–P09 (storage/lakehouse), P10 (query), P11 (serving) + system-design/01.

What they probe: Kafka vs Kinesis (with numbers); partitioning & hot keys; schema registry & DLQ; Flink topology + checkpointing + exactly-once sink; storage layout (partition/sort/file size); Cassandra modeling; replay; observability; failure recovery; the cost model.

Senior → Principal signal: A senior names the components. A principal extracts the numbers first (read:write ratio, p99 + percentile, correctness-per-stream, budget, who operates it), decomposes a latency budget (P00), names the dials as constraints are added ("…and cheap" → which dial), and defends the seams (exactly-once effect re-earned at the Cassandra/Iceberg boundary). Say "5M/sec alone tells me little; the ratios and SLAs tell me everything."


Round 2 — Deep Debugging (Streaming)

Prompt (JD): A Flink job has rising checkpoint duration, increasing RocksDB state, delayed watermarks, and growing Kafka lag. Diagnose.

Prepares it: P04 (Flink internals), P01 (backpressure), P13 (observability).

What they probe: Do you see one story or four bugs? Backpressure → barrier/alignment stall → checkpoint duration ↑ + source throttle → lag ↑; state growth from open windows / no TTL / unbounded keys; watermark delay from idle partitions or backpressure.

Senior → Principal signal: Draw the causal chain (P04 Ch. 8) calmly: find the backpressured operator, ask why it's slow (slow sink? hot-key skew? expensive process?), and give the cause-matched fix (async/scale sink, re-key/salt, incremental aggregation, state TTL, savepoint rescale, unaligned checkpoints to relieve alignment). The signal is treating the four symptoms as one root cause.


Round 3 — Spark & Lakehouse Optimization (Batch)

Prompt (JD): A Spark job on EMR runs 9 hours, spills heavily, writes millions of small Parquet files, and makes Athena scan too much. Diagnose.

Prepares it: P06 (Spark internals), P07 (EMR), P08 (Parquet/files), P09 (compaction), P10 (Athena cost).

What they probe: shuffle/skew (even vs uneven task times — the fork); spill = partition-count problem; broadcast vs sort-merge + stale stats; output file sizing; compaction; partition pruning + columnar → Athena $/TB; safe idempotent backfill.

Senior → Principal signal: Open the Spark UI mentally — is time spread evenly or is one task the straggler? That fork (skew vs sizing) drives the whole diagnosis (P06). Connect the through-line: the small files that slow the write are the same files inflating the Athena bill (P08→P10), and the fix is layout, not a bigger cluster.


Round 4 — Scala Platform Engineering

Prompt (JD): Design a Scala library so product teams define event schemas, validate data contracts, build stream processors, write to lakehouse tables, and get observability automatically.

Prepares it: P12 (Scala/Cats/Effects/SDK), P03 (contracts), P04 (sinks), P13 (observability).

What they probe: type-safe APIs (make illegal states unrepresentable); Validated (accumulate) vs Either (fail-fast); Resource safety; effect systems (referential transparency); error modeling; retry; binary compatibility + versioning; ergonomics.

Senior → Principal signal: The spine is "make misuse uncompilable" (P12). Newtypes + smart constructors for events; ValidatedNel decode → DLQ; effects (IO/ZIO) so processing is lazy/testable; Resource so connections can't leak; the lakehouse write as a single safe exactly-once API; semver + MiMa so upgrades never break consumers. "The happy path is the only path that compiles."


Round 5 — Architecture Leadership (Migration Review)

Prompt (JD): Lead a design review migrating legacy Hive/Tez/Flume to Kafka/Flink/Spark/ Iceberg/Athena.

Prepares it: P07 (legacy), P09 (Iceberg), P15 (migrations/ADRs/leadership), P13 (validation).

What they probe: migration phases; dual-write; data validation/parity; replay; cutover; rollback; cost; governance; team adoption; deprecation; risk management.

Senior → Principal signal: Strangler-fig, never big-bang (P15): dual-write → validate parity (data diffing, P13) → incremental cutover (old path warm) → decommission last, reversible at every step. Plus the leadership layer — present in cost/risk/time/reliability to leadership, set the standards, mention team adoption and the deprecation plan. The signal is patience + reversibility + business framing, not technical bravado.


Behavioral / Principal-Bar (woven through)

The JD's seniority bar: system-wide judgment, operating under ambiguity, deep debugging, influence without authority, written architecture communication, simplifying without making fragile, building platforms not pipelines, mentoring, setting standards, balancing the forces. Prepare stories that show: a decision you wrote as an ADR and revisited; a migration you made reversible; an incident you reduced to one root cause; a standard you set that scaled the team; a time you chose the boring, operable option over the clever one.

How to Practice

  1. Take any phase's WARMUP.md Interview Q&A and answer aloud, with numbers.
  2. Whiteboard each system-design walkthrough in 45 minutes.
  3. For every tool choice, state the dial + deciding factor (P15) — out loud, then as an ADR.
  4. Re-run the labs from memory; the flagship labs are past interview questions in disguise.

System Design — Hard-Problem Walkthroughs

The JD's five "Example Hard Problems" as 45-minute whiteboard walkthroughs — the design format, not the build (the build is Phase 16). Each follows the same principal recipe so you can run it under interview pressure.

The recipe (P00): 1. Extract the numbers (five forces) → 2. Draw the lifecycle3. Place components + name the dials4. Defend the seams (correctness) → 5. State the ADRs (deciding factors) → 6. Observability, cost, failure, scale.


  1. Numbers: 5M/sec write; what's the read rate & shape (drives serving split)? p99 SLO
    • percentile? correctness per stream (ad-impression at-least-once vs payment exactly-once)? budget? operators? Peak = ?× average (size to peak).
  2. Lifecycle: produce → Kafka/Kinesis (Protobuf, schema-validated, DLQ) → Flink (event-time) → exactly-once sink → S3 raw + Iceberg curated → Athena/Trino (analytics) + Cassandra (features) → governance → replay → retention.
  3. Components/dials: Kafka vs Kinesis (ops↔control↔lock-in); partition count = max(produce, consume) bound; Flink checkpoint to S3; Iceberg + compaction; Cassandra modeled to the feature lookup.
  4. Seams: event-time (not processing-time) so replay is correct; 2PC/idempotent sink so recovery doesn't duplicate; tee raw to S3 before processing (recoverability).
  5. ADRs: ingestion substrate; engine; table format; serving store — each a dial + deciding factor.
  6. Ops/cost/failure: lag/watermark/checkpoint/DLQ dashboards + SLOs; cost = Athena $/TB (layout) + cluster + storage, attributed per team; multi-region failover with RPO/RTO.

Maps to: P01–P04, P08–P11, P13–P15.


Design 2 — Stateful fraud detection (near-real-time)

  1. Numbers: events/sec; alert latency budget; tolerated false-negative vs false-positive cost; out-of-order distribution (sets watermark delay); state size = keys × windows.
  2. Lifecycle: event stream → Flink (event-time, sliding + session windows, pattern detection) ← broadcast rules stream; enrich from Cassandra (async); RocksDB state; checkpoint to S3; alert sink (exactly-once); replay for validation.
  3. Dials: detection latency↔completeness (watermark delay); state size↔cost (TTL, key design); rule agility (broadcast state + savepoint deploys).
  4. Seams: as-of enrichment (no temporal leakage, P05); savepoint upgrade preserves state + no missed/dup alerts; late events → corrected or side-output.
  5. ADRs: Flink (why, vs Spark-SS); RocksDB backend; rule-versioning approach.
  6. Ops: replay reproduces alerts exactly; alert-latency SLO; state-growth alerting.

Maps to: P04, P05, P11, P12 (JVM/state), P13.


Design 3 — Hive/Tez/Flume → Kafka/Flink/Iceberg/Athena migration

  1. Numbers: dataset count & sizes; current query $; small-file count; acceptable downtime (≈ zero); migration timeline.
  2. Phases (strangler-fig, P15): (0) stand up new alongside, read same S3 via catalog; (1) dual-write; (2) validate parity (data diffing, P13) until 0-diff for a soak; (3) cutover consumers incrementally, old warm; (4) decommission Flume/Hive last.
  3. Dials: speed↔risk; cloud-native↔portable; cost↔freshness.
  4. Seams: schema compatibility through transition (P03); idempotent backfills (P06); lineage preserved (P13); rollback at every step.
  5. ADRs: Iceberg adoption; engine cutover order; deprecation plan.
  6. Outcome metrics: query $ ↓, small files ↓, freshness ↑, incidents ↓; present to leadership in cost/risk/time.

Maps to: P03, P06, P07, P09, P10, P13, P15.


Design 4 — Cassandra real-time feature serving

  1. Numbers: read QPS + p99 SLO; write rate (streaming upserts); feature TTL; partition size over a year; consistency need.
  2. Model: design around the read — partition key (avoid hot + large, bucket time series), clustering keys; RF + LOCAL_QUORUM; TTL + TWCS; idempotent upserts.
  3. Pipelines: Flink idempotent upserts (live) + rate-limited Spark backfill (history) — the backfill must not blow the read SLO.
  4. Dials: serving speed↔analytical flexibility; consistency↔latency; backfill throughput↔ read SLO.
  5. Seams: upserts replay-safe (LWW); tombstones bounded (TWCS, no queue pattern); multi-region LOCAL_QUORUM.
  6. Ops: read-path p99 + tombstone-per-read alerts; repair schedule; partition-size monitor.

Maps to: P01, P04, P06, P11, P14 (multi-region).


Design 5 — Enterprise data governance platform

  1. Numbers: dataset/stream count (thousands); PII fields; access principals; compliance regime (GDPR/etc.).
  2. Components: dataset + schema registry (CI gate, P03); field ownership + PII tags (P03); IAM + column/row masking via Lake Formation (P14); lineage graph + impact (P13); data-quality framework (P13); audit log (P14); dataset certification (owner/SLA/lineage/quality, P00/P13).
  3. Dials: self-service↔governance (answer: self-service with guardrails); flexible schema↔safe contract.
  4. Seams: breaking change blocked in CI (never prod); compliant erasure = delete + snapshot expiry (P09/P14); PII masking enforced at the query engine (P10).
  5. ADRs: registry/compatibility policy; access model (ABAC vs ARNs); retention/erasure flow.
  6. Outcome: every dataset certified; PII auditable; breaking changes impossible to ship.

Maps to: P03, P09, P10, P13, P14.


How to Use

  • Time yourself: 45 minutes per design, whiteboard only.
  • Force the numbers first — if you start drawing boxes before extracting requirements, reset.
  • For every box, be ready for "…and now make it cheaper / faster / exactly-once" — name the dial it turns (P00/P15).
  • After each, write the ADRs you'd file. The interview is a live ADR session.