Skip to main content

Command Palette

Search for a command to run...

A Repo That Mirrors Your Architecture - Source Control & Structure in Fabric

Your repo structure is your architecture, made versionable

Updated
β€’21 min readβ€’View as Markdown
A Repo That Mirrors Your Architecture - Source Control & Structure in Fabric
P
With over 15 years of experience in Data and BI, I specialize in Microsoft Fabric, helping organizations build scalable data platforms through automation, engineering discipline, and modern DevOps practices. As Head of Product Engineering at Tabular Editor and a Microsoft Data Platform MVP, I focus on Fabric automation, CI/CD, semantic modeling, and the developer tooling landscape around Fabric and Power BI. I share my insights and learnings here on Peer Insights, where I write about real-world implementations, patterns, and experiments across Fabric automation, Git integration, deployment strategies, and everything in between. Alongside the blog, I contribute to the community through open source projects such as FabricOps and fabricstack.dev, and by speaking at conferences and community events across Europe and the US.

This is Episode 3 of the From Chaos to Confidence series. In Episode 2 we designed the Fabric solition: layers, how granular to make them, and the fact that whatever you land on gets replicated across dev, test and prod. That architecture currently exists in the best possible place for an architecture to fail... in a diagram.

So let's make it real. Not automated yet, that's Episode 4, but versionable. The shift that matters is smaller than it sounds: your architecture stops being a drawing and becomes files. Folders you can open, text you can diff, names you can search. That's what turns it from a picture of intent into something that actually exists. Git is what you get once that's true. Branch it, review it, roll it back, hand it to a new colleague without a two-hour whiteboard session.

The thesis of this episode is a single sentence: your repo structure is your architecture, made versionable. If the two don't match, one of them is lying.

Why source control, before anything else

I've watched a lot of teams put off source control. Nobody ever decides against it. It just never becomes urgent. The platform works, people are shipping and nothing is on fire.

Then something is on fire. Someone changed a semantic model on Thursday afternoon. By Friday morning the numbers are wrong, and nobody can say what changed. With a Git repo, that's a five-minute diff and a revert. Without one, it's a support ticket and a memory contest.

Here's what you get the moment your platform lives in Git:

  • History and rollback: Every change becomes a commit with an author, a timestamp and a diff you can read. You can see what changed, who changed it and put it back.

  • Isolation: Branching lets you build something half-finished without anyone else seeing your half-finished thing. This is the difference between "development" and "everyone works in prod and hopes".

  • Collaboration without collisions: Two people, two branches, one pull request each. Nobody overwrites anybody. Nobody has to shout across the office to ask if it's safe to save.

  • Review before reality: A PR is the cheapest quality gate you will ever get.

  • Two-way sync: Fabric's Git integration isn't an export. You commit from the workspace and you update the workspace from Git. So the repo and the platform stay honest with each other.

"But Fabric already has version history"

Yes it does and this is the objection I hear often, so it deserves a fair answer.

Fabric notebooks have built-in version history: system checkpoints created automatically every five minutes as you edit, manual checkpoints for your own milestones, a diff view, and restore. Semantic models have it too. Neither is something you switch on. Fabric just configures it for you, which is part of why some teams may assume they're covered.

But it's an undo button, not source control, and the difference shows up in four places:

  • It's per item, not per solution: A notebook's history knows about that notebook. It has no idea the same change also needed a new field in a report or change to a query in a pipeline. A commit does, because a commit spans the whole solution.

  • It's shallow: Semantic models keep five versions, and restoring anything older than 14 days isn't supported. Notebook system checkpoints expire after a year. Git keeps every version for as long as the repo exists.

  • Most item types don't have it at all: Pipelines, lakehouses, reports, dataflows, environments, variable libraries... nothing. That list might improve, so check its current state rather than trusting me. But your platform will always be mostly items that aren't covered.

  • There's no branch, no PR, no review: Version history lets you go back. It doesn't let two people work in parallel, and it doesn't let anyone look at a change before it lands in the workspace.

Microsoft's own documentation lands in the same place. For full source control and support of more versions, use Git integration. And you can happily run both on the same semantic model. Item version history saves you from the last change you regret. Git is what gives your platform a memory.

Or, put less politely: without source control, your platform has no memory, no safety net, and no way to collaborate at scale.

πŸ’‘
Tip: If you take one thing from this episode and ignore the rest, connect your dev workspaces to Git today. Everything else in this series builds on that one move: validation, promotion, automation. And it costs you half an hour.

