<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://theintelligencestack.tech/feed.xml" rel="self" type="application/atom+xml" /><link href="https://theintelligencestack.tech/" rel="alternate" type="text/html" hreflang="en" /><updated>2026-10-25T20:49:15+00:00</updated><id>https://theintelligencestack.tech/feed.xml</id><title type="html">The Intelligence Stack</title><subtitle>Diagram-first deep dives on designing data platforms, building reliable pipelines and shipping AI systems to production.</subtitle><author><name>subhasis</name></author><entry><title type="html">The Semantic Layer Generator: When Agentic AI Meets Data Architecture</title><link href="https://theintelligencestack.tech/blog/the-semantic-layer-generator-when-agentic-ai-meets-data-architecture/" rel="alternate" type="text/html" title="The Semantic Layer Generator: When Agentic AI Meets Data Architecture" /><published>2026-10-25T00:00:00+00:00</published><updated>2026-10-25T00:00:00+00:00</updated><id>https://theintelligencestack.tech/blog/the-semantic-layer-generator-when-agentic-ai-meets-data-architecture</id><content type="html" xml:base="https://theintelligencestack.tech/blog/the-semantic-layer-generator-when-agentic-ai-meets-data-architecture/"><![CDATA[<p>Every modern data team faces the same bottleneck: the gap between a normalised database schema and the flat, query-ready tables that analysts actually need. The Semantic Layer Generator is an agentic AI system that closes that gap autonomously — transforming raw schemas and business questions into optimised, production-ready wide-table views in seconds.</p>

<h2 id="the-problem-nobody-talks-aboutthe-problem-nobody-talks-about">The Problem Nobody Talks AboutThe Problem Nobody Talks About</h2>

<p>Data warehouses are built by engineers. Dashboards are built by analysts. Between them lies a chasm — the semantic layer — and it’s almost always hand-crafted, poorly documented, and perpetually out of date.</p>

<p>Consider what building a semantic layer actually requires today. A data engineer must deeply understand the normalised schema: every foreign key, every relationship chain, every edge case where a left join becomes an inner join. They must then interview stakeholders to understand the analytical questions the business needs answered. Only then can they begin designing wide tables — denormalised, pre-joined views that flatten a complex relational model into something a BI tool can query without six-table joins.</p>

<p>This process typically takes days to weeks for a mid-complexity schema. And the result? A static artefact that starts decaying the moment the schema changes or a new business question emerges.</p>

<p><strong>The Core Tension</strong></p>

<p>Normalised schemas optimise for data integrity. Analytical queries optimise for speed and simplicity. The semantic layer is the translation layer between these two worlds — and it has been painfully manual for decades.</p>

<p>The downstream costs are significant. When the semantic layer is missing or poorly designed, analysts write ad-hoc queries with redundant joins, dashboards slow to a crawl, Text2SQL tools hallucinate because they can’t navigate complex schemas, and every new business question triggers another round of engineering tickets.</p>

<figure class="diagram">
<div class="diagram-frame">
<img src="/assets/images/posts/the-semantic-layer-generator-when-agentic-ai-meets-data-architecture/3c8e2f99-234b-412e-8739-69c1cbcf8215.png" alt="" loading="lazy" decoding="async" />
<span class="diagram-hint" aria-hidden="true"><svg class="icon icon-zoom" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7" /></svg> Enlarge</span>
</div>
</figure>

<figure class="diagram">
<div class="diagram-frame">
<img src="/assets/images/posts/the-semantic-layer-generator-when-agentic-ai-meets-data-architecture/e7dec391-9cb6-4be6-91ad-e9409973a832.png" alt="" loading="lazy" decoding="async" />
<span class="diagram-hint" aria-hidden="true"><svg class="icon icon-zoom" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7" /></svg> Enlarge</span>
</div>
</figure>

