<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Aniket Abhishek Soni</title>
    <description>The latest articles on DEV Community by Aniket Abhishek Soni (@aniketsoni).</description>
    <link>https://dev.to/aniketsoni</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3954381%2Fc17f147f-e19b-4160-be20-e2d4dd2af1dd.png</url>
      <title>DEV Community: Aniket Abhishek Soni</title>
      <link>https://dev.to/aniketsoni</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aniketsoni"/>
    <language>en</language>
    <item>
      <title>Stop Choosing Between Delta and Iceberg: UniForm is the Pragmatic Exit</title>
      <dc:creator>Aniket Abhishek Soni</dc:creator>
      <pubDate>Thu, 23 Jul 2026 23:36:28 +0000</pubDate>
      <link>https://dev.to/aniketsoni/stop-choosing-between-delta-and-iceberg-uniform-is-the-pragmatic-exit-4i5k</link>
      <guid>https://dev.to/aniketsoni/stop-choosing-between-delta-and-iceberg-uniform-is-the-pragmatic-exit-4i5k</guid>
      <description>&lt;p&gt;It was 3:00 AM on a Tuesday when the PagerDuty alert for our primary billing pipeline hit. We had a job failing on a Spark 3.5 cluster because our upstream vendor decided to switch their export format to Iceberg, while our entire analytical stack was locked into Delta Lake 3.0. The migration cost us six hours of downtime and roughly $45,000 in SLA penalties. The issue wasn’t the data quality; it was a religious war between two metadata layers that refused to speak the same language. &lt;/p&gt;

&lt;p&gt;We treat table formats like sports teams, but they’re just protocols. You don’t need to pledge allegiance to Databricks or the Apache Software Foundation. You need your data to be readable by the specific query engine that actually does the job, whether that’s Trino, DuckDB, or Spark.&lt;/p&gt;

&lt;h2&gt;
  
  
  The illusion of engine-agnosticism
&lt;/h2&gt;

&lt;p&gt;Most engineers assume that because they use an open format, they are portable. That’s a lie. If you write a Delta table, you are tethered to the Delta Standalone reader or a Spark implementation that supports the Delta protocol. If you use Iceberg, you’re at the mercy of the Iceberg Catalog and its specific manifest file structures. &lt;/p&gt;

&lt;p&gt;The thing we rarely look at is the metadata root. In Delta, it’s a directory of JSON logs (&lt;code&gt;_delta_log/&lt;/code&gt;). In Iceberg, it’s a snapshot-based tree structure starting from a metadata file that points to manifest lists. They are fundamentally different ways of tracking state, yet they both aim to solve the same problem: atomic ACID transactions on top of a pile of Parquet files.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1743359275182-60aa80a093f1%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxN3x8cnVzdGVkJTIwaW5kdXN0cmlhbCUyMGdlYXJzfGVufDB8MHx8fDE3ODQ4MzUzOTJ8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1743359275182-60aa80a093f1%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxN3x8cnVzdGVkJTIwaW5kdXN0cmlhbCUyMGdlYXJzfGVufDB8MHx8fDE3ODQ4MzUzOTJ8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="Photo by Paolo Chiabrando on Unsplash" width="1080" height="810"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@chiabra?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Paolo Chiabrando&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  How it actually works
&lt;/h2&gt;

&lt;p&gt;UniForm (Universal Format) is the bridge. Specifically, Delta Lake UniForm allows you to write in Delta format while the engine automatically generates the Iceberg metadata in the background. &lt;/p&gt;

&lt;p&gt;When you enable &lt;code&gt;delta.universalFormat.enabledIceberg&lt;/code&gt; in your table properties, you aren't just tagging metadata; you are triggering an asynchronous background process that translates the Delta log into Iceberg manifests.&lt;/p&gt;

&lt;p&gt;Here is what that looks like in a Spark session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;my_production_table&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;TBLPROPERTIES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'delta.universalFormat.enabledIceberg'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'true'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'delta.iceberg.catalogName'&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'my_hive_catalog'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Behind the scenes, Delta is essentially running a translation layer. Every time a commit happens in the &lt;code&gt;_delta_log/&lt;/code&gt;, a background Spark job (or the writer itself, depending on configuration) maps those file additions and removals into the Iceberg snapshot format. &lt;/p&gt;

&lt;p&gt;For the end user, this is magic. You point your Trino or Starburst cluster at the same S3 prefix you use for Spark, and Trino sees a perfectly valid Iceberg table. You aren't duplicating the data files—the Parquet files remain identical. You are only duplicating the metadata pointers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tradeoffs nobody mentions
&lt;/h2&gt;

&lt;p&gt;If this sounds too good to be true, it’s because it involves operational "hidden" costs. &lt;/p&gt;

&lt;p&gt;First, the background translation job is not free. If you are doing high-frequency streaming writes (e.g., every 30 seconds), the overhead of keeping the Iceberg metadata in sync can cause write latency spikes. I’ve seen commit latencies jump from 200ms to over 2 seconds because the cluster had to lock and update both the Delta log and the Iceberg snapshot history.&lt;/p&gt;

&lt;p&gt;Second, version skew is a real failure mode. If your Delta version is 3.2, but the Iceberg translation logic is lagging behind in the current library version, you might end up in a state where the table is readable, but "time travel" queries fail. I once debugged a case where &lt;code&gt;SELECT * FROM table AS OF VERSION AS OF '2026-05-01'&lt;/code&gt; worked in Spark but returned a &lt;code&gt;TableNotSupported&lt;/code&gt; error in Trino because the Iceberg manifest was missing the specific partition evolution metadata that Delta had handled natively.&lt;/p&gt;

&lt;p&gt;Finally, you are doubling your storage metadata footprint. In a multi-petabyte environment, the &lt;code&gt;_delta_log&lt;/code&gt; and the &lt;code&gt;metadata/&lt;/code&gt; directory for Iceberg will grow to millions of files. If your object store has high latency on LIST operations, your catalog discovery will eventually become the bottleneck, not the data retrieval itself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1516101922849-2bf0be616449%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwyN3x8Y2xlYW4lMjBnbG93aW5nJTIwY29kZXxlbnwwfDB8fHwxNzg0ODM1MzkzfDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1516101922849-2bf0be616449%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwyN3x8Y2xlYW4lMjBnbG93aW5nJTIwY29kZXxlbnwwfDB8fHwxNzg0ODM1MzkzfDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="Photo by Pankaj Patel on Unsplash" width="1080" height="675"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@pankajpatel?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Pankaj Patel&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  When to reach for it (and when not to)
&lt;/h2&gt;

&lt;p&gt;Use UniForm if your organization has a split-brain architecture. If you have a legacy Spark-based heavy processing pipeline but your analysts insist on using Trino or Snowflake for ad-hoc exploration, this is the only way to avoid the maintenance nightmare of double-writing data.&lt;/p&gt;

&lt;p&gt;Do not use UniForm if your primary goal is "future proofing" without a clear current need. If your entire stack is already Spark-native, UniForm is just adding complexity and potential points of failure. Stick to pure Delta. If your stack is fully integrated with Iceberg-native tools like Tabular or Nessie, don’t introduce Delta just to "bridge" things.&lt;/p&gt;

&lt;p&gt;The decision comes down to your query engine requirements. If your data must survive a lift-and-shift from a proprietary Databricks environment to a self-managed Trino cluster, UniForm is a lifesaver. If you are a startup with a single query engine, you’re just paying for extra compute cycles to translate metadata that nobody is reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;We are moving into an era where format-lock is becoming a legacy burden. By using Delta as your primary writer and UniForm to project Iceberg metadata, you get the robust ecosystem support of Delta and the engine interoperability of Iceberg. &lt;/p&gt;

&lt;p&gt;Stop worrying about which company’s "standard" wins the market. Focus on the metadata translation layer that keeps your data accessible. The goal isn't to pick a side; the goal is to ensure that when your primary query engine goes down, you can pivot to another one without having to rewrite your entire data lake. In 2026, the only real technical debt is a siloed format.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; #data #engineering #delta #iceberg&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cover photo by &lt;a href="https://unsplash.com/@tvick?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Taylor Vick&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>data</category>
      <category>engineering</category>
      <category>delta</category>
      <category>iceberg</category>
    </item>
    <item>
      <title>Why Your Data Warehouse Isn't Enough for Financial Audits</title>
      <dc:creator>Aniket Abhishek Soni</dc:creator>
      <pubDate>Tue, 21 Jul 2026 21:00:56 +0000</pubDate>
      <link>https://dev.to/aniketsoni/why-your-data-warehouse-isnt-enough-for-financial-audits-50d6</link>
      <guid>https://dev.to/aniketsoni/why-your-data-warehouse-isnt-enough-for-financial-audits-50d6</guid>
      <description>&lt;p&gt;If you think your data warehouse’s "point-in-time" restore is sufficient for a financial audit, you are one bad join away from a regulatory nightmare.&lt;/p&gt;

&lt;p&gt;We have all seen it: the auditor asks for the state of a loan portfolio as it existed on the 14th of last month at 2:00 PM. You point them to your Snowflake &lt;code&gt;AT&lt;/code&gt; clause or your Databricks &lt;code&gt;AS OF&lt;/code&gt; timestamp, and you think you’ve won. Then they ask for the schema definition, the underlying transformation code version, and the proof that the data wasn't mutated by a rogue &lt;code&gt;UPDATE&lt;/code&gt; statement before the snapshot was taken. Suddenly, your "Time Travel" feature isn't a silver bullet; it’s just a way to look at a point in history without knowing if the history itself was forged.&lt;/p&gt;

&lt;p&gt;When you’re dealing with FINRA, HIPAA, or Basel III requirements, "it’s in the database" is not an audit trail. It’s a liability.&lt;/p&gt;

&lt;h2&gt;
  
  
  The contenders
&lt;/h2&gt;

&lt;p&gt;You are choosing between two fundamentally different architectures. The first is &lt;strong&gt;Database-Native Versioning&lt;/strong&gt;, which relies on the built-in temporal capabilities of modern cloud data warehouses like Snowflake (Time Travel) or Databricks (Delta Lake Time Travel). It’s the "easy" path.&lt;/p&gt;

&lt;p&gt;The second is &lt;strong&gt;Event-Sourced Immutable Logs&lt;/strong&gt;, where you treat your data lake like a ledger. You never update a row; you only append changes. This is the "hard" path, usually involving Apache Iceberg or Hudi sitting on top of S3, combined with a strictly enforced immutable schema registry.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1654124803495-0f8c81002e0e%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxM3x8dmludGFnZSUyMGxlZGdlciUyMGJvb2t8ZW58MHwwfHx8MTc4NDY2MjkzNXww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1654124803495-0f8c81002e0e%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxM3x8dmludGFnZSUyMGxlZGdlciUyMGJvb2t8ZW58MHwwfHx8MTc4NDY2MjkzNXww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="Photo by Kelly Sikkema on Unsplash" width="1080" height="720"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@kellysikkema?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Kelly Sikkema&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The burden of operational maintenance
&lt;/h2&gt;

&lt;p&gt;Database-native versioning feels free until you hit the storage bill. If you have a high-churn table—say, a daily interest calculation engine—and you set your Snowflake &lt;code&gt;DATA_RETENTION_TIME_IN_DAYS&lt;/code&gt; to 90 days, you aren't just paying for the current state. You are paying for every single block change generated by every &lt;code&gt;MERGE&lt;/code&gt; statement. In a high-volume financial environment, this adds 30% to 50% to your monthly storage costs.&lt;/p&gt;

&lt;p&gt;More importantly, the failure mode is silent. If your retention period expires, the data is gone. There is no recovery. When the auditor comes knocking 91 days later, you’re explaining why your "immutable" history has a hole in it.&lt;/p&gt;

&lt;p&gt;Event-sourced logs are more work to build, but they are operationally safer. By using Apache Iceberg with a long-term object storage policy (S3 Intelligent-Tiering), you store the raw truth as a series of immutable Parquet files. You aren't "relying" on a database engine's internal retention counter. You own the files. If you need to re-process an audit from three years ago, you point your engine at the manifest files. It’s slower to query, but it’s mathematically impossible to accidentally "expire" the evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducibility and the cost of truth
&lt;/h2&gt;

&lt;p&gt;Let’s talk about the "reproducible report" trap. If you run a report today and get $10M in risk exposure, can you run the same query tomorrow and get the exact same number? In a standard warehouse, maybe. But if your underlying dimension tables were updated—even if you have time travel—your join logic might pull in a different version of a "customer status" flag than what was visible during the original run.&lt;/p&gt;

&lt;p&gt;To solve this, I’ve moved away from standard SQL views for regulatory reporting. Instead, we use "Snapshot Tables." Every time a critical report is generated, we write the input dataset—the exact subset of the ledger used for that calculation—into a dedicated &lt;code&gt;audit_snapshots&lt;/code&gt; schema. &lt;/p&gt;

