In my current work Iβve been helping establish version-controlled, modular Power BI environments.
The goal is to make semantic models scalable, reviewable, and easily shared across development teams without losing the agility that makes Power BI so useful.
This project uses a public dataset to demonstrate that same architectural pattern in an open, reproducible way.
It highlights how PBIP (Power BI Project files), Git, and a consistent DAX naming convention together create a professional-grade modeling environment.
.pbip project structures enable source-controlled Power BI models..pbix files.Power BI Project (.pbip) files are text-based representations of Power BI Desktop projects.
They break down a .pbix into smaller parts that can be tracked in Git.
A typical PBIP structure looks like this:
π SalesModel.pbip
β£ π Dataset
β β£ model.bim
β β connections.json
β£ π Report
β β£ report.json
β β visuals/
β£ project.json
This separation allows independent updates to dataset or report logic.
Changes to model.bim (which defines measures, relationships, and metadata) can be reviewed like any other code.
Teams can collaborate without overwriting the entire file.
The .pbip folder works naturally with Git.
Because files like model.bim are JSON, Git can detect changes to measures, relationships, and KPIs.
Typical workflow:
This turns a Power BI model into a managed, reviewable code asset.
As models grow, naming becomes just as important as logic.
I use a light Hungarian notation style to make the intent of each measure obvious at a glance.
The idea is simple: the prefix tells you how the measure behaves before you even open it.
Hungarian notation is a naming convention that uses prefixes or symbols to describe the purpose or behavior of a variable.
In Power BI I adapt this approach for DAX measures so each measure name encodes how it behaves in the semantic model.
For example, _ means a raw aggregation, ? means it changes filter context, and $ means it derives its value from other measures.
This makes models self-documenting and easier to maintain at scale.
| Symbol | Meaning | Example | Description |
|---|---|---|---|
_ | Base measure using aggregation only | _sumSales | Simple aggregation (no CALCULATE) |
? | Measure altering filter context | ?sumSales(Year=2025) | Uses CALCULATE or context manipulation |
^ | Measure that uses an iterator function | ^sumxNetInvoiceValue(dim_date) | Uses an iterator function to compute a measurre over a table |
% | Ratio or rate | %Margin | Value between 0 and 1 |
$ | Derived measure using other measures | $Margin | Combines other measures (e.g. DIVIDE([_sumProfit], [_sumSales])) |
> | Selection-driven measure | >SelectedYear | Uses SELECTEDVALUE |
* | Independent measure | *WeekToday | No table dependencies (e.g. WEEKNUM(TODAY())) |
@ | Local-only measure | @DebugValue | Exists only in PBIP, not the parent model |
# | Resolver measure | #CurrentMetric | Returns the correct measure via SWITCH logic |
+ | Formatting or visual measure | +BgColor | Used for conditional formatting |
You can chain prefixes together when a measure has multiple characteristics.
For example:
@$%Margin = DIVIDE([_sumProfit], [_sumSales])
This means the measure only exists in the current model (@), is derived from other measures ($), and returns a percentage (%).
The order of prefixes should always be:
@, #, ?, $, %
All other prefixes (_, >, *, +) must be used on their own.
Chaining > is only valid when used with a resolver (#) because it depends on selection logic.
To demonstrate, I built a model using the Open Government UK Trade dataset.
It tracks imports, exports, and trade balances over time, structured as follows:
π TradeAnalytics.pbip
β£ π Dataset
β β£ model.bim
β β connections.json
β£ π Report
β β£ Overview.pbir
β β visuals/
β£ project.json
Example DAX definitions:
_sumExports = SUM(Trade[Exports])
_sumImports = SUM(Trade[Imports])
$TradeBalanceΒ£ = [_sumExports] - [_sumImports]
%ExportShare = DIVIDE([_sumExports], [_sumExports] + [_sumImports])
Once published, the model shows both the calculations and the logic behind them, all versioned and reviewable.
In my current position this architecture fits into a full Power BI deployment pipeline.
Each semantic model has its own repository, and every environment (Dev, Test, Prod) is linked to a workspace.
PBIP folders deploy through Fabric Deployment Pipelines or Azure DevOps YAML tasks.
Validation scripts run during build to check naming and metadata consistency before publish.
To keep everything compliant, I use a YAML setup that runs a custom validation script during deployment.
It scans all measures in the dataset to confirm they follow the approved prefix order and naming format before allowing the build to proceed.
This helps enforce consistency across projects and stops non-standard naming from slipping through review.
Applying code-based discipline to Power BI through PBIP, version control, and clear naming turns semantic models into structured data products rather than opaque files.
The result is a reliable architecture where teams can collaborate, review, and evolve models confidently.
Model with intent. Govern with structure. Scale with trust.