Unified Batch & Stream Processing (Kappa)
Problem
Maintaining separate batch and stream processing systems forces teams to implement the same business logic twice in divergent codebases. Over time these paths drift, producing conflicting results between historical and real-time views while doubling infrastructure, testing, and operational costs.
Solution
Implement a Kappa Architecture where an append-only event log (e.g., Kafka) is the primary storage system, and both real-time and historical batch processing are handled by a single stream-processing engine.
Cloud Paradigm
- Event Sourcing (append-only immutable log as system of record)
- Stream-First Processing (single engine for real-time and historical)
- Log-Centric Architecture
- Reprocessing via Replay (offset reset for historical recompute)
- Immutable Data Storage
- Separation of Storage and Compute
Solution Flow
- Producers emit every state change as an immutable event to a durable, append-only log topic—there is no separate batch feed and no in-place update, only new records appended in order.
- The Event Log (Kafka) retains these events as the single system of record, with retention configured long enough to replay the full history rather than a short buffer.
- A single Stream Processing Engine (Flink, Kafka Streams, or Spark Structured Streaming) subscribes to the log and applies one codebase of transformation logic to compute aggregates, joins, and enrichments continuously.
- For real-time consumption, the Engine reads from the log tail and writes low-latency results to a serving store.
- For a historical recompute or logic change, the Operator deploys a new job version that resets its offset to the beginning of the log and reprocesses every event through the same code path, materializing a fresh output table.
- The Serving Store (a key-value or columnar view) is atomically swapped or versioned so downstream Consumers—dashboards, APIs, ML features—query a consistent view without dual-write reconciliation.
When to Use
- Your batch and streaming logic are genuinely identical and duplication causes drift.
- Source data is naturally event-shaped (clicks, transactions, IoT telemetry, CDC streams).
- You need to reprocess history after fixing a bug or adding a metric without a separate ETL rebuild.
- Latency requirements span from sub-second to daily under one operational model.
When NOT to Use
- Workloads require large multi-table ad-hoc joins over petabytes that batch engines handle far more cheaply.
- Your data arrives only as periodic bulk dumps with no meaningful event ordering.
- Retaining the full log indefinitely is cost-prohibitive and snapshots suffice.
- Complex, iterative analytics (graph, ML training) that don't map to streaming primitives.
Trade-offs
- Single codebase and consistent logic vs the cost of retaining a long, replayable event log.
- On-demand historical reprocessing vs reprocessing latency and throughput limits when replaying billions of events.
- Simpler operations (one engine) vs stream frameworks being harder to reason about for complex batch-style joins.
- Immutable audit trail vs increased storage and the need for compaction/tiering strategies.
Real-World Example
A ride-hailing company streams every trip lifecycle event—requested, matched, started, completed—into Kafka as the authoritative log. A single Flink application computes driver payout aggregates and surge-pricing signals in real time. When finance discovers a rounding error in the payout formula, engineers deploy a corrected job that resets to offset zero and replays six months of trip events through the identical operator graph, regenerating accurate historical payouts into a new serving table—no separate Spark batch pipeline, no reconciliation between two divergent implementations.
Additional Details
- Ordering & keyed state: Guarantees hold only per-partition; events sharing a key must be partitioned together or windowed aggregates and joins produce wrong results. Cross-key ordering is never guaranteed, so design keys around your consistency boundary.
- Replay determinism: A recompute is only correct if operators are deterministic—wall-clock timestamps, external lookups, and random IDs break replay. Use event-time semantics with watermarks and pin enrichment data to versioned snapshots.
- Schema evolution: Old and new events flow through one code path during a full replay, so consumers must decode every historical schema. Use a registry with backward-compatible rules and never repurpose field meanings.
- Reprocessing cost & throughput: Replaying billions of events saturates source-topic read bandwidth and serving-store write capacity; throttle parallelism and provision the swap table separately from live traffic. Retention length directly drives storage bill—tier cold segments to object storage.
- State backend maintenance: Large keyed state needs checkpoint/savepoint tuning, incremental snapshots, and periodic state TTL to avoid unbounded growth. Log compaction on keyed topics reclaims space but destroys full-history replay—keep compaction and reprocessing topics separate.
- Observability: Track consumer lag, watermark skew, checkpoint duration/failure rate, and replay progress by offset; version output tables and record input offset ranges as lineage so any served number is traceable to a code version.
Security Controls
- Topic-level ACLs: Restrict produce and consume permissions per topic so services only access the event streams they own.
- Encryption in transit and at rest: Enforce TLS for all broker connections and encrypt log segments on disk to protect the immutable system of record.
- Schema Registry governance: Validate every event against a registered, versioned schema to prevent malformed or malicious payloads from entering the log.
- Immutable audit log: Leverage the append-only design itself as a tamper-evident record, with broker-side write restrictions preventing deletion.
- Consumer group isolation: Separate reprocessing jobs into distinct consumer groups so a full replay never starves or interferes with real-time consumers.
- Serving-store access control: Apply row/column-level authorization on materialized views so downstream consumers see only permitted results.