<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.3.2">Jekyll</generator><link href="https://www.basix.dev/atom.xml" rel="self" type="application/atom+xml" /><link href="https://www.basix.dev/" rel="alternate" type="text/html" /><updated>2026-01-29T15:29:30+01:00</updated><id>https://www.basix.dev/atom.xml</id><title type="html">basix</title><subtitle>data, software and the universe</subtitle><author><name>basix</name><email>selver@basix.dev</email></author><entry><title type="html">The Annoying Truth About ‘Serverless’ Data</title><link href="https://www.basix.dev/data-engineering/the-annoying-truth-about-serverless-data/" rel="alternate" type="text/html" title="The Annoying Truth About ‘Serverless’ Data" /><published>2026-01-16T10:00:00+01:00</published><updated>2026-01-16T10:00:00+01:00</updated><id>https://www.basix.dev/data-engineering/the-annoying-truth-about-serverless-data</id><content type="html" xml:base="https://www.basix.dev/data-engineering/the-annoying-truth-about-serverless-data/"><![CDATA[<p>Serverless mostly means ‘someone else runs the servers’. You still pay.</p>

<p>Look, I get it—shipping features is fun. But shipping <strong>correct</strong> pipelines is somehow considered optional.</p>

<h2 id="the-problem-aka-where-most-designs-go-to-die">The problem (a.k.a. where most designs go to die)</h2>

<p>In production, data systems fail in boring ways:</p>

<ul>
  <li><strong>retries</strong> that create duplicates</li>
  <li><strong>backfills</strong> that rewrite history (and then everyone argues about “the source of truth”)</li>
  <li><strong>schema changes</strong> that arrive unannounced because someone added a column “real quick”</li>
  <li><strong>caches</strong> (browser, CDN, query engine) that serve stale results while people swear “nothing changed”</li>
</ul>

<p>If any of that sounds familiar, congratulations—you’re operating a real system.</p>

<h2 id="a-reference-architecture-that-actually-survives-contact-with-reality">A reference architecture that actually survives contact with reality</h2>

<p>A reliable design usually has these properties:</p>

<ol>
  <li><strong>Deterministic inputs</strong> (you can describe <em>exactly</em> what this run consumes)</li>
  <li><strong>Idempotent outputs</strong> (re-running produces the same end state)</li>
  <li><strong>Versioned contracts</strong> at boundaries (schemas, keys, SLAs)</li>
  <li><strong>Observable execution</strong> (metrics, lineage, logs with correlation)</li>
</ol>

<p>Here’s the part everyone skips: you don’t get these by adding a dashboard. You get them by encoding invariants into the pipeline.</p>

<h2 id="implementation-notes-things-youll-thank-yourself-for-later">Implementation notes (things you’ll thank yourself for later)</h2>

<ul>
  <li><strong>Commit metadata</strong>: record batch identifiers, source offsets, and the code version that produced the output.</li>
  <li><strong>Monotonic event ordering</strong>: define ordering rules (event time + tie-breakers) and enforce them in merges.</li>
  <li><strong>Backfill isolation</strong>: run backfills in separate compute pools / queues with explicit throughput caps.</li>
  <li><strong>Contract enforcement</strong>: validate schema + keys at ingestion; quarantine bad records (don’t “fix” them silently).</li>
</ul>

<h3 id="concrete-example">Concrete example</h3>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Example: a minimal data contract / schema expectation</span>
<span class="na">version</span><span class="pi">:</span> <span class="m">1</span>
<span class="na">entity</span><span class="pi">:</span> <span class="s">orders</span>
<span class="na">primary_key</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">order_id</span><span class="pi">]</span>
<span class="na">columns</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">order_id</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">string</span>
    <span class="na">required</span><span class="pi">:</span> <span class="kc">true</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">order_ts</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">timestamp</span>
    <span class="na">required</span><span class="pi">:</span> <span class="kc">true</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">total_amount</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">decimal(18,2)</span>
    <span class="na">required</span><span class="pi">:</span> <span class="kc">true</span>
<span class="na">checks</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">non_negative_amount</span>
    <span class="na">expr</span><span class="pi">:</span> <span class="s">total_amount &gt;= </span><span class="m">0</span>
</code></pre></div></div>

<h2 id="failure-modes-you-should-plan-for-because-they-will-happen">Failure modes you should plan for (because they will happen)</h2>

<ul>
  <li><strong>Late data</strong>: decide what “late” means, and what you do when it happens.</li>
  <li><strong>Out-of-order updates</strong>: if you can’t deterministically reconcile, you’re not doing CDC—you’re doing vibes.</li>
  <li><strong>Partial writes</strong>: either use atomic commits (preferred) or write-then-promote patterns.</li>
  <li><strong>Hot partitions</strong>: your hash key choices are architecture, not implementation detail.</li>
</ul>

<h2 id="observability-checklist-minimal-not-optional">Observability checklist (minimal, not optional)</h2>

<ul>
  <li>A run id / batch id that propagates through logs</li>
  <li>Row counts in/out with anomaly detection</li>
  <li>Lag metrics (event-time lag for streaming; freshness for batch)</li>
  <li>Data quality checks that <strong>fail the run</strong>, not just “notify Slack politely”</li>
</ul>

<h2 id="takeaways">Takeaways</h2>

<ul>
  <li>The fastest path is the one you can safely re-run.</li>
  <li>“Exactly once” is a design constraint, not a product checkbox.</li>
  <li>If you can’t explain your pipeline’s invariants in one page, you can’t operate it.</li>
</ul>

<p><em>Next time someone says “just rerun it”, smile and ask: “Cool—what’s the idempotency key?”</em></p>]]></content><author><name>basix</name><email>selver@basix.dev</email></author><category term="data-engineering" /><category term="serverless architecture" /><summary type="html"><![CDATA[Serverless mostly means ‘someone else runs the servers’. You still pay.]]></summary></entry><entry><title type="html">Lambda Architecture Without the Trauma</title><link href="https://www.basix.dev/data-engineering/lambda-architecture-without-the-trauma/" rel="alternate" type="text/html" title="Lambda Architecture Without the Trauma" /><published>2025-12-30T10:00:00+01:00</published><updated>2025-12-30T10:00:00+01:00</updated><id>https://www.basix.dev/data-engineering/lambda-architecture-without-the-trauma</id><content type="html" xml:base="https://www.basix.dev/data-engineering/lambda-architecture-without-the-trauma/"><![CDATA[<p>Hybrid batch+streaming can work—if you pick a single source of truth and stop duplicating business logic.</p>

<p>Look, I get it—shipping features is fun. But shipping <strong>correct</strong> pipelines is somehow considered optional.</p>

<h2 id="the-problem-aka-where-most-designs-go-to-die">The problem (a.k.a. where most designs go to die)</h2>

<p>In production, data systems fail in boring ways:</p>

