Mert Özen Further With Every Line
Software Design Coding Conventions and Team Standards — #01

What Are Coding Conventions and Why Leave Them to Tools, Not People? Sources, Levels and Team Decisions

Mert Özen Nov 14, 2026 12 min 4 views
What Are Coding Conventions and Why Leave Them to Tools, Not People? Sources, Levels and Team Decisions

The value of a coding convention comes not from the rule itself but from it being settled and enforced by a tool: in Bookstore a one-line discount change was buried in a 55-line diff by editor settings, and .editorconfig plus dotnet format brought it back to one line. The post covers the formatting, style, naming and architecture levels, choosing a source and making the team decision once and recording it.

Version note: The code in this post was built and run with C# 14 and .NET 10 (SDK 10.0.402).

Guess First: How Many Lines Changed in This PR?

A small business rule is changing in Bookstore's ordering module: the bulk purchase discount should start at five books instead of three. Grace makes the change and opens a PR. The commit message is one sentence and the change is one constant. This is what lands in front of the reviewer:

$ git log --oneline
23e4c7b Raise bulk discount threshold to 5
bfa313e Add OrderService

$ git diff --stat HEAD~1 HEAD
 src/Bookstore.Ordering/OrderService.cs | 55 +++++++++++++++++-----------------
 1 file changed, 28 insertions(+), 27 deletions(-)

The file is a 33-line OrderService. When Grace saved it, her editor reformatted it with her personal settings: indentation became tabs, the file-scoped namespace turned into a braced block, explicitly typed variables became var, and field accesses got a this. prefix. Two questions for you as the reviewer: how many of those 55 lines change the program's behavior, and could you spot that line in the diff at first glance? And when git diff -w ignores whitespace, how many lines are left? Make a guess.

What Problem Do Coding Conventions Solve?

Coding conventions are the decisions a team makes about how to write code that the language itself does not force: how many spaces of indentation, whether a private field name starts with an underscore, whether types are spelled out, whether using directives sit inside or outside the namespace. The compiler does not care about any of them. var subtotal = 0m; and decimal subtotal = 0m; produce the same IL.

So why do they matter? Because people, not compilers, read code, and inconsistency raises the cost of reading. If you see _items in one file, m_items in another and this.items in a third, your brain stops each time to ask "is this something different?" The more concrete cost shows up in review: in the opening PR a single behavior change is buried in 54 lines of formatting noise. A reviewer's attention is a limited resource; spend it on indentation and you miss that the discount threshold went from 3 to 5. That is where this post's main claim comes from: the value of a convention lies less in the rule itself than in the rule being settled and automatic. Which indentation you pick hardly matters; what matters is that everyone uses the same one and that a tool, not a person, makes sure of it.

In the Clean Code series, the "Formatting and File Layout" post covered how layout affects readability, and the "Naming in Clean Code" post covered why names are a contract. This series deals with applying those ideas at team scale: choosing the rules, writing them into a file and having tools enforce them.

Four Levels, Four Different Enforcers

The word "convention" puts four very different kinds of rules in the same bag. Each has a different right enforcer; mix them up and either a tool gets the wrong job or people do by hand what a tool could do.

LevelExampleRight enforcerWhere it is caught
FormattingIndentation, spacing, line breaks, line endingsFormatter (CSharpier, dotnet format whitespace, Prettier)On save in the editor, pre-commit, CI
Style / language preferencevar or explicit type, file-scoped namespace, expression-bodied memberAnalyzer (IDE rules, ESLint)Build, CI
Naming_camelCase fields, Async suffix, interfaces starting with IAnalyzer (shape) + review (meaning)Build and PR review
Architecture ruleThe Domain layer does not reference InfrastructureArchitecture testTest run, CI

Formatting is purely mechanical: there is one right answer and a program can produce it. That is why no human should ever comment on formatting; the formatter runs and the debate is over. Style preferences carry a bit more meaning but can still be turned into rules: Roslyn's code-style rules whose IDs start with IDE, and ESLint on the TypeScript side, report them and fix most of them automatically.