How Fabric's Git integration actually works

The mechanic that shapes everything else is this: Git integration is configured per workspace, and a workspace connects to exactly one folder in one branch of one repository.

Four things to know before you design around it:

Providers. Azure DevOps, GitHub and GitHub Enterprise (cloud only in all three cases). Azure DevOps can live in a different tenant than your Fabric tenant, which comes up more often than you'd think in consultancies and post-merger estates.

Authentication. With Azure DevOps you can connect as yourself (the automatic credential, using your Entra identity) or through a configured connection that supports OAuth 2.0 or a service principal. And the service principal option is the one that matters, because it's what lets automation connect a workspace with no human signed in. GitHub's source control connector uses a personal access token. That asymmetry is worth knowing up front. If unattended automation is where you're heading, Azure DevOps currently gives you the cleaner identity story. (It's also the reason FabricOps supports both providers but treats the git credential as a connection rather than a login.)

What travels. Item definitions, not data. A lakehouse in Git is lakehouse.metadata.json and shortcuts.metadata.json. It's the shape of the thing, not a single row inside it. Semantic models arrive as TMDL, reports as PBIR, notebooks as source files. Coverage is broad these days. Very nearly every item type is supported, though some are still marked preview, including the two you'd least expect: reports and semantic models. And if a workspace does hold something unsupported, you can still connect it. Those items are simply invisible to Git. They aren't committed, synced or deleted, and they never appear in the source control pane at all. They're flagged as Unsupported in the workspace's Git status column, which is the only place you'll see them. Either way, check the current list rather than trusting this blogpost.

Folders are preserved. If you organise items into folders inside a workspace, those folders appear in the repo, and vice versa. Nested up to ten levels, empty folders aren't committed. And a small trap, connecting a workspace that already has folders to a branch that doesn't yet have them shows up as uncommitted changes. Commit first, update second.

There's more in the box these days than when I first started doing this: selective branching, related branches, comparing changes before you sync, branched workspaces, variable libraries for environment-specific values, and tooling in VS Code. I'm deliberately not going deep on any of it here. The branching workflow will be covered in a later episode. For now, all you need is the shape: workspace β†’ repo β†’ branch β†’ folder.

One caveat worth saying out loud before we build on this: Git makes your platform versionable, not reproducible. A branch tells you what the items should look like. It doesn't create the workspaces, wire the connections or promise that dev and prod are the same shape. That's a different problem... and again something I will cover in a later episode.

The repo that mirrors your architecture

Here's the part I actually care about. You have seven layers (or three, or four. That is Episode 2's spectrum). A Git connected workspace connects to one folder. So:

One layer = one workspace = one repo folder.

That's it. That's the whole design. It sounds almost too simple to write down and it's the thing that makes everything downstream tractable.

But it has a consequence I glossed over in the first version of this post, and a reader was kind enough to point it out. If a workspace binds to exactly one folder, then a repo with seven flat folders only works when you have seven workspaces. At the Minimum tier, Engineering is one workspace holding ingest, prepare and orchestrate. It can bind to ingest/ or to prepare/, not both. Start small on a flat layout and the day you scale up you're peeling folders apart, which is a migration, not a promotion. That's the opposite of what Episode 2 promised.

The fix is to shape the tree like the floor. Episode 2's hard rule is that three layers is the minimum, never fewer. So the repo gets three top-level folders, and every responsibility below them is a folder you bind a workspace to when you're ready. This is the top of the FabricOps repo. So a real one and not just something idealised:

FabricOps/
β”œβ”€β”€ .azure-pipelines/     # Azure DevOps pipeline definitions
β”œβ”€β”€ .github/              # GitHub Actions workflows
β”œβ”€β”€ automation/           # Scripts, environment definitions, IaC
β”œβ”€β”€ documentation/        # Docs and design notes
└── solution/             # The platform, shaped like the Minimum tier
				   
					 
					  
    β”œβ”€β”€ store/
    β”œβ”€β”€ engineering/
    β”‚   β”œβ”€β”€ ingest/
    β”‚   β”œβ”€β”€ prepare/
    β”‚   β”œβ”€β”€ orchestrate/
    β”‚   └── core/
    └── analytics/
        β”œβ”€β”€ model/
        └── present/

