Data Ingestion and Source System Integration Questions
Getting data out of heterogeneous source systems and landing it reliably: APIs, operational databases, file drops, webhooks, message queues and third-party SaaS. Covers connector selection and design (managed platforms versus Debezium, DMS or Kafka Connect versus building your own), pull versus push and polling versus webhook patterns, incremental extraction and high-watermark strategy including what to do when a source offers no native change capture, authentication and credential rotation against third-party APIs, source-side rate limits and quotas, schema drift and contract breakage at the source boundary, backfill and replay of history, ingestion-time data-quality gates, reconciliation after a source outage, and negotiating with source-system owners. The scope stops at the boundary: once data has landed, transforming it, the architecture of the pipeline that carries it, stream-processing mechanics, and pipeline monitoring are all covered separately.
You are ingesting data from multiple third-party APIs that use OAuth2 and rotating API keys. Describe how you would securely store and refresh credentials, handle a token-refresh failure without losing data, enforce each source's rate limits, and design retry and backoff so ingestion stays reliable and auditable.
Sample Answer
Direct answer
Credential management for many third-party connectors comes down to three disciplines: store every credential in a secrets manager and never in connector configuration files, refresh OAuth2 tokens proactively before they expire rather than reactively after a call fails, and treat a refresh failure as a distinct, alertable condition rather than letting it silently degrade into skipped syncs. Rate limits and retry behavior then have to be tracked per source, since every third-party API enforces its own limit differently.
Structured elaboration
Secure storage
- Store client IDs, client secrets, and refresh tokens in a dedicated secrets manager (a cloud provider's secrets service or an equivalent), never in a connector's plain configuration file or in version control.
- Scope access narrowly: the connector process should have permission to read only the credentials it needs, not every secret in the organization's store, so a compromised connector cannot pivot to unrelated systems.
Refreshing tokens without losing data
- Refresh proactively, ahead of expiry (for example, when a token has less than 10 minutes of validity left), rather than waiting for a call to fail with a 401 and refreshing reactively; this avoids losing an in-flight batch to an expired token mid-pull.
- If a refresh does fail (the refresh token itself is invalid or revoked), do not let ingestion silently stop: raise a distinct, named alert ("source X requires re-authorization") rather than letting the connector fail the same generic way it would for a transient network error, since these need different human responses.
- Persist enough state that a connector recovering from a refresh failure resumes from its last successful checkpoint rather than needing a full re-pull once access is restored.
Enforcing each source's rate limit
- Each third-party API documents its own limit differently (requests per second, per minute, per day, sometimes per specific endpoint); track each source's limit as explicit configuration rather than a single global assumption.
- A token-bucket (a counter that starts at some capacity, refills by a fixed amount on a fixed schedule such as once per second, and is spent one token per request, so you physically cannot send faster than the refill rate once the initial balance is used up) or sliding-window (which instead counts how many requests actually landed in the trailing N-second window and blocks new ones once that count hits the cap) limiter per source, refilled or evaluated at that source's documented rate, keeps you comfortably under the cap without needing to guess a safe interval empirically.
Retry and backoff, and making it auditable
- Exponential backoff with jitter on 429s and 5xxs, capped so a persistent failure surfaces as an alert instead of retrying forever.
- Log every token refresh, every rate-limit-triggered backoff, and every retry with enough context (which source, which credential, timestamp) that an audit can reconstruct exactly what happened to a given source's access over time, which matters both for debugging and for satisfying a security review.
Worked example
A connector integrates with 12 different third-party sources, each with its own OAuth2 app registration and its own rate limit. Credentials for all 12 live in a secrets manager, tagged by source, with the connector's service identity granted read access only to that tag group. A background refresh job checks each source's token expiry every 5 minutes and refreshes any token with under 10 minutes of remaining validity, well before any extraction job would hit it expired. When source #7's refresh token is revoked (an admin at the third-party company deauthorized the app), the refresh job's next attempt fails distinctly, raising a "source 7 needs re-authorization" alert rather than the generic "sync failed" alert every other source's transient hiccup produces, so the on-call engineer knows immediately this needs a human to click through an OAuth consent screen again, not a routine retry.
Trade-offs & pitfalls
- Reactive-only token refresh (refresh on the first 401) is simpler to implement but risks losing an in-flight extraction to a mid-batch expiry, especially for a slow, long-running pull; proactive refresh is worth the extra complexity for anything beyond a trivial connector.
- Logging refresh and retry events "for audit" is only useful if the logs are actually structured and queryable; a wall of unstructured text log lines does not satisfy a real security audit request.
- A single global rate limiter across all sources under-utilizes fast sources and still risks exceeding a slow source's limit; per-source limiting is more code but is the only version that is actually correct.
- Storing a refresh token is itself a long-lived secret with real blast radius if leaked; rotate the underlying OAuth application's credentials periodically even if no incident has occurred, not only in response to one.
Implement a resilient batch uploader in Python that uploads large files to S3 using multipart uploads, with resumability and exponential backoff on failure. Describe how you would persist upload progress (the upload ID and which parts completed) so the uploader can pick up after a crash, and how you ensure a retry never produces a duplicate final object.
Sample Answer
Direct answer
The uploader has to treat "which parts are already uploaded" as durable state, not in-memory state, so a restart after a crash resumes only the missing parts, and it has to guard the final completion step so a retried completion call cannot silently create a second copy of the object. Below is a complete, runnable implementation including its own fake in-memory S3 test double, so the invariants below are proven by execution a reader can reproduce line for line, not just described.
Structured elaboration
import hashlib
class UploadProgressStore:
# Stand-in for a durable side-table (e.g. a DB row) tracking (upload_id, parts_done).
def __init__(self):
self._store = {}
def save(self, key, upload_id, part_number):
rec = self._store.setdefault(key, {"upload_id": upload_id, "parts_done": set()})
rec["parts_done"].add(part_number)
def load(self, key):
return self._store.get(key)
def resilient_multipart_upload(s3, progress, key, chunks, max_retries=3):
existing = progress.load(key)
if existing and existing["upload_id"] in s3.upload_ids.values():
upload_id = existing["upload_id"]
done = existing["parts_done"]
else:
upload_id = s3.create_multipart_upload(key)
done = set()
for i, chunk in enumerate(chunks, start=1):
if i in done:
continue # already uploaded before the crash; do not re-send
attempt = 0
while True:
try:
s3.upload_part(upload_id, i, chunk)
progress.save(key, upload_id, i)
done.add(i)
break
except ConnectionError:
attempt += 1
if attempt > max_retries:
raise
# real exponential backoff sleeps here; omitted in the test harness
etag = s3.complete_multipart_upload(key, upload_id)
return etag
# --- Fake S3 test double, shipped alongside the uploader so the worked example below
# --- is actually reproducible, not just narrated. ---
class FakeS3:
def __init__(self):
self.upload_ids = {}
self.parts = {}
self.completed = set()
self.crash_after_part = None # test hook: raise SystemExit right after this part lands
self.fail_once_on_part = None # test hook: raise ConnectionError once on this part
self._failed_already = set()
self._counter = 0
def create_multipart_upload(self, key):
self._counter += 1
upload_id = f"upload-{self._counter:04d}"
self.upload_ids[key] = upload_id
self.parts[upload_id] = {}
return upload_id
def upload_part(self, upload_id, part_number, chunk):
if self.fail_once_on_part == part_number and part_number not in self._failed_already:
self._failed_already.add(part_number)
raise ConnectionError(f"simulated transient failure on part {part_number}")
self.parts[upload_id][part_number] = chunk
if self.crash_after_part == part_number:
raise SystemExit(f"CRASH injected after part {part_number}")
def complete_multipart_upload(self, key, upload_id):
if upload_id in self.completed:
raise RuntimeError(f"upload_id {upload_id} already completed; refusing duplicate completion")
ordered = [self.parts[upload_id][i] for i in sorted(self.parts[upload_id])]
part_md5s = b"".join(hashlib.md5(p).digest() for p in ordered)
etag = hashlib.md5(part_md5s).hexdigest() + f"-{len(ordered)}"
self.completed.add(upload_id)
return etag
Resumability comes from UploadProgressStore: every completed part is persisted immediately, so on restart the uploader loads parts_done and skips exactly those parts rather than re-uploading the whole file or losing track of where it was. Duplicate-object protection comes from the completion step: a real S3 rejects a second CompleteMultipartUpload call against an upload_id that has already been completed, and FakeS3 enforces the same guard, so a client that retries a completion call after a false-negative acknowledgment fails loudly instead of silently producing a second write. (Note: real S3's multipart ETag format is itself <hex md5 of the concatenated per-part md5 digests>-<part count>, which FakeS3.complete_multipart_upload reproduces exactly, rather than a bare content hash, for realism.)
Worked example
Running this against an 8000-byte, 8-part file with a hard-crash injected after part 4 (the driver code below is exactly what was run to produce this output; run it yourself with python3 this_file.py):
def chunk_file(data, n_parts):
size = len(data)
part_size = size // n_parts
chunks = []
for i in range(n_parts):
start = i * part_size
end = size if i == n_parts - 1 else (i + 1) * part_size
chunks.append(data[start:end])
return chunks
def reassemble(s3, upload_id):
return b"".join(s3.parts[upload_id][i] for i in sorted(s3.parts[upload_id]))
if __name__ == "__main__":
source_data = bytes((i % 256) for i in range(8000))
chunks = chunk_file(source_data, 8)
print("--- Attempt 1: crash injected mid-upload (after part 4) ---")
s3 = FakeS3()
progress = UploadProgressStore()
s3.crash_after_part = 4
try:
resilient_multipart_upload(s3, progress, "bigfile.bin", chunks)
except SystemExit as e:
print("crash caught as expected:", e)
upload_id = s3.upload_ids["bigfile.bin"]
print("parts uploaded before crash:", sorted(s3.parts[upload_id].keys()))
print("progress store after crash:", progress.load("bigfile.bin"))
print("\n--- Attempt 2: process restarts, resumes from progress store ---")
s3.crash_after_part = None
etag = resilient_multipart_upload(s3, progress, "bigfile.bin", chunks)
print("final etag:", etag)
print("parts uploaded total:", sorted(s3.parts[upload_id].keys()))
print("resume correctness verified:", reassemble(s3, upload_id) == source_data)
print("\n--- Attempt 3: a retried completion call for the same key must not duplicate ---")
try:
resilient_multipart_upload(s3, progress, "bigfile.bin", chunks)
print("BUG: retry did not raise")
except RuntimeError as e:
print("guarded correctly:", e)
print("\n--- Attempt 4: a transient failure on ONE part during a fresh upload still converges ---")
s3b = FakeS3()
progress_b = UploadProgressStore()
s3b.fail_once_on_part = 3
etag2 = resilient_multipart_upload(s3b, progress_b, "bigfile2.bin", chunks)
print("final etag (transient failure recovered):", etag2)
print("etags match crash-free vs transient-recovery path:", etag2 == etag)
Actual output from running the above end to end:
--- Attempt 1: crash injected mid-upload (after part 4) ---
crash caught as expected: CRASH injected after part 4
parts uploaded before crash: [1, 2, 3, 4]
progress store after crash: {'upload_id': 'upload-0001', 'parts_done': {1, 2, 3}}
--- Attempt 2: process restarts, resumes from progress store ---
final etag: 89b30949dea82279891eb22aaa0ffbd7-8
parts uploaded total: [1, 2, 3, 4, 5, 6, 7, 8]
resume correctness verified: True
--- Attempt 3: a retried completion call for the same key must not duplicate ---
guarded correctly: upload_id upload-0001 already completed; refusing duplicate completion
--- Attempt 4: a transient failure on ONE part during a fresh upload still converges ---
final etag (transient failure recovered): 89b30949dea82279891eb22aaa0ffbd7-8
etags match crash-free vs transient-recovery path: True
Notice part 4 shows up in "parts uploaded before crash" (the bytes reached the fake S3) but NOT in "progress store after crash" (only parts 1 to 3 are marked done): the crash lands between the successful s3.upload_part call and the progress.save call that would have recorded part 4 as complete. This is a realistic and more interesting crash point than a clean cutoff, and attempt 2 shows the design handles it correctly anyway: part 4 is simply re-uploaded (an idempotent overwrite in the fake store, exactly what a real re-UploadPart call against the same part number does in genuine S3), and the reassembled object is confirmed byte-identical to the original input. Attempt 3 confirms the completion guard actually fires. Attempt 4 shows a genuinely transient single-part failure is absorbed by the retry loop and still produces the identical final etag as the crash-free path.
Trade-offs & pitfalls
- The progress store itself needs to be at least as durable as the objects you are uploading; a progress store that can be lost independently of the upload state defeats the entire point of tracking resumability.
- Real exponential backoff with jitter belongs in the retry loop shown above (the comment marks exactly where); a tight retry loop with no backoff at all can turn one transient blip into a self-inflicted rate-limit problem against S3 itself at high concurrency.
- A completed-upload guard only protects against a RETRIED completion call for the SAME
upload_id; if a caller starts an entirely new multipart upload for the same key after a false failure signal, you can still get two objects, so idempotency also needs to be enforced one level up, at the "should I even start a new upload for this key" decision. - Multipart upload part numbers and sizes have real constraints in actual S3 (a minimum part size except for the last part, a maximum part count); a production version needs to chunk according to those limits, which this simplified example sidesteps for clarity.
You are bringing a new external data source into your analytics warehouse. Design the onboarding process: schema discovery, sample-data validation, deciding how source fields map onto your warehouse's field names and types, and how you version that mapping as the source evolves.
Sample Answer
Direct answer
Onboarding a new source is a four-step process: discover its real schema by sampling actual data rather than trusting documentation alone, validate that sample against basic expectations before committing to a design, build an explicit, reviewable mapping from source fields to your warehouse's names and types, and version that mapping from day one so a later source-side change is a controlled update rather than a silent break.
Structured elaboration
Schema discovery
- Pull a representative sample of real records, not just the source's published schema documentation, since documentation drifts from reality and undocumented fields or inconsistent types are common in practice.
- Note nullability empirically: a field the docs call required may in practice arrive null or missing in some fraction of real records, and your mapping needs to handle that reality, not the aspirational documentation.
Sample-data validation
- Before committing to a mapping, check the sample against basic sanity expectations: are IDs actually unique, do date fields parse, do numeric fields fall in a plausible range.
- Flag anything surprising (an unexpectedly high null rate, a type that varies record to record) as a question for the source-owning team before building the mapping around a guess.
Field mapping
- Build an explicit table: source field name and type to warehouse field name and type, with an entry for every field you are choosing to bring in, not an implicit "just copy everything over."
- Decide and document a specific rule for each common mismatch case: what happens when a field is missing (a default value, or reject the row), what happens on a type mismatch (coerce, or reject), and how you normalize values that can be written multiple equivalent ways (phone numbers, email addresses, date formats).
Versioning the mapping
- Store the mapping itself as versioned configuration, not implicit logic buried in code, so you can see exactly what changed between mapping version 3 and version 4 and when.
- When the source adds a field, extend the mapping additively; when it changes a field's meaning or removes one, that is a breaking mapping change and should go through the same review and rollout discipline as any other breaking schema change, not be silently absorbed.
Worked example
Onboarding a new source that combines data from five upstream systems, each with its own schema, onto one warehouse target: discovery starts by sampling real records from all five, which reveals that what the documentation calls a shared "customer_id" field is actually three different identifier schemes across the five systems (an internal UUID in two of them, an email address in one, and a legacy integer key in the other two). The mapping has to include an explicit reconciliation step, not just a rename, translating each system's identifier into one canonical customer key using an identity-resolution rule agreed with the source-owning teams. That mapping is captured as versioned configuration (mapping v1), and six months later when one of the five systems migrates to a new internal ID scheme, the change ships as mapping v2, a reviewable, diffable update, rather than a code change nobody remembers making being discovered only when downstream numbers stop reconciling.
Trade-offs & pitfalls
- Trusting the source's documented schema without sampling real data is the single most common cause of an onboarding that looks complete but breaks on the first edge case the sample would have caught.
- A mapping with no explicit rule for "field is missing" or "type does not match" tends to accumulate ad hoc, undocumented special cases in code as real-world edge cases are discovered one at a time in production, instead of upfront.
- Unversioned mappings make root-causing a downstream data-quality issue much harder: without a mapping history, "did this change three weeks ago or was it always like this" becomes archaeology instead of a config diff.
- Over-normalizing at mapping time (forcing every source's quirks into one rigid shape immediately) can lose information a downstream consumer actually needed; when in doubt, preserve the original value alongside the normalized one rather than discarding it.
Explain pull-based and push-based data ingestion models. For each, give concrete examples (polling a REST API or periodic file fetch versus webhooks or event streams), and compare latency, throughput, operational complexity, load on the source, error and retry behavior, and typical failure modes in production.
Sample Answer
Direct answer
Pull is you initiating contact with the source on your own schedule, for example polling a REST API or fetching a file drop; push is the source initiating contact with you, for example a webhook call or a message it publishes to a stream you subscribe to. Pull gives you full control over pacing and load on the source, at the cost of built-in latency between when something happens and when you notice. Push gives you near-real-time delivery, at the cost of needing to be reliably available to receive it and coordinate with whatever retry behavior the source uses when you are not.
Structured elaboration
Pull
- Concrete examples: polling a REST endpoint every N minutes, fetching a nightly file drop via SFTP (SSH File Transfer Protocol) or from S3, running a scheduled SQL query against a source database.
- Latency: bounded below by your polling interval; a change occurring right after a poll will not be seen until the next one.
- Throughput and source load: you control the request rate directly, which is good for respecting a source's capacity, but a poorly tuned interval can either waste calls when nothing changed or lag badly when a lot changed.
- Operational complexity: you own scheduling, checkpoint tracking, and retry logic; the source does not need to know or care about you.
- Failure modes: a missed poll (your job did not run) simply gets caught on the next poll if your extraction is incremental; the risk is a silent scheduler failure going unnoticed for a while.
Push
- Concrete examples: an inbound webhook call from a payment processor, a message a source publishes to a queue or event stream you consume.
- Latency: near-real-time, since the source notifies you the moment something happens rather than you having to ask.
- Throughput and source load: the source decides the rate, which can spike unpredictably; you need to be able to absorb bursts without falling over.
- Operational complexity: you must run a reliably-available receiver (an endpoint or a consumer), and you inherit whatever the source's own retry and ordering guarantees are, or are not.
- Failure modes: if your receiver is down when a push arrives, you depend entirely on the source retrying it; some sources retry aggressively, some drop the event, and a few offer no redelivery at all.
How to choose
- Freshness requirement: sub-minute or real-time needs generally rule out pure polling.
- Source support: you cannot choose push if the source does not offer it; not every system has webhooks or a stream to subscribe to.
- Control versus availability: pull lets you throttle yourself to protect a fragile source; push demands your receiver be highly available, since you cannot control when the source sends.
- Operational maturity: a small team with no on-call receiver infrastructure may be better served starting with pull, even at some freshness cost, and moving specific sources to push as reliability matures.
Worked example
A team gathering training data for a model has three needs: (a) a large historical backfill of past user actions, (b) online feature updates that must reflect a user's most recent action within seconds, and (c) periodic collection of new human feedback labels. For (a), pull is the only sensible choice: there is no "event" to push, it is a bulk historical extraction, typically against an API or a warehouse export. For (b), push is close to mandatory, since seconds-level freshness is well below what any reasonable polling interval could deliver without hammering the source. For (c), pull on a modest schedule (hourly or daily) is usually sufficient, since new labels do not need to reach the training pipeline instantly, and a scheduled pull is far simpler to operate than standing up a webhook receiver just for this.
Trade-offs & pitfalls
- A common mistake is polling far too aggressively "to reduce latency," which just moves the bottleneck onto the source's rate limits without meaningfully improving freshness once you are polling faster than data actually changes.
- Push without idempotent handling on your side is a duplicate-processing incident waiting to happen, since almost every push-based source will retry a delivery it believes may have failed, even when you actually received and processed it.
- Do not assume push is strictly better because it sounds more modern; a source with unreliable delivery and no replay mechanism can lose data silently in a way a well-designed poll with checkpointing cannot.
- Micro-batching (short, frequent pulls, seconds to low minutes) is a real middle ground worth naming explicitly: it gets you most of push's freshness without needing a highly-available receiver.
When you are choosing a connector for the source or sink side of an ingestion pipeline, what do you actually evaluate? Walk through reliability, offset/checkpoint management, schema support, latency and throughput, security, and operational maturity, and explain how the calculus differs between a managed connector, a cloud-native connector, and something you build yourself.
Sample Answer
Direct answer
Choosing a connector, on either the source or the sink side, comes down to six things: how reliably it delivers data, how it tracks and persists progress (its offset or checkpoint model), how well it understands and communicates the source or target's schema, whether its latency and throughput fit your freshness needs, how it handles authentication and secrets, and how mature it is to actually operate day to day. A managed connector, a cloud-native one, and something you build yourself trade these off differently, and the right choice depends on which of the six actually matters most for this particular integration.
Structured elaboration
Reliability
- What delivery guarantee does it actually provide: at-least-once, at-most-once, or something closer to exactly-once via idempotent writes (writes that produce the same end result even if the same write is accidentally repeated, for example because a retry re-sends a call that actually succeeded the first time, so a retry never creates a duplicate)? Most connectors are honestly at-least-once; treat any "exactly-once" claim skeptically until you have seen how it is implemented.
- How does it behave on a transient failure: does it retry automatically, or does it require manual intervention to resume?
Offset and checkpoint management
- Does the connector track its own progress durably (so a restart resumes cleanly), and can you inspect or manually adjust that state if something needs to be replayed?
- For a source connector, this is usually a cursor or timestamp; for a sink connector, it is usually the last successfully-committed offset from the upstream topic or queue.
Schema support
- Does it understand the source or target's schema well enough to detect a breaking change, or does it treat every record as an opaque blob?
- For structured targets (a warehouse table, a typed sink), does the connector handle schema evolution (a new column, a type change) gracefully, or does it require manual reconfiguration on every source-side change?
Latency and throughput
- Is the connector fundamentally a polling design (batch-oriented, with latency bounded by the poll interval) or a streaming design (event-driven, near-real-time)? This is often the single biggest constraint on what freshness service-level agreement (SLA) you can promise.
- What is its realistic sustained throughput ceiling, and does that comfortably clear your actual data volume with headroom for growth?
Security
- How does it store and rotate credentials: a secrets manager integration, or configuration files that are easy to leak?
- Does it support the authentication model the source or target actually requires (OAuth2 with refresh tokens, mutual TLS, or cloud IAM (Identity and Access Management) roles), or only a simpler scheme that will not work for a security-conscious source?
Operational maturity
- How much observability does it expose out of the box: lag metrics, error rates, a dead-letter mechanism for records it cannot process?
- How is it upgraded, and what happens to in-flight work during that upgrade?
How the calculus differs by connector type
- A managed connector (Fivetran-style) tends to score well on operational maturity and reliability out of the box, at the cost of less visibility into exactly how it tracks offsets or handles schema changes internally.
- A cloud-native connector (a first-party AWS/GCP service) usually integrates cleanly with the platform's own IAM and secrets model, at the cost of being locked to sources and targets that specific cloud vendor supports well.
- A custom-built connector gives you full control over every one of the six dimensions, at the cost of having to implement and then operate all of them yourself, including the parts (idempotent retries, checkpoint persistence, schema-change detection) that are easy to get subtly wrong.
Worked example
A team choosing between three sink connectors for the same Kafka topic (a managed Snowflake sink, a cloud-native Kinesis Firehose-to-S3 delivery, and a custom Python consumer) needs sub-minute freshness and exactly-once-in-practice writes via a natural key. The managed Snowflake sink turns out to support exactly this pattern (a MERGE-based idempotent write keyed on a record ID) as a documented configuration option, so it wins on both fit and lowest operational burden. If the same team instead needed a target with no managed connector available at all, a proprietary internal service, the custom-build path would be forced regardless of preference, and the evaluation shifts to "how much of these six dimensions can we realistically implement well," not whether to build.
Trade-offs & pitfalls
- Do not evaluate a connector purely on throughput numbers from its marketing page; ask specifically how it behaves on failure, since that is where most real incidents originate.
- "It supports schema evolution" can mean anything from "handles a new nullable column automatically" to "requires you to manually update a mapping file"; get the specific behavior, not just the checkbox.
- A connector's offset model matters more than it looks: one that cannot be manually rewound makes recovering from a bad batch far harder than one that exposes and lets you adjust its checkpoint.
- Security is the dimension teams most often under-weight during evaluation and most regret later, particularly credential rotation, which a "quick proof of concept" connector rarely handles well from day one.
Unlock Full Question Bank
Get access to all 18 Data Ingestion and Source System Integration interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.