Data Integration Services in India
ETL and ELT pipelines, change data capture, warehouse loading and the tests that keep all of it honest, built by an engineering team in India for data and product leaders in the US, UK, Canada, Australia and New Zealand. This is bulk movement of data into somewhere you can analyse it, not request and response plumbing between two applications.
Why Do Your Dashboards Disagree With Your Source Systems?
Somebody in a leadership meeting says the number is wrong. Revenue in the warehouse is two percent under what the billing system shows, and two percent of a big number is worth arguing about. An analyst spends three days on it. The answer, when it finally arrives, is almost never interesting: a nightly job failed on Tuesday and retried in a way that skipped a window, or a refund arrived as an update to a row the pipeline had already loaded and never looked at again, or a currency column changed from cents to dollars in one upstream service and nobody told the data team.
That is what data integration work is really about. Not connecting things. Anyone can connect things. The job is making the connection survive contact with real production systems that change without warning, produce duplicates, restate history, go down at 3am, and are owned by people who have never heard of your warehouse.
There are four failures we see over and over, and they are worth naming because each one has a different fix.
The first is silent partial loads. A job pulls 40 of 60 partitions, hits a timeout, and the orchestrator marks it green because the shell command exited zero. Downstream models run happily on incomplete data. Nothing alerts. The fix is not better retries, it is a completion contract: the load declares how many rows or partitions it expected and refuses to publish when the count is short.
The second is duplication from at-least-once delivery. Every queue, every connector and every retry in this ecosystem promises at-least-once, which means at some point you will process the same record twice. If your load is a plain insert, you now have two of everything and a revenue figure that is too high. The fix lives on the load side, not the delivery side, and we get to it further down.
The third is the invisible schema change. A backend engineer adds a nullable column, which is harmless, and in the same release changes a status enum from integers to strings, which is not. Your pipeline either fails loudly, which is the good outcome, or casts silently to null, which is the bad one. Six weeks later somebody notices a segment of customers has disappeared from a report.
The fourth is cost. Data integration is the only part of a modern stack where a single careless commit can add four figures to next month's bill. A full refresh instead of an incremental build, a join without a partition filter, a dashboard set to auto refresh every five minutes across sixty users. None of those are outages. All of them show up on an invoice.
None of these problems are exotic. They are the standard failure set, and a pipeline built by someone who has seen them all before looks different from one built by someone who has not.
What a Data Integration Engagement Actually Covers
Not every engagement includes everything below, and the sequence matters more than the list. Building a streaming layer before you have a reliable nightly batch is a common and expensive inversion. What follows is the full surface area, in roughly the order we tend to build it.
Source discovery and an access plan
The first week is unglamorous and decides most of the schedule. We inventory every source, and for each one establish what the extraction mechanism will be, who owns it, whether a read replica exists, what the retention and deletion behaviour is, and whether deletes are hard or soft. Soft deletes matter enormously and get missed constantly: a row marked is_active = false is not a delete to your pipeline unless somebody tells it so.
The access plan is the other half. Credentials, network paths, VPN or private link, replica provisioning, and any database configuration change that needs a maintenance window. Enabling logical replication on PostgreSQL requires wal_level = logical, and on most managed instances that is a parameter group change with a restart attached. Discovering that in week five costs you a week. Discovering it in week one costs you a calendar invitation.
Extraction and ingestion
Getting rows out of the source, whether that is log-based change data capture, incremental queries against a high watermark, full snapshots for small reference tables, file drops on SFTP or object storage, API pagination against a rate limited endpoint, or a managed connector doing the work for you. Each of those has a different failure mode and a different cost profile, and mixing them in one estate is normal rather than a sign of mess.
Landing, staging and the raw layer
Raw data lands untransformed and immutable, with the ingestion timestamp and the source system recorded on every row. This is the single most useful architectural decision in the whole stack, because it means every downstream mistake is recoverable by reprocessing rather than by re-extracting from a source that may have already overwritten the truth. Storage is cheap. Asking a partner to resend six months of files is not.
Transformation and modelling
Cleaning, typing, deduplication, joins, slowly changing dimensions, business logic, and the semantic layer analysts actually query. In practice this is a dbt project, version controlled, reviewed like software, with tests attached to the models rather than living in a separate quality tool nobody opens.
Loading and publication
Merge strategies, partition handling, atomic swaps, and the write-audit-publish pattern so that a failed quality check means nothing gets published rather than bad data being published and then corrected. The difference between those two behaviours is the difference between a quiet Tuesday and an email from the CFO.
Orchestration and scheduling
Dependencies, retries with sensible backoff, sensors that wait for upstream files, SLAs, and alerting that distinguishes a transient network blip from a genuine data problem. An orchestrator that pages someone for every retry gets muted within a fortnight, and after that it may as well not exist.
Quality gates and observability
Tests that run inside the pipeline and can stop it, freshness monitoring, volume anomaly detection, and a status page an analyst can check before they raise a ticket asking whether the data is up to date.
Documentation, lineage and handover
Column level lineage, model documentation generated from the code rather than written separately and left to rot, runbooks for the three most likely failures, and an architecture note that explains why each decision was taken and what the alternative was. The test we apply is whether an engineer on your side who was not part of the build can take a 2am page, follow the runbook, and either fix the load or safely skip it until morning.
ETL or ELT: Why Did the Industry Flip the Letters?
For twenty years the T came before the L because it had to. Data warehouses ran on fixed hardware you had bought, capacity was the scarce thing, and letting raw junk into an expensive appliance was reckless. So transformation happened in a middle tier, on tools like Informatica PowerCenter, Talend, DataStage or SQL Server Integration Services, and only clean, modelled, business ready rows were allowed into the warehouse.
Two things broke that model. Cloud warehouses separated storage from compute, so keeping raw data stopped being expensive and adding compute for an hour stopped requiring a purchase order. And SQL running inside Snowflake, BigQuery, Redshift or Databricks turned out to be faster at large joins and aggregations than anything a row-by-row transformation engine could manage on a single node.
So the order changed. Load first, transform in the warehouse, keep the raw layer forever. The practical benefits are larger than they sound. You can reprocess history when you find a bug in your business logic, instead of asking every source system for a resend. Transformations become SQL in a git repository with tests and pull requests, which means a data analyst can contribute and a reviewer can see the diff. And when finance asks why the definition of an active customer changed in March, the answer is a commit.
ELT is not free of trade-offs, and the honest ones are these. You pay warehouse compute for every transformation, repeatedly, so a badly written model is now a recurring bill rather than a one off slow job. Raw personally identifiable data lands in the warehouse before anyone has masked it, which is a governance decision you have to make deliberately rather than by default. And without ownership rules, the raw layer becomes a swamp of tables nobody will delete because nobody is certain who reads them.
There is also a legitimate place left for transformation before the load. If a source produces a fixed width file with a proprietary encoding, parse it on the way in. If a field is regulated data you have decided must never touch the analytics environment, drop or tokenise it in flight rather than loading it and masking it afterwards. If the payload is 400GB of JSON and 95 percent of it is a field nobody uses, prune before you pay to store and scan it. We build ELT by default and put a transformation step in front of the load when one of those three conditions is true.
Change Data Capture Without Hurting the Source Database
Change data capture is how you keep a warehouse in step with an operational database without re-reading the whole table. There are two families, and choosing badly between them is one of the more expensive mistakes available in this discipline.
Query-based CDC and where it quietly fails
The familiar approach: store a high watermark, then run SELECT * FROM orders WHERE updated_at > :last_run every few minutes. It needs nothing special from the database, any engineer can read it, and for a small table with a well maintained timestamp it is perfectly reasonable.
It fails in four specific ways. Hard deletes are invisible, because a deleted row cannot appear in a result set, so your warehouse accumulates records the source no longer has. Rows updated twice between polls give you only the final state, which is fine for a current snapshot and useless if you needed the intermediate history. Transactions that commit out of timestamp order, which happens under load with long running transactions, can slip past the watermark entirely and are never seen again. And the query itself competes with production traffic, so the more current you want your data, the more load you place on the database serving your customers.
That last point is the trap. Teams respond to stale data by polling more often, which slows the source, which makes someone add an index, which slows writes. We have seen a five minute poll on a large table consume more database time than the application it was reading from.
Log-based CDC
Every relational database already writes a durable log of changes so it can recover from a crash and feed replicas. PostgreSQL has the write ahead log and logical decoding, MySQL has the binary log in row format, SQL Server has its own change data capture tables and change tracking, Oracle has redo logs and LogMiner. Log-based CDC reads that stream. Deletes appear because they are logged. Every intermediate update appears in commit order. And the read cost to the primary is a fraction of a repeated table scan, because the log is sequential and was being written anyway.
Debezium is the reference implementation in open source, and it runs either as a set of Kafka Connect connectors or standalone through Debezium Server or the embedded engine when you do not want a Kafka cluster in your life. That second option matters for smaller teams. Running Kafka to move three tables is a decision you should have to justify.
The five things that bite people running Debezium
Replication slot lag is the first and worst. A PostgreSQL slot guarantees the WAL will be kept until the consumer has read it, so a connector that dies on a Friday evening can fill the primary's disk by Sunday. This is an operational property, not a bug, and it needs an alert on slot lag with a threshold agreed with whoever owns the database before anything goes live.
Second, TOAST columns. PostgreSQL stores large values out of line, and an update that does not change a TOASTed column sends a placeholder rather than the value. Your consumer sees an unchanged marker where it expected text. Setting REPLICA IDENTITY FULL solves it and increases WAL volume, so it is a trade rather than a fix.
Third, before-images. By default PostgreSQL logs only the primary key of the old row, so if you need to know what a field changed from rather than to, you have to opt into full replica identity and accept the extra write volume.
Fourth, the initial snapshot. Getting the existing table contents consistent with the start of the stream traditionally meant a lock or a long blocking read. Debezium's incremental snapshots, which use a signalling table and chunked watermarks, allow that backfill to run alongside the live stream without stopping it. It is worth configuring properly rather than accepting the default on a large table.
Fifth, schema history. The connector maintains its own record of the source schema over time, and losing that topic or file is a genuinely painful recovery. It gets backed up alongside everything else, and we have seen more than one team discover it was not.
MySQL has its own version of most of this: the binary log must be in ROW format, and binlog_row_image should be FULL if you want complete before and after images. Retention is the other one to check. If your binlogs are kept for 24 hours and your connector is down for a long weekend, the only recovery path is a fresh snapshot.
Batch or Streaming? Answer This Question First
Ask what decision changes if the number is fifteen minutes old rather than fifteen seconds old. Most of the time the honest answer is that nothing changes, because a human is going to look at it during working hours and act on it during working hours.
Streaming earns its keep when a machine consumes the data automatically and the window between event and action is genuinely short. Fraud scoring on a transaction. Inventory holds during a flash sale. Live pricing. Anomaly detection on device telemetry where a delayed alarm is worthless. In those cases the latency is the product, and everything below is a cost you accept.
Here is what the cost actually consists of, since it rarely appears on the slide that proposes streaming.
The infrastructure runs continuously. A Kafka cluster, whether self managed, on Amazon MSK or on Confluent Cloud, plus a stream processor such as Flink, Spark Structured Streaming or Kafka Streams, bills whether or not any events arrive tonight. A batch job that runs for eleven minutes bills for eleven minutes.
State becomes an operational object. Windowed aggregations hold state that has to be checkpointed, restored, and sized. A Flink job with a large keyed state is a system with its own capacity planning, and rebuilding that state after a failure is measured in minutes or hours rather than instantly.
Time gets complicated. Event time and processing time diverge, so you need watermarks, an allowed lateness policy, and a decision about what happens to events that arrive after the window closed. Sending them to a side output and reconciling later is the usual answer, and it is a second pipeline to build and monitor.
Every bug is a live bug. When a batch job produces wrong output you fix the code and rerun it. When a streaming job produces wrong output you fix the code, then decide how to reprocess a window that has already been consumed downstream, possibly with a parallel run and a cutover.
And somebody has to be awake. Streaming implies on call in a way that a nightly batch does not, because a stopped consumer is an actively growing problem rather than a job you rerun in the morning.
The middle ground handles most requirements and gets proposed too rarely. Micro-batch on a five or fifteen minute schedule gives you data that feels current to any human user, uses the same code, tests and debugging tools as your nightly work, and costs a fraction of a streaming platform to run. When someone asks for real time, the useful next question is what number they would act on and how quickly, and more often than not the answer describes a fifteen minute batch.
Schema Evolution, or How a Producer Breaks a Consumer in Silence
A backend team ships a migration on Thursday afternoon. They rename customer_id to account_id because the domain language changed, update every service that reads it, and deploy without incident. Nobody in that room knows your warehouse exists. Your pipeline breaks that night, or worse, does not break and starts writing nulls into a column three dashboards depend on.
Schema changes come in three severities and it is worth being precise about which is which. Additive changes, such as a new nullable column, are safe if your pipeline ignores unknown fields rather than failing on them. Widening changes, such as an integer becoming a bigint, are usually safe but occasionally truncate on the target. Breaking changes, meaning renames, type changes, dropped columns and altered enum encodings, will corrupt or halt something downstream every time.
The naive defence is to make the pipeline permissive: cast whatever arrives, fill missing fields with nulls, keep going. That converts a loud failure into a silent one, which is the wrong direction. The right posture is to fail fast on the specific change classes that matter and to detect them before the data reaches a consumer.
Three mechanisms do the work, and most estates need more than one.
A schema registry sits in front of event streams. Confluent Schema Registry with Avro, Protobuf or JSON Schema enforces a compatibility mode at the point of publication, so a producer attempting an incompatible change is rejected by their own CI rather than discovered by you at midnight. BACKWARD compatibility, which lets new consumers read old data, is the right default for most analytics use cases. This is the strongest control available, and it only works if the producing teams accept it.
Contracts on the ingestion side cover the sources you do not control. dbt source freshness and model contracts, or expectation suites in Great Expectations, assert the column set and types you require. When reality diverges, the run fails at the staging boundary and nothing publishes. The failure message names the column and the change, which turns a three day investigation into a five minute one.
A quarantine path handles what you decide not to reject outright. Rows that fail parsing or violate a constraint go to a separate table with the raw payload and the reason, instead of being dropped. That table gets a row count alert. It is remarkable how often it turns out that a client has been silently discarding two percent of a feed for months.
The organisational half matters as much as the technical half. Somebody on the producing team has to know the pipeline exists. The cheapest version of this is a check in the source repository's CI that flags changes to tables that are known to be replicated, pointing at a document naming the owner on the data side. It is not sophisticated. It stops most of the incidents.
Idempotent Loads, Late Data and Backfills That Do Not Take Production Down
These three problems get grouped because they share a root cause. Data does not arrive once, in order, exactly as expected. Design for that from the start and the pipeline stays boring. Assume the opposite and you will spend your Tuesdays reconciling.
Idempotency: make replay harmless
Assume every record will be delivered at least twice, because it will. Kafka retries, connectors resume from a checkpoint slightly behind where they stopped, an operator reruns yesterday's task to fix an unrelated bug. If your load is a plain INSERT, each of those events duplicates rows and misstates every aggregate built on top.
The fix goes on the load side. A merge keyed on the natural business key, updating only when the incoming version is newer, makes replay a no-op. In dbt that is an incremental model with a unique_key and a merge strategy, which is a two line change that prevents an entire category of incident. Where the target does not support merge cheaply, the append-only pattern works just as well: insert everything, then read through a view that keeps the latest row per key using row_number() over an ordering column. That trades storage for simplicity and gives you the change history for free.
One detail decides whether this works: the ordering column must come from the source, not from your pipeline. Ordering by ingestion time means an out of order replay of an old record overwrites a newer one. Use the source's own update timestamp or the log sequence number, and if neither exists, that is a discovery finding rather than something to paper over.
Late-arriving data
An event happens at 23:58 on the last day of the month and reaches you at 00:07 the next day. A mobile client is offline for three days and syncs its queue on Thursday. A payment processor sends a settlement file that restates two weeks of transactions. In every case the data belongs to a period you have already closed and published.
The first decision is which timestamp your partitions use. Partitioning by ingestion time makes loading simple and reporting wrong, because a report for last Tuesday will exclude events that happened on Tuesday and arrived on Thursday. Partitioning by event time makes reporting correct and means your loads have to rewrite old partitions. For anything a finance or operations team will read, event time is the right answer and the extra work is the price.
Then you need a lookback window. Rather than processing only the current partition, each run reprocesses a trailing window, commonly three to seven days, and rewrites those partitions idempotently. Choosing the width is empirical: measure the actual distribution of the delay between event time and arrival time in your own data, and set the window past the 99th percentile. Anything later than that goes through a restatement path, with a log so somebody can explain to finance why last month's figure moved.
In streaming the equivalent controls are watermarks and allowed lateness, with anything beyond the threshold routed to a side output for reconciliation. The concept is identical. Only the vocabulary changes.
Backfills that do not take production down
Backfills go wrong in two directions. They overload the source, or they saturate the warehouse and every analyst's query starts queueing behind three years of history.
On the source side: read from a replica rather than the primary, always. Chunk by primary key range instead of by offset, because deep offsets get slower as they go and a keyset scan does not. Rate limit deliberately and checkpoint every chunk so an interrupted backfill resumes rather than restarts. Run it when the source is quiet, which for a US business is usually the Indian working day, and that is a real scheduling advantage rather than a talking point.
On the warehouse side: give the backfill its own compute. A separate Snowflake warehouse, a distinct BigQuery reservation or project, a dedicated Redshift queue or a separate Databricks job cluster. This is the single control that prevents a backfill from turning into an incident, and it is usually one line of configuration. Write into a shadow table and swap atomically at the end, so consumers see either the old complete table or the new complete table and never a half filled one. Snowflake's zero-copy clone and Databricks Delta's atomic replace both make this cheap.
And put a limit on it before you start. A backfill with no cost ceiling and no progress metric is how a bill doubles. We attach an estimated row count, an estimated spend and a kill switch to every one of them, and we tell you the estimate before it runs rather than the actual afterwards.
Data Quality Tests as a Gate, Not a Dashboard
A quality dashboard that shows yesterday's problems is a report on damage. A quality test inside the pipeline that stops publication is a control. The difference is whether bad numbers reach a human before somebody catches them, and it is the change that pays back fastest on most estates.
The pattern that implements this is write-audit-publish. The run writes into a staging location, tests execute against that staging data, and only a clean result promotes it into the tables analysts and dashboards read. When a test fails, yesterday's data stays live and stale, an alert fires, and nobody makes a decision on a broken number. Stale and known beats fresh and wrong every time.
On tooling, dbt tests are where most of this belongs, because the assertions live next to the model definition and travel with it in review. The four built-ins cover more ground than people expect: not_null, unique, accepted_values and relationships for referential integrity. dbt-utils adds equal row counts across models, expression checks and mutually exclusive ranges. dbt-expectations ports much of the Great Expectations vocabulary into the same place. Singular tests, which are just SQL that should return zero rows, handle the business rules that are specific to you, and those are usually the ones that catch real problems.
Great Expectations is the stronger choice when validation has to happen before the warehouse, on files or dataframes in flight, or when a non engineering audience needs readable validation documentation. Soda Core suits teams that want checks written in a compact YAML dialect rather than SQL. Elementary adds anomaly detection over dbt run history, which catches the class of problem no static assertion can express: the table that usually loads two million rows and today loaded eleven thousand, with every individual row perfectly valid.
Severity levels are what keep the whole thing from being switched off. Not every test should halt a pipeline. A null in a legally required identifier should. A slightly unusual row count should warn. dbt's error and warn severities plus store_failures let you keep the failing rows for inspection without blocking the run. Teams that make everything an error end up with an alert channel nobody reads, which is functionally the same as having no tests at all.
Freshness deserves its own mention because it fails differently. A source that stops sending data produces no wrong rows at all. Every test passes. The dashboard just quietly reflects last Wednesday. dbt source freshness thresholds, or an equivalent max-timestamp check, are what catch the pipeline that succeeded at doing nothing.
Orchestration: Airflow, Dagster and Prefect
We do not have a house orchestrator we push onto every client. We have opinions about when each one is right and, more usefully, when it is the wrong answer for the team that has to run it after we leave.
Airflow
The safe answer, and the correct one when your team already runs it or when hiring matters more than elegance. The provider ecosystem is enormous, the operational behaviour is well understood, and every data engineer you interview has used it. Airflow 2's TaskFlow API and dynamic task mapping fixed the worst of the old ergonomics.
Its weaknesses for this work are real. Airflow thinks in tasks rather than in data assets, so the question of which table is stale is one you answer by knowing which DAG produced it. Passing anything larger than a small value between tasks through XCom is awkward and pushes people toward storing state in the warehouse anyway. And self hosting the scheduler is a genuine operational job, which is why Amazon MWAA, Google Cloud Composer and Astronomer exist. If you are running it yourself with a small team, budget for that.
Dagster
Dagster inverts the model around software defined assets, which fits data integration better than the task model does, because what you actually care about is whether the orders table is fresh and correct rather than whether task 14 succeeded. Asset lineage comes for free, declarative freshness policies express what you promise your users, and local development and unit testing are meaningfully better than the alternatives.
The cost is buy-in. The asset abstraction is opinionated, a team that thinks in DAGs will fight it for the first month, and the connector and integration ecosystem is smaller than Airflow's. It is the choice we most often argue for on a greenfield estate, and the one we most often talk clients out of when they already have 200 working Airflow DAGs.
Prefect
Prefect suits Python-first teams who want dynamic, conditional workflows without adopting Kubernetes to get them. Writing a flow feels like writing Python, the hybrid execution model keeps your data in your infrastructure while the control plane is hosted, and the learning curve is the gentlest of the three. It gives you less on lineage and asset awareness, so pair it with something else if answering the lineage question matters.
And the ingestion tools that sit underneath
Orchestration is separate from ingestion, and conflating them causes bad decisions. Fivetran is the fastest way to get commodity SaaS sources landing reliably, and its consumption pricing on monthly active rows can surprise you when a source table has a churny update pattern that marks half the rows modified every day. Airbyte is the open source alternative with a much wider connector catalogue and much more variable connector quality, so evaluate the specific connectors you need rather than the platform. dlt is a Python library for teams who would rather write a loader as code with schema evolution handled for them. Singer taps still exist and still work, and the maintenance level varies by tap.
Our default recommendation is unglamorous: buy the long tail, build the two or three sources that carry your volume or your weirdness. Paying a connector vendor to keep up with the Salesforce API is good economics. Paying per row to replicate your own PostgreSQL, which you could stream with Debezium at a fixed cost, often is not.
Warehouse Targets: What Actually Changes Per Platform
Loading rows looks similar everywhere. What differs is the cost lever, the merge semantics, how you isolate one workload from another, and what you have to get right for performance. Choosing the wrong pattern for your platform is how a pipeline that ran fine in a proof of concept becomes a monthly cost review.
Snowflake
Compute is virtual warehouses, billed per second with a sixty second minimum, which makes auto suspend one of the most valuable settings in the account. Isolation is easy and underused: give loading, transformation, backfills and BI their own warehouses so a heavy job cannot queue behind an analyst or vice versa. MERGE is well supported, micro-partitions are managed for you, and clustering keys are worth defining only on genuinely large tables where the pruning is measurable. Zero-copy clone makes the write-audit-publish pattern almost free, and Time Travel means a botched load can be undone rather than reloaded. The costly mistake is warehouse size inflation, since each step up doubles the credit burn per second and a query that is bottlenecked on a single partition will not go faster for it.
BigQuery
The main cost lever is bytes scanned under the on-demand model, or slot capacity under editions and reservations. That single fact drives the design: partitioning and clustering are not optimisations, they are the budget. A query without a partition filter on a large table is the classic expensive accident, and require_partition_filter exists to prevent exactly that. There are no indexes to tune. Use the Storage Write API rather than legacy streaming inserts for anything new, understand that streamed rows sit in a buffer briefly before they are queryable, and prefer MERGE on partitioned tables with a filter narrow enough that it does not rewrite the whole thing.
Redshift
Physical data layout still matters here more than on the others. Distribution style and sort keys determine whether a join redistributes data across nodes, and getting those wrong on a large fact table is the difference between seconds and minutes. RA3 nodes separated storage from compute and managed the worst of the old sizing pain, and Redshift Serverless removes cluster sizing entirely in exchange for RPU based billing. Concurrency scaling handles the bursts. Copy from S3 in parallel with correctly sized files rather than inserting row by row, and use workload management queues to keep the backfill away from the analysts.
Databricks and the lakehouse
Delta Lake gives you ACID transactions and time travel over object storage, so merges and atomic swaps behave the way you want without a traditional warehouse underneath. OPTIMIZE with compaction, and Z-ordering or liquid clustering on the columns you filter by, are the maintenance that keeps small file proliferation from strangling read performance, and a streaming ingest with no compaction job is the most common way that happens. Unity Catalog is the reason a lot of teams pick this platform: governance, column level lineage and access control in one place rather than three. Keep SQL warehouses and job clusters separate so interactive queries and pipeline runs do not compete.
What we standardise regardless of target
The raw layer stays immutable with source and ingestion timestamp columns on every row. Transformations live in a dbt project in your repository with tests attached. Loads are idempotent through merge or dedup-on-read. Publication is atomic. Compute for backfills is separate from compute for anything a person is waiting on. Those five hold on all four platforms, which means a migration later is a rewrite of the loading layer rather than of everything.
Keeping Warehouse Compute From Becoming Your Biggest Line Item
Cost control is part of the engineering work, not a finance conversation that happens afterwards. In every estate we have picked up where spend had run away, the causes were the same handful of things, and none of them required a platform migration to fix.
Full refreshes on tables that should be incremental. Someone builds a model as a full rebuild because it is simpler, it takes four minutes on a small table, and eighteen months later the table has 900 million rows and the model still rebuilds every hour. Converting the top few models to incremental is usually the single largest saving available, and it is a day of work.
Dashboards refreshing on a schedule nobody chose. A BI tool set to refresh every fifteen minutes across two hundred dashboards, most of which are opened twice a month, generates enormous warehouse activity for no benefit. Aligning refresh cadence with how often the underlying data actually changes costs nothing and often halves the BI portion of the bill.
No isolation between workloads. When everything runs on one warehouse or one reservation, one bad query degrades everyone and the response is to size up, permanently, to cover a peak that lasts ten minutes a day. Separate compute per workload lets you size each one honestly.
No attribution. If you cannot tell which pipeline, team or dashboard generated a cost, every conversation about it becomes a negotiation. Query tags in Snowflake, labels in BigQuery, and consistent naming across jobs turn that into arithmetic. We put cost per pipeline run and cost per thousand rows loaded on a dashboard early, because once those numbers exist the arguments stop.
And the guardrails: auto suspend on idle compute, statement timeouts so a runaway query dies in ten minutes rather than at the end of the month, resource monitors or budget alerts with a real threshold, and a review of the ten most expensive queries once a fortnight. That review takes twenty minutes and finds something almost every time.
PII on the Road to Analytics
Analytics pipelines are where personal data quietly spreads. A production database has access controls, an audit trail and a security owner. A warehouse built by copying that database has all the same data with a different, usually looser, access model and a much wider audience. This is a design decision worth making on purpose.
The work starts with classification, not tooling. Go column by column through the sources and decide, with someone from your side who can make the call, which fields are personal data, which are special category, and which are merely commercially sensitive. It is tedious and it takes a couple of days, and skipping it is how a support ticket free-text column full of customer phone numbers ends up in a table forty people can read.
Then each classified column gets a treatment. Drop it, if analytics genuinely does not need it, which is the answer more often than people expect. Hash it with a salt held outside the warehouse, when you need to join on identity but never to read it. Tokenise through a vault, when a controlled reverse lookup has to remain possible for a small group. Generalise it, turning a date of birth into an age band and a full postcode into its outward part, which usually preserves the analytical value entirely. Or keep it and control access, when there is a real reason.
Where the treatment happens is the second decision. Masking in flight, before the data lands, means the sensitive value never exists in the analytics environment, which is the strongest position and the hardest to reverse when requirements change. Masking in the warehouse, using Snowflake dynamic data masking policies, BigQuery policy tags with column level security, or Unity Catalog column masks and row filters, is more flexible and depends on the access model being correct. For regulated fields we lean towards in-flight, because a policy misconfiguration is a plausible event and a column that was never loaded cannot be misconfigured.
Deletion requests interact awkwardly with an immutable raw layer, and this needs deciding before it is urgent rather than during a request. A workable pattern is a deletion log that is applied on every rebuild, plus crypto-shredding where a per-subject key can be destroyed. Which approach satisfies your obligations under GDPR, the UK equivalent, Australian privacy law or a state level US regime is a question for your counsel, and we will build to the decision they give you rather than offer one.
On our side of the arrangement, the practical controls are the ones that matter day to day. Named identities per engineer in your cloud account, scoped roles rather than shared credentials, work performed through a bastion or a browser based workspace, development against masked or synthetic data, and no production extracts on local machines. Data residency is preserved because compute and storage stay in the region you chose. What crosses a border is access under audit, not rows.
Lineage: Where Did This Number Come From?
A finance director asks why the churn figure moved. The analyst who built the dashboard left in March. Somebody opens the BI tool, finds a query against a table called mart_customer_summary, and then stops, because nothing in the warehouse says where that table's churned_flag column came from or which of eleven upstream models could have changed it.
Lineage answers that question mechanically. Table level lineage tells you which sources feed a table. Column level lineage, which is the version worth having, tells you which upstream fields and which transformation produced a specific column, which is what you need for an impact analysis before a change and a root cause after one.
In practice most of this comes free if the transformation layer is dbt, since the DAG is derived from the code itself and the documentation site is generated rather than maintained. Where dbt stops is at the edges: what happened before the warehouse, and what happens after it in the BI tool. OpenLineage is the open standard that stitches those together, with Marquez as a reference server, and it has integrations for Airflow, Spark and dbt so ingestion and transformation appear in one graph. dbt exposures cover the downstream side by declaring which dashboards depend on which models, so a deprecation notice reaches the right people.
If you want a catalogue on top, Unity Catalog gives you lineage natively on Databricks, and DataHub, OpenMetadata, Atlan and Collibra sit above whatever you run. Our advice is to earn the catalogue rather than buy it first. A catalogue populated from a well documented dbt project with real ownership metadata is useful on day one. A catalogue pointed at an undocumented estate is a very expensive list of table names.
The other half of the answer is ownership. Every model carries an owner and a freshness expectation in its metadata, so the question of who to ask has a recorded answer rather than depending on institutional memory. That is the part that survives people leaving.
Three Situations We Get Called Into
These are patterns rather than named clients. They recur often enough that if one of them describes your situation, the diagnosis below is probably close.
The managed connector bill that tripled without the data growing
A subscription business runs a managed ELT tool across eighteen sources. Sixteen are small SaaS applications and two are large tables in their own PostgreSQL. The invoice climbs steeply quarter over quarter while the underlying business grows at a fraction of that rate. Nobody can explain it.
The cause is usually consumption pricing meeting a churny update pattern. Row based billing counts a row as active if it changed, and a table with a last_seen_at column touched by a background job marks a large share of rows modified every single day. You are paying to re-sync data that has not meaningfully changed.
The fix is to split the estate rather than replace the tool. The two high volume tables move to log-based CDC with Debezium, where the cost is infrastructure that does not scale with row churn. The sixteen small sources stay exactly where they are, because writing and maintaining sixteen API connectors to save a modest amount is a bad trade. The work is a few weeks and the shape of the bill changes permanently.
The reporting replica that is always six hours behind
A B2B software company reports off a read replica of production, on the reasonable theory that this avoids building a warehouse. Then product asks for in-app analytics for customers, finance wants monthly cohorts, and the queries needed for both are heavy enough to make the replica lag. Lag makes the numbers wrong, which makes people distrust the numbers, which is worse than not having them.
Replicas are built for failover, not for analytical scans. The path out is CDC from the replica into a columnar warehouse, incremental dbt models with tenant identity carried through so per-customer analytics can be served safely, and masking applied to the customer fields that the in-app view must never expose. The replica goes back to being a replica.
The part that takes the longest is not the pipeline. It is agreeing what a metric means when the product team and the finance team have been computing it differently for two years, both correctly, from the same rows.
The month-end close that breaks every month
An operations heavy business loads nightly from an on-premise SQL Server. Every month end, the finance team finds that prior period figures have moved, and every month somebody rebuilds a spreadsheet by hand to explain the difference. It is treated as a fact of life.
The cause is late corrections. Adjustments, refunds and reclassifications are entered against original transaction dates, so a record that belongs to March arrives in April. If the pipeline partitions by ingestion date, March is understated forever. If it overwrites without a record, March silently changes and nobody can say by how much.
The fix has three parts. Partition by event date so figures land in the period they belong to. Reprocess a trailing window on every run, sized from the measured arrival delay in that specific business rather than a guess. And keep a restatement log that records exactly which prior period rows changed and when, so the finance team can see the movement rather than discover it. That last piece is what ends the monthly argument, and it is the smallest of the three to build.
How Offshore Delivery Works From India, Hour by Hour
This is where most offshore pitches go vague, so here is the arithmetic instead. India is UTC+5:30 and does not observe daylight saving, which means the gap to you changes twice a year even though our clocks do not move.
On a standard 09:30 to 18:30 IST day, the overlap with UK working hours is roughly four hours, ending around 13:00 in London. With Australian eastern time you get about three hours, at the end of the Australian afternoon. With US Eastern you get essentially nothing, because the Indian working day finishes around the time the US one begins. Anyone claiming a full working overlap with New York on standard Indian hours is describing something that does not exist.
So for North American clients we run a shifted team. A 13:30 to 22:30 IST shift gives you roughly three hours of live overlap with a New York morning. Pushing to 15:30 to 00:30 IST gives you five. Those shifts are real for the people working them, so they are agreed up front rather than assumed, and the specific window is something we set with you before the engagement starts rather than something stated on a web page.
Here is the part that is genuinely better for this particular service rather than a consolation. Batch pipelines run overnight in your timezone. For a US business, the loads that start at 01:00 Eastern are running at 11:30 in the morning in India. A failure at 02:30 Eastern, which for your team means a page in the middle of the night, is a Tuesday lunchtime for ours. That is not a marketing point, it is the reason data pipeline work suits this arrangement better than most software delivery does. Month end closes, quarter end loads and large backfills all fall in the same convenient window.
The rest of the operating model is written-first, because a four hour overlap punishes anything that depends on being in the room. Decisions go in an architecture decision record in the repository, not in a call. Pull requests carry enough context that a reviewer nine and a half hours away can approve or reject without a conversation. A written handover at the end of the Indian day lays out what ran, what failed, and what needs your sign-off, so your morning starts with answers instead of questions.
On the working rhythm: a daily standup inside the overlap window, one longer weekly session for design and priorities, two week sprints with a demo of something actually running rather than a slide about it. Every change goes through review by a second engineer, and pipeline changes also have to pass their tests in a staging environment against a sample of real shaped data before they touch production. No engineer merges their own pipeline change, which sounds obvious and is the rule most commonly broken under deadline pressure.
On communication and English: the assessment is written and verbal, and it is specific to this work rather than generic. We ask candidates to explain a technical trade-off in writing to a non technical reader, because that is the actual job when your counterpart is asleep. You interview the people who will work on your system before they start, and you talk to the engineers directly rather than through an account manager relaying questions.
On the talent pool for this specific skill: India's large services industry means deep experience with Informatica, DataStage, SSIS and Oracle, and a growing population with real production time on dbt, Airflow, Snowflake, BigQuery and Databricks. The screening question that separates them is not tool knowledge. It is asking a candidate to describe a pipeline they built that produced wrong numbers, and how they found out. Engineers who have lived through that answer differently from engineers who have only built the happy path.
What Goes Wrong, and What We Do About It
Every data integration project hits some subset of the following. Naming them in week one is cheaper than discovering them in week six.
Access takes longer than the build
The most common schedule risk by a distance. A read replica has to be provisioned, a network path opened, a database parameter changed with a restart, or a third party vendor has to approve an integration user. None of that is technical difficulty, all of it is calendar time in someone else's queue. We front-load access requests into week one, name the owner for each, and track them as tasks with dates rather than as assumptions.
The source is worse than documented
Timestamps stored without timezone, so the same column means IST in one row and UTC in another. Currency amounts in cents in one table and units in the next. Enumerations with values not present in any documentation. A column called status carrying four historical meanings depending on the year of the row. Profiling before designing catches these. Designing first and discovering later means rework, so the first week is always spent reading the data rather than the documentation about the data.
Nobody agrees what the metric means
Two teams compute active customers differently and both are right within their own context. This is not a pipeline problem and cannot be solved by one, but it will stall the pipeline until it is resolved. We surface the conflict early with a written definition per metric, and push for a named owner who decides. If no owner is nominated, we build both and label them plainly, which is uglier and better than silently picking one.
Deletes are invisible
The source soft deletes with a flag, or hard deletes and expects a full reload to notice, or archives rows to a second table nobody mentioned. Any of those means your warehouse slowly diverges from reality. Handling is decided per table during discovery, and where deletes genuinely cannot be captured, a periodic reconciliation against source counts detects the divergence rather than letting it accumulate quietly.
Key people leave, on either side
Ours and yours. On our side, at least two engineers touch every pipeline from the start and the runbook is written by whoever did not build it, which is the fastest way to find the gaps in it. Handover terms and how a change of staffing is managed are set in the agreement before work begins, and we would rather point you to that document than invent a policy here. On your side, the defence is the same: documentation generated from the code and ownership recorded in metadata, so a departure does not take the map with it.
Scope grows because the data reveals a business problem
This one is usually good news badly timed. You build a pipeline, the numbers become visible, and it turns out the fulfilment process has a gap that has been invisible for years. That is a separate project. We flag it, write it up, and keep it out of the current scope so the pipeline actually ships, because the alternative is an engagement that expands until it delivers nothing.
Engagement Models
Three shapes cover almost everything. Which one fits depends less on budget than on whether the work has an end.
Dedicated team
Engineers working as part of your team, in your tools, your repositories, your standups and your sprint board. The right model when data integration is ongoing rather than a project: new sources arrive, business logic evolves, and somebody has to own the pipelines on a Tuesday morning when a load fails. You direct the work. We handle recruitment, technical vetting and the day to day management, and you interview anyone before they join.
Scoped build
A defined outcome with a defined boundary. Migrate an SSIS estate to dbt and Airflow. Stand up CDC from three PostgreSQL databases into Snowflake with tests and monitoring. Rebuild the finance mart with a restatement log. This works when the requirement is clear enough to describe as a deliverable, and it is usually preceded by a short assessment so the scope is written from evidence rather than from a wish.
Retainer and on-call support
For estates that already exist and need keeping alive: monitoring, failure triage, small changes, cost review and the steady maintenance that pipelines need but nobody has time for. Often follows a scoped build, and often runs alongside your own team taking primary ownership while we cover the hours they do not.
Commercial terms, notice arrangements and how staffing changes are handled all sit in the agreement we put in front of you before any work starts. We are deliberately not quoting them here, because the right answer depends on the shape of the engagement and you should read it in a contract rather than on a marketing page.
Where This Sits Alongside Our Other Work
Data integration overlaps with several things we do and is distinct from all of them. If your problem is moving bulk data into somewhere you can analyse it, this is the page. If your problem is two applications needing to talk to each other transactionally, that is a different discipline and our enterprise integration work covers it. Broader platform and warehouse design belongs with data engineering, which is often the same engagement viewed from a wider angle.
Downstream, once clean data is landing reliably, model training and deployment pipelines are covered by our MLOps services in India rather than duplicated here. The cloud foundations underneath, including networking, private connectivity and account structure, sit with cloud architecture, and the deployment automation that ships pipeline changes safely is part of CI/CD pipeline work. If you need staffing rather than a scoped build, you can hire Python developers in India for the ingestion and tooling side of this work.
Frequently Asked Questions About Data Integration in India
Should we buy Fivetran or build our own pipelines?
Buy for commodity SaaS sources. Nobody should be hand writing a Salesforce or Stripe or Zendesk connector in 2026, because the vendor changes their API and the connector vendor absorbs that pain for you. Build for the systems that are specific to your business: your own production database, a partner feed, an on premise ERP with a schema somebody customised in 2011. The usual split we end up with is a managed tool for the long tail of small sources and owned code for the two or three tables that carry real volume.
Is log-based CDC safe to run against our production database?
Safer than the polling query it usually replaces, with one operational condition attached. Reading the write ahead log costs the primary far less than a full table scan every five minutes. The risk is the replication slot: if your consumer stops and nobody notices, PostgreSQL keeps the WAL segments the slot still needs and the disk fills. So slot lag gets an alert with a real threshold before the first connector goes live, not after.
How real-time does our data actually need to be?
Ask which decision changes if the number is fifteen minutes old instead of fifteen seconds. For finance reporting, marketing attribution and most executive dashboards, nothing changes, and a fifteen minute batch is far cheaper to run and to debug. Streaming earns its cost when a machine acts on the data automatically: fraud scoring, inventory holds, live pricing. We push back on streaming for human dashboards more often than we build it.
What happens to our warehouse bill when we add all this?
It goes up before it goes down, and we would rather say that than discover it together in month two. Loading raw data is cheap. Transforming it repeatedly is not. The controls that matter are incremental models instead of full refreshes, separate compute for backfills so they cannot queue behind analysts, auto suspend on idle warehouses, statement timeouts, and query tagging so every dollar has an owner. We put cost per pipeline run on the dashboard from the first week.
Can a data integration team in India work without copying production data to India?
Yes, and that is the arrangement we push for. Engineers get named identities in your cloud account with scoped roles, work through a bastion or a browser based workspace, and never pull production extracts to a laptop. Compute and storage stay in the region you choose. Development runs against masked or synthetic data. What crosses a border is access under audit rather than rows. Your own counsel should confirm this satisfies your obligations.
How long does the first pipeline take to reach production?
For one source with cooperative access, the pattern is usually two weeks of discovery and access provisioning, then two to three weeks to get a tested, orchestrated, monitored pipeline landing data a business user trusts. Access is nearly always the long pole. If a DBA has to schedule a restart to change wal_level, or a vendor has to approve a read replica, that calendar time belongs on the plan rather than in the risk register.
Our source table has no primary key and no updated_at column. Now what?
Then incremental extraction by query is off the table and you have three options. Log-based CDC works without either, because the log records every row change regardless of what the schema looks like. Failing that, a full snapshot each run is honest and fine for tables under a few million rows. The third option is asking the source team to add the columns, which is worth attempting even when it feels hopeless, because a surrogate key and a modification timestamp usually help them too.
Who owns the pipelines and the code when the engagement ends?
You do, and the repositories live in your organisation from the first commit rather than being handed over at the end. Ownership, IP assignment and confidentiality are set in the agreement before work starts, so nothing about it is a surprise later. Practically, handover means infrastructure as code, orchestration definitions, dbt project, tests, dashboards and runbooks that name the first three things to check when a load fails at 2am.