Read it top down and it's the Minimum tier: Store, Engineering, Analytics. Read the leaves and it's the Enterprise tier: all seven responsibilities, same names as Episode 2. The tree doesn't change between them. What changes is which folders have a workspace bound to them:

  • Minimum (3): Store, Engineering and Analytics each bind to a top-level folder. The responsibility folders show up as ordinary folders inside the workspace, because Fabric mirrors folders both ways.

  • Basic (4): Engineering keeps engineering/. Model and Present each bind to their own leaf under analytics/.

  • Enterprise (7): Every leaf gets a workspace. engineering/ and analytics/ become nothing more than folders in the repo that no workspace binds to.

Someone who has read the architecture diagram can still navigate this repo and someone who has read the repo can still draw the diagram. They just draw it at whatever tier you're on.

core/ sits under engineering/ rather than at the top, and that's deliberate. Below the Enterprise tier there is no Core workspace, so environments, functions and the odd SQL database have to live somewhere, and Engineering is where they belong. Putting the folder there means a Core workspace can bind to it later without anything moving.

Go one level deeper and you get the items:

solution/
β”œβ”€β”€ store/
β”‚   β”œβ”€β”€ Landing.Lakehouse/
β”‚   β”œβ”€β”€ Base.Lakehouse/
β”‚   └── Curated.Lakehouse/
β”‚       β”œβ”€β”€ .platform
β”‚       β”œβ”€β”€ lakehouse.metadata.json
β”‚       └── shortcuts.metadata.json
β”œβ”€β”€ engineering/
β”‚   β”œβ”€β”€ ingest/
β”‚   β”‚   β”œβ”€β”€ FabricOps_API.Notebook/
β”‚   β”‚   └── FabricOps_SQL.DataPipeline/
β”‚   β”œβ”€β”€ prepare/
β”‚   β”‚   β”œβ”€β”€ FabricOps_functions.Notebook/
β”‚   β”‚   β”œβ”€β”€ Enrichment/
β”‚   β”‚   β”‚   └── FabricOps_enrich.Notebook/
β”‚   β”‚   └── Utils/
β”‚   β”‚       └── ManageWorkspaceIcons.Notebook/
β”‚   β”œβ”€β”€ orchestrate/
β”‚   β”‚   └── Load FabricOps.DataPipeline/
β”‚   └── core/
β”‚       └── Metadata.SQLDatabase/
└── analytics/
    β”œβ”€β”€ model/
    β”‚   └── FabricOps.SemanticModel/
    β”‚       β”œβ”€β”€ definition.pbism
    β”‚       └── definition/          # TMDL
    └── present/
        └── FabricOps.Report/
            β”œβ”€β”€ definition.pbir
            β”œβ”€β”€ definition/          # PBIR
            └── StaticResources/

A few things worth pointing out because they're the details people trip over:

  • Every item is a folder named <DisplayName>.<ItemType>. That's Fabric's convention, not mine, and it's what makes a diff readable.

  • Enrichment/ and Utils/ under prepare/ aren't responsibilities - they're workspace folders, mirrored into the repo. Responsibilities are the coarse structure. Folders inside one are yours to organise however your team thinks.

  • Every item carries a .platform file, which holds its type, display name, description and a logicalId that ties the item in the workspace to its representation in the branch:

    {
      "metadata": { 
        "type": "Lakehouse", 
        "displayName": "Curated" 
      },
      "config": { 
        "version": "2.0", 
        "logicalId": "edcd54d9-…" 
      }
    }
    

    Rename an item by changing displayName in that file and not by renaming the folder. And never touch the logicalId. It's the thread connecting workspace and repo and cutting it produces exactly the kind of afternoon you were trying to avoid.

  • The lakehouse folders contain no data. Only metadata and shortcut definitions. Say this out loud to your stakeholders early. "It's all in Git" and "We can restore the data from Git" are very different statements.

Scaling a tier is a rebind, not a move

This is what the nested tree buys you. Going from Minimum to Enterprise is not a restructuring of the repo. It's creating new workspaces and pointing each at a leaf that already exists. The Engineering workspace bound to engineering/ is retired, and Ingest, Prepare, Orchestrate and Core each bind to their leaf. Going the other way works the same, so you can scale down if a domain shrinks. Nothing gets moved in Git. The logicalId in every .platform file stays put, so Git still knows every item by the same name it always did.