Naming has two parts. An analyzer checks the shape (underscore, capitalization, suffix): naming rules in .editorconfig raise warnings on build through rule IDE1006. But no tool can know whether a name says the right thing; _data satisfies the naming rule and is still a bad name. That part stays with review. Architecture rules are about structure, not style; tests written with libraries such as NetArchTest or ArchUnitNET guard which namespaces a class may depend on. This series deals with the first three levels; the fourth was covered in the "Testing and Protecting Clean Architecture" post of the "Clean, Onion and Hexagonal Architecture" series and in the "Protecting Module Boundaries" post of the "Modular Monolith" series.

The practical consequence of this split: if your review checklist has items like "is the indentation right" or "was var used", those items are in the wrong place. Review is reserved for what a person needs to look at: meaning, names and design.

What Did the Tools See in Grace's PR?

First, the answer to the guess. Only one of the 55 lines changes behavior: the BulkDiscountThreshold constant. When git diff -w ignores indentation, the picture shrinks but does not disappear:

$ git diff -w --stat HEAD~1 HEAD
 src/Bookstore.Ordering/OrderService.cs | 17 +++++++++--------
 1 file changed, 9 insertions(+), 8 deletions(-)

Sixteen of the remaining 17 lines are still noise: the namespace block, three vars, two this.s. Those are style differences, not whitespace, and -w cannot see them. This is exactly where the levels part ways. We put the team rules in an .editorconfig at the repo root and enforced them on build:

root = true

[*]
charset = utf-8
insert_final_newline = true
trim_trailing_whitespace = true

[*.cs]
indent_style = space
indent_size = 4

# Explicit types
csharp_style_var_for_built_in_types = false:warning
csharp_style_var_when_type_is_apparent = false:warning
csharp_style_var_elsewhere = false:warning

# File-scoped namespace
csharp_style_namespace_declarations = file_scoped:warning

# No this. qualification
dotnet_style_qualification_for_field = false:warning
dotnet_style_qualification_for_method = false:warning

# Whitespace and indentation (IDE0055)
dotnet_diagnostic.IDE0055.severity = warning
<Project>
  <PropertyGroup>
    <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
  </PropertyGroup>
</Project>

The second file is Directory.Build.props at the repo root; this is the property that carries code-style rules into the command-line build as well. Building Grace's file (we shortened the doc links and project path):

$ dotnet build src/Bookstore.Ordering --no-incremental
src/Bookstore.Ordering/OrderService.cs(1,11): warning IDE0161: Convert to file-scoped namespace
src/Bookstore.Ordering/OrderService.cs(3,1): warning IDE0055: Fix formatting
src/Bookstore.Ordering/OrderService.cs(5,1): warning IDE0055: Fix formatting
...
src/Bookstore.Ordering/OrderService.cs(20,4): warning IDE0008: Use explicit type instead of 'var'
src/Bookstore.Ordering/OrderService.cs(21,4): warning IDE0008: Use explicit type instead of 'var'
...
src/Bookstore.Ordering/OrderService.cs(28,4): warning IDE0008: Use explicit type instead of 'var'
...
Build succeeded.
    30 Warning(s)

Thirty warnings: 26 IDE0055 (indentation), three IDE0008 (var) and one IDE0161 (namespace). Notice that the two this.s are missing, even though we wrote :warning for them in .editorconfig too. Checking the same rules with dotnet format shows the missing piece:

$ dotnet format src/Bookstore.Ordering --verify-no-changes
src/Bookstore.Ordering/OrderService.cs(3,1): error WHITESPACE: Fix whitespace formatting. Replace 1 characters with '\s\s\s\s'.
...
src/Bookstore.Ordering/OrderService.cs(1,11): warning IDE0161: Convert to file-scoped namespace
src/Bookstore.Ordering/OrderService.cs(15,13): warning IDE0003: Name can be simplified
src/Bookstore.Ordering/OrderService.cs(20,13): warning IDE0008: Use explicit type instead of 'var'
src/Bookstore.Ordering/OrderService.cs(21,13): warning IDE0008: Use explicit type instead of 'var'
src/Bookstore.Ordering/OrderService.cs(22,40): warning IDE0003: Name can be simplified
src/Bookstore.Ordering/OrderService.cs(28,13): warning IDE0008: Use explicit type instead of 'var'
$ echo $?
2