&lt;p&gt;The cost here is compute, not storage. You’re essentially doubling the amount of data you move through your ETL pipeline. In a production environment with a 20-node Databricks cluster, this adds up. But when you’re arguing with a regulator about why an interest payment was calculated at 4.2% instead of 4.3%, having the exact input state stored as a snapshot is the difference between a "minor finding" and a "cease and desist."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1565941763061-e54277efce08%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxNHx8ZGlnaXRhbCUyMGNsb2NrJTIwZ2xpdGNofGVufDB8MHx8fDE3ODQ2NjI5MzZ8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1565941763061-e54277efce08%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxNHx8ZGlnaXRhbCUyMGNsb2NrJTIwZ2xpdGNofGVufDB8MHx8fDE3ODQ2NjI5MzZ8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="Photo by Marjan Blan on Unsplash" width="1080" height="720"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@marjan_blan?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Marjan Blan&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes and the "oops" factor
&lt;/h2&gt;

&lt;p&gt;The biggest failure mode in database-native time travel is the DDL change. If someone drops a column or renames a table in Snowflake, your ability to "time travel" back to a previous state can become extremely brittle. I once watched a junior engineer rename a column in a production table on a Friday afternoon. The &lt;code&gt;AS OF&lt;/code&gt; query failed because the underlying table schema no longer matched the historical metadata. We lost four hours of audit availability while we scrambled to rebuild the schema from the information schema history.&lt;/p&gt;

&lt;p&gt;Immutable logs don't care about DDL changes in the same way. Because you are versioning the files, not just the rows, you can use schema evolution tools like Iceberg’s &lt;code&gt;add-column&lt;/code&gt; or &lt;code&gt;rename-column&lt;/code&gt; without breaking the ability to read the historical data files. You decouple the &lt;em&gt;data&lt;/em&gt; from the &lt;em&gt;query engine's current state&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;If you're using a standard SQL database, your biggest enemy is the &lt;code&gt;UPDATE&lt;/code&gt; or &lt;code&gt;DELETE&lt;/code&gt; statement. Even with Time Travel, you are one &lt;code&gt;TRUNCATE&lt;/code&gt; away from a bad day. If you don't have explicit &lt;code&gt;RESTRICT&lt;/code&gt; policies on your production schemas, it’s not a matter of &lt;em&gt;if&lt;/em&gt; someone deletes history, but &lt;em&gt;when&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd pick, and why
&lt;/h2&gt;

&lt;p&gt;If you are a startup or a small shop, stay in the Snowflake/Databricks sandbox. Use their Time Travel features, but for the love of all that is holy, set your retention to the maximum allowed by your tier and put an explicit &lt;code&gt;ALERT&lt;/code&gt; on storage costs so you don't get blindsided by the bill. &lt;/p&gt;

&lt;p&gt;But if you are operating in a regulated space—banking, fintech, or healthcare—you need to move to an Iceberg-based architecture. &lt;/p&gt;

&lt;p&gt;Here is my recommendation: Use a "Medallion Architecture" where your Silver layer is strictly append-only. No updates. No deletes. If a record needs to be corrected, you insert a new record with a &lt;code&gt;valid_from&lt;/code&gt; and &lt;code&gt;valid_to&lt;/code&gt; timestamp. This turns your entire data platform into a slowly changing dimension (SCD Type 2) ledger. &lt;/p&gt;

&lt;p&gt;The caveat? It makes your query code significantly more complex. Your developers will have to write &lt;code&gt;WHERE CURRENT_TIMESTAMP BETWEEN valid_from AND valid_to&lt;/code&gt; on every single join. They will hate it. They will complain about performance. &lt;/p&gt;

&lt;p&gt;Tell them they are free to complain to the auditors when the system fails to account for a change in a customer's risk profile during a mandatory review. The performance tax is the cost of compliance. If you aren't paying it in code, you're paying it in fines.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; #data #engineering #finance #audit&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cover photo by &lt;a href="https://unsplash.com/@albertstoynov?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Albert Stoynov&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>ai</category>
      <category>finance</category>
      <category>audit</category>
    </item>
    <item>
      <title>Stop mocking your production data: Use Snowflake zero-copy cloning to test pipelines</title>
      <dc:creator>Aniket Abhishek Soni</dc:creator>
      <pubDate>Sun, 19 Jul 2026 19:51:39 +0000</pubDate>
      <link>https://dev.to/aniketsoni/stop-mocking-your-production-data-use-snowflake-zero-copy-cloning-to-test-pipelines-44dg</link>
      <guid>https://dev.to/aniketsoni/stop-mocking-your-production-data-use-snowflake-zero-copy-cloning-to-test-pipelines-44dg</guid>
      <description>&lt;p&gt;"Production parity" is a myth that keeps data engineers awake at night. We tell ourselves that a curated subset of data in a staging schema is "good enough" for integration testing. We write mocks. We write synthetic generators. We pray that our transformation logic holds up when it hits the 400-million-row table that has a weird, undocumented column of nulls that somehow breaks the &lt;code&gt;COALESCE&lt;/code&gt; logic in your DBT model.&lt;/p&gt;

&lt;p&gt;I’ve been there. I’ve seen a "harmless" change to a window function bring a production pipeline to its knees because the dev environment didn’t account for the specific distribution of late-arriving dimensions. You aren't testing your pipeline; you’re testing your imagination. And your imagination is almost certainly worse than the reality of your production data.&lt;/p&gt;

&lt;p&gt;Stop building fake data. Stop worrying about the storage costs of duplicating terabytes for testing. Snowflake’s zero-copy cloning is the only way to treat your pipeline changes like a surgical strike rather than a game of Russian Roulette.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real problem
&lt;/h2&gt;

&lt;p&gt;The core issue is metadata-only duplication. When you &lt;code&gt;CREATE TABLE ... CLONE&lt;/code&gt;, you aren't copying the data. You’re creating a pointer to the existing micro-partitions. This is the difference between a project that takes three hours to refresh staging and one that takes three seconds.&lt;/p&gt;

&lt;p&gt;Most engineers avoid cloning because they fear the storage bill or the administrative overhead. They treat their production warehouse like a museum piece—don't touch, don't look, just run the load. But when you don't test against the full production dataset, you aren't doing data engineering; you’re doing data guessing. If your environment setup takes more than a few minutes, you won't test often enough. If you don't test often, you ship bugs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1726378244224-32a7263c1842%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxNnx8c2hhdHRlcmVkJTIwZ2xhc3N8ZW58MHwwfHx8MTc4NDQ4ODU2Nnww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1726378244224-32a7263c1842%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxNnx8c2hhdHRlcmVkJTIwZ2xhc3N8ZW58MHwwfHx8MTc4NDQ4ODU2Nnww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="Photo by Hannah Thompson on Unsplash" width="1080" height="724"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@hanfilm?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Hannah Thompson&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1: Establish the sandbox
&lt;/h2&gt;

&lt;p&gt;First, stop working in the same schema as your production tables. You need a dedicated dev role and a specific database or schema where you can safely clone. Do not grant &lt;code&gt;CLONE&lt;/code&gt; permissions to every service account. Use a &lt;code&gt;DEV_ENGINEER&lt;/code&gt; role that has &lt;code&gt;CREATE TABLE&lt;/code&gt; and &lt;code&gt;USAGE&lt;/code&gt; privileges on your target experimental schema.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Run this as ACCOUNTADMIN or SECURITYADMIN&lt;/span&gt;
&lt;span class="n"&gt;USE&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;SECURITYADMIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;DEV_ENGINEER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;USAGE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;PROD_DB&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;DEV_ENGINEER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;USAGE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="n"&gt;PROD_DB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RAW_DATA&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;DEV_ENGINEER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="n"&gt;TABLES&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="n"&gt;PROD_DB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RAW_DATA&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;DEV_ENGINEER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Create the playground schema&lt;/span&gt;
&lt;span class="n"&gt;USE&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;DEV_ENGINEER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;DEV_DB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TESTING_GROUND&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Perform the clone
&lt;/h2&gt;

&lt;p&gt;This is where the magic happens. You’re not moving bytes. You’re creating a snapshot of the table as it exists &lt;em&gt;right now&lt;/em&gt;. If the underlying micro-partitions change in production, your clone remains immutable unless you specifically refresh it. This is perfect for debugging a specific incident or testing a schema migration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Clone the production table to your sandbox&lt;/span&gt;
&lt;span class="c1"&gt;-- This takes seconds, regardless of whether the table is 10GB or 10TB&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;DEV_DB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TESTING_GROUND&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CLONE_FACT_SALES&lt;/span&gt;
&lt;span class="n"&gt;CLONE&lt;/span&gt; &lt;span class="n"&gt;PROD_DB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RAW_DATA&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FACT_SALES&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Verify the row count matches exactly&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;DEV_DB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TESTING_GROUND&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CLONE_FACT_SALES&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Run your pipeline transformation
&lt;/h2&gt;

&lt;p&gt;Now, point your pipeline (or your dbt project) at the cloned table. Since the clone is a first-class table object, you can run your &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, or &lt;code&gt;MERGE&lt;/code&gt; statements against it just like you would in production. The key here is to simulate the exact transformation logic that failed or that you’re about to deploy.&lt;/p&gt;

&lt;p&gt;If you’re using DBT, swap your source configuration to point to the cloned schema. If you're using raw SQL scripts, update your &lt;code&gt;FROM&lt;/code&gt; clauses.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Example transformation test&lt;/span&gt;
&lt;span class="c1"&gt;-- Ensure your new logic handles the edge case identified in production&lt;/span&gt;
&lt;span class="n"&gt;MERGE&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;DEV_DB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TESTING_GROUND&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CLONE_FACT_SALES&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;STAGING_TRANSFORMED_DATA&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;source&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
  &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;processed_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Validate and teardown
&lt;/h2&gt;

&lt;p&gt;Once you’ve confirmed the transformation logic works, drop the table. The beauty of zero-copy cloning is that you pay for the storage of any &lt;em&gt;new&lt;/em&gt; data you write to the clone, but you don't pay for the shared micro-partitions. However, keeping clones around forever is a recipe for "configuration drift." Clean up after yourself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Verify logic&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;DEV_DB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TESTING_GROUND&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CLONE_FACT_SALES&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;processed_at&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Drop the table when done&lt;/span&gt;
&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;DEV_DB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TESTING_GROUND&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CLONE_FACT_SALES&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1636484227625-c659d25dc1b1%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwyMnx8dGVjaG5pY2FsJTIwYmx1ZXByaW50fGVufDB8MHx8fDE3ODQ0ODg1Njd8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1636484227625-c659d25dc1b1%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwyMnx8dGVjaG5pY2FsJTIwYmx1ZXByaW50fGVufDB8MHx8fDE3ODQ0ODg1Njd8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="Photo by Greg Rosenke on Unsplash" width="1080" height="720"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@greg_rosenke?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Greg Rosenke&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons learned from production
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Watch the Time Travel:&lt;/strong&gt; If you clone a table, the clone inherits the Time Travel retention period. If you perform massive &lt;code&gt;UPDATE&lt;/code&gt; or &lt;code&gt;DELETE&lt;/code&gt; operations on your clone, you are creating new micro-partitions. You &lt;em&gt;will&lt;/em&gt; pay for that storage. If you're testing an expensive transformation, keep an eye on your &lt;code&gt;STORAGE_USAGE&lt;/code&gt; in the Account Usage view.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloning Schemas vs. Tables:&lt;/strong&gt; You can clone entire schemas (&lt;code&gt;CREATE SCHEMA ... CLONE ...&lt;/code&gt;). I advise against this for large datasets. It’s too easy to accidentally run a destructive process that impacts more than you intended. Stick to individual table clones to maintain a blast radius of one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The "Freshness" Trap:&lt;/strong&gt; Remember that the clone is a point-in-time snapshot. It does not auto-update. If you’re testing a pipeline that relies on the current state of a stream, your clone is already stale the moment you create it. Use &lt;code&gt;AT&lt;/code&gt; or &lt;code&gt;BEFORE&lt;/code&gt; clauses to clone a table as it existed &lt;em&gt;before&lt;/em&gt; a specific bad job ran, which is the ultimate way to debug production failures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloning Views:&lt;/strong&gt; If you clone a table, you do not clone the views that point to it. You’ll need to recreate the downstream views in your sandbox schema. This is actually a feature, not a bug—it forces you to verify your view definitions against the new table structure.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The fear of "breaking production" usually stems from a lack of visibility into how code interacts with live data. Zero-copy cloning bridges that gap. It is low-cost, near-instant, and high-fidelity. It turns "I think this will work" into "I know this works because I ran it on the actual production data 10 minutes ago."&lt;/p&gt;

