Picture a payments table built twice. silver_payments.amount is decimal(10,2) in the Databricks notebook and float in the Fabric one, though both were copied from the same original. The Databricks version has a rule that rejects negative amounts; the Fabric version lost it during a refactor. Both pipelines are green. Finance reconciles the two revenue figures every month and the gap is never quite the same.
This is what happens when a business rule lives inside platform code. Once a data product has to run in two places, because of a migration, an acquisition or a team that standardised on the other platform, every copy of the rule can change on its own. No one has to decide that the definitions should diverge for them to do so.
This article separates what a data product can genuinely share across Databricks and Fabric from what must stay platform-specific, compares the ways teams handle that today, and shows where a portable data contract helps. It does not claim the two platforms are interchangeable. They are not, and the platform-specific half is real work.
Why the rules drift
The rule and the plumbing are in the same file. A notebook that reads from a Unity Catalog volume, casts types, applies a rule and writes a Delta table mixes business meaning with paths, catalogs and engine calls. Porting it means rewriting all of it, and the rule is rewritten along the way.
The two platforms organise things differently. Databricks commonly uses Unity Catalog catalogs and schemas, Volumes and Jobs. Fabric uses workspaces, lakehouses (optionally with schemas), OneLake Files and Data Pipelines. A straight copy does not run, so each copy is adapted by hand.
The ways teams handle two platforms
| Approach | How it works | Good fit | Watch out for |
|---|---|---|---|
| Run each product on one platform, share the data | Fabric reads Databricks tables via a mirrored Unity Catalog or shortcuts | Fabric mainly reports on Databricks tables | Metadata only; changes can lag; some table types hidden |
| Rewrite per platform | Each platform team builds its own pipelines | Different products on each platform | Shared products drift, as in the opening |
| Shared Python library | Rules in a package installed on both | Teams comfortable with Spark on both sides | Paths and configuration leak into the library |
| dbt on both | dbt models with the Databricks and Fabric adapters | SQL-first teams | Constraint enforcement differs; tests run after load |
| Platform-native quality | Databricks pipeline expectations and Fabric materialized lake view constraints | Each platform owns separate products | Two syntaxes for one rule, kept in step by people |
Two caveats from the docs. Mirroring copies metadata and reads the data in place, so changes can take seconds to minutes to appear, and views and non-Delta external tables are not shown; nothing is processed twice, but nothing runs on Fabric either. With dbt, Fabric enforces only not_null at build time.
If Fabric only needs to read what Databricks produces, the first option is usually the right answer and the rest of this article does not apply. The problem starts when the same product has to be processed on both, or moved from one to the other, and the question becomes: which file holds the definition both platforms run? That is the gap a portable contract addresses.
The split that works
A useful test: would this setting change if the product moved platform tomorrow? If not, it belongs in the contract. If so, it belongs in platform configuration.
| Belongs in the contract (shared) | Belongs in platform configuration |
|---|---|
| Field names, types and required fields | Storage paths and landing locations |
| Primary and business keys | Catalog, lakehouse and schema names |
| Row and dataset quality rules | Authentication, secrets and permissions |
| What happens to failed rows (quarantine) | Compute size and Spark settings |
| Freshness and volume expectations | Job, notebook and pipeline deployment |
| Declared upstream dependencies | Platform-specific SQL functions |
| Materialisation intent (append, merge, SCD Type 2) | Run log and evidence table locations |
How LakeLogic approaches it
In LakeLogic, the shared column is an Open Lakehouse Contract per table, and the platform column is a system file (_system.yaml) per source system that holds roots, paths and table locations. The LakeLogic runtime reads both and runs the contract on Spark on either platform, or on Polars or DuckDB for local tests.
A contract excerpt that should mean the same thing everywhere:
# silver contract (illustrative excerpt)
model:
fields:
- name: charge_id
type: string
required: true
- name: amount
type: decimal(10,2)
quality:
row_rules:
- name: amount_not_negative
sql: "amount >= 0"
And the part that does not, from the RideFlow system files for the same Stripe source on each platform (excerpts):
# Databricks edition
quarantine_root: '`{domain_catalog_prefix}_{domain}`.quarantine'
landing_root: /Volumes/{ls_catalog}_{domain}/{ls_schema}/{ls_volume}/{system}
# Fabric edition
quarantine_root: "quarantine"
landing_root: "Files/landing_{domain}/{system}"
Databricks puts each domain in its own catalog and lands files in a Volume; the Fabric edition uses a schema per domain in one lakehouse and lands files in OneLake Files. Those differences stay in configuration, so the rule does not have to move.
What our own two editions taught us
RideFlow is our reference project: a fictional ride-hailing business. The Databricks edition is public. A Microsoft Fabric edition, built from an earlier copy of the same contracts, exists but is not public yet, and this article does not claim it has been proven end to end.
Comparing the two editions while writing this article, we found the opening scenario in our own work. The Fabric edition's silver_stripe_charges declares amount as float; the Databricks edition, which has kept evolving, declares decimal(10,2) and carries fuller descriptions. The system files differ by design. The contracts were never meant to, and they did, because they were copied rather than shared.
The format made the difference easy to find, a diff of two YAML files. It did not prevent it. Portability needs one copy of the contract, not two similar ones. Keep contracts in one repository, have each platform's deployment read from it, and review platform configuration separately.
When you might not need this
- Fabric only reads Databricks tables. Mirroring or shortcuts give Fabric access with no second pipeline to keep in step.
- The platforms host different products. If no product runs on both, platform-native quality on each side is simpler.
- A one-way migration with a fixed end date. Rewriting once and retiring the old pipeline may cost less than introducing a contract layer.
- SQL-first teams already on dbt on both platforms, who accept the differences in constraint enforcement and test after load.
Limits worth knowing
- Contracts do not make platforms equal. Permissions, catalog behaviour, capacity limits and orchestration are platform work, every time.
- Support is per capability. The Databricks edition of RideFlow is the public, fuller reference. The Fabric edition is not public and has less published evidence of execution. Do not read one as proof of the other.
- Engines can disagree. The same contract on two Spark runtimes, or on Spark versus a local engine, can still read files or cast types differently. Test on each target platform; our post on avoiding the Spark tax without trusting local results blindly covers how.
- Governance is not portable by default. A contract can declare a field as personal data; enforcing who can read it is Unity Catalog's job on one side and Fabric's on the other.
Key takeaways
- Decide first whether the product must run on both platforms or only be readable from both; mirroring solves the second without any of this.
- Put what would survive a platform move in the contract, and everything else in platform configuration.
- One copy of each contract, read by both deployments. Two similar copies will drift, as ours did.
- Review platform configuration and contract changes separately; they have different owners.
- Test on every target platform. A shared definition does not prove identical execution.
See the reference project
The RideFlow Databricks repository shows the split in practice: contracts under contracts/<domain>/<system>/contracts/, and the platform settings in each system's _system.yaml. For how the domains and quarantine work on Databricks, read who owns a negative fare in a governed data mesh.