<figure class="diagram">
<div class="diagram-frame">
<img src="/assets/images/posts/the-semantic-layer-generator-when-agentic-ai-meets-data-architecture/7e70ba9a-e299-4947-8554-6c3325bfa70a.png" alt="" loading="lazy" decoding="async" />
<span class="diagram-hint" aria-hidden="true"><svg class="icon icon-zoom" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7" /></svg> Enlarge</span>
</div>
</figure>

<figure class="diagram">
<div class="diagram-frame">
<img src="/assets/images/posts/the-semantic-layer-generator-when-agentic-ai-meets-data-architecture/17dc71e5-f90f-4e32-b152-c63a45d32835.png" alt="" loading="lazy" decoding="async" />
<span class="diagram-hint" aria-hidden="true"><svg class="icon icon-zoom" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7" /></svg> Enlarge</span>
</div>
</figure>

<p><strong>The Business Case</strong></p>

<p>This isn’t just a technical inconvenience — it’s a strategic bottleneck. Organisations that can’t translate raw data into queryable, governed, analyst-friendly tables cannot move at the speed of their own questions.</p>

<figure class="diagram">
<div class="diagram-frame">
<img src="/assets/images/posts/the-semantic-layer-generator-when-agentic-ai-meets-data-architecture/9781e834-de40-42ab-94c0-dc6a0333ea28.png" alt="" loading="lazy" decoding="async" />
<span class="diagram-hint" aria-hidden="true"><svg class="icon icon-zoom" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7" /></svg> Enlarge</span>
</div>
</figure>

<p>A well-designed semantic layer delivers three things simultaneously: it makes analysts self-sufficient (no more waiting on engineering for every new slice of data), it makes queries dramatically faster (pre-joined views eliminate runtime join costs), and it makes Text2SQL systems reliable (flat tables with descriptive column names are exactly what language models need to generate accurate SQL).</p>

<p>The question was never whether organisations need a semantic layer. It was whether the process of building one could be automated without sacrificing quality. That’s the question this tool answers.</p>

<p><strong>Why Now? The Convergence Moment</strong></p>

<p>Three capabilities had to mature simultaneously to make this possible.</p>

<p><strong>1. LLMs That Understand Schema Semantics</strong></p>

<p>Modern large language models don’t just parse JSON — they understand what a DEPARTMENT_ID foreign key means, how it relates to an EMPLOYEES table, and why an analyst asking about “salary distribution by role band” needs a join path through SALARIES → EMPLOYEES → ROLES. This semantic reasoning is the foundation the entire pipeline rests on.</p>

<p><strong>2. Agentic Orchestration Patterns</strong></p>