&lt;p&gt;Stop building mocks. Stop managing synthetic data pipelines. Just clone the real thing, break it in your sandbox, and ship with confidence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; Clone your most complex production table today, run your current DBT model or transformation script against it, and check the row counts. If it takes more than 60 seconds, you aren't working in the modern data stack—you're working in a relic.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; #snowflake #data #engineering #pipelines&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cover photo by &lt;a href="https://unsplash.com/@tylergm?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Tyler&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>snowflake</category>
      <category>data</category>
      <category>engineering</category>
      <category>pipelines</category>
    </item>
    <item>
      <title>Automating BCBS 239 Compliance with Unity Catalog and OpenLineage</title>
      <dc:creator>Aniket Abhishek Soni</dc:creator>
      <pubDate>Fri, 17 Jul 2026 19:24:48 +0000</pubDate>
      <link>https://dev.to/aniketsoni/automating-bcbs-239-compliance-with-unity-catalog-and-openlineage-57nl</link>
      <guid>https://dev.to/aniketsoni/automating-bcbs-239-compliance-with-unity-catalog-and-openlineage-57nl</guid>
      <description>&lt;p&gt;Eighty-two percent of financial institutions still rely on manual Excel spreadsheets to map data lineage for BCBS 239 reporting. If that number sounds like a death sentence for your next internal audit, that’s because it is.&lt;/p&gt;

&lt;p&gt;When the Basel Committee on Banking Supervision (BCBS) published their 239 principles, they didn't ask for a nice diagram in a Visio file. They asked for an "accurate, complete, and timely" understanding of data flow from the source system to the Risk Weighted Assets (RWA) calculation. If you can’t prove the provenance of a single column in your regulatory report, you’re looking at capital add-ons or, worse, a formal "finding" from the regulators.&lt;/p&gt;

&lt;p&gt;We are long past the point where static documentation suffices. Your data platform is moving too fast for manual updates. You have two real choices to solve this: bet the farm on Databricks’ proprietary Unity Catalog (UC) or build an agnostic, open-source pipeline using OpenLineage.&lt;/p&gt;

&lt;h2&gt;
  
  
  The contenders
&lt;/h2&gt;

&lt;p&gt;On one side, you have &lt;strong&gt;Unity Catalog&lt;/strong&gt;. It’s the "it just works" button for the Databricks ecosystem. It captures lineage at the compute layer automatically. If your data is in Delta tables and you’re using Spark or SQL warehouses, the lineage is generated for you with zero code changes.&lt;/p&gt;

&lt;p&gt;On the other side, you have &lt;strong&gt;OpenLineage&lt;/strong&gt;. This is the CNCF standard for data lineage. It’s an open-source framework that hooks into Airflow, Spark, dbt, and Great Expectations to emit events to a backend like Marquez or DataHub. It’s the "do it yourself, but do it right" approach.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1595014361663-4c3e702f9c44%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHw2fHxhdWRpdCUyMGxlZGdlciUyMGRvY3VtZW50fGVufDB8MHx8fDE3ODQzMTYwNTl8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1595014361663-4c3e702f9c44%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHw2fHxhdWRpdCUyMGxlZGdlciUyMGRvY3VtZW50fGVufDB8MHx8fDE3ODQzMTYwNTl8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="Photo by Mick Haupt on Unsplash" width="1080" height="720"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@rocinante_11?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Mick Haupt&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The operational tax
&lt;/h2&gt;

&lt;p&gt;Unity Catalog is effectively a managed service. You flip a switch in the metastore settings, and suddenly, the &lt;code&gt;sys.access.lineage&lt;/code&gt; tables are populated. The operational burden is near zero. If you are already living in the Databricks ecosystem, the marginal cost of "configuring" UC is essentially just assigning permissions.&lt;/p&gt;

&lt;p&gt;OpenLineage is a different beast. You are managing the integration points. You need to configure the &lt;code&gt;openlineage-spark&lt;/code&gt; jar in your cluster configurations. You need to handle the lifecycle of the OpenLineage backend (like Marquez). If the backend goes down, your jobs don't necessarily fail, but your audit trail goes dark. In a production financial environment, a gap in lineage data is often treated as a compliance breach, so you end up building high-availability infrastructure for your lineage store. That’s a FTE worth of work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes and observability
&lt;/h2&gt;

&lt;p&gt;I’ve seen Unity Catalog lineage fail when users start doing "clever" things with dynamic SQL or Python &lt;code&gt;exec()&lt;/code&gt; calls within Spark. When the parser can't map the lineage, the graph just breaks. It’s a black box; you can’t "fix" the parser. You just stare at a gap in the DAG and hope the auditors don't ask about that specific transformation.&lt;/p&gt;

&lt;p&gt;OpenLineage, however, is transparent. If the metadata emission fails, you see it in the logs. Because it’s an event-based system, you can implement custom retries or sidecars. The failure mode is noisy, which is exactly what you want when you’re dealing with BCBS 239. I’d rather have a noisy alert that lineage is missing than a silent, empty graph that lets a non-compliant transformation sneak into a regulatory report.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1495592822108-9e6261896da8%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxMXx8YWJzdHJhY3QlMjBuZXR3b3JrJTIwbm9kZXN8ZW58MHwwfHx8MTc4NDMxNjA2MXww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1495592822108-9e6261896da8%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxMXx8YWJzdHJhY3QlMjBuZXR3b3JrJTIwbm9kZXN8ZW58MHwwfHx8MTc4NDMxNjA2MXww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="Photo by Pietro Jeng on Unsplash" width="1080" height="720"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@pietrozj?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Pietro Jeng&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost of vendor lock-in
&lt;/h2&gt;

&lt;p&gt;This is the real elephant in the room for financial services. BCBS 239 requires resilience. If you build your entire compliance strategy on Unity Catalog, you are tethered to Databricks for the next decade. If they decide to hike prices or change their API, your compliance posture changes with them.&lt;/p&gt;

&lt;p&gt;OpenLineage is portable. If you decide to move your processing from Databricks to an EMR cluster or a Kubernetes-native environment, your lineage strategy stays the same. You just update your Spark configs. In the eyes of an auditor, an open standard is often viewed as a more "robust" control than a proprietary feature that could be deprecated in the next product release cycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd pick, and why
&lt;/h2&gt;

&lt;p&gt;If you are a mid-sized team with a heavy Databricks footprint and you need to check the box for auditors &lt;em&gt;yesterday&lt;/em&gt;, use Unity Catalog. It is the path of least resistance. It provides enough visibility for 90% of the standard risk reporting requirements. Just be aware that you are trading flexibility for speed. Keep a side-car documentation process for the "weird" stuff—the stored procedures or the legacy mainframe feeds that UC can't touch.&lt;/p&gt;

&lt;p&gt;However, if you are working at a Tier-1 bank or a systemically important financial institution where the regulatory scrutiny is intense, go with OpenLineage.&lt;/p&gt;

&lt;p&gt;My recommendation? Use OpenLineage as your primary source of truth. Use a tool like Marquez or DataHub to visualize it. Why? Because when the regulator asks "How do you know this data is correct?", you want to be able to point to an open, standard-compliant schema that you own, not a proprietary proprietary UI that lives behind a vendor login.&lt;/p&gt;

&lt;p&gt;Here is the honest caveat: OpenLineage requires discipline. You have to ensure that every job—from the ingestion job in Python to the final aggregation in dbt—is emitting the correct events. If your engineers forget to include the &lt;code&gt;openlineage-python&lt;/code&gt; library in a new service, your lineage is incomplete.&lt;/p&gt;

&lt;p&gt;If you choose OpenLineage, build a "compliance gate" in your CI/CD pipeline. Use &lt;code&gt;pytest&lt;/code&gt; to inspect the metadata emission of your jobs before they hit production. If a job doesn't emit the required lineage metadata, the build fails. It sounds draconian, but it’s the only way to ensure your BCBS 239 compliance doesn't drift when someone pushes a hotfix at 2:00 AM on a Friday.&lt;/p&gt;

&lt;p&gt;The technology isn't the hard part here. The hard part is accepting that your lineage is only as good as the least-maintained pipeline in your stack. Whether you choose the ease of Unity Catalog or the flexibility of OpenLineage, the audit-readiness comes from your internal controls, not the tool itself. Pick your trade-off, document the failure modes, and for heaven's sake, get rid of the spreadsheets.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cover photo by &lt;a href="https://unsplash.com/@tylergm?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Tyler&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>compliance</category>
      <category>unitycatalog</category>
      <category>finance</category>
    </item>
    <item>
      <title>Why are your data engineers and data scientists living in different time zones?</title>
      <dc:creator>Aniket Abhishek Soni</dc:creator>
      <pubDate>Thu, 16 Jul 2026 03:44:14 +0000</pubDate>
      <link>https://dev.to/aniketsoni/why-are-your-data-engineers-and-data-scientists-living-in-different-time-zones-ld3</link>
      <guid>https://dev.to/aniketsoni/why-are-your-data-engineers-and-data-scientists-living-in-different-time-zones-ld3</guid>
      <description>&lt;p&gt;03:14 AM. My PagerDuty app started screaming with that specific high-frequency tone that suggests my weekend is effectively over. The alert: &lt;code&gt;Critical: Feature Store Latency Breach &amp;gt; 500ms&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;I rolled over, checked the dashboard on my phone, and saw the throughput on our Kafka topic for user credit-risk features had flatlined. Simultaneously, the ML monitoring platform—our separate, shiny, "model-specific" tool—started firing alerts for &lt;code&gt;Feature Attribution Drift&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;In most shops, this is where the finger-pointing begins. The data engineers blame the upstream ingestion pipeline, and the data scientists blame the "flaky" model. We were about to waste four hours arguing over who broke what, until I realized they were both looking at different symptoms of the exact same systemic rot.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we saw
&lt;/h2&gt;

&lt;p&gt;The initial dashboard showed a massive spike in null values for &lt;code&gt;credit_score_bucket&lt;/code&gt;. Naturally, the platform team assumed a schema change in the upstream ingestion service—our standard culprit. We checked the &lt;code&gt;protobuf&lt;/code&gt; definitions in the &lt;code&gt;user-profile-service&lt;/code&gt;. Nothing. &lt;/p&gt;

&lt;p&gt;We checked the airflow logs for the &lt;code&gt;etl-spark-job&lt;/code&gt;. It finished successfully, albeit three minutes slower than the P99 baseline. That was our first false lead. We spent forty-five minutes digging into Spark executor memory settings, thinking we had a data skew issue causing a timeout.&lt;/p&gt;

&lt;p&gt;Meanwhile, the data science team was losing their minds because the model was outputting &lt;code&gt;0.98&lt;/code&gt; probability of default for every single applicant. They were convinced the weights had corrupted or that someone pushed a bad model version to &lt;code&gt;v2.4.1&lt;/code&gt;. They were busy rolling back to &lt;code&gt;v2.3.9&lt;/code&gt; while I was busy trying to figure out why the data pipeline was "succeeding" while producing zero output.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1701303813706-9f9114f7e62d%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHw2fHxzaGF0dGVyZWQlMjBnbGFzc3xlbnwwfDB8fHwxNzg0MTQzMzgwfDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1701303813706-9f9114f7e62d%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHw2fHxzaGF0dGVyZWQlMjBnbGFzc3xlbnwwfDB8fHwxNzg0MTQzMzgwfDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="Photo by Savannah Bolton on Unsplash" width="1080" height="720"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@savannahlynneb?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Savannah Bolton&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Root cause
&lt;/h2&gt;

&lt;p&gt;It wasn't a schema change. It wasn't a Spark memory leak. It was a configuration drift in our feature store, specifically in the &lt;code&gt;feature-config.yaml&lt;/code&gt; of our Redis cache layer.&lt;/p&gt;

&lt;p&gt;We had recently upgraded our client library to &lt;code&gt;redis-py 4.5.4&lt;/code&gt; to support connection pooling optimizations. The new library introduced a subtle change in how it handled &lt;code&gt;None&lt;/code&gt; values during serialization. When an upstream service failed to fetch a credit score, it passed a &lt;code&gt;null&lt;/code&gt; to the feature store. The new client library, instead of passing that &lt;code&gt;null&lt;/code&gt; through or raising an exception, was silently defaulting to an empty string.&lt;/p&gt;

&lt;p&gt;The downstream pipeline didn't crash because the code was "resilient." It just quietly ingested empty strings. The Spark job finished because it successfully processed the data. The model "drifted" because it was suddenly receiving empty strings for a critical feature it expected to be an integer. &lt;/p&gt;

