CodeKitchen prep
0%
Contest in--:--:--
Data Engineering Β· lesson 08/13

🏭Data engineering concepts

⏳ ~15 min

This is DataChef's day job: data platforms on AWS with Spark, Kafka, Airflow and dbt. The contest may have conceptual questions or pipeline-logic problems, and the interview will certainly cover this. Learn the vocabulary and the "why".

Pipelines: ETL vs ELT, batch vs streaming

ETL

Transform before loading (Spark/Glue jobs write clean data). This was traditional when warehouse compute was expensive.

ELT

Load raw data first, then transform inside the warehouse with SQL (dbt on Snowflake/BigQuery). This is the modern default.

Batch

Bounded data on a schedule (hourly/daily). Simple, cheap, easy to backfill.

Streaming

Unbounded events with seconds of latency (Kafka β†’ Flink/Spark Structured Streaming). Brings windows, watermarks and late data.

Medallion / layered architecture

Raw / Bronze: data as it arrived, append-only and immutable, so you can always replay it
Cleaned / Silver: typed, deduplicated, validated, conformed keys
Curated / Gold: business-level aggregates, star schemas, feature tables

Data lake: files in object storage (S3) plus a catalog. Warehouse: managed SQL with columnar storage. Lakehouse: a lake plus table formats (Delta Lake, Apache Iceberg, Hudi) that add ACID, time travel, MERGE and schema evolution on top of Parquet.