<p>A single LLM call can’t do this well. You need a multi-phase pipeline where each stage’s output feeds the next — analysis, optimisation, design, generation, reporting — with deterministic optimisation steps (pure set-logic pruning) interleaved with LLM-powered reasoning. This is agentic AI in its most productive form: not a chatbot, but an autonomous system that completes a complex engineering workflow end-to-end.</p>]]></content><author><name>Subhasis Mukherjee</name></author><category term="data-architecture" /><category term="agentic-ai" /><category term="data-architecture" /><summary type="html"><![CDATA[How a 5-phase autonomous pipeline eliminates weeks of manual data modelling — and why it matters for every analytics team shipping at scale.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://theintelligencestack.tech/assets/images/posts/the-semantic-layer-generator-when-agentic-ai-meets-data-architecture/3c8e2f99-234b-412e-8739-69c1cbcf8215.png" /><media:content medium="image" url="https://theintelligencestack.tech/assets/images/posts/the-semantic-layer-generator-when-agentic-ai-meets-data-architecture/3c8e2f99-234b-412e-8739-69c1cbcf8215.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Applying AI in Data Engineering - the Agentic Data Engineering</title><link href="https://theintelligencestack.tech/blog/agentic-de/" rel="alternate" type="text/html" title="Applying AI in Data Engineering - the Agentic Data Engineering" /><published>2026-10-16T00:00:00+00:00</published><updated>2026-10-16T00:00:00+00:00</updated><id>https://theintelligencestack.tech/blog/agentic-de</id><content type="html" xml:base="https://theintelligencestack.tech/blog/agentic-de/"><![CDATA[<p>Data engineering is being transformed by AI agents faster than any other part of the modern data stack. Pipeline generation, transformation authoring, schema evolution, data quality monitoring, impact analysis, and documentation, tasks that consumed the majority of a data engineer’s week, are being automated or augmented by agents that can plan, execute, and iterate.</p>

<p>The term “agentic data engineering” describes this shift: AI agents that autonomously perform data engineering tasks through structured interfaces, with human engineers reviewing, approving, and governing the output. It is the same architectural pattern as agentic analytics, applied one layer down in the stack.</p>

<p>And the two layers are more connected than the industry typically acknowledges.</p>

<h2 id="what-is-agentic-data-engineering">What is agentic data engineering?</h2>

<p>Agentic data engineering is the practice of using AI agents to autonomously perform data engineering tasks (building pipelines, writing transformations, managing schemas, monitoring data quality, and maintaining documentation) through code-native interfaces with human oversight.</p>

<p>The “agentic” part matters. The agent goes well beyond autocompleting SQL. It can:</p>

<p>Accept a high-level goal (“create a pipeline that ingests Stripe subscription events into our warehouse, deduplicated by event ID, partitioned by date”)
Decompose the goal into steps
Generate the pipeline definition, transformation logic, and tests
Validate against existing schema and conventions
Submit the result for review
The human engineer reviews, tests, and approves, the same workflow as reviewing a pull request from a junior engineer, except the junior engineer runs at machine speed and never forgets to write tests (if told to).</p>

<figure class="diagram">
<div class="diagram-frame">
<img src="/assets/images/posts/agentic-de/agentic-de.jpg" alt="Agentic Data Engineering works on the premise that all the information that is available to a data engineer, is available to it, in the form of context" loading="lazy" decoding="async" />
<span class="diagram-hint" aria-hidden="true"><svg class="icon icon-zoom" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7" /></svg> Enlarge</span>
</div>
<figcaption><strong>Figure 1.</strong> Agents explore sources, deduce lineage and perform quality audit</figcaption>
</figure>

<h2 id="what-can-ai-agents-do-in-data-engineering-today">What can AI agents do in data engineering today?</h2>

<p>The capabilities are real and growing. Here is what works in production today, verified beyond demos.</p>

<h3 id="pipeline-generation">Pipeline generation</h3>

<p>Agents generate pipeline definitions (Airflow DAGs, Dagster jobs, dbt models) from natural language descriptions or schema specifications. The output is code: reviewable, testable, version-controlled.</p>

<h3 id="transformation-authoring">Transformation authoring</h3>

<p>Given a source schema and a target requirement, agents write SQL transformations, dbt models, or other transformation logic. They handle joins, type casting, deduplication, and incremental load patterns.</p>

<h3 id="schema-migration-and-evolution">Schema migration and evolution</h3>

<p>When source schemas change (a new column appears, a type changes, a table is deprecated), agents analyze the impact downstream, generate migration scripts, and update affected transformations.</p>

<h3 id="data-quality-monitoring">Data quality monitoring</h3>
<p>Agents generate data quality tests (row counts, null checks, distribution anomalies, freshness checks) based on observed data patterns. They can monitor continuously and flag deviations.</p>

<h2 id="the-emerging-tool-ecosystem">The emerging tool ecosystem</h2>
<p>Several major platforms are building agentic data engineering capabilities.</p>

<h3 id="google-bigquery-data-engineering-agent">Google BigQuery, Data Engineering Agent</h3>
<p>Google’s Gemini-powered agent generates SQL transformations, creates pipelines, and manages BigQuery datasets through natural language. Integrated into the Google Cloud console. Strongest for teams already on BigQuery.</p>

<h3 id="databricks-ai-powered-data-engineering">Databricks, AI-powered data engineering</h3>
<p>Databricks integrates AI across its lakehouse platform. Agents assist with notebook development, pipeline creation, and data quality. Unity Catalog provides the governance layer. Strong for organizations with a Databricks-centric stack.</p>]]></content><author><name>Subhasis Mukherjee</name></author><category term="data-engineering" /><category term="agentic-ai" /><category term="ai" /><summary type="html"><![CDATA[How to apply Agentic AI principles and processes to ease the frictions of the current state of data engineering]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://theintelligencestack.tech/assets/images/posts/agentic-de/agentic-de.jpg" /><media:content medium="image" url="https://theintelligencestack.tech/assets/images/posts/agentic-de/agentic-de.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Anatomy of a Production RAG System</title><link href="https://theintelligencestack.tech/blog/production-rag-architecture/" rel="alternate" type="text/html" title="Anatomy of a Production RAG System" /><published>2026-10-05T00:00:00+00:00</published><updated>2026-10-05T00:00:00+00:00</updated><id>https://theintelligencestack.tech/blog/production-rag-architecture</id><content type="html" xml:base="https://theintelligencestack.tech/blog/production-rag-architecture/"><![CDATA[<p>A retrieval-augmented generation (RAG) prototype fits in fifty lines of code.
A production RAG system is a data platform with an LLM at the end of it — and
most of its failures are data failures, not model failures.</p>

<h2 id="two-paths-one-index">Two paths, one index</h2>

<p>Every RAG system has two halves that run on very different schedules:</p>

<ul>
  <li><strong>Offline indexing</strong> turns source documents into searchable chunks. It is a
batch or streaming data pipeline and should be engineered like one.</li>
  <li><strong>Online querying</strong> turns a user question into a grounded answer within a
latency budget, typically a few seconds end to end.</li>
</ul>

<figure class="diagram">
<div class="diagram-frame">
<img src="/assets/images/posts/production-rag-architecture/rag-architecture.jpg" alt="Offline path: sources, parse and clean, chunk, embed, then vector and keyword index. Online path: user question, query embedding and rewrite, hybrid retrieval, rerank, prompt plus LLM, answer with citations. An evaluation and guardrails layer spans both" loading="lazy" decoding="async" />
<span class="diagram-hint" aria-hidden="true"><svg class="icon icon-zoom" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7" /></svg> Enlarge</span>
</div>
<figcaption><strong>Figure 3.</strong> The offline indexing path and the online query path meet at the index. Evaluation spans both.</figcaption>
</figure>

<h2 id="the-offline-path">The offline path</h2>

<h3 id="parse-and-clean">Parse and clean</h3>

<p>PDFs, wikis and tickets all need different parsers. Strip navigation chrome,
keep headings and tables, and record where every piece of text came from — you
will need that provenance for citations.</p>

<h3 id="chunk-with-structure">Chunk with structure</h3>

<p>Fixed-size chunks are a fine baseline, but chunking along document structure
(sections, list items, table rows) usually retrieves better. Store metadata
such as title, section path, timestamp and access-control labels with every
chunk.</p>

<h3 id="embed-and-index">Embed and index</h3>

<p>Index each chunk twice: as a dense vector for semantic similarity, and as text
for keyword search. Hybrid retrieval rescues the queries that embeddings
handle poorly — product codes, error messages and acronyms.</p>

<h2 id="the-online-path">The online path</h2>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">answer</span><span class="p">(</span><span class="n">question</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Answer</span><span class="p">:</span>
    <span class="n">query</span> <span class="o">=</span> <span class="n">rewrite</span><span class="p">(</span><span class="n">question</span><span class="p">)</span>                       <span class="c1"># expand acronyms, fix typos
</span>    <span class="n">candidates</span> <span class="o">=</span> <span class="n">index</span><span class="p">.</span><span class="n">hybrid_search</span><span class="p">(</span>
        <span class="n">query</span><span class="p">,</span> <span class="n">top_k</span><span class="o">=</span><span class="mi">40</span><span class="p">,</span> <span class="n">filters</span><span class="o">=</span><span class="n">user_acl_filter</span><span class="p">()</span>  <span class="c1"># never retrieve what the user can't see
</span>    <span class="p">)</span>
    <span class="n">passages</span> <span class="o">=</span> <span class="n">reranker</span><span class="p">.</span><span class="n">rerank</span><span class="p">(</span><span class="n">question</span><span class="p">,</span> <span class="n">candidates</span><span class="p">)[:</span><span class="mi">6</span><span class="p">]</span>
    <span class="n">prompt</span> <span class="o">=</span> <span class="n">build_prompt</span><span class="p">(</span><span class="n">question</span><span class="p">,</span> <span class="n">passages</span><span class="p">)</span>
    <span class="n">response</span> <span class="o">=</span> <span class="n">llm</span><span class="p">.</span><span class="n">generate</span><span class="p">(</span><span class="n">prompt</span><span class="p">,</span> <span class="n">temperature</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">Answer</span><span class="p">(</span><span class="n">text</span><span class="o">=</span><span class="n">response</span><span class="p">.</span><span class="n">text</span><span class="p">,</span> <span class="n">citations</span><span class="o">=</span><span class="p">[</span><span class="n">p</span><span class="p">.</span><span class="n">source</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">passages</span><span class="p">])</span>