IDE0003 (unnecessary this.) is one of a handful of rules that never run on build; the Roslyn source marks it "never on build" for performance reasons. We will look at the details in the next post. The lesson here is more general: writing a rule down does not mean it is enforced at every layer; you need to know which tool owns which rule. As a last step we ran dotnet format in fix mode and compared the file with the original commit:

$ dotnet format src/Bookstore.Ordering
$ git diff --stat HEAD~1 -- src
 src/Bookstore.Ordering/OrderService.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

The 55-line diff came down to one line: BulkDiscountThreshold = 3 became 5. That is the only line the reviewer needs to see.

Choosing a Source: Whose Conventions Are We Copying?

Writing conventions from scratch is a waste of time. C# has a few serious starting points, and knowing how they differ shortens the "but Microsoft says" debates.

The .NET Coding Conventions page on Microsoft Learn holds the rules for sample code in the dotnet/docs and dotnet/samples repos. The page says openly that it adapted these rules from the styles of the runtime and Roslyn (the C# compiler) teams, and explains why they differ: the goal of the doc samples is to teach and to spread new language features, while the runtime cares about performance on hot paths. That is why the doc rules are more eager about new features; they also cap line length at 65 characters so code reads well on the docs site, which is absurdly short for an application codebase. The page presents itself as an example rather than a binding list and points to the Framework Design Guidelines for detailed API design.

The C# Coding Style file in the dotnet/runtime repo is shorter and stricter. Private and internal fields are _camelCase, static fields take an s_ prefix and thread-static ones t_; var is used only when the type is obvious from the right-hand side; this. is written only when needed; visibility is always explicit. The general principle at the top is instructive too: if an existing file differs from these rules, follow that file's own style.

The Framework Design Guidelines (Krzysztof Cwalina, Jeremy Barton and Brad Abrams; third edition 2020) cover a different layer: not the inside of the code but the API exposed to the outside. How public types, members and exceptions are named and designed lives here. For a team writing libraries, this should be the source of naming decisions. The Framework Design Guidelines section on Microsoft Learn is excerpted from the 2008 second edition, and the page itself warns that some of it may be out of date; the current version is the third edition.

On the Angular side there is one source: the Angular Style Guide on angular.dev. The guide describes itself as recommended rather than mandatory, points to Google's TypeScript style guide for general TypeScript rules, and puts a file's own consistency first when they conflict; in its own words, "When in doubt, prefer consistency." (Angular Style Guide, angular.dev). In the fourth post we look at the guide's main decisions.

The practical route: pick one as a base, copy it, then adapt it to your team's real needs. Microsoft recommends the same in its docs and offers its own .editorconfig as a starting point. For Bookstore we based ourselves on the runtime style, because it is closer to an application codebase than the doc rules; our one deviation is about var, below.

The Team Decision: Debate Once, Record It, Close It

Conventions are technically easy and socially hard. Everyone has an indentation preference, and those preferences come more from habit than from logic. There is a name for this: bikeshedding. In his 1957 book Parkinson's Law, C. Northcote Parkinson describes a committee that approves a nuclear reactor budget in minutes and then spends a long time arguing about the materials for a bicycle shed; everyone understands a shed, so everyone has an opinion. The term spread in software after a 1999 message Poul-Henning Kamp wrote to a FreeBSD mailing list. Code style is a bicycle shed through and through.

The way to manage this that works has three steps. First, have the debate once and time-box it: one meeting or one PR, a week at most. If no decision comes out, the default of your base source wins; a default is somebody's decision anyway. Second, record the decision. A short convention document (one page) says what was chosen and why; the reasoning behind contentious decisions can be written as an ADR. The template from the post in the "Architecture Decisions" series that covers the Nygard template and MADR, "Writing ADRs: Decision Records with the Nygard Template, MADR 4.0 and Y-Statements", works here too: context, decision, consequences. Six months later, when a newcomer asks "why is var banned?", the answer is a link, not a new debate. Third, next to every rule in the document, write the tool that enforces it. A rule with no tool is a wish.

The top of Bookstore's convention document:

# Bookstore Coding Conventions

Base: dotnet/runtime C# Coding Style, Angular Style Guide (v22).
Decision log: docs/adr/0007-coding-conventions.md

| Rule                                   | Enforced by                      |
|----------------------------------------|----------------------------------|
| Explicit types, no var                 | .editorconfig IDE0008            |
| File-scoped namespaces                 | .editorconfig IDE0161            |
| _camelCase private fields              | .editorconfig IDE1006            |
| Async suffix on async methods          | .editorconfig IDE1006            |
| No this. qualification                 | dotnet format (IDE0003, not on build) |
| Formatting (C#)                        | CSharpier                        |
| Formatting (TS, HTML, SCSS)            | Prettier                         |
| Angular selector prefix: bs            | angular-eslint                   |
| Domain must not reference Infrastructure | Architecture tests             |

Changing a rule: open a PR that edits .editorconfig and this table together.

The last line matters: the way to change a rule is to change the document and the configuration in the same PR. When the document and the tool drift apart, the tool always wins because the build enforces it, and the document starts lying.

Bookstore's Rule Set

Throughout this series, Bookstore follows these decisions:

  • Explicit types: the type name instead of var for locals. Microsoft's docs and the runtime allow var when the type is obvious from the right-hand side; the team reads diffs and reviews without an IDE (in the browser), so it chose explicit types and wrote the reasoning into an ADR.
  • File-scoped namespaces and using directives outside the namespace.
  • Private fields in _camelCase, static private fields the same way (we did not adopt the runtime's s_ prefix; the team has almost no static fields).
  • Async suffix on asynchronous methods, I prefix on interfaces, PascalCase for constants.
  • Primary constructors and collection expressions are preferred where they fit, not enforced (suggestion).
  • C# formatting belongs to CSharpier, TypeScript, HTML (HyperText Markup Language) and SCSS formatting to Prettier; both are configured almost entirely with defaults.
  • Angular: the Style Guide's post-v20 naming (no type suffix in file names), selector prefix bs, inject().

Almost all of this list becomes .editorconfig in the second post, formatter configuration in the third, ESLint and Prettier in the fourth, and hooks and CI gates in the fifth.

Common Pitfalls

Leaving conventions in a wiki. Someone writes a nice Confluence page, nobody opens it two months later, and the codebase drifts away from the page. The test is simple: if violating a rule does not produce a line of output in the build, a hook or CI, that rule effectively does not exist.

Making every rule an error on day one. In an existing codebase with hundreds of violations, raising all style rules to error breaks the build, and the team either turns the rules off or silences them with blanket #pragmas. Warnings first, new code first, then gradual tightening; that is why the fifth post treats migrating a legacy codebase separately.

Debating formatting in review. A "there's an extra space here" comment loads a tool's job onto the most expensive resource, a person's attention. If there is a formatter, the comment is never written; if there is not, install the formatter first.

Leaving all of naming to the tool. An analyzer finds _bookCount and _x equally correct. The meaning of a name is review's job; the tool only guarantees the shape.

Mistaking personal IDE settings for rules. The noise in the opening example did not come from Grace doing something wrong; the repo had no rules, so her editor applied its own defaults. Rules must live in the repo, in .editorconfig and configuration files; personal settings only kick in when the repo says nothing.

Changing every style in a file at once. An "while I'm here" formatting cleanup inside a bug fix is exactly the opening 55-line diff. Formatting changes go in a separate commit, ideally a separate PR.

A Small Experiment

★☆☆ In three randomly chosen files of your own project, count how private fields are named: how many different shapes are there (_items, items, m_items)? That number tells you whether your team has an unwritten convention.

★★☆ In a PR, compare the output of git diff --stat and git diff -w --stat. The difference is indentation noise; also count the lines that remain after -w but change no behavior. These numbers show how many lines of review attention a formatter and an analyzer would save you.

★★★ Draft a one-page convention document for your team: the base source, the three most contentious decisions and their reasoning, and next to each rule the tool that will enforce it. Mark the lines without a tool; they either get connected to a tool or leave the document.

We have chosen the rules and recorded them. In the next post we turn the C# decisions into .editorconfig: the file hierarchy, code-style options, the symbols, style and severity triple of naming rules, and how to enforce these rules on build instead of leaving them in the IDE.