← Back to blog

Data Pipeline Engineering: ETL Architecture from Collection to Storage

ETL (Extract, Transform, Load) is the most fundamental — and most error-prone — part of data engineering. This article covers key decisions in pipeline design: incremental vs full load, batch vs stream processing, and data quality assurance — for backend engineers and data engineers building or optimizing data pipelines.

The Bottom Line: Data Pipelines Look Simple, But Are Full of Traps

ETL requirements sound simple — “move data from A to B, do some cleaning in between.” But in practice, edge cases, data quality, failure recovery, and performance issues turn simple tasks into complex ones.

This article covers three stages: Extract, Transform, Load — with key decisions and engineering practices for each.


1. Extract

1.1 Extraction Modes

ModeBest ForProsCons
Full loadSmall tables (< 1M rows)Simple, no incremental logicPoor performance at scale
Incremental loadLarge tables (> 10M rows)Pulls only changed dataHandles deletes and updates
Log subscriptionReal-time data (CDC)Non-invasive, real-timeComplex, requires message queue

1.2 Three Incremental Methods

Timestamp: Source table has updated_at field, pull WHERE updated_at > last_sync. Simplest, but cannot handle deletes.

Version number: Source table has version field, incremented on each update. Reliable, but requires source system cooperation.

CDC (Change Data Capture): Capture changes via database binlog or WAL (e.g., Debezium + Kafka). Most real-time, but most complex architecture.

Recommendation: Start with timestamps, add soft-delete (deleted_at) for delete detection, and only consider CDC when scale demands it.


2. Transform

2.1 Common Transform Types

// Clean: remove nulls, deduplicate, normalize format
function clean(row: RawRow): CleanRow {
  return {
    id: row.id,
    email: row.email?.toLowerCase().trim(),
    name: row.name?.trim() || 'unknown',
    created_at: new Date(row.created_at).toISOString(),
  };
}

// Transform: type mapping, unit conversion, enum mapping
function transform(row: CleanRow): TargetRow {
  return {
    ...row,
    status: STATUS_MAP[row.status] ?? 'unknown',
    amount: Math.round(row.amount * 100),
  };
}

2.2 Data Quality Checks

After each transform step, check three things:

CheckMethodThreshold
Row countSource rows vs current rowsAlert if deviation > 5%
Null rateNull percentage of key fieldsAlert if > 1%
Distribution shiftMean/percentile changeAlert if deviation > 10%

2.3 Idempotency

Every task must be rerunnable. Core principle: running the same task multiple times produces the same result.

const batchId = uuid();
await db.query(`DELETE FROM target_table WHERE batch_id = $1`, [batchId]);
await db.query(`INSERT INTO target_table SELECT *, $1 as batch_id FROM staging`, [batchId]);

3. Load

3.1 Loading Strategies

StrategyMethodBest For
Full replaceTRUNCATE + INSERTSmall tables, daily full
Incremental mergeUPSERT (INSERT ON CONFLICT)Large tables, incremental
Partition swapWrite to temp table → atomic swapLarge tables, full refresh

3.2 Partition Swap Implementation

CREATE TABLE target_20260722 (LIKE target INCLUDING ALL);
INSERT INTO target_20260722 SELECT * FROM staging;

BEGIN;
DROP TABLE IF EXISTS target_old;
ALTER TABLE target RENAME TO target_old;
ALTER TABLE target_20260722 RENAME TO target;
COMMIT;

DROP TABLE IF EXISTS target_old;

Atomic swap: queries are unaffected during the swap, and rollback is fast if the swap fails.


4. Monitoring and Alerting

check_pipeline() {
  local last_run=$(psql -t -c "SELECT max(created_at) FROM pipeline_log" | xargs)
  local now=$(date -u +%s)
  local diff=$((now - $(date -d "$last_run" +%s)))
  if [[ $diff -gt 3600 ]]; then
    echo "Pipeline has not run for over 1 hour" | send_alert
  fi
}

Summary

StageKey DecisionRecommended Practice
ExtractFull vs incrementalSmall tables full, large tables incremental (timestamp)
TransformData qualityCheck row count, null rate, distribution per step
LoadIdempotencyBatch ID + delete-then-insert
RecoveryFailure retryExponential backoff, max 3 retries
MonitoringPipeline healthCheck last run time, alert on threshold breach

The core of a data pipeline is not “moving fast” — it is “moving stably, accurately, and recoverably.” Idempotent design is far more important than performance optimization. A pipeline that can be safely rerun with consistent results is worth a hundred times more than one that is fast but breaks without recovery.

Need data collection or ETL pipeline design? Contact us — tell us about your data sources and scale, feasibility within 24 hours.

FAQ

Batch or stream processing — which should I choose?

It depends on latency requirements. Batch processing is suitable for non-real-time scenarios (daily reports, offline analysis) — simple to implement, low cost, easy to rerun. Stream processing is for second-level or minute-level response (real-time monitoring, risk control, recommendations) — complex to implement, high cost. Recommendation: start with batch processing for most scenarios, introduce stream processing (Kafka + Flink) only when real-time needs are confirmed.

Incremental or full load?

For data < 1M rows, full load is simplest — pull everything daily, replace everything. No need to handle incremental changes. For data > 10M rows, incremental load is almost mandatory — pull only changed data since the last sync, using timestamps or version numbers. The 1M-10M range is a gray area depending on database performance and update frequency.

How do you ensure data quality?

Three-layer checks: ① Source check — schema validation before data enters the pipeline, reject unexpected data; ② In-pipeline check — completeness checks after each transform step (row count, null rate, distribution shift); ③ Target check — reconciliation after loading (source count vs target count), alert on mismatch. Catch issues early — the earlier you find them, the easier they are to fix.

What if an ETL task fails?

Idempotency is the most important design principle — rerunning the same task should produce the same result. Implementation: ① Generate a unique batch ID for each task, record it in the target table; ② On failure, use the batch ID to clear partially written data, then rerun; ③ Set up retry with exponential backoff (max 3 retries), alert on persistent failure for manual intervention.

This article comes from AI Enable Harness front-line delivery practice. Need a similar system or optimization service?

📡 Also published on: CSDN 知乎

Subscribe to Updates

Get notified when new articles are published. No spam, occasional updates only.

Subscribe →