</code></pre></div></div>

<blockquote class="warning">
  <p>Apply access-control filters at <strong>retrieval</strong> time. Filtering the final
answer is too late — the model has already seen the restricted text.</p>
</blockquote>

<h2 id="evaluate-retrieval-and-generation-separately">Evaluate retrieval and generation separately</h2>

<p>When an answer is wrong, you need to know whether the right passage was never
retrieved or was retrieved and then ignored.</p>

<table>
  <thead>
    <tr>
      <th>Failure mode</th>
      <th>Symptom</th>
      <th>Typical fix</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Retrieval miss</td>
      <td>Correct passage not in the top <em>k</em></td>
      <td>Better chunking, hybrid search, rewrite</td>
    </tr>
    <tr>
      <td>Ranking miss</td>
      <td>Passage retrieved but ranked too low</td>
      <td>Add or tune a reranker</td>
    </tr>
    <tr>
      <td>Grounding failure</td>
      <td>Answer contradicts the retrieved passages</td>
      <td>Stricter prompt, citations, lower temp.</td>
    </tr>
    <tr>
      <td>Stale knowledge</td>
      <td>Answer reflects an outdated document</td>
      <td>Incremental re-indexing, freshness score</td>
    </tr>
  </tbody>
</table>

<blockquote class="tip">
  <p>Build a small “golden set” of 50–100 real questions with known source
