Requirements clarification:- Ingest Kafka event stream into a feature table for training/serving- Deduplicate retries, tolerate out-of-order events, provide at-least-once or exactly-once when possible- Support efficient compaction (merge/upsert) for feature storeHigh-level design:- Framework: Spark Structured Streaming with Delta Lake (or Flink + Iceberg). I choose Spark Structured Streaming because it provides transactional sinks (Delta), event-time watermarking, exactly-once semantics end-to-end with Kafka source offsets and checkpointing, and integrates well with ML pipelines (batch retrain + serving).- Pipeline stages: 1. Read from Kafka with event.time and event.id (unique event key) and metadata (partition, offset). 2. Parse and assign event_time; use watermarking to bound lateness (e.g., 30m). 3. Stateful deduplication keyed by entity_id or event_key using event_id + event_time; keep minimal state (last_processed_event_id or event_time window) and eviction policy. 4. Compute/upsert features per entity. 5. Write to Delta table using atomic MERGE (compaction) with foreachBatch or Delta's transactional writes to get exactly-once.Idempotency & semantics:- Deduplication ensures idempotent processing of retries.- Exactly-once: achievable if using Spark structured streaming with Kafka source (read offsets) + Delta transactional sink + checkpointing. This yields end-to-end exactly-once for deterministic transforms.- If sink can't be transactional, use at-least-once + idempotent upserts keyed by entity + event_id.Handling out-of-order:- Use event-time processing with watermarking and late-arrival policy. For very late events beyond watermark, route to a "late events" store and optionally reprocess by replaying raw events + compacting historical features.Compaction:- Use Delta Lake MERGE to compact updates: write incremental updates then compact/optimize periodically (OPTIMIZE + ZORDER) to keep serving latency low.Pseudocode (Spark Structured Streaming, Python-like):python
from pyspark.sql import functions as F
from delta.tables import DeltaTable
# Read Kafka
df = (spark.readStream
.format("kafka")
.option("subscribe", "events")
.option("startingOffsets","earliest")
.load()
)
events = (df.selectExpr("CAST(value AS STRING) as json")
.select(F.from_json("json", schema).alias("e"))
.select("e.event_id","e.entity_id","e.event_time","e.payload")
.withColumn("event_time", F.to_timestamp("event_time"))
)
# Deduplicate using event_id with watermark and stateful dropDuplicates
deduped = (events
.withWatermark("event_time", "30 minutes")
.dropDuplicates(["entity_id","event_id"]) # stateful, keeps minimal state until watermark
)
# Compute features (example aggregation)
features = (deduped
.groupBy("entity_id")
.agg(F.last("payload.value").alias("latest_value"),
F.max("event_time").alias("last_seen"))
)
# Write using foreachBatch to perform MERGE into Delta table (idempotent upsert)
def upsert_to_delta(microbatch_df, batch_id):
delta_table = DeltaTable.forPath(spark, "/mnt/delta/features")
(delta_table.alias("t")
.merge(microbatch_df.alias("s"), "t.entity_id = s.entity_id")
.whenMatchedUpdateAll()
.whenNotMatchedInsertAll()
.execute())
(features.writeStream
.foreachBatch(upsert_to_delta)
.option("checkpointLocation","/mnt/checkpoints/features")
.start())
Key considerations:- Keep dedupe state small: dedupe by (entity_id,event_id) and rely on watermark TTL to evict.- For high cardinality, consider bloom filters or external exact-store (Redis) with TTL for dedupe tokens.- Monitor lateness, watermark drops, checkpointing health.- Periodically compact Delta table and validate merges to avoid write amplification.This design yields idempotent ingest, handles out-of-order events via event-time + watermark, and achieves exactly-once when using Spark+Delta with checkpointing; otherwise it provides at-least-once with idempotent upserts.