<ul>
  <li><strong>retries</strong> that create duplicates</li>
  <li><strong>backfills</strong> that rewrite history (and then everyone argues about “the source of truth”)</li>
  <li><strong>schema changes</strong> that arrive unannounced because someone added a column “real quick”</li>
  <li><strong>caches</strong> (browser, CDN, query engine) that serve stale results while people swear “nothing changed”</li>
</ul>

<p>If any of that sounds familiar, congratulations—you’re operating a real system.</p>

<h2 id="a-reference-architecture-that-actually-survives-contact-with-reality">A reference architecture that actually survives contact with reality</h2>

<p>A reliable design usually has these properties:</p>

<ol>
  <li><strong>Deterministic inputs</strong> (you can describe <em>exactly</em> what this run consumes)</li>
  <li><strong>Idempotent outputs</strong> (re-running produces the same end state)</li>
  <li><strong>Versioned contracts</strong> at boundaries (schemas, keys, SLAs)</li>
  <li><strong>Observable execution</strong> (metrics, lineage, logs with correlation)</li>
</ol>

<p>Here’s the part everyone skips: you don’t get these by adding a dashboard. You get them by encoding invariants into the pipeline.</p>

<h2 id="implementation-notes-things-youll-thank-yourself-for-later">Implementation notes (things you’ll thank yourself for later)</h2>

<ul>
  <li><strong>Commit metadata</strong>: record batch identifiers, source offsets, and the code version that produced the output.</li>
  <li><strong>Monotonic event ordering</strong>: define ordering rules (event time + tie-breakers) and enforce them in merges.</li>
  <li><strong>Backfill isolation</strong>: run backfills in separate compute pools / queues with explicit throughput caps.</li>
  <li><strong>Contract enforcement</strong>: validate schema + keys at ingestion; quarantine bad records (don’t “fix” them silently).</li>
</ul>

<h3 id="concrete-example">Concrete example</h3>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Example: a minimal data contract / schema expectation</span>
<span class="na">version</span><span class="pi">:</span> <span class="m">1</span>
<span class="na">entity</span><span class="pi">:</span> <span class="s">orders</span>
<span class="na">primary_key</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">order_id</span><span class="pi">]</span>
<span class="na">columns</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">order_id</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">string</span>
    <span class="na">required</span><span class="pi">:</span> <span class="kc">true</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">order_ts</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">timestamp</span>
    <span class="na">required</span><span class="pi">:</span> <span class="kc">true</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">total_amount</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">decimal(18,2)</span>
    <span class="na">required</span><span class="pi">:</span> <span class="kc">true</span>
<span class="na">checks</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">non_negative_amount</span>
    <span class="na">expr</span><span class="pi">:</span> <span class="s">total_amount &gt;= </span><span class="m">0</span>
</code></pre></div></div>

<h2 id="failure-modes-you-should-plan-for-because-they-will-happen">Failure modes you should plan for (because they will happen)</h2>

<ul>
  <li><strong>Late data</strong>: decide what “late” means, and what you do when it happens.</li>
  <li><strong>Out-of-order updates</strong>: if you can’t deterministically reconcile, you’re not doing CDC—you’re doing vibes.</li>
  <li><strong>Partial writes</strong>: either use atomic commits (preferred) or write-then-promote patterns.</li>
  <li><strong>Hot partitions</strong>: your hash key choices are architecture, not implementation detail.</li>
</ul>

<h2 id="observability-checklist-minimal-not-optional">Observability checklist (minimal, not optional)</h2>

<ul>
  <li>A run id / batch id that propagates through logs</li>
  <li>Row counts in/out with anomaly detection</li>
  <li>Lag metrics (event-time lag for streaming; freshness for batch)</li>
  <li>Data quality checks that <strong>fail the run</strong>, not just “notify Slack politely”</li>
</ul>

<h2 id="takeaways">Takeaways</h2>

<ul>
  <li>The fastest path is the one you can safely re-run.</li>
  <li>“Exactly once” is a design constraint, not a product checkbox.</li>
  <li>If you can’t explain your pipeline’s invariants in one page, you can’t operate it.</li>
</ul>

<p><em>Next time someone says “just rerun it”, smile and ask: “Cool—what’s the idempotency key?”</em></p>]]></content><author><name>basix</name><email>selver@basix.dev</email></author><category term="data-engineering" /><category term="lambda batch streaming" /><summary type="html"><![CDATA[Hybrid batch+streaming can work—if you pick a single source of truth and stop duplicating business logic.]]></summary></entry><entry><title type="html">Vector Search Pipelines: Embeddings Are Data Engineering Too</title><link href="https://www.basix.dev/data-engineering/vector-search-pipelines-embeddings-are-data-engineering-too/" rel="alternate" type="text/html" title="Vector Search Pipelines: Embeddings Are Data Engineering Too" /><published>2025-12-16T10:00:00+01:00</published><updated>2025-12-16T10:00:00+01:00</updated><id>https://www.basix.dev/data-engineering/vector-search-pipelines-embeddings-are-data-engineering-too</id><content type="html" xml:base="https://www.basix.dev/data-engineering/vector-search-pipelines-embeddings-are-data-engineering-too/"><![CDATA[<p>Embeddings drift; treat them like any other dataset.</p>

<p>Look, I get it—shipping features is fun. But shipping <strong>correct</strong> pipelines is somehow considered optional.</p>

<h2 id="the-problem-aka-where-most-designs-go-to-die">The problem (a.k.a. where most designs go to die)</h2>

<p>In production, data systems fail in boring ways:</p>

<ul>
  <li><strong>retries</strong> that create duplicates</li>
  <li><strong>backfills</strong> that rewrite history (and then everyone argues about “the source of truth”)</li>
  <li><strong>schema changes</strong> that arrive unannounced because someone added a column “real quick”</li>
  <li><strong>caches</strong> (browser, CDN, query engine) that serve stale results while people swear “nothing changed”</li>
</ul>

<p>If any of that sounds familiar, congratulations—you’re operating a real system.</p>

<h2 id="a-reference-architecture-that-actually-survives-contact-with-reality">A reference architecture that actually survives contact with reality</h2>

<p>A reliable design usually has these properties:</p>

<ol>
  <li><strong>Deterministic inputs</strong> (you can describe <em>exactly</em> what this run consumes)</li>
  <li><strong>Idempotent outputs</strong> (re-running produces the same end state)</li>
  <li><strong>Versioned contracts</strong> at boundaries (schemas, keys, SLAs)</li>
  <li><strong>Observable execution</strong> (metrics, lineage, logs with correlation)</li>
</ol>

<p>Here’s the part everyone skips: you don’t get these by adding a dashboard. You get them by encoding invariants into the pipeline.</p>

<h2 id="implementation-notes-things-youll-thank-yourself-for-later">Implementation notes (things you’ll thank yourself for later)</h2>

<ul>
  <li><strong>Commit metadata</strong>: record batch identifiers, source offsets, and the code version that produced the output.</li>
  <li><strong>Monotonic event ordering</strong>: define ordering rules (event time + tie-breakers) and enforce them in merges.</li>
  <li><strong>Backfill isolation</strong>: run backfills in separate compute pools / queues with explicit throughput caps.</li>
  <li><strong>Contract enforcement</strong>: validate schema + keys at ingestion; quarantine bad records (don’t “fix” them silently).</li>