Data Mesh (DataChef's specialty)

  1. Domain ownership: the teams that produce data own it end-to-end.
  2. Data as a product: discoverable, addressable, trustworthy, with an SLA, schema and docs.
  3. Self-serve data platform: a central platform team provides the tooling (e.g. DataChef's AWS Data Landing Zone).
  4. Federated computational governance: global standards (security, interoperability) enforced automatically.

The properties of a good pipeline

♻️ Idempotent

Re-running gives the same result. Overwrite partition dt=2026-10-23 or MERGE on keys. Never blind-append on retry.

⏩ Incremental

Process only new or changed data using a watermark (max updated_at), CDC, or new partitions.

πŸ”™ Backfillable

Parameterise jobs by logical date, so you can recompute history for any range.

πŸ§ͺ Tested

Schema contracts and data-quality checks (nulls, uniqueness, row counts, freshness) with Great Expectations, dbt tests or Deequ.

πŸ‘€ Observable

Logging, metrics, lineage, alerting, data SLAs (Datadog, OpenLineage).

πŸ” Governed

Lake Formation / IAM permissions, PII masking, GDPR deletes (DataChef is EU-based!).

CDC & delivery semantics

  • CDC (change data capture): read the DB's log (MySQL binlog, Postgres WAL) with Debezium β†’ Kafka. You get insert/update/delete events, with op, before, after and ts.
  • At-most-once (may lose), at-least-once (may duplicate; the most common), exactly-once (idempotent producer, transactions, or at-least-once plus an idempotent sink). In practice, choose at-least-once and dedupe on a key.
  • Late and out-of-order events: use the event time vs processing time distinction, plus watermarks ("I've probably seen everything before Tβˆ’10 min"). Window types are tumbling (fixed, non-overlapping), sliding/hopping (overlapping) and session (gap-based).

Apache Spark essentials

from pyspark.sql import SparkSession, functions as F, Window
spark = SparkSession.builder.getOrCreate()
orders = spark.read.parquet("s3://lake/silver/orders/")            # lazy
w = Window.partitionBy("customer_id").orderBy(F.col("updated_at").desc())
latest = (orders
          .withColumn("rn", F.row_number().over(w))
          .filter("rn = 1").drop("rn"))                             # transformations: lazy
daily = (latest.groupBy(F.to_date("order_ts").alias("dt"))
               .agg(F.sum("amount").alias("revenue"),
                    F.countDistinct("customer_id").alias("buyers")))
(daily.write.mode("overwrite").partitionBy("dt")
      .parquet("s3://lake/gold/daily_revenue/"))                    # action: triggers the job
  • Lazy evaluation: transformations (select, filter, withColumn, join, groupBy) build a DAG. Actions (count, collect, show, write) execute it.
  • Narrow transformations (map, filter) need no data movement. Wide ones (groupBy, join, distinct, repartition) shuffle data and create stage boundaries. Shuffles are expensive.
  • Partitions = units of parallelism. repartition(n) does a full shuffle; coalesce(n) only reduces partitions with no full shuffle. Too many tiny output files is the small files problem.
  • Joins: broadcast hash join (small table under ~10 MB by default; hint with F.broadcast) vs sort-merge join (big Γ— big). Data skew (one hot key) can be fixed with salting or AQE skew-join handling.
  • cache()/persist() pays off when a DataFrame is reused. Avoid Python UDFs (row-by-row serialization) and prefer built-in F.* functions.
  • Catalyst optimizer and predicate/projection pushdown into Parquet. AQE (Adaptive Query Execution) re-optimizes at runtime.

File formats

FormatLayoutGood for
CSV / JSONRow, text, no schemaInterchange; slow, no types, fragile
ParquetColumnar, compressed, typed, with row-group statisticsAnalytics (column pruning + predicate pushdown). The default in lakes
ORCColumnarHive ecosystem
AvroRow, binary, schema embedded, good schema evolutionKafka messages, write-heavy pipelines
Delta / IcebergParquet plus a transaction logACID on the lake, MERGE, time travel

Partitioning on disk (Hive-style): s3://bucket/events/dt=2026-10-23/hour=16/part-0000.parquet. Partition on low-cardinality columns that queries filter on (date), never on user_id.

Kafka in 60 seconds

  • Topic β†’ partitions (ordered, append-only logs) β†’ messages with offsets. Ordering is guaranteed per partition only, and the message key decides the partition.
  • Consumer group: each partition goes to exactly one consumer in the group, so parallelism ≀ #partitions. Consumers commit offsets.
  • Retention by time/size, or log compaction (keeps the latest value per key, giving you a changelog/table).
  • Replication factor, ISR, acks=all for durability. The schema registry holds Avro/Protobuf schemas.

Orchestration & transformation tools

  • Airflow: DAGs of tasks, scheduler, operators/sensors, execution_date/logical date, retries, backfill and catchup. Tasks should be idempotent and atomic. Use XCom only for small metadata. Alternatives: Dagster, Prefect, AWS Step Functions.
  • dbt: SQL SELECT models plus Jinja {{ ref('stg_orders') }}, which builds the dependency DAG. Materializations: view / table / incremental / ephemeral. Tests: unique, not_null, relationships, accepted_values. Snapshots give you SCD2.

AWS data services map

NeedAWS
Storage / data lakeS3 (+ lifecycle rules, storage classes)
Catalog / ETLGlue Data Catalog, Glue crawlers, Glue (Spark) jobs
Permissions on the lakeLake Formation, IAM
SQL on S3Athena (Presto/Trino, pay per TB scanned, so partition and use Parquet!)
WarehouseRedshift (+ Spectrum)
StreamingKinesis Data Streams / Firehose, MSK (managed Kafka)
Serverless computeLambda (15-min limit; event-driven, e.g. on S3 put), EMR / EMR Serverless for Spark
OrchestrationMWAA (managed Airflow), Step Functions, EventBridge
Infrastructure as CodeCDK (DataChef uses TypeScript CDK), Terraform, CloudFormation

What these look like in code

An idempotent Airflow DAG

from datetime import datetime, timedelta
from airflow import DAG
from airflow.operators.python import PythonOperator

def load_orders(data_interval_start, data_interval_end, **_):
    # read ONLY this run's interval, write ONLY this run's partition β†’ reruns are safe
    dt = data_interval_start.strftime("%Y-%m-%d")
    df = extract(since=data_interval_start, until=data_interval_end)
    df.to_parquet(f"s3://lake/bronze/orders/dt={dt}/", engine="pyarrow")   # overwrite the partition

with DAG("orders_daily", start_date=datetime(2026, 10, 1), schedule="@daily", catchup=True,
         default_args={"retries": 3, "retry_delay": timedelta(minutes=5)}) as dag:
    load = PythonOperator(task_id="load_orders", python_callable=load_orders)
    dq = PythonOperator(task_id="check_orders", python_callable=check_row_counts)
    load >> dq                                   # dependency: load runs before the check

A dbt incremental model

-- models/silver/orders.sql
{{ config(materialized='incremental', unique_key='order_id') }}

SELECT order_id, customer_id, status, amount, updated_at
FROM {{ ref('stg_orders') }}
{% if is_incremental() %}
  WHERE updated_at > (SELECT MAX(updated_at) FROM {{ this }})   -- watermark on the target table
{% endif %}
-- with unique_key set, dbt MERGEs new versions over old rows instead of appending duplicates

Spark Structured Streaming with a watermark

events = (spark.readStream.format("kafka")
          .option("subscribe", "clicks").load()
          .select(F.from_json(F.col("value").cast("string"), schema).alias("e")).select("e.*"))
counts = (events
          .withWatermark("event_time", "10 minutes")                 # accept events up to 10 min late
          .groupBy(F.window("event_time", "5 minutes"), "page")      # tumbling 5-min windows
          .count())
(counts.writeStream.outputMode("append")
       .option("checkpointLocation", "s3://lake/_chk/clicks/")       # offsets + state β†’ exactly-once sink
       .format("delta").start("s3://lake/silver/click_counts/"))

Architecture vocabulary

  • Lambda architecture: a batch layer (complete, slow) plus a speed layer (approximate, fast), merged at query time. Two code paths to maintain.
  • Kappa architecture: one streaming path; to reprocess, replay the log (Kafka) from the beginning.
  • Reverse ETL: push warehouse results back into operational tools (CRM, ads).
  • Data contract: an agreed schema + semantics + SLA between a producer and its consumers, checked in CI.
  • Schema evolution: backward compatible = new readers can read old data (add a field with a default); forward compatible = old readers can read new data (remove an optional field). Renaming a field breaks both.
  • Partition pruning: the engine skips partitions that the WHERE clause rules out. It only works when you filter on the partition column itself, not on a function of it.
  • Backpressure: a slow consumer makes the system slow down the producer (or buffer), instead of falling over.
  • Lineage: which jobs and tables produced this column. Needed for impact analysis and GDPR.

Whiteboard warm-up: three classic design questions

πŸ“₯ Ingest a 3rd-party API daily

Scheduler (Airflow) β†’ paginated pull with retries and back-off β†’ raw JSON to S3 dt= partition (replayable) β†’ parse, validate, dedupe β†’ Parquet/Iceberg silver β†’ MERGE into marts. Store the watermark/cursor after success only.

πŸ“Š Real-time dashboard of orders/min

CDC (Debezium) β†’ Kafka keyed by order_id β†’ Flink/Spark streaming with event-time windows and a watermark β†’ serving store (Druid, ClickHouse, Redis) β†’ dashboard. Late events update or are dropped by policy.

🧹 Duplicate events downstream

Find the cause (at-least-once delivery, retries). Give every event a deterministic id, dedupe on it (window function in batch, state with TTL in streaming), and make sinks idempotent with MERGE/upsert.

Quiz A daily job failed midway and you rerun it. What property makes this safe?
Quiz In Spark, which is an action?
Quiz Kafka guarantees ordering…
Quiz Join a 2 TB fact table with a 20 MB dimension in Spark. Best strategy?
Quiz An Airflow daily task runs for logical date 2026-10-22. Which data should it process?
Quiz Consumers using the new schema can still read data written with the old schema. That is…
Quiz A Spark job writes 40,000 files of 50 KB per day and Athena queries are slow. Best fix?
🏁 --:--