SaaS API Connector with Incremental Sync

A medium Pipeline Design interview practice problem on DataDriven. Design the pipeline on the canvas: sources, transforms, storage and checks, then get feedback on it.

Domain
Pipeline Design
Difficulty
medium
Seniority
L5

Problem

A third-party CRM holds about 40 million records behind an API whose schema changes without notice. The warehouse needs a full historical copy of them plus ongoing incremental updates. Design the pipeline that keeps the warehouse in sync with this API.

The API has rate limits. You have deadlines.

Business requirements

Worked solution and explanation

What this problem really is

This is a state-management problem dressed up as an API connector. Anyone can page through an endpoint and load a table. What separates candidates is who owns the state between runs: the cursor, the rate-limit budget, and the memory of which ids existed last time. Skip that and the warehouse drifts from the CRM. Deleted records stay forever, because an API that stops returning a row never sends you an event about it.

Step 1: Sync incrementally inside the freshness window

The orchestrator triggers api_extractor on an hourly cadence. The extractor reads the last cursor from incremental_tracker, pulls only rows changed since then, and writes the new cursor back. The first run has an empty cursor, so the historical backfill and the ongoing sync are the same code path. A nightly dump misses the hours-fresh dashboard and repeats the full rate-limit cost every night.

Step 2: Pace under the cap and back off on 429s

api_extractor runs as a single task that throttles itself below the vendor's fixed rate, and on a 429 it retries with exponential backoff. Because the cursor only advances after a page lands, a retry resumes instead of restarting. The weekly failure comes from hammering the cap and then redoing everything.

Step 3: Land raw, then normalize

api_extractor writes the payload untouched into raw_landing. schema_normalizer maps it into warehouse columns. The vendor changes schema without notice, so a rename should break only the mapping, never the fetch.

Step 4: Reconcile the id set to catch deletions

On a slower cadence, reconciliation_job pulls only the current ids from the API and merges the result into warehouse_target. Any id the vendor no longer returns is marked deleted. Cursor sync cannot do this, since it only sees rows that changed.

The reference design

Cursor-driven extraction that paces and backs off its own API calls, raw landing before normalizing, and a separate reconciliation path that marks vanished ids as deleted.
  • vendor_api (source, API)
  • orchestrator (transform, Airflow)
  • incremental_tracker (storage, PostgreSQL)
  • api_extractor (transform, Pandas); parallelism: 1 task; on error: retry; retries: 5; backoff: exponential
  • raw_landing (storage, object storage); backfill: partition_overwrite
  • schema_normalizer (transform, Spark)
  • reconciliation_job (transform, Spark)
  • warehouse_target (storage, warehouse); freshness: < 2h; idempotency: upsert
  • vendor_api -> api_extractor
  • orchestrator -> api_extractor
  • orchestrator -> reconciliation_job
  • incremental_tracker -> api_extractor
  • api_extractor -> incremental_tracker
  • api_extractor -> raw_landing
  • raw_landing -> schema_normalizer
  • schema_normalizer -> warehouse_target
  • vendor_api -> reconciliation_job
  • reconciliation_job -> warehouse_target
Nightly full dump
Pages the whole API every night, so one 429 mid-run means starting again. Data is a day stale, and deleted rows stay in the warehouse because nothing removes them.
Cursor plus reconciliation
Pulls only changed rows within the freshness window and resumes after a backoff. A slower reconciliation_job removes deleted rows. The cost is extra state and a second, smaller API budget.

Common follow-up questions