</ul>

<h3 id="concrete-example">Concrete example</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Example: deterministic batch id + exactly-once-ish writes
</span><span class="kn">import</span> <span class="n">hashlib</span>

<span class="k">def</span> <span class="nf">batch_id</span><span class="p">(</span><span class="n">run_date</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">dataset</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
    <span class="k">return</span> <span class="n">hashlib</span><span class="p">.</span><span class="nf">sha256</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">dataset</span><span class="si">}</span><span class="s">:</span><span class="si">{</span><span class="n">run_date</span><span class="si">}</span><span class="sh">"</span><span class="p">.</span><span class="nf">encode</span><span class="p">()).</span><span class="nf">hexdigest</span><span class="p">()</span>

<span class="c1"># Use batch_id as part of output path or commit metadata.
</span></code></pre></div></div>

<h2 id="failure-modes-you-should-plan-for-because-they-will-happen">Failure modes you should plan for (because they will happen)</h2>

<ul>
  <li><strong>Late data</strong>: decide what “late” means, and what you do when it happens.</li>
  <li><strong>Out-of-order updates</strong>: if you can’t deterministically reconcile, you’re not doing CDC—you’re doing vibes.</li>
  <li><strong>Partial writes</strong>: either use atomic commits (preferred) or write-then-promote patterns.</li>
  <li><strong>Hot partitions</strong>: your hash key choices are architecture, not implementation detail.</li>
</ul>

<h2 id="observability-checklist-minimal-not-optional">Observability checklist (minimal, not optional)</h2>

<ul>
  <li>A run id / batch id that propagates through logs</li>
  <li>Row counts in/out with anomaly detection</li>
  <li>Lag metrics (event-time lag for streaming; freshness for batch)</li>
  <li>Data quality checks that <strong>fail the run</strong>, not just “notify Slack politely”</li>
</ul>

<h2 id="takeaways">Takeaways</h2>

<ul>
  <li>The fastest path is the one you can safely re-run.</li>
  <li>“Exactly once” is a design constraint, not a product checkbox.</li>
  <li>If you can’t explain your pipeline’s invariants in one page, you can’t operate it.</li>
</ul>

<p><em>Next time someone says “just rerun it”, smile and ask: “Cool—what’s the idempotency key?”</em></p>]]></content><author><name>basix</name><email>selver@basix.dev</email></author><category term="data-engineering" /><category term="llm embeddings architecture" /><summary type="html"><![CDATA[Embeddings drift; treat them like any other dataset.]]></summary></entry><entry><title type="html">Feature Stores: Centralize Reuse, Decentralize Blame</title><link href="https://www.basix.dev/data-engineering/feature-stores-centralize-reuse-decentralize-blame/" rel="alternate" type="text/html" title="Feature Stores: Centralize Reuse, Decentralize Blame" /><published>2025-12-04T10:00:00+01:00</published><updated>2025-12-04T10:00:00+01:00</updated><id>https://www.basix.dev/data-engineering/feature-stores-centralize-reuse-decentralize-blame</id><content type="html" xml:base="https://www.basix.dev/data-engineering/feature-stores-centralize-reuse-decentralize-blame/"><![CDATA[<p>A feature store is a contract system with extra steps.</p>

<p>Look, I get it—shipping features is fun. But shipping <strong>correct</strong> pipelines is somehow considered optional.</p>

<h2 id="the-problem-aka-where-most-designs-go-to-die">The problem (a.k.a. where most designs go to die)</h2>

<p>In production, data systems fail in boring ways:</p>

<ul>
  <li><strong>retries</strong> that create duplicates</li>
  <li><strong>backfills</strong> that rewrite history (and then everyone argues about “the source of truth”)</li>
  <li><strong>schema changes</strong> that arrive unannounced because someone added a column “real quick”</li>
  <li><strong>caches</strong> (browser, CDN, query engine) that serve stale results while people swear “nothing changed”</li>
</ul>

<p>If any of that sounds familiar, congratulations—you’re operating a real system.</p>

<h2 id="a-reference-architecture-that-actually-survives-contact-with-reality">A reference architecture that actually survives contact with reality</h2>

<p>A reliable design usually has these properties:</p>

<ol>
  <li><strong>Deterministic inputs</strong> (you can describe <em>exactly</em> what this run consumes)</li>
  <li><strong>Idempotent outputs</strong> (re-running produces the same end state)</li>
  <li><strong>Versioned contracts</strong> at boundaries (schemas, keys, SLAs)</li>
  <li><strong>Observable execution</strong> (metrics, lineage, logs with correlation)</li>
</ol>

<p>Here’s the part everyone skips: you don’t get these by adding a dashboard. You get them by encoding invariants into the pipeline.</p>

<h2 id="implementation-notes-things-youll-thank-yourself-for-later">Implementation notes (things you’ll thank yourself for later)</h2>

<ul>
  <li><strong>Commit metadata</strong>: record batch identifiers, source offsets, and the code version that produced the output.</li>
  <li><strong>Monotonic event ordering</strong>: define ordering rules (event time + tie-breakers) and enforce them in merges.</li>
  <li><strong>Backfill isolation</strong>: run backfills in separate compute pools / queues with explicit throughput caps.</li>
  <li><strong>Contract enforcement</strong>: validate schema + keys at ingestion; quarantine bad records (don’t “fix” them silently).</li>
</ul>

<h3 id="concrete-example">Concrete example</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Example: deterministic batch id + exactly-once-ish writes
</span><span class="kn">import</span> <span class="n">hashlib</span>

<span class="k">def</span> <span class="nf">batch_id</span><span class="p">(</span><span class="n">run_date</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">dataset</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
    <span class="k">return</span> <span class="n">hashlib</span><span class="p">.</span><span class="nf">sha256</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">dataset</span><span class="si">}</span><span class="s">:</span><span class="si">{</span><span class="n">run_date</span><span class="si">}</span><span class="sh">"</span><span class="p">.</span><span class="nf">encode</span><span class="p">()).</span><span class="nf">hexdigest</span><span class="p">()</span>

<span class="c1"># Use batch_id as part of output path or commit metadata.
</span></code></pre></div></div>

<h2 id="failure-modes-you-should-plan-for-because-they-will-happen">Failure modes you should plan for (because they will happen)</h2>

<ul>
  <li><strong>Late data</strong>: decide what “late” means, and what you do when it happens.</li>
  <li><strong>Out-of-order updates</strong>: if you can’t deterministically reconcile, you’re not doing CDC—you’re doing vibes.</li>
  <li><strong>Partial writes</strong>: either use atomic commits (preferred) or write-then-promote patterns.</li>
  <li><strong>Hot partitions</strong>: your hash key choices are architecture, not implementation detail.</li>
</ul>

<h2 id="observability-checklist-minimal-not-optional">Observability checklist (minimal, not optional)</h2>

