TL;DR
The dbt best practices that matter in production are twelve rules that keep a project safe to change as more people work on it. Organize models into staging, intermediate and marts layers with linter-checkable names, and start each as a view until query or build time demands a table. Test the grain, keys, relationships and logic each model depends on, and keep unit tests in development and CI. In CI, build only the models that changed plus their children, inside an isolated schema. Document the grain and owner of every mart. Contract the models other teams rely on, and clear deprecation warnings before upgrading so the move to dbt v2 stays routine.
Key Takeaways
Organize models by responsibility into staging, intermediate and marts, and keep staging thin. Make naming and SQL style checkable by a linter instead of by reviewers’ memory. Start with views, and earn tables and incremental models with measured query or build time. Test grain, keys, relationships and logic, with unit tests in CI and never in production. Build only what changed in CI (state:modified+, defer, clone) inside isolated schemas. Document grain and owner, contract public marts, and resolve deprecations so a dbt v2 upgrade is routine.
Watch on YouTube
Databricks Genie ZeroOps: AI That Fixes Broken Pipelines
Kanerika looks at how AI can diagnose and repair broken Databricks pipelines, a useful companion to rules that stop breaks before production.
The 12:40 a.m. Page About a Model Nobody Owned
Picture an on-call analytics engineer whose phone buzzes at 12:40 a.m. The nightly dbt job has failed on final_orders_v2_new, and the finance dashboard refreshes at 6:00. In the SQL she finds a hard-coded schema name and a join on a column the source team renamed that afternoon.
The scene is made up, but every part of it is common. The model had no owner in its YAML. It also read straight from a raw table, so one rename broke five marts and stalled the data pipeline behind the morning reports.
None of these failures needs a smarter engineer, only a rule that a tool checks before the code merges. That is the job of dbt best practices once a project leaves its first owner. If you know what dbt does , the harder question is how a team of five or fifty keeps one project consistent.
Each rule below gives the reason behind it, the exception a team can allow, and the check that enforces it. A rule that lives only on a wiki page is ignored by the third new hire.
Set a Production Baseline Before You Standardize
Production rules come in three weights, and mixing them up causes many of the arguments in code review. Some controls are non-negotiable because a broken one corrupts data or blocks a release. Others, however, are team conventions, where any consistent choice works. Finally, the rest depend on your warehouse, so they belong in platform-specific docs rather than in the house rulebook.
dbt handles the transform step of an ELT workflow , after data has landed in the warehouse. As a result, most of its rules concern SQL, YAML and build order, not ingestion.
Non-negotiable control Team convention Platform-dependent choice Every model reads upstream data through ref() or source() Folder names inside each layer Incremental strategy (merge, delete+insert, insert_overwrite) Every mart has a tested unique, not-null primary key fct_ and dim_ prefixes, or plain entity names Zero-copy cloning for CI Failed builds or tests block the merge SQL formatting details beyond the linter config Warehouse sizing, clustering and partitioning Every mart has an owner and a documented grain Description length and template wording Python models, where the adapter supports them
Write the first column into CI first. Then agree on the second column once, record it in the repository, and stop debating it.
Rules 1 and 2: Structure the Project in Staging, Intermediate and Marts Layers
dbt Labs’ project structure guide sets out the layering most teams use. Because each layer has one job, a reviewer can tell from the folder alone what a model is allowed to do.
Rule 1: Give Every Model One Layer and One Job
Staging models map one to one with source tables, and they are the only place source() appears. Intermediate models then hold reusable logic in business-area folders, hidden from end users. Finally, marts are wide, denormalized models named after entities such as customers or orders.
models/
staging/
stripe/
_stripe__sources.yml
stg_stripe__payments.sql
shop/
stg_shop__orders.sql
intermediate/
finance/
int_payments_pivoted_to_orders.sql
marts/
finance/
orders.sql
_finance__models.yml
macros/
seeds/
snapshots/
tests/
That tree also tells a newcomer where things go. The same idea sits behind the bronze, silver and gold zones of a medallion architecture , although the mapping is close rather than exact.
Layer Job Default materialization Must not Staging Rename, cast and clean one source table View Join or aggregate Intermediate Reusable joins, pivots and business rules Ephemeral or view Be queried by dashboards Marts Serve one entity or process to consumers Table or incremental Call source() directly
Why it holds. When a source column changes, only one staging model needs an edit, and every mart downstream picks up the fix. That single point of change is exactly what the 12:40 a.m. model lacked.
Rule 2: Keep Staging Thin and Let Marts Skip Layers Only for Simple Logic
Staging should rename, cast and clean, nothing more. Once joins creep in, it stops being a clean contract with the source. Instead, business logic belongs in intermediate models, where it can be tested and reused.
Allowed exception. A mart can read a staging model directly when the logic is simple and no other model needs it. Otherwise, adding an intermediate model just to satisfy a diagram creates files without adding clarity. For modeling depth, such as grain choices and dimensional patterns, see our guide to dbt data modeling patterns and the broader data modeling best practices .
How to enforce it. Run the dbt_project_evaluator package in CI, because it flags structural problems such as models that read raw sources outside staging. Then treat each flag as a merge blocker unless the pull request records an exception.
Rules 3 and 4: Name Things So a Linter Can Check Them, and Declare Every Dependency
Naming arguments eat review time because nobody wrote the answer down where a machine could check it. A linted convention ends the debate, since the build either passes or fails.
Rule 3: Make Names and SQL Style Checkable
dbt Labs’ style guide for dbt models gives a sensible default, summarized below. Names are plural and use snake_case. They also avoid abbreviations and prefer business terms over source-system terms.
Object Convention Example Staging model stg_[source]__[entity]s stg_stripe__payments Intermediate model int_[entity]s_[verb]s int_payments_pivoted_to_orders Mart Plain entity name (fct_/dim_ as a team option) customers, or fct_orders Primary key column [object]_id customer_id Boolean column is_ or has_ prefix is_active Timestamp column [event]_at, in UTC created_at
Allowed exception. Teams that build dimensional models often prefer fct_ and dim_ prefixes for marts, which suits a star schema design . That is a valid team option, not the dbt Labs default, so pick one pattern and apply it everywhere.
For SQL style, the same guide asks for trailing commas, four-space indents, lowercase keywords and explicit as aliases. It also puts every ref() call in import CTEs at the top of the model. If a CTE appears in two models, move it into an intermediate model instead of copying it.
How to enforce it. Commit a .sqlfluff config and run SQLFluff in CI, or add dbt-checkpoint as a pre-commit hook for naming and YAML checks. Both projects are actively maintained. On dbt v2, the built-in dbt lint command reads the same .sqlfluff file and rule codes. However, it is compatible with SQLFluff rather than identical to it.
Rule 4: Declare Every Dependency With ref() and source()
A hard-coded relation name hides an edge from the dependency graph, so build order, CI selection and lineage docs all go wrong. The fix is short.
-- Before
select * from analytics_prod.stripe_payments_raw
-- After
with payments as (
select * from {{ ref('stg_stripe__payments') }}
)
select
payment_id,
order_id,
amount_usd
from payments
Allowed exception. An object that dbt does not build should be declared as a source. So the practical exception is a new source entry, never a hard-coded name. Reusable code follows a similar test. Turn logic into a macro only after it has appeared at least twice and stopped changing every week.
How to enforce it. Use maintained packages such as dbt_utils and dbt_project_evaluator before writing your own macros. The dbt-expectations package continues as metaplane/dbt_expectations , a maintained fork of the original package. Then add a CI check that fails on schema-qualified table names in model SQL.
Case Study
71% Higher Reporting Accuracy With One Rule Framework
A healthcare provider replaced conflicting transformation rules with one centralized rule framework on Azure Databricks, cutting data handling costs 38%.
Read the Case Study →
Rules 5 and 6: Choose Materializations by Workload and Make Incremental Models Replayable
Materialization is where cost and correctness meet. A wrong choice either burns warehouse time on every run or leaves a table drifting away from its source.
Rule 5: Start With Views, Then Earn Tables and Incremental Models
dbt Labs gives a simple rule of thumb. Start with a view, and move to a table when the view is too slow to query. Then move to incremental when the table is too slow to build. Each step up buys speed, but it also adds state you have to manage.
Materialization Use when Cost and debugging trade-off View Queries stay fast enough on the live logic No storage, but every query pays the compute Table Consumers query often and the view is slow Full rebuild each run, simple to reason about Incremental The full rebuild is too slow or too costly Cheaper runs, harder correctness and backfills Ephemeral Light logic used by one or two models Nothing stored, but nothing to query when debugging
Two beliefs deserve pushback. Incremental is not automatically cheaper, because a badly filtered merge can scan as much data as a rebuild. Ephemeral is not automatically simpler either, since its SQL gets inlined into every model that uses it and becomes hard to inspect.
Allowed exception. Platform features can change the answer, for example Delta merge behaviour on Databricks or dynamic tables on Snowflake. Keep those choices in platform docs, such as our dbt on Databricks implementation guide , rather than in the house rulebook.
Rule 6: Make Every Incremental Model Replayable
First, an incremental model must handle more than new rows. Late-arriving records, status corrections, deletes and new source columns all reach production sooner or later. Also, unique_key controls how rows merge, so it is not a uniqueness test, and the column still needs its own unique test.
For large time series, use the microbatch strategy, available from dbt v1.9. It splits processing into batches by an event_time column, one day at a time by default. That makes backfills and reruns of a single bad day far easier to control.
Now prove the model with a small replay fixture. Load four orders, run the model, then add a late order and correct one status, and run it again.
Fixture event Full refresh result Naive incremental (filter on max updated_at) Four orders loaded 4 rows 4 rows Order 5 arrives with yesterday’s timestamp 5 rows 4 rows (late row skipped) Order 2 status changes to returned Status is returned Status updates only if unique_key is set Compare the two outputs Reference Must match before merge
Allowed exception. A truly append-only event log that never changes can use a plain append. Even then, write down why, because sources that promise immutability often break that promise.
How to enforce it. Keep the fixture as seed data, run it in CI on every change to the model, and compare both outputs with the audit_helper package. If they differ, the pull request does not merge.
Rules 7 and 8: Test the Assumptions That Define Each Model
A test earns its place when it checks something the model promises, for example its grain. By contrast, tests written to fill a coverage number mostly add noise and slow the build.
Rule 7: Test Grain, Keys and Relationships on Every Model That Matters
dbt ships four generic data tests, which are unique, not_null, accepted_values and relationships. Together they cover grain, completeness, allowed values and joins. Since dbt 1.10.8, test inputs belong under an arguments: block by default, and dbt v2 refuses the deprecated form, so write new YAML this way.
models:
- name: orders
columns:
- name: order_id
data_tests:
- unique
- not_null
- name: customer_id
data_tests:
- relationships:
arguments:
to: ref('customers')
field: customer_id
Even so, not every model needs the same depth, so scale tests to how much damage a wrong number would do.
Model criticality Required checks Failure blocks deploy? Owner Finance or executive mart Key, relationships, accepted values, freshness, unit tests on logic Yes Named domain owner Shared intermediate model Key, plus relationships on every join Yes for errors, warn for the rest Data platform team Staging or exploratory model Source key not null and unique, source freshness Warn Source owner
Package-based checks go deeper, so see the dbt data quality testing strategy guide for them. Then fit dbt tests into your enterprise data quality framework , so they share one set of thresholds.
Rule 8: Unit Test Logic in Development and CI, and Page the Right Owner
Data tests check the data. Unit tests check the logic instead. Available since dbt v1.8, they run a model against small static inputs before it builds. According to dbt’s unit testing documentation , they belong in development and CI and should be excluded from production builds with –exclude-resource-type.
Write unit tests for logic that is easy to get wrong, for example date boundaries and status mapping. However, skip them for a rename-and-cast staging model, since there is no logic to break.
How to enforce it. Set severity deliberately. A freshness breach on a finance source should error and page the domain owner, while a stale marketing feed can warn into a channel. Then pair dbt’s checks with data observability monitoring for issues no test anticipated, so every alert reaches a person who can act on it.
Datasheet
Elevate Data Governance , Compliance and Security
How Kanerika sets up classification, lineage and access policy so ownership and quality rules follow your data from source to dashboard.
View the Datasheet →
Rule 9: Document Grain and Ownership, and Contract the Models Others Depend On
Documentation fails when it describes what a column is called instead of what a row means. So the minimum for every mart is a short, fixed template that a reviewer can check in seconds.
Purpose: the business question the model answers Grain: what one row represents Primary key: the column that is unique per row Owner: a named person or team, set in meta Refresh expectation: how fresh the data should be Sensitive fields: columns that need masking or restricted access Known limits: gaps, exclusions and edge cases Consumers: dashboards and apps, declared as exposures
Exposures also feed data lineage views and an enterprise data catalog , so the same YAML serves engineers and business users.
However, public models need more than prose. With an enforced dbt model contract , every column must declare its name and data type. The build then fails if the SQL returns anything else. Also, contracts work on tables, views and incremental models, but not on Python models, ephemeral models or materialized views.
Allowed exception. Internal staging and intermediate models rarely need contracts, since only your own team reads them. Remember also that a contract checks structure, not business meaning, so a renamed column fails it while a wrong calculation passes.
How to enforce it. Treat a backwards-incompatible contract change as a breaking change that needs a new major model version. Consumers can then migrate on a schedule, which matters most in a data mesh where other domains build on your marts.
Rule 10: Build Only What Changed in CI, in Isolated Environments
CI is where dbt best practices stop being advice. Without it, the linter, tests and contracts are only suggestions that depend on someone’s memory.
Separate Dev, CI and Prod Targets
Each developer builds in a personal schema, and each pull request builds in its own temporary schema. Only the deploy job writes to production, so ten people can test at once without overwriting each other’s tables.
Slim CI With state:modified+, Defer and Clone
Rebuilding the whole project on every pull request wastes time and money, because most of it has not changed. Instead, pass the artifacts from the last production run with –state. That enables state selection, deferral and dbt clone, and together state selection and deferral form what dbt Labs calls slim CI.
# Artifacts from the last production run live in ./prod-artifacts
# 1. Clone large incremental parents instead of rebuilding them
dbt clone --select state:modified+,config.materialized:incremental --state ./prod-artifacts
# 2. Build and test changed models and their children; read the rest from prod
dbt build --select state:modified+ --defer --state ./prod-artifacts
dbt clone uses zero-copy cloning on Snowflake, Databricks and BigQuery, much like cloning a Snowflake table . That way, CI can test an incremental model against realistic data without copying it. Also, the standard dbt CI job template presets dbt build –select state:modified+, which is a sensible starting point.
Lint and Project-Evaluator Gates on Every Pull Request
dbt Labs’ guide to setting up CI adds two more jobs, the dbt_project_evaluator package and a SQLFluff lint step, on GitHub Actions, GitLab or Bitbucket. Put them in this order.
Pull request opens and runs lint plus the project evaluator. Slim build and test runs on changed models and their children. A reviewer approves the code and the CI results together. The deploy job runs from the approved commit only. Post-deploy freshness and test checks confirm production.
Scale Checks to the Blast Radius of the Change
Change type Minimum CI check Docs or descriptions only Parse, lint and docs generation Staging logic Slim build of the model and all children Shared intermediate model Slim build plus row-count and key comparison against prod Mart grain or contract New model version, consumer sign-off and full downstream build Incremental strategy Replay fixture plus a full-refresh comparison
A reduced build is only as good as its artifacts and selectors. State selection and deferral also assume idempotent models, so a model that changes its output on every run will mislead them. When a bad change does reach production, recovery means a code revert plus a data rebuild. Your data orchestration tools should make that rerun a single command.
Rule 11: Tune Performance and Cost With Evidence
Slow runs tempt teams into guesses, such as making everything incremental. Measurement is cheaper, so follow the same five steps and change one thing at a time.
Find the expensive node in run_results.json or your warehouse query history. Read the compiled SQL and the query plan, not just the model file. Check scan volume and join fan-out, since a duplicated join key multiplies rows silently. Revisit the materialization only after the SQL is sound. Re-measure the same workload and keep the before and after numbers in the pull request.
Platform tuning, such as clustering, sits outside dbt itself, so see our guides on Snowflake query optimization and Snowflake cost optimization . The broader data engineering best practices also apply to the pipelines around the project.
Testing also costs warehouse time. On dbt v2, the batch tests option groups compatible unique and not_null tests on the same model into one query per test type. In dbt Labs’ internal testing, that cut test runtime by 40%, queries issued by 65% and the warehouse cost of running tests by 15%. Tests with custom severity, a where clause or stored failures still run alone, so treat those figures as dbt Labs’ own result, not a promise.
Allowed exception. A known hot path, such as a mart behind a customer app, can skip straight to a table when the evidence already exists.
Rule 12: Keep the Project Upgrade-Ready for dbt v2
The last of these dbt best practices protects every other one, because a project stuck on deprecated syntax eventually stops getting fixes. Also, dbt changed a lot in 2026, so the names matter.
What Changed in dbt During 2026
Fivetran and dbt Labs completed their merger on 1 June 2026, a consolidation covered in our modern data stack architecture guide. Then, on 16 September 2026, dbt v2, formerly the Fusion engine, became generally available under the plain name dbt . It is proprietary to dbt Labs but free to use, according to the dbt licensing page .
The Apache 2.0 build has a different story. It launched as an alpha on 1 June 2026, was renamed dbt OSS when v2 reached general availability, and PyPI still tags dbt-oss as Alpha . dbt v1 stays under Apache 2.0, and as of 6 November 2026 PyPI lists dbt-core 1.12.6 as its latest stable release, with 1.13 in pre-release.
An Upgrade Checklist for v1 Projects
dbt Labs’ v2 upgrade guide is blunt about one point. v2 does not support deprecated functionality, so every deprecation warning must be resolved first. Use this short checklist on your v1 project.
Clear every deprecation warning in the current build log. Move generic test inputs under arguments:, as in the Rule 7 example. Run dbt 1.12 with the opt-in –use-v2-parser flag, and use dbt-autofix for the fixes it suggests. Check third-party packages, since dbt_utils, audit_helper, dbt_external_tables and dbt_project_evaluator are already compatible but some others may fail to parse. Pick a distribution, dbt or dbt OSS, knowing the two cannot share a Python environment.
Upgrading is worth more than continued support. For instance, v2 compiles SQL locally to catch errors before the warehouse runs them. In dbt Labs’ own benchmark, a 10,000-node project compiled in 17 seconds on v2 against 70 seconds on 1.12.0. Its dbt lint and dbt check commands also turn Rules 3 and 9 into built-in checks.
Allowed exception. A team that needs fully open-source tooling can stay on dbt v1 until dbt OSS leaves alpha. Still, clear the deprecations so a later move is routine.
dbt Anti-Patterns That Break Production Projects
These five habits show up again and again. Each one breaks a rule above, but each also has a simple fix.
Smell Why it hurts Fix Joins in staging models Source changes ripple through hidden logic Move joins to an intermediate model select * in marts New source columns leak to consumers unannounced List columns and add a contract Hard-coded schema names Breaks lineage, CI and environment isolation Use ref() and source() Everything incremental Silent drift and painful backfills Start with views and earn each step Test inputs outside arguments: Deprecation warnings now, a blocked v2 upgrade later Move inputs under arguments:
If a smell keeps returning, the rule behind it probably lacks an automated check, so add one.
A dbt Best Practices Release Checklist
The rules above collapse into eight gates across three moments, before merge, before deploy and before downstream publication. Each gate names the evidence a reviewer should see, so approval is a check rather than a feeling.
Gate Requirement Evidence Owner Before merge Lint passes SQLFluff or dbt lint job Author Before merge No new evaluator violations dbt_project_evaluator output Author Before merge Changed models and children build and pass tests Slim CI run Author Before merge Grain, owner and description present Model YAML diff Reviewer Before deploy Deploy runs from the approved commit Job log with commit hash Platform team Before deploy Incremental changes pass the replay fixture audit_helper comparison Model owner Before publication Contract changes carry a new model version Version entry in YAML Model owner Before publication Freshness and post-deploy tests pass Production run results Domain owner
Then keep this table in the repository next to the pull request template. Teams that already run data governance best practices can map each gate to an existing control, which saves writing a second policy.
Checklist
Data Engineering Checklist for Enterprise Teams
A practical list of pipeline, testing, orchestration and monitoring checks to run alongside your dbt release gates.
Get the Checklist →
How Kanerika Helps Teams Run dbt Projects in Production
Kanerika’s data engineering services work through three engagement models. Pipeline Audit and Design maps existing pipelines and gaps, defines the target architecture and tooling, and produces a build-ready blueprint. Build and Modernize rebuilds legacy ETL on a cloud-native stack and hands off tested, production-ready pipelines.
Ongoing Support then adds dedicated engineers who monitor pipeline health and data quality.
The rules in this guide line up with the DataOps and CI/CD for Data service line on that page. There, every pipeline change is reviewed, automated tests run on every build, and data contracts are enforced. Kanerika is a Databricks Consulting Partner and a Snowflake Select Tier partner , and holds ISO 27001, ISO 27701 and SOC 2 Type II.
A healthcare migration makes the same point. At a leading healthcare provider, transformation rules differed across clinical, claims and billing systems. Kanerika migrated the provider’s Informatica workflows to Azure Databricks and put in one centralized rule framework for coding standards and key healthcare metrics.
After that, reporting accuracy rose 71%, data handling costs fell 38% and decisions came 64% faster. That project did not use dbt, but it shows what one standard for transformation rules is worth. Teams still weighing tools can start with our comparison of dbt, Informatica and Azure Data Factory or our guide to choosing a data transformation tool .
Kanerika Service
Data Engineering Services
Pipeline Audit and Design, Build and Modernize, and Ongoing Support, with DataOps and CI/CD for data so every pipeline change is reviewed and tested on every build.
Explore Data Engineering
Making dbt Best Practices Stick
Twelve rules sound like a lot. In practice they reduce to one habit. First write the decision down once, then make a tool check it on every pull request. So start with the non-negotiable column from the baseline table, wire it into CI this sprint, and add the rest as the project grows. That way, the next 12:40 a.m. failure gets caught at 3:00 p.m. in review instead.
Frequently Asked Questions
What are the most important dbt best practices?
The most important dbt best practices are a staging, intermediate and marts structure, names a linter can check, ref() and source() for every dependency, views before tables, tests on grain and keys, and slim CI in isolated schemas. Owned documentation, contracts on public marts and a project free of deprecation warnings complete the set.
How should I structure a dbt project?
Split models into three folders. Staging holds one model per source table that only renames, casts and cleans. Intermediate holds reusable joins and business rules grouped by business area. Marts hold wide, consumer-facing models named after entities such as customers or orders. Keep macros, seeds, snapshots and singular tests in their own top-level folders.
Should staging models be views or tables?
Staging models should usually be views. They map one to one with source tables and do light work, so storing them adds cost without much speed. dbt Labs recommends views for staging. Move one to a table only if downstream queries are measurably slow and the source is large, and record the reason in the model’s YAML.
When should I use incremental models in dbt?
Use an incremental model when a full table rebuild is too slow or too costly. Before switching, confirm the model handles late-arriving rows, corrections and deletes, set a unique_key and still test that column for uniqueness. Prove it with a replay fixture that compares incremental output to a full refresh before the change merges.
What is slim CI in dbt?
Slim CI builds and tests only the models a pull request changed, plus their children, instead of the whole project. It passes artifacts from the last production run with –state, selects state:modified+ and uses –defer to read unchanged parents from production. dbt clone can add zero-copy copies of large incremental parents on supported warehouses.
Is dbt Core still open source after dbt v2?
dbt v1 remains open source under Apache 2.0, with 1.12.6 as its latest stable release. dbt v2, generally available since 16 September 2026, ships under the name dbt and is proprietary but free to use. The Apache 2.0 version of v2 was renamed dbt OSS and is still tagged Alpha on PyPI.
How many tests does a dbt model need?
There is no fixed number. Every mart needs a unique and not_null test on its primary key, plus relationships tests on its joins and accepted values on status-style columns. Finance and executive marts should add source freshness and unit tests on complex logic, while simple staging models can stay lighter.
What is the difference between data tests and unit tests in dbt?
Data tests check the data a model produces, such as whether a key is unique or a value is allowed. Unit tests, available since dbt v1.8, check the model’s logic against small static inputs before it builds. Run data tests in every environment, and keep unit tests in development and CI rather than production.
How do I prepare a dbt project for dbt v2?
Clear every deprecation warning, because dbt v2 does not support deprecated functionality. Move generic test inputs under the arguments property, run dbt 1.12 with the –use-v2-parser flag to find parse issues, and check third-party packages for compatibility. Then choose between the dbt and dbt OSS distributions, which cannot share a Python environment.
Should I use fct_ and dim_ prefixes for dbt marts?
It depends on your modeling style. dbt Labs’ own guide names marts after plain entities such as customers and orders. Teams that build dimensional star schemas often prefer fct_ and dim_ prefixes because they signal the table type. Either choice works if the whole project applies it consistently and a linter checks it.