Data & Analytics 9

Four Mistakes I Caught Migrating a Staffing Operation to Microsoft Fabric

By Eyüp Türkay

Microsoft Fabric demos beautifully. You point it at some data, build a Lakehouse, drop a semantic model on top, and a Power BI report renders in seconds. Everyone nods. The trouble starts three weeks later, in production, when the operation is real and the data is messy.

I recently took a Dutch staffing and housing operator — more than 2,000 field workers, payroll, occupancy and transport data — off a tangle of spreadsheets and a tired SQL Server onto a Fabric medallion architecture with Power BI on top. The architecture diagram was clean. The first production weeks were not. Here are the four mistakes that mattered, none of which appear in any “getting started with Fabric” tutorial — and the one more I’ll mention at the end because it’s the one that can actually hurt you.

I write these down the way they happened: what broke, how I noticed, and what I changed. The fixes are the boring part. Noticing is the skill.

1. Direct Lake quietly falling back to DirectQuery

Direct Lake is the reason to be on Fabric at all: the semantic model reads Parquet straight from OneLake, no import, no scheduled refresh, near-import performance. That’s the promise. What the demo never shows you is that Direct Lake has hard guardrails — on capacity SKU, on row counts per table, on model memory — and when a query exceeds them, Fabric does not error. It silently falls back to DirectQuery, hits the warehouse row by row, and your “instant” report turns into a ten-second spinner.

The reports looked correct, so nobody filed a bug. They were just slowly getting worse as the housing and shift tables grew. I only caught it because the numbers were right but the feel was wrong, and the feel is a signal I’ve learned to trust. I confirmed it in Power BI’s Performance Analyzer and the Fabric capacity metrics: the queries that should have been Direct Lake were showing DirectQuery fallback.

The fix was not “buy a bigger capacity.” It was modelling discipline: trim the semantic model to the columns the reports actually use, push wide transformations down into the gold layer instead of dragging them into the model, and keep the hot tables under the fallback threshold. Direct Lake rewards a narrow, deliberate model and punishes a “just import everything” habit carried over from Power BI Import mode.

Lesson: with Direct Lake, performance is a modelling decision, not a hardware decision. If you don’t watch for the fallback, the platform degrades without ever telling you.

2. One capacity doing ingestion, transformation and reporting at once

The tidy first design ran everything — nightly ingestion, medallion transformations, and interactive Power BI — on a single Fabric capacity. It worked in testing because testing happens when nobody else is using it.

In production, the night-time pipeline and the morning reporting collided. Fabric’s capacity throttling and smoothing meant a heavy overnight transformation borrowed against the next few hours of compute, so the operations managers opening their dashboards at 8am got the slow, throttled tail of the night’s batch. The platform wasn’t broken; it was doing exactly what a shared capacity does. It just made the most visible users pay for the least visible work.

I separated the workloads: heavier background processing scheduled and sized so it finishes and releases capacity well before the business day, and interactive reporting protected from the batch window. The point isn’t a specific SKU — it’s understanding that on Fabric, time of day is part of your architecture. Compute is smoothed across a window, so what runs at 3am still shapes what 9am feels like.

Lesson: capacity planning on Fabric is scheduling, not just sizing. Map your compute to the clock, or your busiest users will absorb your batch jobs.

3. Treating the medallion layers as folders instead of contracts

Bronze, silver, gold get drawn as three boxes, and it’s tempting to treat them as three folders you copy data between. That’s how you end up doing business logic in bronze “just to get the report out,” duplicating data with OneLake shortcuts you thought were references, and discovering that a refresh now reprocesses things that never changed.

The specific trap here was shortcuts versus copies. A OneLake shortcut is a pointer; a copy is data you now own and have to refresh. Mixing them without intent meant some “single source” tables were quietly duplicated, drifting out of sync, and inflating every downstream refresh. The symptom was refresh times creeping up for no obvious reason — the classic sign that your layers have stopped being contracts and started being a pile.

I rebuilt the boundaries as rules, not suggestions: bronze is raw and immutable, silver is cleaned and conformed with no presentation logic, gold is shaped exactly for how the business asks its questions. Business logic lives in one layer, once. Shortcuts are used deliberately where I want a reference and never where I want a managed copy.

Lesson: medallion is a set of contracts about where each kind of work is allowed to happen. The moment a layer does someone else’s job, your refresh cost and your trust both start leaking.

4. No incremental refresh — reloading the world every night

At pilot size, a full reload every night is invisible; it finishes in minutes and you move on. At 2,000+ workers with daily occupancy, payroll and transport movements, “reload everything” stops being free. The refresh window stretched, started eating into the morning, and turned into mistake #2’s problem from a different direction.

The data was overwhelmingly append-only — yesterday doesn’t change — but the pipeline was rereading all of history every single night because nobody had told it not to. The fix was incremental refresh with proper partitioning: process the window that actually changed, leave settled history alone, and let the warehouse partitions do the pruning. The refresh window collapsed back to something that comfortably finishes in the quiet hours.

Lesson: “it’s fast enough” at pilot scale is a trap. Design for the data volume you’ll have in month six, and make the pipeline touch only what changed.

The one more: OneLake security that didn’t match the report

The four above are about performance and cost. This one is about not leaking payroll.

Row-level security was correctly set up in the Power BI semantic model, so the reports showed each manager only their own region. But the underlying OneLake data was more open than the report implied — and the moment anyone reaches the data through a different door (a notebook, a direct OneLake connection, another tool), the report’s RLS is irrelevant. For a dataset with payroll and housing in it, that’s not a performance bug; it’s an incident waiting to happen.

I pulled security down to where the data lives instead of relying on the presentation layer to enforce it, so the boundary holds no matter how someone connects.

Lesson: security enforced only at the report is a costume, not a wall. On a unified platform like OneLake, govern at the data, because there is always more than one way in.

Why I wrote this

None of these four — five — failures show up in a proof of concept. They show up in week three of production, with real volume and real users, which is exactly when they’re most expensive to find and most embarrassing to explain. Fabric is a genuinely strong platform; the mistakes here aren’t Fabric’s fault, they’re the difference between a migration that demos and a migration that operates.

If you’re moving an operation onto Fabric and any of this sounds like a problem you’re about to have — or are quietly already having — that’s the conversation I’m good at. Tell me what you’re building.

Message on WhatsApp