<ul>
  <li>A run id / batch id that propagates through logs</li>
  <li>Row counts in/out with anomaly detection</li>
  <li>Lag metrics (event-time lag for streaming; freshness for batch)</li>
  <li>Data quality checks that <strong>fail the run</strong>, not just “notify Slack politely”</li>
</ul>

<h2 id="takeaways">Takeaways</h2>

<ul>
  <li>The fastest path is the one you can safely re-run.</li>
  <li>“Exactly once” is a design constraint, not a product checkbox.</li>
  <li>If you can’t explain your pipeline’s invariants in one page, you can’t operate it.</li>
</ul>

<p><em>Next time someone says “just rerun it”, smile and ask: “Cool—what’s the idempotency key?”</em></p>]]></content><author><name>basix</name><email>selver@basix.dev</email></author><category term="data-engineering" /><category term="ml feature-store" /><summary type="html"><![CDATA[A feature store is a contract system with extra steps.]]></summary></entry><entry><title type="html">Observability: Trace IDs for Data Pipelines (Yes, It Works)</title><link href="https://www.basix.dev/data-engineering/observability-trace-ids-for-data-pipelines-yes-it-works/" rel="alternate" type="text/html" title="Observability: Trace IDs for Data Pipelines (Yes, It Works)" /><published>2025-11-24T10:00:00+01:00</published><updated>2025-11-24T10:00:00+01:00</updated><id>https://www.basix.dev/data-engineering/observability-trace-ids-for-data-pipelines-yes-it-works</id><content type="html" xml:base="https://www.basix.dev/data-engineering/observability-trace-ids-for-data-pipelines-yes-it-works/"><![CDATA[<p>Correlate events across ingest → transform → serve. Debugging gets boring.</p>

<p>Look, I get it—shipping features is fun. But shipping <strong>correct</strong> pipelines is somehow considered optional.</p>

<h2 id="the-problem-aka-where-most-designs-go-to-die">The problem (a.k.a. where most designs go to die)</h2>

<p>In production, data systems fail in boring ways:</p>

<ul>
  <li><strong>retries</strong> that create duplicates</li>
  <li><strong>backfills</strong> that rewrite history (and then everyone argues about “the source of truth”)</li>
  <li><strong>schema changes</strong> that arrive unannounced because someone added a column “real quick”</li>
  <li><strong>caches</strong> (browser, CDN, query engine) that serve stale results while people swear “nothing changed”</li>
</ul>

<p>If any of that sounds familiar, congratulations—you’re operating a real system.</p>

<h2 id="a-reference-architecture-that-actually-survives-contact-with-reality">A reference architecture that actually survives contact with reality</h2>

<p>A reliable design usually has these properties:</p>

<ol>
  <li><strong>Deterministic inputs</strong> (you can describe <em>exactly</em> what this run consumes)</li>
  <li><strong>Idempotent outputs</strong> (re-running produces the same end state)</li>
  <li><strong>Versioned contracts</strong> at boundaries (schemas, keys, SLAs)</li>
  <li><strong>Observable execution</strong> (metrics, lineage, logs with correlation)</li>
</ol>

<p>Here’s the part everyone skips: you don’t get these by adding a dashboard. You get them by encoding invariants into the pipeline.</p>

<h2 id="implementation-notes-things-youll-thank-yourself-for-later">Implementation notes (things you’ll thank yourself for later)</h2>

<ul>
  <li><strong>Commit metadata</strong>: record batch identifiers, source offsets, and the code version that produced the output.</li>
  <li><strong>Monotonic event ordering</strong>: define ordering rules (event time + tie-breakers) and enforce them in merges.</li>
  <li><strong>Backfill isolation</strong>: run backfills in separate compute pools / queues with explicit throughput caps.</li>
  <li><strong>Contract enforcement</strong>: validate schema + keys at ingestion; quarantine bad records (don’t “fix” them silently).</li>
</ul>

<h3 id="concrete-example">Concrete example</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Example: deterministic batch id + exactly-once-ish writes
</span><span class="kn">import</span> <span class="n">hashlib</span>

<span class="k">def</span> <span class="nf">batch_id</span><span class="p">(</span><span class="n">run_date</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">dataset</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
    <span class="k">return</span> <span class="n">hashlib</span><span class="p">.</span><span class="nf">sha256</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">dataset</span><span class="si">}</span><span class="s">:</span><span class="si">{</span><span class="n">run_date</span><span class="si">}</span><span class="sh">"</span><span class="p">.</span><span class="nf">encode</span><span class="p">()).</span><span class="nf">hexdigest</span><span class="p">()</span>

<span class="c1"># Use batch_id as part of output path or commit metadata.
</span></code></pre></div></div>

<h2 id="failure-modes-you-should-plan-for-because-they-will-happen">Failure modes you should plan for (because they will happen)</h2>

<ul>
  <li><strong>Late data</strong>: decide what “late” means, and what you do when it happens.</li>
  <li><strong>Out-of-order updates</strong>: if you can’t deterministically reconcile, you’re not doing CDC—you’re doing vibes.</li>
  <li><strong>Partial writes</strong>: either use atomic commits (preferred) or write-then-promote patterns.</li>
  <li><strong>Hot partitions</strong>: your hash key choices are architecture, not implementation detail.</li>
</ul>

<h2 id="observability-checklist-minimal-not-optional">Observability checklist (minimal, not optional)</h2>

<ul>
  <li>A run id / batch id that propagates through logs</li>
  <li>Row counts in/out with anomaly detection</li>
  <li>Lag metrics (event-time lag for streaming; freshness for batch)</li>
  <li>Data quality checks that <strong>fail the run</strong>, not just “notify Slack politely”</li>
</ul>

<h2 id="takeaways">Takeaways</h2>

<ul>
  <li>The fastest path is the one you can safely re-run.</li>
  <li>“Exactly once” is a design constraint, not a product checkbox.</li>
  <li>If you can’t explain your pipeline’s invariants in one page, you can’t operate it.</li>
</ul>

<p><em>Next time someone says “just rerun it”, smile and ask: “Cool—what’s the idempotency key?”</em></p>]]></content><author><name>basix</name><email>selver@basix.dev</email></author><category term="data-engineering" /><category term="observability tracing" /><summary type="html"><![CDATA[Correlate events across ingest → transform → serve. Debugging gets boring.]]></summary></entry><entry><title type="html">Serving Layers: Materialized Views, Caches, and the Myth of ‘Realtime’</title><link href="https://www.basix.dev/data-engineering/serving-layers-materialized-views-caches-and-the-myth-of-realtime/" rel="alternate" type="text/html" title="Serving Layers: Materialized Views, Caches, and the Myth of ‘Realtime’" /><published>2025-11-05T10:00:00+01:00</published><updated>2025-11-05T10:00:00+01:00</updated><id>https://www.basix.dev/data-engineering/serving-layers-materialized-views-caches-and-the-myth-of-realtime</id><content type="html" xml:base="https://www.basix.dev/data-engineering/serving-layers-materialized-views-caches-and-the-myth-of-realtime/"><![CDATA[<p>Realtime is a budget decision.</p>