passages before tuning anything. Without it, every change is a guess.</p>
</blockquote>

<h2 id="wrapping-up">Wrapping up</h2>

<p>Treat the index as a data product with owners, freshness SLAs and quality
checks, and treat the LLM as one component of a pipeline you can measure. Do
that, and RAG stops being a demo and starts being a system.</p>]]></content><author><name>Subhasis Mukherjee</name></author><category term="ai-engineering" /><category term="rag" /><category term="llm" /><category term="vector-search" /><category term="evaluation" /><summary type="html"><![CDATA[Retrieval-augmented generation is easy to demo and hard to run. A walkthrough of the offline indexing path, the online query path and the evaluation loop that ties them together.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://theintelligencestack.tech/assets/images/posts/production-rag-architecture/rag-architecture.jpg" /><media:content medium="image" url="https://theintelligencestack.tech/assets/images/posts/production-rag-architecture/rag-architecture.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Idempotent Data Pipelines: Making Re-runs Boring</title><link href="https://theintelligencestack.tech/blog/idempotent-pipelines/" rel="alternate" type="text/html" title="Idempotent Data Pipelines: Making Re-runs Boring" /><published>2026-09-28T00:00:00+00:00</published><updated>2026-09-28T00:00:00+00:00</updated><id>https://theintelligencestack.tech/blog/idempotent-pipelines</id><content type="html" xml:base="https://theintelligencestack.tech/blog/idempotent-pipelines/"><![CDATA[<p>Every pipeline will be re-run: a node dies, an upstream file arrives late, a
bug fix needs a backfill. An <strong>idempotent</strong> pipeline produces the same output
no matter how many times a run is repeated — which turns those incidents from
data-quality emergencies into non-events.</p>

<h2 id="what-idempotency-means-for-data">What idempotency means for data</h2>

<p>A task is idempotent when <code class="language-plaintext highlighter-rouge">run(x)</code> followed by <code class="language-plaintext highlighter-rouge">run(x)</code> leaves the target in
exactly the same state as a single <code class="language-plaintext highlighter-rouge">run(x)</code>. For data pipelines that implies:</p>

<ol>
  <li><strong>No duplicates</strong> when a run is retried.</li>
  <li><strong>No gaps</strong> when a run is skipped and later backfilled.</li>
  <li><strong>Deterministic output</strong> for a given input and run parameter.</li>
</ol>

<figure class="diagram">
<div class="diagram-frame">
<img src="/assets/images/posts/idempotent-pipelines/idempotent-pipeline.jpg" alt="An orchestrator triggers a pipeline of source, landing zone, deterministic transform and keyed write into a target table; a re-run loop feeds back with identical output" loading="lazy" decoding="async" />
<span class="diagram-hint" aria-hidden="true"><svg class="icon icon-zoom" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7" /></svg> Enlarge</span>
</div>
<figcaption><strong>Figure 2.</strong> Every stage is parameterised by the logical run date, so retries and backfills converge on the same result.</figcaption>
</figure>

<h2 id="pattern-1--replace-a-whole-partition">Pattern 1 — replace a whole partition</h2>

<p>When each run owns a well-defined slice of data (usually a date), overwrite
that slice atomically instead of appending to it:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span>
    <span class="n">daily_events</span>
    <span class="p">.</span><span class="n">write</span>
    <span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="s">"delta"</span><span class="p">)</span>
    <span class="p">.</span><span class="n">mode</span><span class="p">(</span><span class="s">"overwrite"</span><span class="p">)</span>
    <span class="p">.</span><span class="n">option</span><span class="p">(</span><span class="s">"replaceWhere"</span><span class="p">,</span> <span class="sa">f</span><span class="s">"event_date = '</span><span class="si">{</span><span class="n">run_date</span><span class="si">}</span><span class="s">'"</span><span class="p">)</span>
    <span class="p">.</span><span class="n">saveAsTable</span><span class="p">(</span><span class="s">"silver.events"</span><span class="p">)</span>
<span class="p">)</span>
</code></pre></div></div>