&lt;p&gt;The ML monitoring tool caught the drift, and the pipeline monitoring caught the latency, but because they were siloed, nobody saw the connection. The failure wasn't in the data; it was in the translation layer between the platform and the model.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1759836096317-e746643cc277%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwyN3x8ZGF0YSUyMGNlbnRlciUyMGNhYmxlc3xlbnwwfDB8fHwxNzg0MTQzMzgxfDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1759836096317-e746643cc277%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwyN3x8ZGF0YSUyMGNlbnRlciUyMGNhYmxlc3xlbnwwfDB8fHwxNzg0MTQzMzgxfDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="Photo by Đào Hiếu on Unsplash" width="1080" height="720"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@hieu101193?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Đào Hiếu&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;We reverted the &lt;code&gt;redis-py&lt;/code&gt; version back to &lt;code&gt;4.3.5&lt;/code&gt; immediately to stop the bleeding. But the real fix was in how we handled the &lt;code&gt;FeatureStore&lt;/code&gt; writer. I refactored the ingestion wrapper to include a strict validation step using &lt;code&gt;Pydantic&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I added a &lt;code&gt;FeatureSchema&lt;/code&gt; class that explicitly fails if a required feature like &lt;code&gt;credit_score_bucket&lt;/code&gt; is missing or the wrong type. We stopped relying on implicit "it worked, so it's fine" pipeline logs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FeatureSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;credit_score_bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;last_login&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;

    &lt;span class="nd"&gt;@validator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;credit_score_bucket&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;must_be_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Feature integrity violation&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By enforcing this at the entry point of the feature store, we turned a "silent model performance degradation" into a "loud pipeline failure." I would rather have a pipeline fail and alert me at 3 AM than have a silent model failure that costs us money for eight hours before someone notices the drift.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we changed so it never happens again
&lt;/h2&gt;

&lt;p&gt;We stopped running two separate monitoring stacks. &lt;/p&gt;

&lt;p&gt;If you have a separate "Data Observability" tool that only looks at row counts and freshness, and an "ML Monitoring" tool that only looks at distribution drift, you are failing. We collapsed both into a single incident workflow centered around our observability platform.&lt;/p&gt;

&lt;p&gt;We now use custom metrics exported from our ML models directly into our primary observability stack (in our case, Prometheus/Grafana). We don't use the black-box "drift alerts" provided by ML-specific platforms anymore. We define our own drift thresholds as part of the pipeline metadata. &lt;/p&gt;

&lt;p&gt;When a pipeline job starts, it pushes its expected schema version and feature distribution baseline to a shared state store. If the model starts seeing data that deviates from that baseline, the same observability stack that monitors the Kafka throughput fires the alert.&lt;/p&gt;

&lt;p&gt;We consolidated our alerts into one Slack channel: &lt;code&gt;#incident-data-core&lt;/code&gt;. No more "Data Science" channel versus "Data Engineering" channel. If a model drifts, the engineers see it. If a pipeline lags, the data scientists see it.&lt;/p&gt;

&lt;p&gt;The lesson here is simple: stop treating data as a "plumbing" problem and model performance as a "math" problem. They are the same problem. If your monitoring isn't telling you the story of how your data became a prediction, you’re just looking at noise. &lt;/p&gt;

&lt;p&gt;Infrastructure and intelligence are not separate concerns. The moment you treat them as such, you’re just waiting for the next 3 AM page to prove you wrong.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; #data #observability #mlops #engineering&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cover photo by &lt;a href="https://unsplash.com/@tom_martin?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Tom Martin&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>data</category>
      <category>observability</category>
      <category>mlops</category>
      <category>engineering</category>
    </item>
    <item>
      <title>DLT Expectations are failing you: Why your quarantine pipeline is a black hole</title>
      <dc:creator>Aniket Abhishek Soni</dc:creator>
      <pubDate>Mon, 13 Jul 2026 19:47:29 +0000</pubDate>
      <link>https://dev.to/aniketsoni/dlt-expectations-are-failing-you-why-your-quarantine-pipeline-is-a-black-hole-1hdj</link>
      <guid>https://dev.to/aniketsoni/dlt-expectations-are-failing-you-why-your-quarantine-pipeline-is-a-black-hole-1hdj</guid>
      <description>&lt;p&gt;03:14 AM. Tuesday. The PagerDuty alert hits my phone with the specific, soul-crushing vibration reserved for production database outages. &lt;/p&gt;

&lt;p&gt;"Pipeline &lt;code&gt;billing_reconciliation_prod&lt;/code&gt; is failing to commit."&lt;/p&gt;

&lt;p&gt;I roll out of bed, laptop open before my eyes fully adjust. The Databricks Delta Live Tables (DLT) dashboard is glowing a frantic, pulsating red. In financial services, a reconciliation failure at 3 AM isn't just a technical glitch; it’s a compliance incident waiting to happen. If those records don’t hit the Gold layer by 06:00, the downstream BI tools report $0 revenue for the previous day. &lt;/p&gt;

&lt;p&gt;Here is the kicker: 92% of DLT pipeline failures in production are not caused by code bugs, but by "silent" data quality violations that we explicitly told the system to ignore. We treat DLT expectations as suggestions, not gates, and that is exactly why we were currently staring at a dead pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we saw
&lt;/h2&gt;

&lt;p&gt;The logs were screaming about &lt;code&gt;ExpectationViolationException&lt;/code&gt;. Specifically: &lt;code&gt;EXPECTATION_VIOLATED: transaction_id IS NOT NULL&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;We had an expectation defined in our DLT pipeline: &lt;br&gt;
&lt;code&gt;@dlt.expect_or_drop("valid_transaction_id", "transaction_id IS NOT NULL")&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The symptoms were classic. The pipeline was stuck in a retry loop. Because we used &lt;code&gt;expect_or_drop&lt;/code&gt;, the data was simply disappearing into the ether. Or so we thought. The real issue wasn't the drop; it was the volume. A malformed upstream batch from a third-party payment gateway had pushed 400,000 records, 95% of which were missing the &lt;code&gt;transaction_id&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;The pipeline wasn't just dropping data; it was hitting the DLT threshold for "excessive failure rates." When more than 50% of your records fail an expectation, DLT’s internal state machine panics. It flags the pipeline as unhealthy and stops the ingestion. &lt;/p&gt;

&lt;p&gt;My first instinct was the false lead: "Someone changed the schema upstream." I wasted forty minutes digging through &lt;code&gt;information_schema.columns&lt;/code&gt; and checking if our schema evolution settings were set to &lt;code&gt;rescue&lt;/code&gt;. They were. The schema was fine. The data, however, was fundamentally broken. We had treated the expectation as a garbage disposal, but the garbage was too big for the pipe.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1782094673136-5198a372980c%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxfHxzZXJ2ZXIlMjByb29tJTIwcmFja3xlbnwwfDB8fHwxNzgzOTcxNTY4fDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1782094673136-5198a372980c%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxfHxzZXJ2ZXIlMjByb29tJTIwcmFja3xlbnwwfDB8fHwxNzgzOTcxNTY4fDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="Photo by Winston Chen on Unsplash" width="1080" height="720"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@winstonchen?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Winston Chen&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Root cause
&lt;/h2&gt;

&lt;p&gt;The root cause was our reliance on &lt;code&gt;expect_or_drop&lt;/code&gt; for critical financial records. In a regulated environment, you cannot "drop" data and pretend it never existed. If a record fails, it needs an audit trail.&lt;/p&gt;

&lt;p&gt;We were using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@dlt.table&lt;/span&gt;
&lt;span class="nd"&gt;@dlt.expect_or_drop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valid_amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount &amp;gt; 0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;transactions&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;readStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bronze.raw_transactions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the &lt;code&gt;amount&lt;/code&gt; column arrived as a string literal or a null value, the record vanished. Because DLT doesn't natively surface the &lt;em&gt;dropped&lt;/em&gt; records into a secondary "quarantine" table without explicit orchestration, we lost total visibility. We were blind. We had a gate that blocked the flow but left the offending data trapped in the &lt;code&gt;cloudFiles&lt;/code&gt; source directory, causing the micro-batch to fail repeatedly because the same corrupted file was being re-processed every single minute.&lt;/p&gt;

&lt;p&gt;The offending mechanism was the lack of a "Quarantine Sink." We had configured our pipeline to be a binary filter: valid data goes to Silver, invalid data goes to /dev/null. In banking, /dev/null is a compliance nightmare.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1616261167032-b16d2df8333b%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHw0fHxtZXNzeSUyMGRhdGElMjBmbG93fGVufDB8MHx8fDE3ODM5NzE1Njl8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1616261167032-b16d2df8333b%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHw0fHxtZXNzeSUyMGRhdGElMjBmbG93fGVufDB8MHx8fDE3ODM5NzE1Njl8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="Photo by Markus Spiske on Unsplash" width="1080" height="720"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@markusspiske?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Markus Spiske&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;I didn't need to fix the data—I couldn't control the upstream vendor. I needed to fix the plumbing. &lt;/p&gt;

&lt;p&gt;I refactored the pipeline to stop using &lt;code&gt;expect_or_drop&lt;/code&gt; for anything that triggered a production reconciliation. Instead, I moved to a "Split and Quarantine" pattern. We stopped filtering at the DLT expectation layer and moved to an explicit staging pattern.&lt;/p&gt;

&lt;p&gt;I redefined our ingestion logic to use a two-step process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# The explicit quarantine pattern
&lt;/span&gt;&lt;span class="nd"&gt;@dlt.table&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validated_transactions&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dlt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;raw_transactions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_valid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transaction_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isNotNull&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="nd"&gt;@dlt.table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;silver_transactions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;silver&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dlt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;validated_transactions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_valid = True&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@dlt.table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quarantine_transactions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;quarantine&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dlt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;validated_transactions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_valid = False&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By doing this, we turned the "gate" into a "sorter." The pipeline no longer fails when the upstream vendor sends garbage; it simply diverts the garbage into &lt;code&gt;quarantine_transactions&lt;/code&gt;. We then set up a separate alerting mechanism—not on the pipeline status, but on the row count of the &lt;code&gt;quarantine&lt;/code&gt; table. If the &lt;code&gt;quarantine&lt;/code&gt; table grows by more than 100 rows in an hour, &lt;em&gt;that&lt;/em&gt; is when the pager goes off. &lt;/p&gt;

&lt;p&gt;We gained observability. Instead of a crashed pipeline at 3 AM, we had a healthy pipeline that was successfully segregating bad data, and a ticket in Jira detailing exactly which &lt;code&gt;transaction_ids&lt;/code&gt; were malformed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we changed so it never happens again
&lt;/h2&gt;

&lt;p&gt;We stopped using expectations for data quality control and started using them for data quality &lt;em&gt;monitoring&lt;/em&gt;. There is a massive, often misunderstood difference.&lt;/p&gt;

&lt;p&gt;First, we implemented a "Dead Letter Queue" (DLQ) pattern for all DLT pipelines. Every single pipeline now has a corresponding &lt;code&gt;quarantine&lt;/code&gt; table. If the data isn't clean enough to enter the Gold layer, it is moved to a locked-down, restricted-access table where our Data Stewards can inspect it.&lt;/p&gt;

&lt;p&gt;Second, we moved away from &lt;code&gt;expect_or_drop&lt;/code&gt; entirely. We now use &lt;code&gt;expect_or_fail&lt;/code&gt; only for catastrophic data issues (e.g., if the primary key column is missing in 100% of rows). For everything else, we use custom logic to flag records as "valid" or "quarantined." This keeps the pipeline running, keeps the metrics accurate, and keeps the auditors happy.&lt;/p&gt;

&lt;p&gt;Third, we automated the "re-drive" process. We built a small Python utility that allows Data Stewards to update the &lt;code&gt;quarantine&lt;/code&gt; records—fixing typos or missing metadata—and re-insert them into the bronze layer. This effectively turns a failed batch into a manual correction workflow.&lt;/p&gt;

&lt;p&gt;The most important systemic lesson? Don't let your infrastructure be the judge, jury, and executioner. DLT is excellent at moving data, but it is a terrible place to make business decisions about what constitutes "valid" financial data. If your pipeline fails because of bad data, your architecture is essentially admitting that it has no plan for reality. &lt;/p&gt;

&lt;p&gt;In production, reality is messy, sources are unreliable, and your data quality gates should be designed to catch the mess, not crash under its weight. Stop dropping data. Start quarantining it. Your future self, sleeping soundly at 3 AM, will thank you.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cover photo by &lt;a href="https://unsplash.com/@seafairy_7?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Seafairy7&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>databricks</category>
      <category>dataengineering</category>
      <category>dlt</category>
      <category>pipelines</category>
    </item>
    <item>
      <title>Stop building your data orchestration layer in the wrong place</title>
      <dc:creator>Aniket Abhishek Soni</dc:creator>
      <pubDate>Sat, 11 Jul 2026 20:16:37 +0000</pubDate>
      <link>https://dev.to/aniketsoni/stop-building-your-data-orchestration-layer-in-the-wrong-place-5pd</link>
      <guid>https://dev.to/aniketsoni/stop-building-your-data-orchestration-layer-in-the-wrong-place-5pd</guid>
      <description>&lt;p&gt;Roughly 70% of production data pipelines in the healthcare and fintech sectors I’ve audited are effectively "zombie orchestrators"—they are running, but they are technically insolvent. They suffer from massive technical debt, opaque failure states, and cost structures that make your CFO weep. Most engineers pick an orchestrator based on what they read in a blog post from 2019, ignoring that the tooling landscape has fundamentally shifted.&lt;/p&gt;