<p>Look, I get it—shipping features is fun. But shipping <strong>correct</strong> pipelines is somehow considered optional.</p>

<h2 id="the-problem-aka-where-most-designs-go-to-die">The problem (a.k.a. where most designs go to die)</h2>

<p>In production, data systems fail in boring ways:</p>

<ul>
  <li><strong>retries</strong> that create duplicates</li>
  <li><strong>backfills</strong> that rewrite history (and then everyone argues about “the source of truth”)</li>
  <li><strong>schema changes</strong> that arrive unannounced because someone added a column “real quick”</li>
  <li><strong>caches</strong> (browser, CDN, query engine) that serve stale results while people swear “nothing changed”</li>
</ul>

<p>If any of that sounds familiar, congratulations—you’re operating a real system.</p>

<h2 id="a-reference-architecture-that-actually-survives-contact-with-reality">A reference architecture that actually survives contact with reality</h2>

<p>A reliable design usually has these properties:</p>

<ol>
  <li><strong>Deterministic inputs</strong> (you can describe <em>exactly</em> what this run consumes)</li>
  <li><strong>Idempotent outputs</strong> (re-running produces the same end state)</li>
  <li><strong>Versioned contracts</strong> at boundaries (schemas, keys, SLAs)</li>
  <li><strong>Observable execution</strong> (metrics, lineage, logs with correlation)</li>
</ol>

<p>Here’s the part everyone skips: you don’t get these by adding a dashboard. You get them by encoding invariants into the pipeline.</p>

<h2 id="implementation-notes-things-youll-thank-yourself-for-later">Implementation notes (things you’ll thank yourself for later)</h2>

<ul>
  <li><strong>Commit metadata</strong>: record batch identifiers, source offsets, and the code version that produced the output.</li>
  <li><strong>Monotonic event ordering</strong>: define ordering rules (event time + tie-breakers) and enforce them in merges.</li>
  <li><strong>Backfill isolation</strong>: run backfills in separate compute pools / queues with explicit throughput caps.</li>
  <li><strong>Contract enforcement</strong>: validate schema + keys at ingestion; quarantine bad records (don’t “fix” them silently).</li>
</ul>

<h3 id="concrete-example">Concrete example</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Example: idempotent merge for CDC into a lake table</span>
<span class="n">MERGE</span> <span class="k">INTO</span> <span class="n">silver</span><span class="p">.</span><span class="n">customer</span> <span class="n">t</span>
<span class="k">USING</span> <span class="n">staging</span><span class="p">.</span><span class="n">customer_cdc</span> <span class="n">s</span>
<span class="k">ON</span> <span class="n">t</span><span class="p">.</span><span class="n">customer_id</span> <span class="o">=</span> <span class="n">s</span><span class="p">.</span><span class="n">customer_id</span>
<span class="k">WHEN</span> <span class="n">MATCHED</span> <span class="k">AND</span> <span class="n">s</span><span class="p">.</span><span class="n">op</span> <span class="o">=</span> <span class="s1">'D'</span> <span class="k">THEN</span> <span class="k">DELETE</span>
<span class="k">WHEN</span> <span class="n">MATCHED</span> <span class="k">AND</span> <span class="n">s</span><span class="p">.</span><span class="n">op</span> <span class="k">IN</span> <span class="p">(</span><span class="s1">'U'</span><span class="p">,</span><span class="s1">'C'</span><span class="p">)</span> <span class="k">AND</span> <span class="n">s</span><span class="p">.</span><span class="n">event_ts</span> <span class="o">&gt;=</span> <span class="n">t</span><span class="p">.</span><span class="n">last_event_ts</span> <span class="k">THEN</span>
  <span class="k">UPDATE</span> <span class="k">SET</span> <span class="o">*</span>
<span class="k">WHEN</span> <span class="k">NOT</span> <span class="n">MATCHED</span> <span class="k">AND</span> <span class="n">s</span><span class="p">.</span><span class="n">op</span> <span class="k">IN</span> <span class="p">(</span><span class="s1">'C'</span><span class="p">)</span> <span class="k">THEN</span>
  <span class="k">INSERT</span> <span class="o">*</span><span class="p">;</span>
</code></pre></div></div>

<h2 id="failure-modes-you-should-plan-for-because-they-will-happen">Failure modes you should plan for (because they will happen)</h2>

<ul>
  <li><strong>Late data</strong>: decide what “late” means, and what you do when it happens.</li>
  <li><strong>Out-of-order updates</strong>: if you can’t deterministically reconcile, you’re not doing CDC—you’re doing vibes.</li>
  <li><strong>Partial writes</strong>: either use atomic commits (preferred) or write-then-promote patterns.</li>
  <li><strong>Hot partitions</strong>: your hash key choices are architecture, not implementation detail.</li>
</ul>

<h2 id="observability-checklist-minimal-not-optional">Observability checklist (minimal, not optional)</h2>

<ul>
  <li>A run id / batch id that propagates through logs</li>
  <li>Row counts in/out with anomaly detection</li>
  <li>Lag metrics (event-time lag for streaming; freshness for batch)</li>
  <li>Data quality checks that <strong>fail the run</strong>, not just “notify Slack politely”</li>
</ul>

<h2 id="takeaways">Takeaways</h2>

<ul>
  <li>The fastest path is the one you can safely re-run.</li>
  <li>“Exactly once” is a design constraint, not a product checkbox.</li>
  <li>If you can’t explain your pipeline’s invariants in one page, you can’t operate it.</li>
</ul>

<p><em>Next time someone says “just rerun it”, smile and ask: “Cool—what’s the idempotency key?”</em></p>]]></content><author><name>basix</name><email>selver@basix.dev</email></author><category term="data-engineering" /><category term="serving performance" /><summary type="html"><![CDATA[Realtime is a budget decision.]]></summary></entry><entry><title type="html">Metadata-Driven Pipelines: Dynamic Doesn’t Mean Uncontrolled</title><link href="https://www.basix.dev/data-engineering/metadata-driven-pipelines-dynamic-doesnt-mean-uncontrolled/" rel="alternate" type="text/html" title="Metadata-Driven Pipelines: Dynamic Doesn’t Mean Uncontrolled" /><published>2025-10-22T11:00:00+02:00</published><updated>2025-10-22T11:00:00+02:00</updated><id>https://www.basix.dev/data-engineering/metadata-driven-pipelines-dynamic-doesnt-mean-uncontrolled</id><content type="html" xml:base="https://www.basix.dev/data-engineering/metadata-driven-pipelines-dynamic-doesnt-mean-uncontrolled/"><![CDATA[<p>Drive config from metadata, but validate like a paranoid adult.</p>

<p>Look, I get it—shipping features is fun. But shipping <strong>correct</strong> pipelines is somehow considered optional.</p>

<h2 id="the-problem-aka-where-most-designs-go-to-die">The problem (a.k.a. where most designs go to die)</h2>