<p>Running this twice for the same <code class="language-plaintext highlighter-rouge">run_date</code> replaces the partition twice —
the end state is identical.</p>

<h2 id="pattern-2--merge-on-a-natural-key">Pattern 2 — merge on a natural key</h2>

<p>For entity tables that receive updates, upsert on a business key and only
apply a change if it is newer than what is already stored:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">MERGE</span> <span class="k">INTO</span> <span class="n">silver</span><span class="p">.</span><span class="n">customers</span> <span class="k">AS</span> <span class="n">t</span>
<span class="k">USING</span> <span class="n">staged_customers</span> <span class="k">AS</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">updated_at</span> <span class="o">&gt;</span> <span class="n">t</span><span class="p">.</span><span class="n">updated_at</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">THEN</span>
  <span class="k">INSERT</span> <span class="o">*</span><span class="p">;</span>
</code></pre></div></div>

<blockquote class="note">
  <p><code class="language-plaintext highlighter-rouge">UPDATE SET *</code> / <code class="language-plaintext highlighter-rouge">INSERT *</code> is Delta Lake syntax. Other engines need explicit
column lists, but the pattern is identical.</p>
</blockquote>

<h2 id="pattern-3--derive-time-from-the-run-not-the-clock">Pattern 3 — derive time from the run, not the clock</h2>

<p><code class="language-plaintext highlighter-rouge">current_date()</code> inside a transformation makes the output depend on <em>when</em> the
job ran. Pass the logical run date from the orchestrator instead, and use it
everywhere: in filters, partition values and output paths.</p>