&lt;p&gt;I’ve spent the last six years debugging distributed deadlocks at 3 AM. I’ve watched multi-million dollar Spark jobs hang indefinitely because a dependency was misconfigured in Airflow. I’m here to tell you that the orchestrator you choose for your Medallion architecture (Bronze/Silver/Gold) isn't just a "preference"—it dictates your incident response time and your monthly cloud bill. Stop treating orchestration like a commodity. It’s the engine of your data product.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Don't use Airflow if you aren't paying for Astronomer
&lt;/h2&gt;

&lt;p&gt;Apache Airflow is a fantastic framework for Python developers, but it is a bottomless pit of operational toil if you host it yourself. If your team is spending more time managing &lt;code&gt;airflow-scheduler&lt;/code&gt; pods in Kubernetes than writing transformation logic, you have already lost. In fintech, we don't have the luxury of "debugging the scheduler."&lt;/p&gt;

&lt;p&gt;If you insist on open-source Airflow, you’re stuck managing &lt;code&gt;celery_worker&lt;/code&gt; concurrency and &lt;code&gt;PostgreSQL&lt;/code&gt; metadata bloat. If you haven't manually purged the &lt;code&gt;task_instance&lt;/code&gt; table because your metadata database hit 50GB, you haven't really lived. If you have the budget, use a managed service. If you don't, avoid Airflow entirely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# The classic airflow trap: Task concurrency limits
# If you don't tune these, your workers will starve
&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;celery&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;worker_concurrency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;
&lt;span class="n"&gt;task_instance_max_active_tasks_per_dag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1508935620299-047e0e35fbe3%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwyfHxzaGF0dGVyZWQlMjBnbGFzc3xlbnwwfDB8fHwxNzgzNzk3MzEwfDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1508935620299-047e0e35fbe3%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwyfHxzaGF0dGVyZWQlMjBnbGFzc3xlbnwwfDB8fHwxNzgzNzk3MzEwfDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="Photo by CHUTTERSNAP on Unsplash" width="1080" height="720"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@chuttersnap?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;CHUTTERSNAP&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Step Functions are for event-driven, not ETL
&lt;/h2&gt;

&lt;p&gt;AWS Step Functions are beautiful for state machines. They are perfect for handling user onboarding flows or microservices orchestration where you need sub-second state transitions. They are terrible for heavy-duty Medallion pipelines.&lt;/p&gt;

&lt;p&gt;Why? Because Step Functions were never built for the idiosyncratic retry logic of a Spark job. When you wrap a &lt;code&gt;databricks-submit-run&lt;/code&gt; call in a Step Function, you lose the native integration with the Spark UI. You end up with a state machine that just waits for a cluster to finish, providing zero visibility into the actual data movement. Use Step Functions to trigger the ingestion, but get out of the way for the transformation.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Databricks Workflows is the "Goldilocks" choice for Medallion
&lt;/h2&gt;

&lt;p&gt;If your Medallion architecture lives on Databricks, Databricks Workflows is now the default winner. The tight coupling between the job scheduler and the cluster lifecycle (especially with Serverless Compute) eliminates 90% of the cold-start and orchestration overhead Airflow forces on you.&lt;/p&gt;

&lt;p&gt;The key benefit here is "Job Clusters." By using Job Clusters instead of All-Purpose clusters, you’re paying significantly less per DBU. Integrating this into your CI/CD pipeline via &lt;code&gt;databricks-cli&lt;/code&gt; or &lt;code&gt;terraform&lt;/code&gt; is trivial.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Terraform snippet for a Databricks Job&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"databricks_job"&lt;/span&gt; &lt;span class="s2"&gt;"medallion_silver_layer"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"silver-layer-transformation"&lt;/span&gt;
  &lt;span class="nx"&gt;job_cluster&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;job_cluster_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"shared_job_cluster"&lt;/span&gt;
    &lt;span class="nx"&gt;new_cluster&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;spark_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"13.3.x-scala2.12"&lt;/span&gt;
      &lt;span class="nx"&gt;node_type_id&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"i3.xlarge"&lt;/span&gt;
      &lt;span class="nx"&gt;num_workers&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;task&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;task_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"transform"&lt;/span&gt;
    &lt;span class="nx"&gt;notebook_task&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;notebook_path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/pipelines/silver_transform"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. The "Bronze-to-Silver" dependency trap
&lt;/h2&gt;

&lt;p&gt;The biggest mistake I see in Medallion architectures is over-orchestrating. You don't need a complex DAG to move data from Bronze to Silver if your transformation is idempotent. &lt;/p&gt;

&lt;p&gt;If your Silver layer is just a Spark job reading from a Delta table, use &lt;code&gt;dbt&lt;/code&gt; models run via Databricks Workflows. Do not write complex Airflow logic to check if data arrived. Use Delta Live Tables (DLT) declarative syntax. DLT handles the dependency graph internally. When you let the framework handle the graph, you stop writing glue code that eventually breaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Failure modes define your sanity
&lt;/h2&gt;

&lt;p&gt;Airflow’s &lt;code&gt;Sensor&lt;/code&gt; pattern is the silent killer. If you have a sensor checking for a file in S3 every 60 seconds, you are burning money. It’s a classic "distributed systems smell." &lt;/p&gt;

&lt;p&gt;Contrast this with Databricks Workflows: when a job fails, the cluster terminates, you get an email, and the state is cleaned up. With Airflow, a worker node might get "zombie" status, holding onto resources while the UI says the task is running. If you are in healthcare, auditability is king. Databricks Workflows provides a native audit log of who ran what and when, which is far easier to present to compliance officers than a tangled collection of Airflow logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Version control is not optional
&lt;/h2&gt;

&lt;p&gt;Whatever you choose, it must live in Git. If I see a team manually editing DAGs in an Airflow UI or clicking "Run Now" in the Databricks console without a corresponding PR, I know the pipeline is broken. &lt;/p&gt;

&lt;p&gt;The move toward "Orchestration as Code" (using &lt;code&gt;dbt-databricks&lt;/code&gt; or Terraform) is the only way to scale. If your orchestrator configuration isn't version-controlled, you don't have a pipeline; you have a collection of brittle scripts that will break the moment the senior engineer who wrote them goes on vacation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1774875586219-c6b40deb20cb%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwyM3x8Y2xvY2t3b3JrJTIwbWVjaGFuaXNtfGVufDB8MHx8fDE3ODM3OTczMTF8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1774875586219-c6b40deb20cb%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwyM3x8Y2xvY2t3b3JrJTIwbWVjaGFuaXNtfGVufDB8MHx8fDE3ODM3OTczMTF8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="Photo by Haberdoedas on Unsplash" width="1080" height="720"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@haberdoedas?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Haberdoedas&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  7. The cost of visibility
&lt;/h2&gt;

&lt;p&gt;You will eventually have a "Gold" table that is wrong. When that happens, you need to trace the provenance of the data back to the Bronze ingestion. &lt;/p&gt;

&lt;p&gt;In Airflow, this requires complex &lt;code&gt;XCom&lt;/code&gt; tracking and log scraping. In Databricks Workflows, &lt;code&gt;Unity Catalog&lt;/code&gt; handles the lineage for you. The orchestrator is becoming less about "when to run" and more about "what did I run and where did it go." Unity Catalog integration with your workflows is non-negotiable in 2024.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Stop treating your orchestrator like a generic task runner. If you’re already in the Databricks ecosystem, stop fighting the platform and use Databricks Workflows. Airflow is a Ferrari: it’s beautiful, fast, and will cost you a fortune in maintenance if you aren't an expert mechanic. Step Functions are a reliable utility vehicle, but they aren't meant to race on the data track.&lt;/p&gt;

&lt;p&gt;Pick the path of least resistance for your infrastructure team. Medallion architectures are complex enough without adding an unnecessary layer of "orchestration glue."&lt;/p&gt;

&lt;p&gt;Are you managing your pipeline, or is your pipeline managing you?&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cover photo by &lt;a href="https://unsplash.com/@tylergm?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Tyler&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>airflow</category>
      <category>databricks</category>
      <category>ai</category>
    </item>
    <item>
      <title>Stop moving data to Spark when your warehouse is already Snowflake</title>
      <dc:creator>Aniket Abhishek Soni</dc:creator>
      <pubDate>Thu, 09 Jul 2026 21:49:20 +0000</pubDate>
      <link>https://dev.to/aniketsoni/stop-moving-data-to-spark-when-your-warehouse-is-already-snowflake-4pm7</link>
      <guid>https://dev.to/aniketsoni/stop-moving-data-to-spark-when-your-warehouse-is-already-snowflake-4pm7</guid>
      <description>&lt;p&gt;Six months ago, our Friday deployments involved babysitting a 40-node EMR cluster. We were dumping 2TB of Parquet files from Snowflake to S3, spinning up a massive Spark job to perform window functions, and then wrestling with &lt;code&gt;COPY INTO&lt;/code&gt; commands to shove the results back into Snowflake for the BI team. If the network flickered or the &lt;code&gt;spark.executor.memoryOverhead&lt;/code&gt; wasn’t tuned perfectly, we’d wake up to a PagerDuty alert at 3:00 AM. Total runtime: 45 minutes.&lt;/p&gt;

&lt;p&gt;Last week, I deleted the entire Spark infrastructure. We refactored those pipelines into Snowpark Python stored procedures running directly inside the Snowflake warehouse. The same transformation now executes in 12 minutes. We stopped paying AWS for the cluster, stopped managing IAM roles for S3 buckets, and most importantly, we stopped debugging serialization errors between PySpark and the Snowflake connector.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real problem
&lt;/h2&gt;

&lt;p&gt;The industry narrative for years has been "Snowflake for storage, Spark for transformation." It was sound advice when Snowflake's Python support was a glorified UDF wrapper. But the paradigm has shifted. Data gravity is a real, measurable cost. Every byte you move out of your warehouse is a tax you pay in latency, egress fees, and maintenance overhead.&lt;/p&gt;

&lt;p&gt;The "real" problem isn't performance—Spark can technically be faster if you have a massive, highly-tuned cluster. The real problem is the operational tax of managing a secondary compute engine. If your data lives in Snowflake, moving it to Spark is an admission of failure. You aren't just writing code; you’re managing a distributed system, a network layer, and a security boundary. Snowpark isn't about replacing Spark in every scenario; it’s about recognizing that for 95% of financial reporting and ETL workloads, the overhead of "external compute" is a liability, not an asset.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1724235858460-25d5504d952a%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwyMHx8c2hhdHRlcmVkJTIwZ2xhc3N8ZW58MHwwfHx8MTc4MzYyNzAwMnww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1724235858460-25d5504d952a%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwyMHx8c2hhdHRlcmVkJTIwZ2xhc3N8ZW58MHwwfHx8MTc4MzYyNzAwMnww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="Photo by Yue Ma on Unsplash" width="1080" height="720"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@eternalzard?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Yue Ma&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1: Rethinking the execution model
&lt;/h2&gt;

&lt;p&gt;In Spark, you are responsible for the JVM, memory management, and shuffling. In Snowpark, you are writing Python code that translates into Snowflake's optimized SQL execution plan. The first step is stop thinking about &lt;code&gt;RDDs&lt;/code&gt; and &lt;code&gt;DataFrames&lt;/code&gt; as objects that live in memory. You are building a lazy evaluation graph that Snowflake compiles into a single, massive query plan.&lt;/p&gt;

&lt;p&gt;If you try to write Snowpark exactly like PySpark, you will hit a wall. You cannot &lt;code&gt;collect()&lt;/code&gt; a 50GB dataframe to your local machine to inspect it. You have to embrace the &lt;code&gt;session.sql()&lt;/code&gt; and &lt;code&gt;dataframe.show()&lt;/code&gt; patterns. Here is how we define a pipeline that replaces an old Spark job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# The Snowpark approach: Keep it inside the warehouse
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;snowflake.snowpark&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Session&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Instead of reading from S3, we reference the table directly
&lt;/span&gt;    &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RAW.TRANSACTIONS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Transformations stay in the engine
&lt;/span&gt;    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;STATUS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;COMPLETED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
               &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;USER_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
               &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AMOUNT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TOTAL_SPEND&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;overwrite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;save_as_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ANALYTICS.USER_SPEND&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference here is that no data leaves the Snowflake boundary. You aren't serializing objects; you are building an expression tree that the query optimizer handles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Configuring the environment without the hell of JARs
&lt;/h2&gt;

&lt;p&gt;One of the biggest time-sinks in Spark is dependency management. You’ve been there: &lt;code&gt;java.lang.NoClassDefFoundError&lt;/code&gt; because a library version in your &lt;code&gt;requirements.txt&lt;/code&gt; didn't match the one on the worker nodes. In Snowpark, you handle dependencies via &lt;code&gt;packages&lt;/code&gt; in your stored procedure definition.&lt;/p&gt;

