Welcome to your next lesson in the "Scalable Data Processing and Platforms" module.
In our last lesson, we contrasted the high-level architectures of data warehouses and data lakes, establishing the revolutionary impact of separating storage and compute. We also introduced the core difference between ETL (transform-then-load) and ELT (load-then-transform) as a key architectural choice.
Today, we'll dive deep into the design of the systems that perform this work. This lesson directly addresses the learning outcome: Design a high-level ETL/ELT pipeline, choosing between batch, streaming, and hybrid models. This is a fundamental skill tested in every data-focused system design interview at companies like Google, Meta, and Netflix. You'll be expected to reason about the trade-offs between different processing models to meet specific business requirements.
We will structure this knowledge into five interconnected concepts, moving from foundational models to the sophisticated hybrid designs seen in production at scale.
- The Batch Processing Pipeline: The Scheduled Workhorse
- The Stream Processing Pipeline: The Real-Time Reactor
- Choosing Your Model: Latency, Throughput & Cost Trade-offs
- Practical Design: A Batch Pipeline for Analytics
- Evolving the Design: Streaming and Hybrid Architectures
1. The Batch Processing Pipeline: The Scheduled Workhorse (Easy)
-
Technical Explanation:
Batch processing is a data processing model where data is collected, stored, and then processed in large, discrete groups or "batches." These jobs are typically executed on a recurring schedule (e.g., hourly, daily). This model is synonymous with the traditional ETL (Extract, Transform, Load) and modern ELT (Extract, Load, Transform) paradigms we discussed previously. It prioritizes high-volume throughput and cost-efficiency over low latency.The architecture typically involves:
- Extract: Pulling data from various sources (databases, logs, APIs).
- Transform: A powerful compute engine like Apache Spark or a legacy system like Hadoop MapReduce cleans, joins, aggregates, and restructures the entire batch of data.
- Load: The processed data is loaded into a destination system, often a data warehouse like Snowflake or a data lake for historical analysis.
-
Metaphor for a 5-Year-Old:
Imagine a mail carrier who doesn't deliver letters one by one. Instead, they collect all the mail for your entire neighborhood in a big bag all day long. At the end of the day, they sit down, sort all the letters into neat piles for each house (transform), and then deliver all the mail for each house in one big drop-off (load). It's efficient for the mail carrier, but you have to wait until the end of the day to get your letters. -
Python Code Snippet (Conceptual Batch Job):
This script simulates a daily batch job. It reads all the log files generated in a day, processes them together to create a summary, and writes a single report.import os import glob # Setup: Create some dummy daily log files (our "batch" of data) os.makedirs("daily_logs", exist_ok=True) with open("daily_logs/2023-10-28_10-00.log", "w") as f: f.write("ERROR: Connection failed\nINFO: User logged in") with open("daily_logs/2023-10-28_14-00.log", "w") as f: f.write("INFO: Data processed\nERROR: Disk full") with open("daily_logs/2023-10-28_18-00.log", "w") as f: f.write("WARN: High latency detected") def run_daily_batch_report(): """Simulates a batch job that runs once a day.""" print("Starting daily batch job...") log_files = glob.glob("daily_logs/*.log") total_lines = 0 error_count = 0 # EXTRACT & TRANSFORM: Process the entire batch of files for file_path in log_files: with open(file_path, "r") as f: for line in f: total_lines += 1 if "ERROR" in line: error_count += 1 # LOAD: Write the final aggregated report report = f"Daily Summary:\nTotal Lines Processed: {total_lines}\nTotal Errors: {error_count}" with open("daily_report.txt", "w") as f: f.write(report) print("Daily batch job finished. Report generated.") print(report) run_daily_batch_report() # Cleanup os.remove("daily_report.txt") for f in glob.glob("daily_logs/*.log"): os.remove(f) os.rmdir("daily_logs")
2. The Stream Processing Pipeline: The Real-Time Reactor (Easy)
-
Technical Explanation:
Stream processing is a model where data is processed continuously, event by event, as it is generated. The goal is to minimize latency and enable real-time reactions. The data is considered "unbounded"—a never-ending flow.The architecture typically consists of:
- Ingestion: An event broker like Apache Kafka or AWS Kinesis captures the stream of events from sources (e.g., IoT sensors, user clicks) in a durable and scalable way.
- Processing: A stream processing engine, such as Apache Flink or Spark Streaming, consumes events from the broker. It applies transformations, runs aggregations over windows of time, or detects patterns on each event as it arrives.
- Delivery: The processed data is immediately sent to a destination system, like a monitoring dashboard, a notification service, or a real-time application database.
-
Metaphor for a 5-Year-Old:
This is like a text message conversation. As soon as your friend sends you a message (an event), your phone buzzes, and you can read and reply to it instantly. You don't wait for all of your friend's messages to arrive at the end of the day in a big bundle. You react to each one as it comes in. -
Python Code Snippet (Conceptual Stream Processor):
This script uses a Python generator to simulate a real-time stream of financial transactions. Theprocess_streamfunction inspects each transaction the moment it "arrives."import time import random def real_time_transaction_stream(): """A generator to simulate a never-ending stream of transactions.""" transaction_id = 0 while True: yield { 'id': transaction_id, 'amount': round(random.uniform(1.0, 2000.0), 2), 'timestamp': time.time() } transaction_id += 1 time.sleep(random.uniform(0.5, 2.0)) # Simulate time between events def process_stream(stream, fraud_threshold=1000.0): """Processes each event from the stream as it arrives.""" print(f"Starting stream processor. Fraud alert threshold: ${fraud_threshold}") for event in stream: # Process one event at a time print(f" -> Received transaction {event['id']} for ${event['amount']:.2f}") if event['amount'] > fraud_threshold: # Real-time reaction print(f"!!! ALERT: Potential fraud detected! Transaction {event['id']} of ${event['amount']:.2f} exceeds threshold.") # In a real system, this would run forever. We'll simulate for a few events. try: transaction_generator = real_time_transaction_stream() # Create a limited version of the generator for this example limited_stream = (next(transaction_generator) for _ in range(5)) process_stream(limited_stream) except StopIteration: print("Stream simulation ended.")
3. Choosing Your Model: Latency, Throughput & Cost Trade-offs (Medium)
Choosing between batch and stream processing is a classic system design trade-off. The decision hinges on business requirements, not just technical superiority.
To get a clear overview of these trade-offs, let's turn to a well-structured guide.
stream processing vs batch processing: A Data Strategy ...
The article 'stream processing vs batch processing' provides an excellent breakdown of the core differences in architecture, performance, and cost.
Please read the following sections from the article: Understanding The Core Difference and the Quick Comparison table. A Tale of Two Architectures: Pay close attention to the descriptions of a batch pipeline (ETL) and a stream processing pipeline. Analyzing Performance: Focus on how latency, throughput, and cost profiles differ. Choosing The Right Model For Your Use Case: Read the concrete examples for both stream and batch processing.
Here is a summary of the key trade-offs you must be able to articulate in an interview:
| Attribute | Batch Processing | Stream Processing |
|---|---|---|
| Data Scope | Bounded: Processes finite datasets with a clear start and end. | Unbounded: Processes a potentially infinite, continuous flow of data. |
| Latency | High: Minutes to hours. Data freshness is tied to the job schedule. | Low: Milliseconds to seconds. Provides near real-time insights. |
| Throughput | Optimized for high volume. Can process petabytes in a single job. | Optimized for high velocity. Can ingest millions of events per second. |
| Cost Profile | Lower baseline cost. Compute resources can be spun up for a job and then shut down. | Higher baseline cost. The system must be "always-on" to process events 24/7. |
| Primary Goal | Deep, historical analysis; comprehensive reporting. | Immediate reaction and analysis; real-time monitoring. |
| Example | End-of-month financial reporting. | Real-time credit card fraud detection. |
Real-world Example (Netflix):
- Batch Pipeline: A nightly job that processes all of the viewing data from the previous 24 hours to calculate and update the "Top 10 in your country" list. Accuracy and completeness are key; a 12-hour delay is acceptable.
- Stream Pipeline: When you hit pause on a show, an event is processed in real time to save your exact timestamp. When you resume on another device, that real-time data is used to start playback from the correct spot. Latency is critical for a good user experience.
4. Practical Design: A Batch Pipeline for Analytics (Hard)
Let's ground these concepts in a realistic system design problem. Imagine you're at Meta, designing a system to aggregate ad clicks for advertisers. The primary requirement is to provide a dashboard where advertisers can see how many clicks their ads received yesterday.
This scenario, where data freshness is measured in days or hours, is a perfect fit for a batch pipeline.
Design an Ad Click Aggregator w/ a Ex-Meta Staff Engineer System Design Interview
Let's watch an ex-Meta Staff Engineer design this exact system. Pay close attention to his initial, batch-oriented design. This is a highly realistic example of what a good v1 architecture looks like for an analytical workload.
Watch the 'high level design' section where the batch architecture is drawn out (14:20 - 26:55). Focus on: The initial data flow: clicks are stored in a write-optimized database (Cassandra). The introduction of a scheduled Spark job to perform aggregation. The reasoning for two databases: a write-optimized store for raw events and a read-optimized OLAP store for aggregated results.
-
Technical Explanation:
The architecture proposed in the video follows a classic ELT pattern suitable for batch processing:- Extract & Load: A
Click Processorservice receives click events and immediately loads them into a write-optimized, scalable database like Cassandra. This raw data store is optimized to handle a massive influx of writes (high velocity). - Transform: A scheduled
Cron Jobkicks off anApache Sparkjob (e.g., every hour or once a day). This job reads a large batch of raw click events from Cassandra. - Aggregate: The Spark job performs a massive-scale
MapReduceoperation to transform the raw data, aggregating clicks by ad ID and time window (e.g., per minute). - Load (to Analytics DB): The aggregated, structured results are loaded into a read-optimized
OLAP Database. This database is designed for the fast analytical queries that will power the advertiser's dashboard.
- Extract & Load: A
-
Metaphor:
This is like a restaurant kitchen. The waiters (Click Processors) take raw orders (clicks) and put them on a spike (Cassandra) all evening. It's fast and simple. At the end of the night, the head chef (Spark) takes all the orders from the spike, calculates how many of each dish were sold (aggregation), and writes the final totals on a small whiteboard (OLAP DB) for the manager to review in the morning. -
Python Code Snippet (Conceptual PySpark Batch Aggregation):
This is how the core logic inside the Spark job might look.# This is a conceptual PySpark code snippet. It will not run without a Spark environment. from pyspark.sql import SparkSession from pyspark.sql.functions import count, window # Assume 'spark' is a SparkSession entry point # Assume 'raw_clicks_df' is a DataFrame read from the raw data source (e.g., Cassandra) # with columns: 'ad_id', 'timestamp' # TRANSFORM: Aggregate clicks into 1-minute windows aggregated_df = raw_clicks_df \ .groupBy( window("timestamp", "1 minute"), # Group events into 1-minute tumbling windows "ad_id" # And also group by the ad ID ) \ .agg(count("*").alias("total_clicks")) # Count the clicks in each group # LOAD: Write the aggregated batch results to a read-optimized store # In a real system, this would write to an OLAP database or data warehouse. aggregated_df.write.mode("overwrite").parquet("analytics_data/") # The advertiser dashboard would now query these pre-aggregated results.
5. Evolving the Design: Streaming and Hybrid Architectures (Harder)
The batch design is great for cost and throughput, but what if the business demands faster insights? "I don't want to wait until tomorrow, I want to see clicks from the last 5 minutes!" This is where the design must evolve.
-
Technical Explanation:
To solve the latency problem, we replace the batch components with a streaming architecture.- From DB to Stream: Instead of writing to Cassandra, the
Click Processornow sends events to aKinesisstream (or Kafka). - From Batch to Stream Processor: The scheduled Spark job is replaced with an always-on
Apache Flinkjob that reads from the stream in real-time. Flink maintains the aggregations in memory, flushing updates to the OLAP database every minute (or even every few seconds).
However, this introduces a new risk: what if the streaming processor has a bug or goes down, leading to inaccurate counts? This leads to a Hybrid Architecture.
- Hybrid Model (Speed + Reconciliation): We keep the real-time streaming pipeline for low-latency dashboards. But, we also run the original, robust Spark batch job once a day. This batch job re-processes all the raw events (which are also dumped to a data lake like S3 from Kinesis) and reconciles any discrepancies in the OLAP database. This gives us the best of both worlds: fast, near-real-time data that is eventually corrected to be 100% accurate.
This practical, hybrid approach is a variation of well-known patterns like the Lambda and Kappa architectures.
- From DB to Stream: Instead of writing to Cassandra, the
Design an Ad Click Aggregator w/ a Ex-Meta Staff Engineer System Design Interview
Let's return to the system design interview. Watch how the engineer evolves his batch design first to streaming to solve for latency, and then to a hybrid model to solve for data integrity. This is a masterclass in articulating design trade-offs.
Watch the following deep-dive sections: Latency Issues (29:36 - 35:21): See how the Spark batch job is replaced with Kinesis and Flink to achieve real-time processing. Fault Tolerance and Data Integrity (41:22 - 49:04): Pay close attention to the concept of 'periodic reconciliation' using a daily Spark job to ensure high data integrity. This is the hybrid model in action.
Test your understanding! (Interview-Style Question)
You are designing a data pipeline for a ride-sharing app like Uber. The business has two critical needs:
- For Riders/Drivers: Real-time location updates must be visible on the app map with a latency of no more than a few seconds.
- For City Planners: The company provides a daily report to the city about traffic congestion patterns, which is used for urban planning. A 24-hour delay is acceptable for this report.
Propose a high-level data pipeline architecture. Should you use batch, streaming, or a hybrid model? Justify your choice and describe the key components for each data flow.
Show answer
I would propose a hybrid architecture because the two requirements have fundamentally different latency needs. A single processing model would be inefficient and fail to meet one of the requirements.
The architecture would have two distinct paths originating from the same raw data source:
1. Real-Time Streaming Path (for App Location Updates):
- Data Source: GPS location events from driver apps.
- Ingestion: Events are published to a high-throughput, low-latency message broker like Apache Kafka or AWS Kinesis. Each event would contain
driver_id,latitude,longitude, and atimestamp. - Processing: An always-on stream processing service (built with Apache Flink or Spark Streaming) subscribes to the Kafka topic. It does minimal transformation—perhaps just validating the data or enriching it with driver status (e.g., 'on-trip', 'available').
- Delivery: The processed location data is pushed to a low-latency key-value store or a specialized geospatial database (e.g., Redis with location features). The rider and driver apps would query this system to display real-time locations on the map.
- Justification: This path is optimized exclusively for low latency, meeting the sub-second requirement for the user-facing application.
2. Batch Processing Path (for City Planner Reports):
- Data Source: The same GPS location events from the driver apps.
- Ingestion & Storage: A separate process consumes the events from the same Kafka topic and dumps the raw event data into a cheap, scalable data lake (e.g., Amazon S3), partitioned by hour. This creates a durable, historical archive of all location data.
- Processing: A daily scheduled batch job using Apache Spark runs once every 24 hours. This job reads the entire previous day's worth of raw GPS data from the data lake.
- Transformation: The Spark job performs heavy-duty transformations: it aggregates data into geographic grids, calculates average speeds, identifies periods of high congestion, and joins it with other datasets (like weather or public holidays).
- Delivery: The final, aggregated report is loaded into a data warehouse or a reporting database, where it can be queried by city planners via a BI tool.
- Justification: This path is optimized for high-volume throughput and cost-effectiveness. It allows for complex, large-scale analysis on a complete historical dataset, which is exactly what's needed for the daily report.
By using a hybrid model, we use the right tool for each job: streaming for speed, and batch for deep, cost-effective analysis, all while using a single source of raw data.
Conclusion
You have now learned how to design data pipelines by choosing the appropriate processing model based on clear business trade-offs. This is a core competency for any senior data or backend engineer.
Key Takeaways:
- Batch Processing is the scheduled workhorse, ideal for historical analysis where latency is not a primary concern. It prioritizes throughput and cost-efficiency.
- Stream Processing is the real-time reactor, essential for use cases requiring immediate action and low-latency insights.
- The Choice is a Trade-off between latency, throughput, data scope (bounded vs. unbounded), and cost.
- Hybrid Architectures are common in the real world. They combine the speed of streaming for real-time views with the accuracy and reliability of batch processing for reconciliation and deep analysis, giving you the best of both worlds.
Next Lesson Preview
We've designed pipelines to ingest and process data. But what happens when the structure of that data changes? A source system might add a new field, remove an old one, or change a data type. If not handled properly, this "schema drift" can break your entire pipeline. In our next lesson, we will learn how to Manage schema evolution in data pipelines using strategies like schema-on-read and schema registries.