<p>In production, data systems fail in boring ways:</p>

<ul>
  <li><strong>retries</strong> that create duplicates</li>
  <li><strong>backfills</strong> that rewrite history (and then everyone argues about “the source of truth”)</li>
  <li><strong>schema changes</strong> that arrive unannounced because someone added a column “real quick”</li>
  <li><strong>caches</strong> (browser, CDN, query engine) that serve stale results while people swear “nothing changed”</li>
</ul>

<p>If any of that sounds familiar, congratulations—you’re operating a real system.</p>

<h2 id="a-reference-architecture-that-actually-survives-contact-with-reality">A reference architecture that actually survives contact with reality</h2>

<p>A reliable design usually has these properties:</p>

<ol>
  <li><strong>Deterministic inputs</strong> (you can describe <em>exactly</em> what this run consumes)</li>
  <li><strong>Idempotent outputs</strong> (re-running produces the same end state)</li>
  <li><strong>Versioned contracts</strong> at boundaries (schemas, keys, SLAs)</li>
  <li><strong>Observable execution</strong> (metrics, lineage, logs with correlation)</li>
</ol>

<p>Here’s the part everyone skips: you don’t get these by adding a dashboard. You get them by encoding invariants into the pipeline.</p>

<h2 id="implementation-notes-things-youll-thank-yourself-for-later">Implementation notes (things you’ll thank yourself for later)</h2>

<ul>
  <li><strong>Commit metadata</strong>: record batch identifiers, source offsets, and the code version that produced the output.</li>
  <li><strong>Monotonic event ordering</strong>: define ordering rules (event time + tie-breakers) and enforce them in merges.</li>
  <li><strong>Backfill isolation</strong>: run backfills in separate compute pools / queues with explicit throughput caps.</li>
  <li><strong>Contract enforcement</strong>: validate schema + keys at ingestion; quarantine bad records (don’t “fix” them silently).</li>
</ul>

<h3 id="concrete-example">Concrete example</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Example: deterministic batch id + exactly-once-ish writes
</span><span class="kn">import</span> <span class="n">hashlib</span>

<span class="k">def</span> <span class="nf">batch_id</span><span class="p">(</span><span class="n">run_date</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">dataset</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
    <span class="k">return</span> <span class="n">hashlib</span><span class="p">.</span><span class="nf">sha256</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">dataset</span><span class="si">}</span><span class="s">:</span><span class="si">{</span><span class="n">run_date</span><span class="si">}</span><span class="sh">"</span><span class="p">.</span><span class="nf">encode</span><span class="p">()).</span><span class="nf">hexdigest</span><span class="p">()</span>

<span class="c1"># Use batch_id as part of output path or commit metadata.
</span></code></pre></div></div>

<h2 id="failure-modes-you-should-plan-for-because-they-will-happen">Failure modes you should plan for (because they will happen)</h2>

<ul>
  <li><strong>Late data</strong>: decide what “late” means, and what you do when it happens.</li>
  <li><strong>Out-of-order updates</strong>: if you can’t deterministically reconcile, you’re not doing CDC—you’re doing vibes.</li>
  <li><strong>Partial writes</strong>: either use atomic commits (preferred) or write-then-promote patterns.</li>
  <li><strong>Hot partitions</strong>: your hash key choices are architecture, not implementation detail.</li>
</ul>

<h2 id="observability-checklist-minimal-not-optional">Observability checklist (minimal, not optional)</h2>

<ul>
  <li>A run id / batch id that propagates through logs</li>
  <li>Row counts in/out with anomaly detection</li>
  <li>Lag metrics (event-time lag for streaming; freshness for batch)</li>
  <li>Data quality checks that <strong>fail the run</strong>, not just “notify Slack politely”</li>
</ul>

<h2 id="takeaways">Takeaways</h2>

<ul>
  <li>The fastest path is the one you can safely re-run.</li>
  <li>“Exactly once” is a design constraint, not a product checkbox.</li>
  <li>If you can’t explain your pipeline’s invariants in one page, you can’t operate it.</li>
</ul>

<p><em>Next time someone says “just rerun it”, smile and ask: “Cool—what’s the idempotency key?”</em></p>]]></content><author><name>basix</name><email>selver@basix.dev</email></author><category term="data-engineering" /><category term="metadata orchestration" /><summary type="html"><![CDATA[Drive config from metadata, but validate like a paranoid adult.]]></summary></entry><entry><title type="html">Bronze Table Quality Gates: Yes, Even Bronze</title><link href="https://www.basix.dev/data-engineering/bronze-table-quality-gates-yes-even-bronze/" rel="alternate" type="text/html" title="Bronze Table Quality Gates: Yes, Even Bronze" /><published>2025-10-09T11:00:00+02:00</published><updated>2025-10-09T11:00:00+02:00</updated><id>https://www.basix.dev/data-engineering/bronze-table-quality-gates-yes-even-bronze</id><content type="html" xml:base="https://www.basix.dev/data-engineering/bronze-table-quality-gates-yes-even-bronze/"><![CDATA[<p>If you ingest garbage, you’ll analyze garbage. That’s not ‘agile’.</p>

<p>Look, I get it—shipping features is fun. But shipping <strong>correct</strong> pipelines is somehow considered optional.</p>

<h2 id="the-problem-aka-where-most-designs-go-to-die">The problem (a.k.a. where most designs go to die)</h2>

<p>In production, data systems fail in boring ways:</p>

<ul>
  <li><strong>retries</strong> that create duplicates</li>
  <li><strong>backfills</strong> that rewrite history (and then everyone argues about “the source of truth”)</li>
  <li><strong>schema changes</strong> that arrive unannounced because someone added a column “real quick”</li>
  <li><strong>caches</strong> (browser, CDN, query engine) that serve stale results while people swear “nothing changed”</li>
</ul>

<p>If any of that sounds familiar, congratulations—you’re operating a real system.</p>

<h2 id="a-reference-architecture-that-actually-survives-contact-with-reality">A reference architecture that actually survives contact with reality</h2>

<p>A reliable design usually has these properties:</p>

<ol>
  <li><strong>Deterministic inputs</strong> (you can describe <em>exactly</em> what this run consumes)</li>
  <li><strong>Idempotent outputs</strong> (re-running produces the same end state)</li>
  <li><strong>Versioned contracts</strong> at boundaries (schemas, keys, SLAs)</li>
  <li><strong>Observable execution</strong> (metrics, lineage, logs with correlation)</li>
</ol>

<p>Here’s the part everyone skips: you don’t get these by adding a dashboard. You get them by encoding invariants into the pipeline.</p>

<h2 id="implementation-notes-things-youll-thank-yourself-for-later">Implementation notes (things you’ll thank yourself for later)</h2>

<ul>
  <li><strong>Commit metadata</strong>: record batch identifiers, source offsets, and the code version that produced the output.</li>
  <li><strong>Monotonic event ordering</strong>: define ordering rules (event time + tie-breakers) and enforce them in merges.</li>
  <li><strong>Backfill isolation</strong>: run backfills in separate compute pools / queues with explicit throughput caps.</li>
  <li><strong>Contract enforcement</strong>: validate schema + keys at ingestion; quarantine bad records (don’t “fix” them silently).</li>