I want to be honest about what does change, because "retarget the workspace" makes it sound free and it isn't quite:

  • **References have to be dynamic, or they break. * ** Items in the new workspaces are new items with new IDs. A notebook that resolves its lakehouse by name won't notice. A pipeline or a report deployed through the tooling in Episode 4 gets its references resolved on the way in and won't notice either. A reference hardcoded to an ID will, and that's most of what people have before they automate. Design for automation and the split is just a run.

  • Shared code gets duplicated. %run only reaches notebooks in the same workspace. A helper used by both ingest and prepare has to exist in both once they're separate.

  • Workspace settings don't travel. Permissions, capacity, Spark settings and the Git connection itself are per workspace and have to be applied to the new ones. With the recipe from Episode 4 that's a run. By hand it's an afternoon.

So it's cheap, reversible and genuinely not a migration of content. It's still a change you plan.

One thing the design depends on: items have to actually live in their responsibility folder. Fabric puts new items at the root of a workspace unless you say otherwise, and a notebook dropped at the root of Engineering belongs to no leaf, so the day you scale up it's a manual sort. The platform won't enforce that for you. Your repo can. A check in CI that fails the build when an item under engineering/ isn't inside a declared responsibility folder is about ten lines, and it's the difference between a convention and a guarantee.

Credit where it's due: the nested layout came from Sebastian in the comments on the first version of this post, which had the flat tree and exactly the problem he describes.

Mono-repo or multi-repo?

Start with a mono-repo. One repository, all layers, just as above.

The first reason is coordination. A change in Prepare that feeds a change in Model that surfaces in a Present report is one change to your platform, even though it touches three layers. In a mono-repo that's one branch and one PR, reviewed as a unit. Split across three repos, it's three PRs, in an order somebody has to remember, with no atomic way to roll back the middle one.

The second is documentation. Most teams keep their docs in Git too and docs belong next to the thing they describe: the architecture, the naming conventions, the runbook for whatever breaks at 2am. In a mono-repo that's one documentation folder and one place to look. Split the platform across five repos and you get five partial documentation sets, or a sixth repo that holds all the docs and starts drifting from every one of them the day it's created. Neither is a good outcome and the second is the one teams actually end up with.

You'd split when the people split, not when the folders get big:

  • Separate teams with genuinely separate release cadences (the Enterprise+ domain model from Episode 2 is the usual trigger)

  • Hard access boundaries where one team must not read another's source;

  • A repo that has grown so large that clone and CI times hurt. It's real, but rarer than people expect for item definitions, which are mostly small text files.

Even then, my preference is to split by domain (all of Finance's layers together) rather than by layer (everybody's Model workspaces in one repo). Splitting by layer maximises cross-repo chatter which is the thing you were trying to avoid.

Tip: this is a decision you can defer cheaply, and reverse expensively. Mono-repo until it hurts - and be specific about what "hurts" means when you claim it does.

The mapping, made concrete

Put it all together and every row of this table is a fact you can check, not a convention you have to remember. Same folders at every tier. Only the workspace column changes:

Repo folder Minimum (3) Basic (4) Enterprise (7)
solution/store Store Store Store
solution/engineering Engineering Engineering folder only
solution/engineering/ingest ↑ ↑ Ingest
solution/engineering/prepare ↑ ↑ Prepare
solution/engineering/orchestrate ↑ ↑ Orchestrate
solution/engineering/core ↑ ↑ Core
solution/analytics Analytics folder only folder only
solution/analytics/model ↑ Model Model
solution/analytics/present ↑ Present Present

Where the arrow points up, that folder syncs as part of the workspace above it. Workspace names follow the same template whatever the tier: Confidence - Store [dev], Confidence - Engineering [dev] and so on.

And now the environment axis from Episode 2 quietly pays for itself. Note what isn't in the repo: there's no store-dev sitting beside a store-test. One folder per responsibility, whatever the tier. Nine folders, and nine, twelve or twenty-one workspaces depending on how far you've split. What differs between environments isn't the folder, it's how the content arrives, and that is easier to draw than to describe.

That topology is a choice, not a rule, so let me explain my approach. I keep dev Git-connected because it mainly buys you two things nothing else does. Every merge to main gets proven against Fabric's own Git engine before it goes anywhere near a release, and the source control pane becomes a drift report anyone can read. No pipeline history to dig through to find out whether the workspace matches the branch.

I keep test and prod out of Git for two reasons. The first is that promotion should be a decision someone makes, not a side effect of a merge landing. The second is more mechanical and it's the one that settles the argument: item definitions carry environment-specific references. A semantic model points at a particular SQL endpoint, a notebook at a particular lakehouse, a pipeline at a particular connection. Sync prod straight from a branch and you've handed prod the dev GUIDs. Something has to rewrite those references in flight, which makes promotion a deployment concern rather than a sync concern. That's the job of fabric-cicd, Microsoft's own Python library for exactly this problem. It mutates the item definitions on the way into the target workspace, resolving references at deploy time so test gets test's GUIDs and prod gets prod's. Variable Libraries cover some of this ground and Microsoft recommends them where they fit. They're scoped to a single workspace though, which is awkward when your architecture is deliberately seven of them. Also semantic models aren't among the item types that can consume one. I'll walk through both in a later episode.