&lt;p&gt;We pin our environment using a specific Snowflake package set, which ensures that the library versions are consistent across the warehouse nodes. You don't need to build a custom Docker image or manage a private PyPI mirror.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sproc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_from_function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;my_transformation_logic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PROCESS_SALES&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;is_permanent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;stage_location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@MY_STAGE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;packages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pandas&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scikit-learn==1.2.2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;snowflake-snowpark-python&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;replace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By explicitly pinning &lt;code&gt;scikit-learn==1.2.2&lt;/code&gt;, we avoid the "it worked on my machine" nightmare that plagues Spark clusters. The Snowflake environment is isolated, immutable, and versioned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Handling the failure modes of the engine
&lt;/h2&gt;

&lt;p&gt;Spark fails with &lt;code&gt;OutOfMemoryError&lt;/code&gt; or &lt;code&gt;ShuffleFetchFailed&lt;/code&gt; exceptions. Snowpark fails with standard SQL errors, which are significantly easier to debug. When a Snowpark job fails, you don't dig through YARN logs or Spark UI task histories. You look at the &lt;code&gt;QUERY_HISTORY&lt;/code&gt; view in Snowflake.&lt;/p&gt;

&lt;p&gt;If your Python code hits a limit, the error message tells you exactly which query failed, which line of SQL caused it, and why. Here is the configuration I use to prevent "runaway query" costs, which is a different kind of failure mode compared to Spark:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Set a warehouse-level limit to prevent runaway Snowpark code&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="n"&gt;WAREHOUSE&lt;/span&gt; &lt;span class="n"&gt;COMPUTE_WH&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;MAX_CONCURRENCY_LEVEL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="n"&gt;WAREHOUSE&lt;/span&gt; &lt;span class="n"&gt;COMPUTE_WH&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;STATEMENT_TIMEOUT_IN_SECONDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Spark, you manage the "cluster" size. In Snowpark, you manage the "warehouse" budget. If you want to scale, you don't add more nodes to a cluster; you scale the warehouse size (e.g., &lt;code&gt;X-Small&lt;/code&gt; to &lt;code&gt;Medium&lt;/code&gt;), and Snowflake handles the parallelism automatically.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1518773553398-650c184e0bb3%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxMnx8Y29kZSUyMG9uJTIwbW9uaXRvcnxlbnwwfDB8fHwxNzgzNjI3MDAzfDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1518773553398-650c184e0bb3%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxMnx8Y29kZSUyMG9uJTIwbW9uaXRvcnxlbnwwfDB8fHwxNzgzNjI3MDAzfDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="Photo by Pankaj Patel on Unsplash" width="1080" height="720"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@pankajpatel?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Pankaj Patel&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons learned from production
&lt;/h2&gt;

&lt;p&gt;After six months of running mission-critical financial pipelines, these are the cold, hard truths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Avoid UDFs if possible:&lt;/strong&gt; User-Defined Functions in Snowpark are powerful, but they trigger a serialization layer that can slow down performance. If you can express your logic in Snowpark Dataframe API methods (which translate to SQL), do it. Use UDFs only for complex Python-native logic that cannot be vectorized.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;The "Small File" problem is gone:&lt;/strong&gt; Spark struggles with small files because of metadata overhead. Snowflake doesn't care. You don't need to run a &lt;code&gt;compaction&lt;/code&gt; job after every write. Snowflake’s micro-partitioning handles this natively.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Debug via SQL:&lt;/strong&gt; If your Snowpark job is hanging, don't try to look at the Python trace first. Run &lt;code&gt;SELECT * FROM TABLE(INFORMATION_SCHEMA.QUERY_HISTORY()) WHERE ...&lt;/code&gt; and look for the underlying SQL queries being generated. 9 times out of 10, the "Python issue" is actually a SQL join that is missing a partition key.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Memory is not infinite:&lt;/strong&gt; Even though you aren't managing Spark executors, Python memory limits still exist in Snowpark. If you are doing &lt;code&gt;to_pandas()&lt;/code&gt; on a massive table, you will crash the warehouse node. Always filter your data as much as possible before pulling it into memory.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Is Spark dead? No. If you are doing heavy-duty machine learning with iterative training on petabytes of unstructured text, you might still need the flexibility of a dedicated Spark cluster. But for 90% of data engineering—filtering, joining, aggregating, and transforming—Spark is overkill that introduces unnecessary complexity.&lt;/p&gt;

&lt;p&gt;You are likely already paying for Snowflake, which is the most sophisticated distributed query engine on the planet. Why spend 40% of your engineering time building a bridge to a second, inferior engine?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; Take one of your low-impact Spark jobs—the one that triggers the most PagerDuty alerts—and rewrite it in Snowpark. Don't worry about "performance tuning" initially. Just get the logic moved over. You’ll find that the time you lose in initial refactoring is dwarfed by the time you save in operational maintenance. Stop being a cluster administrator and start being a data engineer.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cover photo by &lt;a href="https://unsplash.com/@tylergm?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Tyler&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>snowflake</category>
      <category>python</category>
      <category>spark</category>
    </item>
    <item>
      <title>Stop building a feature store: When a Delta table is enough</title>
      <dc:creator>Aniket Abhishek Soni</dc:creator>
      <pubDate>Tue, 07 Jul 2026 20:55:42 +0000</pubDate>
      <link>https://dev.to/aniketsoni/stop-building-a-feature-store-when-a-delta-table-is-enough-4ko7</link>
      <guid>https://dev.to/aniketsoni/stop-building-a-feature-store-when-a-delta-table-is-enough-4ko7</guid>
      <description>&lt;p&gt;The "Feature Store as a mandatory architectural layer" is the most expensive myth in modern MLOps. We have collectively convinced ourselves that unless you are running a bespoke, high-latency serving layer, you aren't doing "real" machine learning. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why I chose this topic:&lt;/strong&gt; I’ve spent the last six months untangling a "bespoke" feature store built on Redis and Kafka that cost my team three FTEs to maintain while serving a model that literally only needed three features. I’m tired of seeing engineers build distributed systems they don't need for problems they haven't actually validated yet.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We treat feature stores like a silver bullet for data leakage and training-serving skew, but we ignore the operational tax of maintaining a two-tier storage system. If you aren't handling sub-10ms inference requirements on a massive scale, you are likely just building a distributed cache with extra steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it actually works
&lt;/h2&gt;

&lt;p&gt;At its core, a feature store is a glorified join-and-cache mechanism. You have a "batch store" (usually Parquet files in S3 or a Delta table) for model training, and an "online store" (usually Redis, DynamoDB, or Cassandra) for low-latency retrieval during inference.&lt;/p&gt;

&lt;p&gt;The "magic" is the sync process. You are essentially implementing a distributed CDC (Change Data Capture) pipeline. When a new user profile is updated in your primary database, a trigger fires—maybe via Debezium—pushing that record into a Kafka topic. A consumer then parses that Avro/Protobuf payload, computes the feature transformation, and performs a &lt;code&gt;SET&lt;/code&gt; operation in Redis.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# The "Simple" Feature Store Sync Logic
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sync_user_features&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# event is coming from Debezium/Kafka
&lt;/span&gt;    &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;after&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;last_login&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;after&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;last_login&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# Feature computation
&lt;/span&gt;    &lt;span class="n"&gt;is_active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;last_login&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="c1"&gt;# Redis write
&lt;/span&gt;    &lt;span class="n"&gt;redis_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;features:user:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mapping&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_active&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;is_active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;last_updated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks clean in a tutorial. In production, you hit the wall of partial failures. What happens when the Redis write fails but the Kafka offset commits? What happens when your feature computation logic in the Python microservice drifts from the PySpark job running your offline training set? You end up with "silent skew," where your model is essentially hallucinating because it’s looking at feature values that don't match the distribution it saw during training.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1515833305013-e64177301e04%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHw5fHxzaGF0dGVyZWQlMjBnbGFzc3xlbnwwfDB8fHwxNzgzNDU0ODA0fDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1515833305013-e64177301e04%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHw5fHxzaGF0dGVyZWQlMjBnbGFzc3xlbnwwfDB8fHwxNzgzNDU0ODA0fDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="Photo by Ivan Vranić on Unsplash" width="1080" height="720"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@hvranic?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Ivan Vranić&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The tradeoffs nobody mentions
&lt;/h2&gt;

&lt;p&gt;Let’s talk about the operational reality of 2026. If you are using a managed feature store, you are paying a "convenience tax" that often exceeds the cost of a dedicated team. If you are rolling your own, you are now a database administrator for two different storage engines.&lt;/p&gt;

&lt;p&gt;The biggest issue is the "dual-write" problem. You essentially have to ensure that your feature store and your primary transactional database are perfectly in sync. They never are. You will inevitably run into clock skew, network partitions, and serialization mismatches between your Go-based microservices and your Python-based ML training pipelines.&lt;/p&gt;

&lt;p&gt;Then there is the schema evolution problem. Imagine you update your feature schema in your Delta table. Now you have to write a migration script to update every record in your online store. If your feature store doesn't support atomic schema updates (and most don't), your inference service will start throwing &lt;code&gt;KeyError&lt;/code&gt; exceptions or, worse, parsing bad data because the schema version in the cache is stale.&lt;/p&gt;

&lt;p&gt;The debugging process is a nightmare. When a model prediction looks wrong, you aren't just checking the inference log. You’re SSH’ing into a Redis cluster to dump keys, checking the Kafka lag, and then re-running a SQL query against your Delta lake to see if the ground truth matches the cached value. It’s a distributed debugging loop that can take hours.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1584931423312-5d53d862446a%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxM3x8ZGF0YWJhc2UlMjBkYXNoYm9hcmR8ZW58MHwwfHx8MTc4MzQ1NDgwNXww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1584931423312-5d53d862446a%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwxM3x8ZGF0YWJhc2UlMjBkYXNoYm9hcmR8ZW58MHwwfHx8MTc4MzQ1NDgwNXww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="Photo by Martin Sanchez on Unsplash" width="1080" height="720"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@martinsanchez?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Martin Sanchez&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  When to reach for it (and when not to)
&lt;/h2&gt;

&lt;p&gt;If you are a startup or a mid-sized engineering org, stop. You don't need a feature store. You need a well-structured Delta table and an efficient API.&lt;/p&gt;

&lt;p&gt;Use a Delta table as your "source of truth" and serve it directly. In 2026, with the speed of Delta Lake 4.0 and optimized Z-Ordering, you can perform point-lookups on your S3-backed tables with acceptable latency for 90% of use cases. &lt;/p&gt;

&lt;p&gt;If your inference service needs a feature, pass the &lt;code&gt;user_id&lt;/code&gt; to a microservice that queries the Delta table via a high-performance engine like Trino or even a cached Spark dataframe. If you need it faster, cache the result in a simple local LRU cache in your inference container. If the cache expires, you go back to the Delta table. &lt;/p&gt;

&lt;p&gt;You reach for a full-blown feature store (like Feast or Tecton) only when you meet these three criteria:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You have 100+ production models that share overlapping feature sets.&lt;/li&gt;
&lt;li&gt;Your inference latency requirements are strictly under 50ms and require complex, pre-computed feature aggregations (like "number of transactions in the last 24 hours").&lt;/li&gt;
&lt;li&gt;You have a dedicated ML Platform team whose only job is to manage the consistency of these features.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you don't have a dedicated team for this, the "Feature Store" will become a graveyard for undocumented, stale, and broken feature pipelines that no one knows how to retire.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The industry is slowly waking up from the MLOps hype cycle. We spent years building complex "platform" layers because we were told it was the only way to scale. In reality, scaling is about reducing moving parts, not adding more databases to your stack.&lt;/p&gt;

&lt;p&gt;Keep your features in your Delta lake. Use dbt to manage your transformations. Serve them via a simple, versioned API. If you find yourself spending more time managing your "feature store" infrastructure than you do improving model accuracy, you’ve already lost. Build for the complexity you have today, not the scale you hope to have in three years. Your future self—and your on-call rotation—will thank you.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cover photo by &lt;a href="https://unsplash.com/@tylergm?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Tyler&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>datascience</category>
      <category>machinelearning</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Your Hive Metastore Migration is a Ticking Time Bomb: Why Are You Still Using It?</title>
      <dc:creator>Aniket Abhishek Soni</dc:creator>
      <pubDate>Sun, 05 Jul 2026 23:18:29 +0000</pubDate>
      <link>https://dev.to/aniketsoni/your-hive-metastore-migration-is-a-ticking-time-bomb-why-are-you-still-using-it-26k4</link>
      <guid>https://dev.to/aniketsoni/your-hive-metastore-migration-is-a-ticking-time-bomb-why-are-you-still-using-it-26k4</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why I chose this topic:&lt;/strong&gt; I’ve spent the last six months cleaning up the aftermath of "in-place" migrations that nuked production partitions, and I’m tired of seeing engineers treat schema evolution like a suggestion rather than a requirement. If you aren't running parallel pipelines during a migration, you aren't doing engineering; you're playing roulette with your data lake's consistency.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You’ve hit the limit. You’re running &lt;code&gt;MSCK REPAIR TABLE&lt;/code&gt; for the thousandth time, your Spark jobs are failing because a downstream process added a column to a Parquet file without telling anyone, and your S3 list latency is becoming a full-blown outage. You read the blog posts about "converting" your tables to Iceberg in place. Don’t. If you run a conversion script on a multi-petabyte production table, you are betting your entire career on the hope that the conversion process doesn't hit a transient I/O error mid-write.&lt;/p&gt;