</ul>

<h3 id="concrete-example">Concrete example</h3>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Example: a minimal data contract / schema expectation</span>
<span class="na">version</span><span class="pi">:</span> <span class="m">1</span>
<span class="na">entity</span><span class="pi">:</span> <span class="s">orders</span>
<span class="na">primary_key</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">order_id</span><span class="pi">]</span>
<span class="na">columns</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">order_id</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">string</span>
    <span class="na">required</span><span class="pi">:</span> <span class="kc">true</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">order_ts</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">timestamp</span>
    <span class="na">required</span><span class="pi">:</span> <span class="kc">true</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">total_amount</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">decimal(18,2)</span>
    <span class="na">required</span><span class="pi">:</span> <span class="kc">true</span>
<span class="na">checks</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">non_negative_amount</span>
    <span class="na">expr</span><span class="pi">:</span> <span class="s">total_amount &gt;= </span><span class="m">0</span>
</code></pre></div></div>

<h2 id="failure-modes-you-should-plan-for-because-they-will-happen">Failure modes you should plan for (because they will happen)</h2>

<ul>
  <li><strong>Late data</strong>: decide what “late” means, and what you do when it happens.</li>
  <li><strong>Out-of-order updates</strong>: if you can’t deterministically reconcile, you’re not doing CDC—you’re doing vibes.</li>
  <li><strong>Partial writes</strong>: either use atomic commits (preferred) or write-then-promote patterns.</li>
  <li><strong>Hot partitions</strong>: your hash key choices are architecture, not implementation detail.</li>
</ul>

<h2 id="observability-checklist-minimal-not-optional">Observability checklist (minimal, not optional)</h2>

<ul>
  <li>A run id / batch id that propagates through logs</li>
  <li>Row counts in/out with anomaly detection</li>
  <li>Lag metrics (event-time lag for streaming; freshness for batch)</li>
  <li>Data quality checks that <strong>fail the run</strong>, not just “notify Slack politely”</li>
</ul>

<h2 id="takeaways">Takeaways</h2>

<ul>
  <li>The fastest path is the one you can safely re-run.</li>
  <li>“Exactly once” is a design constraint, not a product checkbox.</li>
  <li>If you can’t explain your pipeline’s invariants in one page, you can’t operate it.</li>
</ul>

<p><em>Next time someone says “just rerun it”, smile and ask: “Cool—what’s the idempotency key?”</em></p>]]></content><author><name>basix</name><email>selver@basix.dev</email></author><category term="data-engineering" /><category term="quality lakehouse" /><summary type="html"><![CDATA[If you ingest garbage, you’ll analyze garbage. That’s not ‘agile’.]]></summary></entry><entry><title type="html">Kubernetes for Data Jobs: The Part Where YAML Becomes a Lifestyle</title><link href="https://www.basix.dev/data-engineering/kubernetes-for-data-jobs-the-part-where-yaml-becomes-a-lifestyle/" rel="alternate" type="text/html" title="Kubernetes for Data Jobs: The Part Where YAML Becomes a Lifestyle" /><published>2025-09-25T11:00:00+02:00</published><updated>2025-09-25T11:00:00+02:00</updated><id>https://www.basix.dev/data-engineering/kubernetes-for-data-jobs-the-part-where-yaml-becomes-a-lifestyle</id><content type="html" xml:base="https://www.basix.dev/data-engineering/kubernetes-for-data-jobs-the-part-where-yaml-becomes-a-lifestyle/"><![CDATA[<p>It’s great until you run 5000 pods and discover quotas.</p>

<p>Look, I get it—shipping features is fun. But shipping <strong>correct</strong> pipelines is somehow considered optional.</p>

<h2 id="the-problem-aka-where-most-designs-go-to-die">The problem (a.k.a. where most designs go to die)</h2>

<p>In production, data systems fail in boring ways:</p>

<ul>
  <li><strong>retries</strong> that create duplicates</li>
  <li><strong>backfills</strong> that rewrite history (and then everyone argues about “the source of truth”)</li>
  <li><strong>schema changes</strong> that arrive unannounced because someone added a column “real quick”</li>
  <li><strong>caches</strong> (browser, CDN, query engine) that serve stale results while people swear “nothing changed”</li>
</ul>

<p>If any of that sounds familiar, congratulations—you’re operating a real system.</p>

<h2 id="a-reference-architecture-that-actually-survives-contact-with-reality">A reference architecture that actually survives contact with reality</h2>

<p>A reliable design usually has these properties:</p>

<ol>
  <li><strong>Deterministic inputs</strong> (you can describe <em>exactly</em> what this run consumes)</li>
  <li><strong>Idempotent outputs</strong> (re-running produces the same end state)</li>
  <li><strong>Versioned contracts</strong> at boundaries (schemas, keys, SLAs)</li>
  <li><strong>Observable execution</strong> (metrics, lineage, logs with correlation)</li>
</ol>

<p>Here’s the part everyone skips: you don’t get these by adding a dashboard. You get them by encoding invariants into the pipeline.</p>

<h2 id="implementation-notes-things-youll-thank-yourself-for-later">Implementation notes (things you’ll thank yourself for later)</h2>

<ul>
  <li><strong>Commit metadata</strong>: record batch identifiers, source offsets, and the code version that produced the output.</li>
  <li><strong>Monotonic event ordering</strong>: define ordering rules (event time + tie-breakers) and enforce them in merges.</li>
  <li><strong>Backfill isolation</strong>: run backfills in separate compute pools / queues with explicit throughput caps.</li>
  <li><strong>Contract enforcement</strong>: validate schema + keys at ingestion; quarantine bad records (don’t “fix” them silently).</li>
</ul>

<h3 id="concrete-example">Concrete example</h3>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Example: a minimal data contract / schema expectation</span>
<span class="na">version</span><span class="pi">:</span> <span class="m">1</span>
<span class="na">entity</span><span class="pi">:</span> <span class="s">orders</span>
<span class="na">primary_key</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">order_id</span><span class="pi">]</span>
<span class="na">columns</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">order_id</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">string</span>
    <span class="na">required</span><span class="pi">:</span> <span class="kc">true</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">order_ts</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">timestamp</span>
    <span class="na">required</span><span class="pi">:</span> <span class="kc">true</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">total_amount</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">decimal(18,2)</span>
    <span class="na">required</span><span class="pi">:</span> <span class="kc">true</span>
<span class="na">checks</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">non_negative_amount</span>
    <span class="na">expr</span><span class="pi">:</span> <span class="s">total_amount &gt;= </span><span class="m">0</span>
</code></pre></div></div>

<h2 id="failure-modes-you-should-plan-for-because-they-will-happen">Failure modes you should plan for (because they will happen)</h2>