And Fabric happily supports the alternatives. You can give every stage its own branch and sync each workspace from it, which makes Git the literal source of truth per environment. You can go the other way and connect nothing but feature workspaces, deploying to dev, test and prod through the same pipeline. This is kind of what Microsoft's own internal data engineering team does, and it has the tidy property that there's only one path into a workspace. Both are defensible. Mine is the middle and I'll make the full argument in a later episode, where the branching workflow belongs.

The mechanics of all of it: branching workflow and promotion are coming. The structure that makes them possible is what we're laying down here.

Conventions worth agreeing on before the first commit

None of these are clever. All of them are cheaper now than later.

Naming. Item display names become folder names, so the naming discipline you agreed in Episode 2 shows up in your directory listing whether you meant it to or not. Keep environment out of item names. The workspace already carries it. Curated, not Curated_dev_v2. Better still, don't rely on discipline for the workspace names at all. In FabricOps the name is a template, Confidence - {layer} [{environment}], substituted at provision time. Nobody can typo a workspace name into existence, because nobody types workspace names.

What lives outside solution/. Everything that operates the platform rather than being it: .github/ and .azure-pipelines/ for workflows, automation/ for scripts and environment definitions, and documentation/ for the things a new colleague needs: the architecture, the naming conventions and the reference for whatever configuration your automation reads. This separation matters more than it looks: solution/ is the only part Fabric syncs, so anything else you keep in the repo is yours to organise freely. It travels with the platform it deploys, reviewed in the same pull request. Documentation that lives next to the solution it documents actually has a chance of staying true.

What not to commit. Secrets, obviously, so NO PATs, no client secrets, no connection strings with a password in them and no credentials.json that "was only for testing". Also: local environment folders, editor junk (.DS_Store is a personal grudge) and anything a run produces rather than a human writes. Put the rules in .gitignore on day one and add a scan to CI when you get the chance. The interesting thing about committed secrets is that removing the file doesn't remove the history.

πŸ“£
Agree the commit-size reality too. Azure DevOps: Git files should be ≀100 MB. Pushes are limited to 5 GB. GitHub: Files >50 MiB trigger a warning and files >100 MiB are blocked and require Git LFS. Item definitions are small, but a report with heavy static resources can surprise you.

But wait! How do I get all this created and wired up?

This is exactly the right question and exactly where this episode stops.

Look at what we've just specified: seven workspaces per environment at the Enterprise tier, each with the right capacity, the right permissions and the right items inside. Three environments, so twenty-one workspaces. The seven in dev each need a Git connection on top of that, pointing at its own folder in the right branch of the right repo, with the right git credential. And then you do it all again, identically, when someone adds a layer or a new environment or a fourth domain.

You can absolutely do that in the portal. I've done it. It takes an afternoon and it works. But it is quietly turning wrong because the one thing hand-wiring cannot give you is the guarantee that dev, test and prod are actually the same shape.

So in Episode 4 we stop clicking. We'll look at the options for setting this up as infrastructure-as-code covering the Fabric CLI, REST APIs, fabric-cicd and more. And where each of the tools and services fits in. Then we will go deep on how FabricOps does it all: a declarative definition of your platform that provisions the workspaces, creates the items, wires the git integration and can tear the whole thing down again. Including the bit nobody shows you: what happens when you run it twice.

Stay tuned!

S
ssteinau1mo ago

Hi Peer,

this post, the previous posts, and the future ones are great. You put a lot of thought into this and I agree to most of it (as if that were the benchmark, right?).

One problem I have here is the layout of the solution folder in Git, because it does not work well with what you promised in the previous post: Think, start small (and scale up later).

A layer in your terminology is a combination of Fabric workspace and a concept of what goes into that workspace - let's call that a "responsibility".

So there is a workspace "Store" and it has the "store" responsibility of holding all storage related fabric items like lakehouses etc. We might as well call the workspace "Workspace1", as responsibility is now a separate thing associated to the workspace. Where am I going with this?

In the minimal reference architecture, we have Workspaces "Engineering" and "Analysis" that hold multiple responsibilities:

Engineering holds responsibilities "ingest" and "prepare", and Analysis holds "model" and "present".

We can see that because in the advanced architectures, the Engineering and Analysis layers get split into multiple layers that have the same name as the responsibilities.

The problem with that is: Fabric only allows git sync with one folder, not many. So with the folder layout in Git as in this post, the Engineering workspace can either link to "prepare" or "ingest, not both. And that defeats the purpose of the Engineering layer. In consequence, any other than the Enterprise architectures fall flat, in the strict sense.

The solution appears obvious: With Engineering being the combination of "prepare" and "ingest", let's combine the Git folders as well, so only an 'engineering' folder exists in Git. This is fine, as long as you never scale up. Because if you do, the burden of peeling out and separating "ingest" from "preparation" makes a scale up a migration. It becomes costly.

I think the better solution to this problem is to represent combined layers like Engineering and Analysis in Git by introducing nested folders.

So the folder structure becomes:

FabricOps/

β”œβ”€β”€ .azure-pipelines/ # Azure DevOps pipeline definitions

β”œβ”€β”€ .github/ # GitHub Actions workflows

β”œβ”€β”€ automation/ # Scripts, environment definitions, IaC

β”œβ”€β”€ documentation/ # Docs and design notes

└── solution/ # The platform layers themself

β”œβ”€β”€ core/

β”œβ”€β”€ engineering

    β”œβ”€β”€ ingest/

    β”œβ”€β”€ prepare/

β”œβ”€β”€ store/

β”œβ”€β”€ analysis

    β”œβ”€β”€ model/

    β”œβ”€β”€ present/

└── orchestrate/

With this structure, a scale up becomes a workspace retarget (from engineering to engineering/ingest, plus another workspace targeting engineering/prepare), or a git mv + a workspace retarget if you want to get rid of the engineering and analysis folders in Git to produce the flat structure of the post. Git mv and workspace retarget (= choosing another Git sync folder for the workspace) are two very simple operations, definitely not a migration. And this makes scale up also reversible, using the same means. You can scale down if you want to.

This isn't foolproof of course. The folders "ingest/prepare" and "model/present" appears as folders withing the Fabric workspace, which incentivizes a user to honor their intention. If the user doesn't and just puts items in the root folder, in the wrong folder or simply another folder, then we're back to doing a migration. But from the technical side, I think this is clean, we just cannot solve every problem with technology. The user has just to play along sometimes.

Best regards, Sebastian

Full disclosure: I am working on something very similar to FabricOps of Peer and might have been influenced to use some of his ideas. Isn't plagiarism the nicest form of recognition? Anyway, this is me giving back.

P

Sebastian, you're right. And thank you for the care that went into this. A flat tree only works at the top of the ladder, which contradicts what I promised in Episode 2. I've adopted nested folders and updated the post, with your name on it.

One thing I changed while adopting it. Your tree has orchestrate and core at the top level, but at the Minimum tier those live in Engineering, and a workspace can only bind one folder. So I shaped the tree after the three-layer floor instead: store, engineering (ingest, prepare, orchestrate, core) and analytics (model, present). That way a tier is only a choice of which folders get a workspace, and nothing ever moves.

On the "user has to play along" part, I think you're being harder on the idea than it deserves. Whether an item is in its responsibility folder is just a path in the repo, so CI can check it on every commit. Someone can still put a notebook in the wrong place, but they find out the same day instead of on the day they scale up, and fixing it is a drag and a commit, not a migration. I've added that to the post as well.

On "definitely not a migration" I agreed with one condition. It's only a rebind if the references were dynamic to begin with. Notebooks that resolve lakehouses by name, and pipelines and models whose references are resolved at deploy time, don't notice the split at all. Static IDs baked into definitions do, and those are the ones people have. So the folder layout makes scaling cheap, and designing for automation is what makes it free. Which is a decent summary of where this series is going.

Good luck with your own version. Borrow whatever's useful. This is exactly the kind of feedback that makes the ideas better.

Fabric Automation at Scale: From Chaos to Confidence

Part 3 of 3

A hands-on series on building a Microsoft Fabric data platform that scales β€” architecture, Git, IaC and CI/CD β€” going from a chaotic tenant to a confident, automated platform. Backed by the open-source FabricOps repo.

Start from the beginning

From Chaos to Confidence: A Fabric Automation, Git & CI/CD Series

Why I'm turning my "Fabric Automation at Scale" session into a series... and what's coming