<table>
  <thead>
    <tr>
      <th>Pattern</th>
      <th>Use when</th>
      <th>Watch out for</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Partition overwrite</td>
      <td>Each run owns a time slice</td>
      <td>Late data landing in older partitions</td>
    </tr>
    <tr>
      <td>Merge on key</td>
      <td>Entities are updated over time</td>
      <td>Missing or unstable business keys</td>
    </tr>
    <tr>
      <td>Run-date parameter</td>
      <td>Always</td>
      <td>Hidden <code class="language-plaintext highlighter-rouge">now()</code> calls in UDFs and views</td>
    </tr>
  </tbody>
</table>

<h2 id="wrapping-up">Wrapping up</h2>

<p>Idempotency is cheap to design in and expensive to retrofit. Pick a write
pattern per table, parameterise everything by the logical run date, and
retries become the most boring part of your on-call rotation.</p>]]></content><author><name>Subhasis Mukherjee</name></author><category term="data-engineering" /><category term="pipelines" /><category term="idempotency" /><category term="orchestration" /><category term="spark" /><summary type="html"><![CDATA[Retries and backfills are inevitable. Design pipelines so that running them twice gives exactly the same result as running them once.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://theintelligencestack.tech/assets/images/posts/idempotent-pipelines/idempotent-pipeline.jpg" /><media:content medium="image" url="https://theintelligencestack.tech/assets/images/posts/idempotent-pipelines/idempotent-pipeline.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Designing a Lakehouse with the Medallion Architecture</title><link href="https://theintelligencestack.tech/blog/medallion-architecture/" rel="alternate" type="text/html" title="Designing a Lakehouse with the Medallion Architecture" /><published>2026-09-18T00:00:00+00:00</published><updated>2026-09-18T00:00:00+00:00</updated><id>https://theintelligencestack.tech/blog/medallion-architecture</id><content type="html" xml:base="https://theintelligencestack.tech/blog/medallion-architecture/"><![CDATA[<p>A lakehouse without structure turns into a swamp surprisingly quickly. The
medallion architecture is a simple layering convention — bronze, silver, gold —
that gives every table a clear contract and every engineer a shared vocabulary.</p>

<h2 id="why-layer-a-lakehouse-at-all">Why layer a lakehouse at all?</h2>

<p>Most data problems are really <em>trust</em> problems. When a dashboard number looks
wrong, the team needs to answer three questions quickly: what did the source
actually send, what did we do to it, and which business rule produced the
final figure? Layering separates those concerns so each one can be inspected
on its own.</p>

<figure class="diagram">
<div class="diagram-frame">
<img src="/assets/images/posts/medallion-architecture/medallion-architecture.jpg" alt="Operational databases, SaaS APIs and event streams flow into bronze, then silver, then gold layers inside the lakehouse, which serve BI dashboards, ML feature stores and LLM applications" loading="lazy" decoding="async" />
<span class="diagram-hint" aria-hidden="true"><svg class="icon icon-zoom" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7" /></svg> Enlarge</span>
</div>
<figcaption><strong>Figure 1.</strong> Data moves through progressively refined layers before it is served to consumers.</figcaption>
</figure>

<h2 id="the-three-layers">The three layers</h2>

<h3 id="bronze--raw-and-append-only">Bronze — raw and append-only</h3>

<p>Bronze is a faithful, replayable copy of what the source sent, plus ingestion
metadata such as load time, source file and batch id. Resist the urge to clean
anything here: if a transformation bug is found next quarter, bronze is what
lets you rebuild everything downstream.</p>

<h3 id="silver--cleaned-and-conformed">Silver — cleaned and conformed</h3>

<p>Silver is where data becomes <em>usable</em>: types are enforced, duplicates removed,
late-arriving records merged and entities conformed to shared keys. Silver
tables are modelled around business entities (customers, orders, devices), not
around source systems.</p>

<h3 id="gold--business-ready">Gold — business-ready</h3>

<p>Gold tables answer specific questions: aggregates, dimensional marts, feature
tables and governed metrics. They are small, documented and owned by a team
that can explain every column.</p>

<table>
  <thead>
    <tr>
      <th>Layer</th>
      <th>Purpose</th>
      <th>Typical operations</th>
      <th>Main consumers</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Bronze</td>
      <td>Source fidelity, replay</td>
      <td>Append, schema-on-read, audit columns</td>
      <td>Data engineers</td>
    </tr>
    <tr>
      <td>Silver</td>
      <td>Clean, conformed entities</td>
      <td>Dedupe, type, merge/upsert, join keys</td>
      <td>Engineers, analysts, ML</td>
    </tr>
    <tr>
      <td>Gold</td>
      <td>Business answers</td>
      <td>Aggregate, model, apply business rules</td>
      <td>BI, applications, AI</td>
    </tr>
  </tbody>
</table>

<blockquote class="tip">
  <p>Keep bronze retention long and cheap (object storage is inexpensive), but
keep gold small and well-documented. Most cost and confusion comes from
sprawling, undocumented gold tables.</p>
</blockquote>

<h2 id="a-gold-table-end-to-end">A gold table, end to end</h2>

<p>Gold transformations should be boring, declarative SQL over silver:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">OR</span> <span class="k">REPLACE</span> <span class="k">TABLE</span> <span class="n">gold</span><span class="p">.</span><span class="n">daily_revenue</span> <span class="k">AS</span>
<span class="k">SELECT</span>
  <span class="n">order_date</span><span class="p">,</span>
  <span class="n">region</span><span class="p">,</span>
  <span class="k">SUM</span><span class="p">(</span><span class="n">net_amount</span><span class="p">)</span>              <span class="k">AS</span> <span class="n">revenue</span><span class="p">,</span>
  <span class="k">COUNT</span><span class="p">(</span><span class="k">DISTINCT</span> <span class="n">customer_id</span><span class="p">)</span>  <span class="k">AS</span> <span class="n">customers</span>
<span class="k">FROM</span> <span class="n">silver</span><span class="p">.</span><span class="n">orders</span>
<span class="k">WHERE</span> <span class="n">status</span> <span class="o">=</span> <span class="s1">'COMPLETED'</span>
<span class="k">GROUP</span> <span class="k">BY</span> <span class="n">order_date</span><span class="p">,</span> <span class="n">region</span><span class="p">;</span>
</code></pre></div></div>

<p>If a gold query needs to reach back into bronze, that is a signal that a
silver table is missing.</p>

<blockquote class="warning">
  <p>Never point BI tools at bronze. Raw data changes shape without warning, and
dashboards built on it become the first thing to break.</p>
</blockquote>

<h2 id="common-anti-patterns">Common anti-patterns</h2>

<ul>
  <li><strong>Source-shaped silver.</strong> Copying each source table into silver one-to-one
just moves the mess one layer down.</li>
  <li><strong>Business logic in bronze.</strong> Filtering or “fixing” records on ingestion
destroys your ability to replay.</li>
  <li><strong>Gold for everything.</strong> Not every query deserves a persisted table; views
over silver are often enough.</li>
  <li><strong>Non-idempotent loads.</strong> Re-running a job should never duplicate data — see
<a href="/blog/idempotent-pipelines/">Idempotent Data Pipelines</a>.</li>
</ul>

<h2 id="wrapping-up">Wrapping up</h2>

<p>The medallion architecture is less about the names of the layers and more
about the guarantees each layer gives. Write those guarantees down, enforce
them with tests, and the lakehouse stays explainable as it grows.</p>]]></content><author><name>Subhasis Mukherjee</name></author><category term="data-architecture" /><category term="lakehouse" /><category term="medallion" /><category term="data-modeling" /><summary type="html"><![CDATA[How to structure raw, refined and curated layers so a lakehouse stays trustworthy, debuggable and cheap to evolve.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://theintelligencestack.tech/assets/images/posts/medallion-architecture/medallion-architecture.jpg" /><media:content medium="image" url="https://theintelligencestack.tech/assets/images/posts/medallion-architecture/medallion-architecture.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>