&lt;p&gt;Most engineers try to "cut over" by pointing the metastore to a new location or running an &lt;code&gt;ALTER TABLE&lt;/code&gt; conversion. When that fails—and it will fail when a stray job tries to write to the old partition layout at the same time—your state becomes inconsistent. You end up with a mix of hidden files, phantom partitions, and a massive ticket queue from the BI team asking why their dashboards are returning nulls.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real problem
&lt;/h2&gt;

&lt;p&gt;The problem isn't the file format; it's the metadata management. Hive-style partitioning is a legacy relic that relies on filesystem structure to define schema. This is inherently fragile. When you migrate, you shouldn't be "converting" data; you should be building a dual-write architecture that treats the new Iceberg table as the source of truth while keeping the legacy Parquet table as a hot-standby fallback.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1768330215975-53bc216cbc0d%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHw5fHxydXN0ZWQlMjBtYWNoaW5lcnl8ZW58MHwwfHx8MTc4MzI4MDE0Nnww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1768330215975-53bc216cbc0d%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHw5fHxydXN0ZWQlMjBtYWNoaW5lcnl8ZW58MHwwfHx8MTc4MzI4MDE0Nnww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="Photo by Michael Evans on Unsplash" width="1080" height="720"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@michael_jay_photography?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Michael Evans&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1: The dual-write bridge
&lt;/h2&gt;

&lt;p&gt;Before you touch your production tables, set up a Spark Structured Streaming job that consumes the same upstream raw data or Kafka topic that feeds your existing Parquet pipeline. Do not attempt to "copy" existing files into Iceberg. Instead, create a brand-new Iceberg table and let the streaming job backfill it. &lt;/p&gt;

&lt;p&gt;Configure your Spark session to point to your Catalog—I prefer the REST catalog for multi-engine support—and define the Iceberg schema explicitly. Do not rely on schema inference. It will bite you the moment a &lt;code&gt;decimal(10,2)&lt;/code&gt; turns into a &lt;code&gt;decimal(38,18)&lt;/code&gt; during a silent upstream change.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scala"&gt;&lt;code&gt;&lt;span class="k"&gt;val&lt;/span&gt; &lt;span class="nv"&gt;spark&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;SparkSession&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;config&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"spark.sql.extensions"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;config&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"spark.sql.catalog.prod_catalog"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"org.apache.iceberg.spark.SparkCatalog"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;config&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"spark.sql.catalog.prod_catalog.type"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"rest"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;config&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"spark.sql.catalog.prod_catalog.uri"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"https://your-iceberg-catalog-service"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;getOrCreate&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// Create the target Iceberg table&lt;/span&gt;
&lt;span class="nv"&gt;spark&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"""
  CREATE TABLE prod_catalog.db.orders_iceberg (
    order_id bigint,
    user_id bigint,
    amount decimal(10,2),
    ts timestamp
  ) USING iceberg
  PARTITIONED BY (days(ts))
"""&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Validating the shadow state
&lt;/h2&gt;

&lt;p&gt;Once your shadow table is streaming, you need to verify it. Don't just check record counts; count checksums. A record count of 1 million rows in Parquet vs 1 million in Iceberg means nothing if the schema types don't align.&lt;/p&gt;

&lt;p&gt;I run a validation job every hour that compares the &lt;code&gt;sum(amount)&lt;/code&gt; and &lt;code&gt;max(ts)&lt;/code&gt; between the legacy Parquet table and the Iceberg shadow table. If these don't match, you trigger an alert. If they do match, you have high confidence that your streaming logic is sound.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Validation check in PySpark
&lt;/span&gt;&lt;span class="n"&gt;parquet_df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hive_metastore.db.orders_old&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;iceberg_df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prod_catalog.db.orders_iceberg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_stats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cnt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Compare results
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;get_stats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parquet_df&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nf"&gt;get_stats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iceberg_df&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Integrity mismatch between Parquet and Iceberg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: The hidden cutover
&lt;/h2&gt;

&lt;p&gt;The biggest mistake is a "big bang" switch. Instead, use a view to abstract the table location. Create a view that initially points to your legacy Parquet table. When you are ready to flip the switch, you update the view definition to point to the Iceberg table. This allows you to toggle back in seconds if your BI tools start throwing errors.&lt;/p&gt;

&lt;p&gt;Crucially, ensure your Iceberg table is configured with &lt;code&gt;write.format.default = parquet&lt;/code&gt; and &lt;code&gt;write.metadata.delete-after-commit.enabled = true&lt;/code&gt;. You want the performance of Iceberg with the compatibility of Parquet files underneath.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Initially&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;prod_catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders_view&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; 
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;hive_metastore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders_old&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- During migration&lt;/span&gt;
&lt;span class="c1"&gt;-- Update the view to point to the new Iceberg table&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;prod_catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders_view&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; 
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;prod_catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders_iceberg&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1781330170780-76052428a258%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHw5fHxkaWdpdGFsJTIwYmx1ZXByaW50fGVufDB8MHx8fDE3ODMyODAxNDd8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1781330170780-76052428a258%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHw5fHxkaWdpdGFsJTIwYmx1ZXByaW50fGVufDB8MHx8fDE3ODMyODAxNDd8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="Photo by Daniel Miksha on Unsplash" width="1080" height="608"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@danielmiksha?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Daniel Miksha&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons learned from production
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Partition evolution is your friend:&lt;/strong&gt; Unlike Hive, Iceberg allows you to change partition schemes without rewriting the entire table. Don't be afraid to start with &lt;code&gt;days(ts)&lt;/code&gt; and move to &lt;code&gt;hours(ts)&lt;/code&gt; if query performance drops as the table grows.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Watch the &lt;code&gt;metadata-log&lt;/code&gt; folder:&lt;/strong&gt; If you are using S3, metadata files can grow significantly. Set &lt;code&gt;write.metadata.delete-after-commit.enabled&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt; and keep &lt;code&gt;write.metadata.previous-versions-max&lt;/code&gt; low (e.g., 5-10) unless you have a strict regulatory requirement to keep months of metadata history.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Snapshot isolation is not magic:&lt;/strong&gt; If your downstream jobs use &lt;code&gt;spark.read&lt;/code&gt;, they will see the current snapshot. If a long-running job starts before you switch the view and ends after, it might see inconsistent data if you are not careful with snapshot expiration. Set your &lt;code&gt;expire_snapshots&lt;/code&gt; to run every 24 hours to prevent your storage costs from exploding due to dangling files.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Handle empty writes:&lt;/strong&gt; If your upstream source has gaps, some Spark streaming configurations will write empty Iceberg snapshots. This creates unnecessary metadata overhead. Filter out empty micro-batches before calling &lt;code&gt;write&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Migrating to Iceberg is less about the data and more about the orchestration. If you treat your migration like a controlled release—with a shadow table, validation logic, and a view-based abstraction—you remove the "fear" factor of the migration. You aren't just moving files; you're building a system that allows you to evolve your schema without breaking the downstream.&lt;/p&gt;

&lt;p&gt;Stop relying on the filesystem to define your data structure. Move to Iceberg, keep your legacy tables as a hot-standby, and validate, validate, validate. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; Start your shadow pipeline today. Create the Iceberg table, stream to it for one week, and run a daily diff between it and your legacy Parquet table. If the data matches for 7 straight days, you're ready to cut over.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; #data #iceberg #engineering #cloud&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cover photo by &lt;a href="https://unsplash.com/@davidpupaza?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;David Pupăză&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>data</category>
      <category>iceberg</category>
      <category>engineering</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Exactly-once is a lie: why your Spark stream is actually at-least-once</title>
      <dc:creator>Aniket Abhishek Soni</dc:creator>
      <pubDate>Fri, 03 Jul 2026 21:39:30 +0000</pubDate>
      <link>https://dev.to/aniketsoni/exactly-once-is-a-lie-why-your-spark-stream-is-actually-at-least-once-11bo</link>
      <guid>https://dev.to/aniketsoni/exactly-once-is-a-lie-why-your-spark-stream-is-actually-at-least-once-11bo</guid>
      <description>&lt;p&gt;If you think your Spark Structured Streaming pipeline is actually achieving end-to-end exactly-once processing, you are likely just lucky that your infrastructure hasn't had a truly bad day yet.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why I chose this topic:&lt;/strong&gt; I’ve spent the last six years cleaning up "perfect" pipelines that bloated their databases with duplicate records the moment a Kafka partition rebalanced during a checkpoint commit. We treat the word "exactly-once" as a religious tenet, but in the trenches of financial ledger reconciliation, it’s a leaky abstraction that hides the brutal reality of distributed systems.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why the common approach falls short
&lt;/h2&gt;

&lt;p&gt;The industry loves the marketing slide that says Spark Structured Streaming is "exactly-once." In reality, what Spark provides is exactly-once &lt;em&gt;processing&lt;/em&gt; within the Spark engine itself, not end-to-end. &lt;/p&gt;

&lt;p&gt;The mechanism relies on checkpointing—writing state to HDFS or S3—and the deterministic replay of inputs. If your task fails, Spark rolls back to the last successful offset and re-processes. That sounds clean, right? But the moment you write that data to an external sink, you are at the mercy of the sink's idempotency. If your sink is a generic JDBC connector or a legacy database that doesn't support transactional writes keyed to the Spark batch ID, you are not doing exactly-once. You are doing at-least-once, and you are quietly praying that your deduplication logic catches the debris.&lt;/p&gt;

&lt;p&gt;I once debugged a PII-scrubbing pipeline where a node failure during a &lt;code&gt;foreachBatch&lt;/code&gt; sink operation caused a partial write. Because the sink wasn't atomic and the &lt;code&gt;checkpointLocation&lt;/code&gt; hadn't updated yet, the next retry wrote the entire batch again. We ended up with duplicate sensitive records in our downstream warehouse. The Spark logs looked "successful," but the data integrity was trash.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1541746972996-4e0b0f43e02a%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHw5fHxzZXJ2ZXIlMjByb29tfGVufDB8MHx8fDE3ODMxMDc2Mjl8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1541746972996-4e0b0f43e02a%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHw5fHxzZXJ2ZXIlMjByb29tfGVufDB8MHx8fDE3ODMxMDc2Mjl8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="Photo by Mario Gogh on Unsplash" width="1080" height="720"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@mariogogh?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Mario Gogh&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The state of the sink
&lt;/h2&gt;

&lt;p&gt;To achieve true exactly-once, your sink must be able to handle the same batch ID twice without side effects. If you are using &lt;code&gt;spark-sql-kafka&lt;/code&gt;, you have the advantage of the Kafka offset tracking being baked into the checkpoint. But the second you leave the Kafka ecosystem, you are in the wild west.&lt;/p&gt;

&lt;p&gt;Consider the &lt;code&gt;delta&lt;/code&gt; sink. When you use Delta Lake, the transaction log acts as the source of truth for the batch ID. Spark writes the data, then commits the transaction. If a crash happens midway, the data files are written, but the commit fails. Upon restart, Spark sees the failed transaction, ignores the orphaned files, and tries again. This works because Delta supports atomic commits.&lt;/p&gt;

&lt;p&gt;Compare that to a standard &lt;code&gt;mode("append")&lt;/code&gt; write to a legacy SQL database. There is no atomic commit here. There is no "batch ID" metadata stored in the target table. If your executor dies after writing 50% of the rows but before finishing the commit, those 50% remain. The retry writes them again. You are now leaking duplicates. Unless you are manually implementing a &lt;code&gt;MERGE INTO&lt;/code&gt; using a unique constraint or a primary key—which carries a heavy performance tax—you aren't doing exactly-once. You are doing "at-least-once with a post-hoc cleanup script."&lt;/p&gt;

&lt;h2&gt;
  
  
  The reality of checkpointing failure
&lt;/h2&gt;

&lt;p&gt;We treat &lt;code&gt;checkpointLocation&lt;/code&gt; as a holy object. We assume that if we store it on S3, we are safe. We aren't.&lt;/p&gt;

&lt;p&gt;In production environments using &lt;code&gt;spark 3.x&lt;/code&gt;, I’ve seen consistent-hashing issues and S3 eventual consistency bugs (though largely mitigated by newer S3A committers) lead to corrupted checkpoints. When the &lt;code&gt;offsets&lt;/code&gt; or &lt;code&gt;commits&lt;/code&gt; directory in your checkpoint path gets corrupted, your streaming job enters a death spiral. &lt;/p&gt;