<ul>
  <li><strong>Late data</strong>: decide what “late” means, and what you do when it happens.</li>
  <li><strong>Out-of-order updates</strong>: if you can’t deterministically reconcile, you’re not doing CDC—you’re doing vibes.</li>
  <li><strong>Partial writes</strong>: either use atomic commits (preferred) or write-then-promote patterns.</li>
  <li><strong>Hot partitions</strong>: your hash key choices are architecture, not implementation detail.</li>
</ul>

<h2 id="observability-checklist-minimal-not-optional">Observability checklist (minimal, not optional)</h2>

<ul>
  <li>A run id / batch id that propagates through logs</li>
  <li>Row counts in/out with anomaly detection</li>
  <li>Lag metrics (event-time lag for streaming; freshness for batch)</li>
  <li>Data quality checks that <strong>fail the run</strong>, not just “notify Slack politely”</li>
</ul>

<h2 id="takeaways">Takeaways</h2>

<ul>
  <li>The fastest path is the one you can safely re-run.</li>
  <li>“Exactly once” is a design constraint, not a product checkbox.</li>
  <li>If you can’t explain your pipeline’s invariants in one page, you can’t operate it.</li>
</ul>

<p><em>Next time someone says “just rerun it”, smile and ask: “Cool—what’s the idempotency key?”</em></p>]]></content><author><name>basix</name><email>selver@basix.dev</email></author><category term="data-engineering" /><category term="kubernetes batch ops" /><summary type="html"><![CDATA[It’s great until you run 5000 pods and discover quotas.]]></summary></entry><entry><title type="html">Change Data Capture on Azure: Event Hubs, Debezium, and Reality</title><link href="https://www.basix.dev/data-engineering/change-data-capture-on-azure-event-hubs-debezium-and-reality/" rel="alternate" type="text/html" title="Change Data Capture on Azure: Event Hubs, Debezium, and Reality" /><published>2025-09-12T11:00:00+02:00</published><updated>2025-09-12T11:00:00+02:00</updated><id>https://www.basix.dev/data-engineering/change-data-capture-on-azure-event-hubs-debezium-and-reality</id><content type="html" xml:base="https://www.basix.dev/data-engineering/change-data-capture-on-azure-event-hubs-debezium-and-reality/"><![CDATA[<p>Azure can do CDC fine—if you respect throughput units and partition keys.</p>

<p>Look, I get it—shipping features is fun. But shipping <strong>correct</strong> pipelines is somehow considered optional.</p>

<h2 id="the-problem-aka-where-most-designs-go-to-die">The problem (a.k.a. where most designs go to die)</h2>

<p>In production, data systems fail in boring ways:</p>

<ul>
  <li><strong>retries</strong> that create duplicates</li>
  <li><strong>backfills</strong> that rewrite history (and then everyone argues about “the source of truth”)</li>
  <li><strong>schema changes</strong> that arrive unannounced because someone added a column “real quick”</li>
  <li><strong>caches</strong> (browser, CDN, query engine) that serve stale results while people swear “nothing changed”</li>
</ul>

<p>If any of that sounds familiar, congratulations—you’re operating a real system.</p>

<h2 id="a-reference-architecture-that-actually-survives-contact-with-reality">A reference architecture that actually survives contact with reality</h2>

<p>A reliable design usually has these properties:</p>

<ol>
  <li><strong>Deterministic inputs</strong> (you can describe <em>exactly</em> what this run consumes)</li>
  <li><strong>Idempotent outputs</strong> (re-running produces the same end state)</li>
  <li><strong>Versioned contracts</strong> at boundaries (schemas, keys, SLAs)</li>
  <li><strong>Observable execution</strong> (metrics, lineage, logs with correlation)</li>
</ol>

<p>Here’s the part everyone skips: you don’t get these by adding a dashboard. You get them by encoding invariants into the pipeline.</p>

<h2 id="implementation-notes-things-youll-thank-yourself-for-later">Implementation notes (things you’ll thank yourself for later)</h2>

<ul>
  <li><strong>Commit metadata</strong>: record batch identifiers, source offsets, and the code version that produced the output.</li>
  <li><strong>Monotonic event ordering</strong>: define ordering rules (event time + tie-breakers) and enforce them in merges.</li>
  <li><strong>Backfill isolation</strong>: run backfills in separate compute pools / queues with explicit throughput caps.</li>
  <li><strong>Contract enforcement</strong>: validate schema + keys at ingestion; quarantine bad records (don’t “fix” them silently).</li>
</ul>

<h3 id="concrete-example">Concrete example</h3>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Example: a minimal data contract / schema expectation</span>
<span class="na">version</span><span class="pi">:</span> <span class="m">1</span>
<span class="na">entity</span><span class="pi">:</span> <span class="s">orders</span>
<span class="na">primary_key</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">order_id</span><span class="pi">]</span>
<span class="na">columns</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">order_id</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">string</span>
    <span class="na">required</span><span class="pi">:</span> <span class="kc">true</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">order_ts</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">timestamp</span>
    <span class="na">required</span><span class="pi">:</span> <span class="kc">true</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">total_amount</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">decimal(18,2)</span>
    <span class="na">required</span><span class="pi">:</span> <span class="kc">true</span>
<span class="na">checks</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">non_negative_amount</span>
    <span class="na">expr</span><span class="pi">:</span> <span class="s">total_amount &gt;= </span><span class="m">0</span>
</code></pre></div></div>

<h2 id="failure-modes-you-should-plan-for-because-they-will-happen">Failure modes you should plan for (because they will happen)</h2>

<ul>
  <li><strong>Late data</strong>: decide what “late” means, and what you do when it happens.</li>
  <li><strong>Out-of-order updates</strong>: if you can’t deterministically reconcile, you’re not doing CDC—you’re doing vibes.</li>
  <li><strong>Partial writes</strong>: either use atomic commits (preferred) or write-then-promote patterns.</li>
  <li><strong>Hot partitions</strong>: your hash key choices are architecture, not implementation detail.</li>
</ul>

<h2 id="observability-checklist-minimal-not-optional">Observability checklist (minimal, not optional)</h2>

<ul>
  <li>A run id / batch id that propagates through logs</li>
  <li>Row counts in/out with anomaly detection</li>
  <li>Lag metrics (event-time lag for streaming; freshness for batch)</li>
  <li>Data quality checks that <strong>fail the run</strong>, not just “notify Slack politely”</li>
</ul>

<h2 id="takeaways">Takeaways</h2>

<ul>
  <li>The fastest path is the one you can safely re-run.</li>
  <li>“Exactly once” is a design constraint, not a product checkbox.</li>
  <li>If you can’t explain your pipeline’s invariants in one page, you can’t operate it.</li>
</ul>

<p><em>Next time someone says “just rerun it”, smile and ask: “Cool—what’s the idempotency key?”</em></p>]]></content><author><name>basix</name><email>selver@basix.dev</email></author><category term="data-engineering" /><category term="azure cdc eventhubs" /><summary type="html"><![CDATA[Azure can do CDC fine—if you respect throughput units and partition keys.]]></summary></entry></feed>