{"id":111,"date":"2026-03-05T13:51:31","date_gmt":"2026-03-05T13:51:31","guid":{"rendered":"https:\/\/joeltan.me\/?p=111"},"modified":"2026-03-05T13:51:34","modified_gmt":"2026-03-05T13:51:34","slug":"microsoft-fabric-etl-escape-the-notebook-monolith","status":"publish","type":"post","link":"https:\/\/joeltan.me\/?p=111","title":{"rendered":"Microsoft Fabric ETL: Escape the Notebook Monolith"},"content":{"rendered":"\n<p>We\u2019ve all been there: it starts as a simple proof-of-concept. You open up a new Notebook in Microsoft Fabric, write a few cells to ingest data, add some transformations, and maybe append a couple of data quality checks. Fast forward three months, and that &#8220;quick script&#8221; has evolved into a monolithic monster sprawling across 100+ cells. <\/p>\n\n\n\n<p>Suddenly, your notebook contains utility functions, control flow logic, documentation, environment parameters, and inline unit tests &#8211; all tangled together in a single execution flow.<\/p>\n\n\n\n<p>When data engineering code lives entirely inside monolithic notebooks, it becomes notoriously difficult to maintain. You end up violating the DRY (Don&#8217;t Repeat Yourself) principle, unit testing becomes effectively impossible, and when a scheduled job fails on cell 87 out of 112, you&#8217;re faced with operational blindness while trying to dig through execution states.<\/p>\n\n\n\n<p>In this post, we&#8217;re going to fix that. We&#8217;ll look at how to leverage Microsoft Fabric&#8217;s built-in workspace resources to escape the notebook monolith by adopting an Object-Oriented (OO) Python approach alongside Fabric Pipelines. <\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"the-solution-separation-of-concerns\">The Solution: Separation of Concerns<\/h2>\n\n\n\n<p>To solve the monolith problem, we need to enforce a rigid separation of concerns. Instead of letting a notebook do everything from defining functions to managing execution order, we split the responsibilities:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Reusable Logic:<\/strong> We use Fabric&#8217;s built-in resources (Environment\/Workspace files) to store pure Python modules. This allows us to write repeatable ETL code in an Object-Oriented style.<\/li>\n\n\n\n<li><strong>Execution Context:<\/strong> The Notebook itself becomes incredibly thin, acting only as the entry point that retrieves parameters and instantiates our Python classes.<\/li>\n\n\n\n<li><strong>Control Flow:<\/strong> We use <strong>Fabric Pipelines<\/strong> to orchestrate the dependency chains and looping, moving that responsibility out of the notebook entirely.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"deep-dive-the-lakehouseloader-base-class-template-method--strategy-pattern\">Deep Dive: The <em>Lakehouseloader <\/em>Base Class (Template Method + Strategy Pattern)<\/h2>\n\n\n\n<p>To eliminate the repetition of writing <code>create table<\/code> and <code>upsert<\/code> queries across our Medallion Architecture (Bronze, Silver, Gold), we combine two complementary design patterns: the <strong>Template Method Pattern<\/strong> and the <strong>Strategy Pattern<\/strong>.<\/p>\n\n\n\n<p>The <strong>Template Method<\/strong> gives us the <em>skeleton<\/em>: a private <code>_table_load<\/code> method that enforces the strict, non-negotiable sequence of steps every table load must follow &#8211; drop (if recreating), create, truncate (if needed), then insert. That order never changes.<\/p>\n\n\n\n<p>The <strong>Strategy Pattern<\/strong> gives us the <em>flexibility<\/em>: instead of locking the <code>create<\/code> and <code>insert<\/code> logic into abstract methods on the class, <code>_table_load<\/code> accepts them as plain callables (<code>create_fn<\/code> and <code>insert_fn<\/code>). This means every individual <code>load_*<\/code> method can supply its own completely independent implementation &#8211; one might use the Spark DataFrame API, another a raw Spark SQL <code>MERGE<\/code> statement &#8211; without any of those choices bleeding into each other.<\/p>\n\n\n\n<p>We start by creating this base class (e.g., in a workspace file named <code>lakehouse_utils.py<\/code>):<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: python; title: ; notranslate\" title=\"\">\nfrom abc import ABC\nfrom typing import Callable\nfrom pyspark.sql import SparkSession, DataFrame\n\nclass LakehouseLoader(ABC):\n    def __init__(self, spark: SparkSession, schema: str = &quot;dbo&quot;):\n        self.spark = spark\n        self.schema = schema\n\n    def _table_load(\n        self,\n        df: DataFrame,\n        table_name: str,\n        create_fn: Callable,\n        insert_fn: Callable,\n        recreate: bool = False,\n        truncate: bool = False\n    ):\n        &quot;&quot;&quot;\n        The Template Method.\n        Accepts create_fn and insert_fn callables so each load method\n        can supply its own independent implementation.\n        &quot;&quot;&quot;\n        print(f&quot;Starting load process for {self.schema}.{table_name} (recreate={recreate}, truncate={truncate})...&quot;)\n\n        if recreate:\n            print(f&quot;Dropping table {self.schema}.{table_name} to recreate...&quot;)\n            self.spark.sql(f&quot;DROP TABLE IF EXISTS {self.schema}.{table_name}&quot;)\n\n        create_fn(df, table_name)\n\n        if truncate and not recreate:\n            print(f&quot;Truncating table {self.schema}.{table_name}...&quot;)\n            self.spark.sql(f&quot;TRUNCATE TABLE {self.schema}.{table_name}&quot;)\n\n        insert_fn(df, table_name)\n        print(&quot;Load complete.&quot;)\n        return True\n<\/pre><\/div>\n\n\n<h2 class=\"wp-block-heading\" id=\"deep-dive-the-medallion-subclasses\">Deep Dive: The Medallion Subclasses<\/h2>\n\n\n\n<p>With the callable-injection approach, each <code>load_*<\/code> method in a subclass is entirely self-contained. It defines its own <code>create<\/code> and <code>insert<\/code> as inner functions (closures), which naturally capture <code>self<\/code> from the enclosing method scope \u2014 giving them full access to <code>self.spark<\/code> and <code>self.database_name<\/code> without any extra wiring. These two functions are then passed directly into <code>_table_load<\/code> as the strategy for that specific table.<\/p>\n\n\n\n<p>This means there is no shared <code>create<\/code> or <code>insert<\/code> logic on the class itself to accidentally inherit or override incorrectly. A <strong>Bronze<\/strong> <code>load_dim_date<\/code> can use explicit Spark SQL DDL to define a strict schema, while a <strong>Bronze<\/strong> <code>load_dim_customer<\/code> can infer the schema from the DataFrame. Meanwhile, a <strong>Silver<\/strong> <code>load_dim_customer<\/code> can run a full Delta Lake <code>MERGE<\/code> statement \u2014 all living independently in the same codebase without any coupling between them.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: python; title: ; notranslate\" title=\"\">\nclass BronzeLoader(LakehouseLoader):\n\n    def load_dim_date(self, df: DataFrame, recreate: bool = False, truncate: bool = False):\n\n        def create(df: DataFrame, table_name: str):\n            # Use Spark SQL DDL for an explicit schema definition\n            self.spark.sql(f&quot;&quot;&quot;\n                CREATE TABLE IF NOT EXISTS {self.schema}.{table_name} (\n                    date_key INT,\n                    full_date DATE,\n                    year INT,\n                    month INT,\n                    day INT\n                ) USING DELTA\n            &quot;&quot;&quot;)\n\n        def insert(df: DataFrame, table_name: str):\n            # Bronze logic: straight append via DataFrame API\n            print(&quot;Appending records to Bronze dim_date...&quot;)\n            df.write.format(&quot;delta&quot;).mode(&quot;append&quot;).saveAsTable(f&quot;{self.schema}.{table_name}&quot;)\n\n        self._table_load(df, &quot;dim_date&quot;, create, insert, recreate=recreate, truncate=truncate)\n\n    def load_dim_customer(self, df: DataFrame, recreate: bool = False, truncate: bool = False):\n\n        def create(df: DataFrame, table_name: str):\n            # Infer schema from the DataFrame itself\n            df.limit(0).write.format(&quot;delta&quot;).mode(&quot;ignore&quot;).saveAsTable(f&quot;{self.schema}.{table_name}&quot;)\n\n        def insert(df: DataFrame, table_name: str):\n            # Bronze logic: straight append via DataFrame API\n            print(&quot;Appending records to Bronze dim_customer...&quot;)\n            df.write.format(&quot;delta&quot;).mode(&quot;append&quot;).saveAsTable(f&quot;{self.schema}.{table_name}&quot;)\n\n        self._table_load(df, &quot;dim_customer&quot;, create, insert, recreate=recreate, truncate=truncate)\n\n\nclass SilverLoader(LakehouseLoader):\n\n    def load_dim_customer(self, df: DataFrame, recreate: bool = False, truncate: bool = False):\n\n        def create(df: DataFrame, table_name: str):\n            # Infer schema from the DataFrame itself\n            df.limit(0).write.format(&quot;delta&quot;).mode(&quot;ignore&quot;).saveAsTable(f&quot;{self.schema}.{table_name}&quot;)\n\n        def insert(df: DataFrame, table_name: str):\n            # Silver logic: Deduplicate and MERGE via Spark SQL\n            print(&quot;Deduplicating and merging into Silver dim_customer...&quot;)\n            df.createOrReplaceTempView(&quot;silver_dim_customer_staging&quot;)\n            self.spark.sql(f&quot;&quot;&quot;\n                MERGE INTO {self.schema}.{table_name} AS target\n                USING silver_dim_customer_staging AS source\n                ON target.customer_key = source.customer_key\n                WHEN MATCHED THEN UPDATE SET *\n                WHEN NOT MATCHED THEN INSERT *\n            &quot;&quot;&quot;)\n\n        self._table_load(df, &quot;dim_customer&quot;, create, insert, recreate=recreate, truncate=truncate)\n\n<\/pre><\/div>\n\n\n<p><em>(You could easily follow this same pattern for a <code>GoldLoader<\/code> \u2014 its <code>load_*<\/code> methods would define <code>insert<\/code> inner functions that perform aggregation and write to summary tables, while <code>create<\/code> could provision the Gold schema via DDL.)<\/em><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"the-skinny-notebook-paradigm\">The &#8220;Skinny Notebook&#8221; Paradigm<\/h2>\n\n\n\n<p>Because all the complex, repeatable logic is abstracted away into our Python module, the actual Fabric Notebook becomes incredibly clean. A 100-cell monolith can often be reduced to just 2 or 3 cells.<\/p>\n\n\n\n<p>One of the conveniences that makes this even cleaner is Microsoft Fabric&#8217;s <strong>default Lakehouse<\/strong> feature. When you attach a default Lakehouse to a notebook, Spark resolves table references using the short <code>schema.table<\/code> two-part name \u2014 there is no need to qualify the full three-part <code>lakehouse.schema.table<\/code> path. Our <code>LakehouseLoader<\/code> is built around this: the <code>schema<\/code> argument (typically <code>\"dbo\"<\/code> for the Lakehouse default schema) is the only context needed to resolve any table, keeping the instantiation simple and portable across environments.<\/p>\n\n\n\n<p>The notebook&#8217;s <em>only<\/em> job is to retrieve parameters and call the loader:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: python; title: ; notranslate\" title=\"\">\n# Cell 1: Retrieve Parameters (passed in by Fabric Pipeline)\nsource_table = mssparkutils.env.get(&quot;SOURCE_TABLE&quot;)\n# The framework knows the target based on the specific load method\n\n# Cell 2: Execute Load via our OO Classes\n# Requires a default Lakehouse to be attached to this notebook.\n# Tables are resolved as schema.table (e.g. dbo.dim_customer).\nfrom lakehouse_utils import SilverLoader\n\ndf_bronze = spark.read.table(source_table)\nloader = SilverLoader(spark, schema=&quot;dbo&quot;)\nloader.load_dim_customer(df_bronze, recreate=False, truncate=False)\n\n<\/pre><\/div>\n\n\n<h2 class=\"wp-block-heading\" id=\"orchestration-with-fabric-pipelines\">Orchestration with Microsoft Fabric Pipelines<\/h2>\n\n\n\n<p>The final piece of the puzzle is moving the control flow out of the notebook. If you have Python <code>if\/else<\/code> statements determining which tables to load, or <code>for<\/code> loops iterating over dependencies, those should be ripped out and placed into <strong>Fabric Pipelines<\/strong>.<\/p>\n\n\n\n<p>A typical pipeline structure looks much cleaner:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Copy Data Activity<\/strong>: Ingests files into the Lakehouse.<\/li>\n\n\n\n<li><strong>Notebook Activity (Bronze)<\/strong>: Triggers the heavily structured Bronze load.<\/li>\n\n\n\n<li><strong>Notebook Activity (Silver)<\/strong>: Triggers only upon the success of the Bronze activity.<\/li>\n<\/ol>\n\n\n\n<p>The Pipeline handles retries, concurrency, and dependencies. The Notebook handles Spark execution. The <code>.py<\/code> modules handle the repeatable business logic. <\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"benefits--results\">Benefits &amp; Results<\/h2>\n\n\n\n<p>Making this architectural shift usually yields massive improvements:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Code Reduction:<\/strong> By removing duplicated <code>MERGE<\/code> and <code>INSERT<\/code> statement templates, it&#8217;s common to see a 60%+ reduction in total codebase lines.<\/li>\n\n\n\n<li><strong>Faster Code Reviews:<\/strong> Pull requests are much easier to read when business logic is safely partitioned into skinny notebooks, while the core mechanics are locked inside standard Python modules.<\/li>\n\n\n\n<li><strong>True Unit Testability:<\/strong> You can now run <code>pytest<\/code> against your <code>BronzeLoader<\/code> and <code>SilverLoader<\/code> classes directly, entirely independently of the Notebook UI.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"lessons-learned--next-steps\">Lessons Learned &amp; Next Steps<\/h2>\n\n\n\n<p>If you want to start moving away from monolithic notebooks, keep these things in mind:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Don&#8217;t Over-Abstract:<\/strong> It can be tempting to put <em>everything<\/em> into the base class. Keep your classes focused.<\/li>\n\n\n\n<li><strong>Call to Action:<\/strong> To start, find the single most repeated code block in your Fabric notebooks (usually your upsert statement). Extract it to a <code>.py<\/code> file attached to your workspace, import it into a single notebook, and prove out the pattern!<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"bonus-local-style-development-in-vs-code\">Bonus: Local-Style Development in VS Code<\/h2>\n\n\n\n<p>For developers comfortable with local IDEs, the <a href=\"https:\/\/learn.microsoft.com\/en-us\/fabric\/data-engineering\/setup-vs-code-extension\">Synapse VS Code extension for Microsoft Fabric<\/a> is an absolute game-changer for this OO architecture. <\/p>\n\n\n\n<p>Using the extension&#8217;s Workspace explorer, you can right-click your notebook folder and select <strong>&#8220;Open in Virtual Workspace&#8221;<\/strong>. This drops you into an authentic VS Code editing experience where you can seamlessly edit your notebook alongside your shared Python (<code>.py<\/code>) modules. This gives you local software engineering experiences &#8211; like proper syntax highlighting, native code editing, and integrated source control &#8211; right against your Microsoft Fabric artifacts (provided git is enabled in Fabric).<\/p>\n\n\n\n<p><strong>A Technical Gotcha:<\/strong>\nIf you start developing this way, be aware of a sneaky issue regarding Python module caching! When you import custom <code>.py<\/code> modules from Fabric&#8217;s built-in resources into your active notebook session, the PySpark session cache grabs that module code. If you update the <code>.py<\/code> file and re-run the notebook cell, the notebook will often use the <em>stale<\/em>, cached version of the module. You will need to account for this by forcefully reloading the modules during interactive development to ensure the notebook always fetches your newest changes!<\/p>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>We\u2019ve all been there: it starts as a simple proof-of-concept. You open up a new Notebook in Microsoft Fabric, write&#46;&#46;&#46;<\/p>\n","protected":false},"author":1,"featured_media":120,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_import_markdown_pro_load_document_selector":0,"_import_markdown_pro_submit_text_textarea":"","footnotes":""},"categories":[3],"tags":[],"class_list":["post-111","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-data-engineering-systems-that-stay-up"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v26.8 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Microsoft Fabric ETL: Escape the Notebook Monolith - Joel Tan Tech Blogs<\/title>\n<meta name=\"description\" content=\"Learn how to escape monolithic Microsoft Fabric notebooks using Object-Oriented Python and Fabric Pipelines for scalable ETL.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/joeltan.me\/?p=111\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Microsoft Fabric ETL: Escape the Notebook Monolith - Joel Tan Tech Blogs\" \/>\n<meta property=\"og:description\" content=\"Learn how to escape monolithic Microsoft Fabric notebooks using Object-Oriented Python and Fabric Pipelines for scalable ETL.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/joeltan.me\/?p=111\" \/>\n<meta property=\"og:site_name\" content=\"Joel Tan Tech Blogs\" \/>\n<meta property=\"article:published_time\" content=\"2026-03-05T13:51:31+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-03-05T13:51:34+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/joeltan.me\/wp-content\/uploads\/2026\/03\/Coding-Image-1024x687.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1024\" \/>\n\t<meta property=\"og:image:height\" content=\"687\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"Joel Tan\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Joel Tan\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"6 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/joeltan.me\/?p=111#article\",\"isPartOf\":{\"@id\":\"https:\/\/joeltan.me\/?p=111\"},\"author\":{\"name\":\"Joel Tan\",\"@id\":\"https:\/\/joeltan.me\/#\/schema\/person\/db13342201787db723bfdeadcd792743\"},\"headline\":\"Microsoft Fabric ETL: Escape the Notebook Monolith\",\"datePublished\":\"2026-03-05T13:51:31+00:00\",\"dateModified\":\"2026-03-05T13:51:34+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/joeltan.me\/?p=111\"},\"wordCount\":1156,\"commentCount\":0,\"image\":{\"@id\":\"https:\/\/joeltan.me\/?p=111#primaryimage\"},\"thumbnailUrl\":\"https:\/\/joeltan.me\/wp-content\/uploads\/2026\/03\/Coding-Image.png\",\"articleSection\":[\"Data Engineering: Systems That Stay Up\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/joeltan.me\/?p=111#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/joeltan.me\/?p=111\",\"url\":\"https:\/\/joeltan.me\/?p=111\",\"name\":\"Microsoft Fabric ETL: Escape the Notebook Monolith - Joel Tan Tech Blogs\",\"isPartOf\":{\"@id\":\"https:\/\/joeltan.me\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/joeltan.me\/?p=111#primaryimage\"},\"image\":{\"@id\":\"https:\/\/joeltan.me\/?p=111#primaryimage\"},\"thumbnailUrl\":\"https:\/\/joeltan.me\/wp-content\/uploads\/2026\/03\/Coding-Image.png\",\"datePublished\":\"2026-03-05T13:51:31+00:00\",\"dateModified\":\"2026-03-05T13:51:34+00:00\",\"author\":{\"@id\":\"https:\/\/joeltan.me\/#\/schema\/person\/db13342201787db723bfdeadcd792743\"},\"description\":\"Learn how to escape monolithic Microsoft Fabric notebooks using Object-Oriented Python and Fabric Pipelines for scalable ETL.\",\"breadcrumb\":{\"@id\":\"https:\/\/joeltan.me\/?p=111#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/joeltan.me\/?p=111\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/joeltan.me\/?p=111#primaryimage\",\"url\":\"https:\/\/joeltan.me\/wp-content\/uploads\/2026\/03\/Coding-Image.png\",\"contentUrl\":\"https:\/\/joeltan.me\/wp-content\/uploads\/2026\/03\/Coding-Image.png\",\"width\":1264,\"height\":848,\"caption\":\"Microsoft Fabric - Objected Orientated Programming\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/joeltan.me\/?p=111#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/joeltan.me\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Microsoft Fabric ETL: Escape the Notebook Monolith\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/joeltan.me\/#website\",\"url\":\"https:\/\/joeltan.me\/\",\"name\":\"Joel Tan Tech Blogs\",\"description\":\"Building systems that survive real life\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/joeltan.me\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Person\",\"@id\":\"https:\/\/joeltan.me\/#\/schema\/person\/db13342201787db723bfdeadcd792743\",\"name\":\"Joel Tan\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/joeltan.me\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/d9b5d1ab218cb2478280027d371ea60543f6551132d31a8cbd45a5a5b3fbadc9?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/d9b5d1ab218cb2478280027d371ea60543f6551132d31a8cbd45a5a5b3fbadc9?s=96&d=mm&r=g\",\"caption\":\"Joel Tan\"},\"sameAs\":[\"http:\/\/192.168.1.146\"],\"url\":\"https:\/\/joeltan.me\/?author=1\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Microsoft Fabric ETL: Escape the Notebook Monolith - Joel Tan Tech Blogs","description":"Learn how to escape monolithic Microsoft Fabric notebooks using Object-Oriented Python and Fabric Pipelines for scalable ETL.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/joeltan.me\/?p=111","og_locale":"en_US","og_type":"article","og_title":"Microsoft Fabric ETL: Escape the Notebook Monolith - Joel Tan Tech Blogs","og_description":"Learn how to escape monolithic Microsoft Fabric notebooks using Object-Oriented Python and Fabric Pipelines for scalable ETL.","og_url":"https:\/\/joeltan.me\/?p=111","og_site_name":"Joel Tan Tech Blogs","article_published_time":"2026-03-05T13:51:31+00:00","article_modified_time":"2026-03-05T13:51:34+00:00","og_image":[{"width":1024,"height":687,"url":"https:\/\/joeltan.me\/wp-content\/uploads\/2026\/03\/Coding-Image-1024x687.png","type":"image\/png"}],"author":"Joel Tan","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Joel Tan","Est. reading time":"6 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/joeltan.me\/?p=111#article","isPartOf":{"@id":"https:\/\/joeltan.me\/?p=111"},"author":{"name":"Joel Tan","@id":"https:\/\/joeltan.me\/#\/schema\/person\/db13342201787db723bfdeadcd792743"},"headline":"Microsoft Fabric ETL: Escape the Notebook Monolith","datePublished":"2026-03-05T13:51:31+00:00","dateModified":"2026-03-05T13:51:34+00:00","mainEntityOfPage":{"@id":"https:\/\/joeltan.me\/?p=111"},"wordCount":1156,"commentCount":0,"image":{"@id":"https:\/\/joeltan.me\/?p=111#primaryimage"},"thumbnailUrl":"https:\/\/joeltan.me\/wp-content\/uploads\/2026\/03\/Coding-Image.png","articleSection":["Data Engineering: Systems That Stay Up"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/joeltan.me\/?p=111#respond"]}]},{"@type":"WebPage","@id":"https:\/\/joeltan.me\/?p=111","url":"https:\/\/joeltan.me\/?p=111","name":"Microsoft Fabric ETL: Escape the Notebook Monolith - Joel Tan Tech Blogs","isPartOf":{"@id":"https:\/\/joeltan.me\/#website"},"primaryImageOfPage":{"@id":"https:\/\/joeltan.me\/?p=111#primaryimage"},"image":{"@id":"https:\/\/joeltan.me\/?p=111#primaryimage"},"thumbnailUrl":"https:\/\/joeltan.me\/wp-content\/uploads\/2026\/03\/Coding-Image.png","datePublished":"2026-03-05T13:51:31+00:00","dateModified":"2026-03-05T13:51:34+00:00","author":{"@id":"https:\/\/joeltan.me\/#\/schema\/person\/db13342201787db723bfdeadcd792743"},"description":"Learn how to escape monolithic Microsoft Fabric notebooks using Object-Oriented Python and Fabric Pipelines for scalable ETL.","breadcrumb":{"@id":"https:\/\/joeltan.me\/?p=111#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/joeltan.me\/?p=111"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/joeltan.me\/?p=111#primaryimage","url":"https:\/\/joeltan.me\/wp-content\/uploads\/2026\/03\/Coding-Image.png","contentUrl":"https:\/\/joeltan.me\/wp-content\/uploads\/2026\/03\/Coding-Image.png","width":1264,"height":848,"caption":"Microsoft Fabric - Objected Orientated Programming"},{"@type":"BreadcrumbList","@id":"https:\/\/joeltan.me\/?p=111#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/joeltan.me\/"},{"@type":"ListItem","position":2,"name":"Microsoft Fabric ETL: Escape the Notebook Monolith"}]},{"@type":"WebSite","@id":"https:\/\/joeltan.me\/#website","url":"https:\/\/joeltan.me\/","name":"Joel Tan Tech Blogs","description":"Building systems that survive real life","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/joeltan.me\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Person","@id":"https:\/\/joeltan.me\/#\/schema\/person\/db13342201787db723bfdeadcd792743","name":"Joel Tan","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/joeltan.me\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/d9b5d1ab218cb2478280027d371ea60543f6551132d31a8cbd45a5a5b3fbadc9?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/d9b5d1ab218cb2478280027d371ea60543f6551132d31a8cbd45a5a5b3fbadc9?s=96&d=mm&r=g","caption":"Joel Tan"},"sameAs":["http:\/\/192.168.1.146"],"url":"https:\/\/joeltan.me\/?author=1"}]}},"_links":{"self":[{"href":"https:\/\/joeltan.me\/index.php?rest_route=\/wp\/v2\/posts\/111","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/joeltan.me\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/joeltan.me\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/joeltan.me\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/joeltan.me\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=111"}],"version-history":[{"count":7,"href":"https:\/\/joeltan.me\/index.php?rest_route=\/wp\/v2\/posts\/111\/revisions"}],"predecessor-version":[{"id":121,"href":"https:\/\/joeltan.me\/index.php?rest_route=\/wp\/v2\/posts\/111\/revisions\/121"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/joeltan.me\/index.php?rest_route=\/wp\/v2\/media\/120"}],"wp:attachment":[{"href":"https:\/\/joeltan.me\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=111"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/joeltan.me\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=111"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/joeltan.me\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=111"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}