&lt;p&gt;You can’t just "fix" a corrupted checkpoint. You are forced to choose: lose the state and reset the source offset (creating a gap in data), or force-start from a previous checkpoint and deal with the inevitable re-processing of data you’ve already sunk. Neither of these options is "exactly-once." They are "emergency recovery procedures." If you aren't logging your source offsets in a separate, immutable metadata store, you are flying blind during these failures.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1758657286956-f944e1d2e75a%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwyNnx8ZGlnaXRhbCUyMG5ldHdvcmslMjBsaW5lc3xlbnwwfDB8fHwxNzgzMTA3NjMxfDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1758657286956-f944e1d2e75a%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwyNnx8ZGlnaXRhbCUyMG5ldHdvcmslMjBsaW5lc3xlbnwwfDB8fHwxNzgzMTA3NjMxfDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="Photo by MARIOLA GROBELSKA on Unsplash" width="1080" height="720"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@mariolagr?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;MARIOLA GROBELSKA&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The objections (and my answers)
&lt;/h2&gt;

&lt;p&gt;"But the documentation says it’s exactly-once!"&lt;/p&gt;

&lt;p&gt;The documentation is correct about the &lt;em&gt;internal&lt;/em&gt; state management. If you are doing aggregations in memory using &lt;code&gt;mapGroupsWithState&lt;/code&gt; and only writing to a Delta table, the engine guarantees that the state store and the transaction log stay in sync. My objection isn't to the engine's internal math; it’s to the delusion that the engine lives in a vacuum. Your system includes your sink, your network, and your storage provider. If any of those don't support atomic, idempotent writes, the guarantee breaks.&lt;/p&gt;

&lt;p&gt;"Just use Kafka as a sink and it's fine."&lt;/p&gt;

&lt;p&gt;Kafka is a great buffer, but it’s not an analytical store. If your pipeline is feeding a BI tool, you eventually have to land that data somewhere else. The moment you use a custom sink or a non-transactional database, the "exactly-once" promise evaporates. You are then responsible for the &lt;code&gt;write-ahead-log&lt;/code&gt; pattern yourself.&lt;/p&gt;

&lt;p&gt;"We use a primary key to deduplicate, so it's effectively exactly-once."&lt;/p&gt;

&lt;p&gt;That is an &lt;em&gt;operational workaround&lt;/em&gt;, not a semantic guarantee. If your database performance degrades because you’re running &lt;code&gt;UPSERT&lt;/code&gt; logic on every incoming stream to handle the duplicates that Spark created, you haven't solved the problem; you've just shifted the cost from the storage layer to the compute layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Exactly-once is a goal, but in Spark Structured Streaming, it is never a default. It is a configuration of your entire stack. &lt;/p&gt;

&lt;p&gt;If you want to get closer to the truth, stop trusting the framework to handle everything. Use transactional sinks like Delta Lake or Apache Hudi. If you are forced to use a legacy sink, build idempotency into your data model using unique business keys. Monitor your &lt;code&gt;checkpointLocation&lt;/code&gt; as if it were your production database, because that’s exactly what it is.&lt;/p&gt;

&lt;p&gt;Stop telling stakeholders you have "exactly-once" semantics. Tell them you have "idempotent processing pipelines with a defined recovery point." It sounds less like a marketing brochure and more like the actual engineering work you're doing.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; #spark #streaming #data #architecture&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cover photo by &lt;a href="https://unsplash.com/@scutiform?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Brian Cockley&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>databricks</category>
      <category>streaming</category>
      <category>architecture</category>
      <category>spark</category>
    </item>
    <item>
      <title>Is Your Lakehouse Architecture Just a High-Priced Tax on Your Data Team?</title>
      <dc:creator>Aniket Abhishek Soni</dc:creator>
      <pubDate>Wed, 01 Jul 2026 22:48:54 +0000</pubDate>
      <link>https://dev.to/aniketsoni/is-your-lakehouse-architecture-just-a-high-priced-tax-on-your-data-team-2jp6</link>
      <guid>https://dev.to/aniketsoni/is-your-lakehouse-architecture-just-a-high-priced-tax-on-your-data-team-2jp6</guid>
      <description>&lt;p&gt;Ninety-two percent of data platform migrations I’ve audited in the last three years ended up costing more in "operational tax" than they saved in raw compute efficiency. We talk about TCO (Total Cost of Ownership) like it’s a math problem, but it’s actually a human behavior problem. The choice between BigQuery and Databricks SQL isn't about which engine can scan a petabyte faster; it’s about whether you want to spend your weekends debugging slot allocation or tuning Delta Lake vacuum intervals.&lt;/p&gt;

&lt;p&gt;I’ve spent the last six years keeping financial services and healthcare workloads upright. I’ve seen BigQuery’s &lt;code&gt;INFORMATION_SCHEMA&lt;/code&gt; save a QBR and I’ve seen Databricks’ &lt;code&gt;OPTIMIZE&lt;/code&gt; commands accidentally lock a table during a critical financial close. If you’re choosing based on a vendor slide deck, you’re already behind. Here is the field guide to not blowing your cloud budget while trying to build a "lakehouse."&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The "Slot" Trap vs. The "Warehouse" Mirage
&lt;/h2&gt;

&lt;p&gt;BigQuery’s shift to &lt;code&gt;Edition&lt;/code&gt; pricing (Standard, Enterprise, Enterprise Plus) was the industry’s way of saying "we want predictable, Databricks-style billing." But here’s the reality: if you aren't using Reservations, you aren't using BigQuery. I’ve seen teams blow $50k in a weekend because a rogue &lt;code&gt;SELECT *&lt;/code&gt; on a multi-petabyte partitioned table hit on-demand pricing.&lt;/p&gt;

&lt;p&gt;In Databricks, you’re buying "SQL Warehouses." The failure mode here is over-provisioning. If you leave a 2XL warehouse running 24/7 because your analysts "need it to be fast," you’re lighting money on fire. BigQuery is inherently multi-tenant; Databricks is isolated. If you have 50 different departments, BigQuery manages the concurrency better out of the box. If you have a few massive, complex jobs that need predictable performance, you want a dedicated Databricks SQL Warehouse.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1640012046731-3296bc0b7642%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwyMHx8dGFuZ2xlZCUyMGZpYmVyJTIwb3B0aWMlMjBjYWJsZXN8ZW58MHwwfHx8MTc4MjkzNjMxNHww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1640012046731-3296bc0b7642%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwyMHx8dGFuZ2xlZCUyMGZpYmVyJTIwb3B0aWMlMjBjYWJsZXN8ZW58MHwwfHx8MTc4MjkzNjMxNHww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="Photo by Monisha Selvakumar on Unsplash" width="1080" height="720"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@monishaselv?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Monisha Selvakumar&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Partitioning Isn't Optional; It’s Your Only Defense
&lt;/h2&gt;

&lt;p&gt;In BigQuery, if you don't filter by your partition column (usually &lt;code&gt;_PARTITIONDATE&lt;/code&gt; or a timestamp), you are paying for a full table scan. Period. I’ve seen junior engineers write queries that scanned 40TB of data for a single dashboard refresh. &lt;/p&gt;

&lt;p&gt;In Databricks, the &lt;code&gt;Z-ORDER&lt;/code&gt; command is your best friend. If you aren't Z-ordering your high-cardinality columns, you’re missing the point of Delta Lake.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- BigQuery: Never skip the filter, or get fired.&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;`my_project.my_dataset.events`&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;_PARTITIONDATE&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;DATE_SUB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CURRENT_DATE&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Databricks: Z-ORDER is the performance multiplier.&lt;/span&gt;
&lt;span class="n"&gt;OPTIMIZE&lt;/span&gt; &lt;span class="n"&gt;my_table&lt;/span&gt; 
&lt;span class="n"&gt;ZORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you ignore these, you’re paying for the vendor’s inefficiency. In BigQuery, you pay for the scan. In Databricks, you pay for the time the cluster spent scanning.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The "Vacuum" and "Snapshot" Tax
&lt;/h2&gt;

&lt;p&gt;One of the biggest hidden costs in Databricks is storage bloat. Because Delta Lake keeps snapshots for time travel, if you don't run &lt;code&gt;VACUUM&lt;/code&gt; regularly, your storage bill will grow indefinitely. I’ve seen terabytes of "deleted" data sitting in S3/ADLS buckets that Databricks users forgot to prune.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Databricks: Pruning old snapshots to save storage costs&lt;/span&gt;
&lt;span class="k"&gt;VACUUM&lt;/span&gt; &lt;span class="n"&gt;my_table&lt;/span&gt; &lt;span class="n"&gt;RETAIN&lt;/span&gt; &lt;span class="mi"&gt;168&lt;/span&gt; &lt;span class="n"&gt;HOURS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- Keep 7 days of history&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;BigQuery handles this via internal TTLs on datasets and tables. It’s "set it and forget it." If you lack the discipline to manage a &lt;code&gt;VACUUM&lt;/code&gt; schedule, Databricks will eventually bite your budget in the ass.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Concurrency is a Lie
&lt;/h2&gt;

&lt;p&gt;Marketing teams love to talk about "limitless concurrency." Both platforms handle it, but they handle it differently. BigQuery uses a distributed scheduler that tries to fit your query into the available slots. If you have 2,000 slots and you trigger 5,000 slots worth of work, BigQuery will queue your queries. That's a latency hit, but not a failure.&lt;/p&gt;

&lt;p&gt;Databricks SQL Warehouses (Serverless) have a "scaling out" threshold. When your cluster gets slammed, it spawns new clusters to handle the load. This is great until you hit your regional limit for cloud instances or your bill hits the stratosphere because you triggered five extra clusters to run a 2-second query. Monitor your &lt;code&gt;dbr_sql_warehouse_scaling_events&lt;/code&gt; like a hawk.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The "Governance" Penalty
&lt;/h2&gt;

&lt;p&gt;Healthcare data requires ironclad access control. BigQuery’s integration with IAM is native and absolute. If you are already deep in the Google Cloud ecosystem, BigQuery’s row-level security and column-level masking (via Policy Tags) are incredibly easy to implement.&lt;/p&gt;

&lt;p&gt;Databricks uses Unity Catalog. It’s powerful, but it’s a second layer of governance you have to maintain outside of your cloud provider’s IAM. If your organization is already struggling with identity management, adding Unity Catalog adds another point of failure. Don't underestimate the "cognitive load" of managing two sets of permissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Cold Starts and Serverless Latency
&lt;/h2&gt;

&lt;p&gt;BigQuery is always "warm." You send a request, it runs. Databricks SQL Serverless has gotten much faster, but there is still a spin-up time for those clusters if they’ve been idle. If your users are clicking around a Looker dashboard, they will notice the 3-5 second lag on the first click if your warehouse was cold.&lt;/p&gt;

&lt;p&gt;If your users are impatient (and they are), you will end up keeping warehouses running longer than you need to, just to avoid the "Why is the dashboard slow?" Slack messages. That’s a hidden cost of the Databricks architecture.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1534006215338-cfa9ef7903a1%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwyNnx8bW91bnRhaW4lMjBvZiUyMHJlY2VpcHRzfGVufDB8MHx8fDE3ODI5MzYzMTV8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.unsplash.com%2Fphoto-1534006215338-cfa9ef7903a1%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3w5NzU0MjJ8MHwxfHNlYXJjaHwyNnx8bW91bnRhaW4lMjBvZiUyMHJlY2VpcHRzfGVufDB8MHx8fDE3ODI5MzYzMTV8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080" alt="Photo by Giancarlo Revolledo on Unsplash" width="1080" height="720"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by &lt;a href="https://unsplash.com/@giancarlor_photo?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Giancarlo Revolledo&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Vendor Lock-in is a Myth; Portability is a Pipe Dream
&lt;/h2&gt;

&lt;p&gt;People choose Databricks because they want to "own" their data in Parquet/Delta format. They choose BigQuery because they want it to "just work." &lt;/p&gt;

&lt;p&gt;Here is the truth: you aren't going to migrate 500TB of data from BigQuery to Databricks because you had a bad quarter. You are locked in by your ingestion pipelines and your BI tool semantic layers. Pick the one that fits your current team’s skillset. If your team knows Spark, Databricks is the path of least resistance. If your team is SQL-first and hates infrastructure management, BigQuery is the only logical choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;BigQuery is a managed service that demands you play by its rules—partitioning, slot management, and Google-native IAM. Databricks is a platform that gives you more control but demands you manage the complexity—vacuuming, Z-ordering, and catalog governance. &lt;/p&gt;

&lt;p&gt;If you want a "lakehouse" that functions like a database, pay the BigQuery tax and embrace the simplicity. If you want a data science powerhouse that happens to run SQL, pay the Databricks tax and hire a good platform engineer to clean up your mess. &lt;/p&gt;

&lt;p&gt;Which one is keeping your CFO up at night, and what are you going to do about it tomorrow morning?&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cover photo by &lt;a href="https://unsplash.com/@fp4?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Gavin Allanwood&lt;/a&gt; on &lt;a href="https://unsplash.com/?utm_source=articles_pipeline&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>databricks</category>
      <category>data</category>
      <category>engineering</category>
      <category>bigquery</category>
    </item>
  </channel>
</rss>
