Modernize Your Code, Modernize Your Thinking
Why “One Class per File” Is
Outdated—and What Modern C# Developers Should Do Instead
For decades, C# developers have lived by a simple rule:
“One class or interface per file.”
This rule was so common that many of us (myself included) never
questioned it. It became part of our coding DNA—just like putting braces on
their own line (hello, 2005) or creating separate IService and Service folders.
But today, we’re writing code in C# 12, designing systems with Vertical
Slice Architecture, navigating code with AI-powered tools, and
building applications that emphasize features, not ceremony.
Yet this ancient rule still lingers.
Let’s be honest:
✔ It made sense years
ago.
✘ It makes far less
sense today.
If you want to modernize your codebase, you must begin by modernizing
your mindset.
Let’s dive in.
🏛️ The Legacy of “One Class per File”
This rule grew out of a different era—an era when:
✔ IDEs were primitive
Early Visual Studio couldn’t navigate symbols well. Finding things meant
scrolling. Lots of scrolling.
✔ Java influenced the
C# community
Java requires one public class per file. Many C# devs carried that
thinking forward.
✔ We architected with
large, rigid OOP systems
Everything was broken into dozens of micro-interfaces and boilerplate
classes.
✔ Partial classes
created forced separation
WinForms, WebForms, and auto-generated EF entities all split files
automatically.
Back then, this rule made sense.
But today?
❌ It creates clutter
❌ It works against
feature-based architectures
❌ It leads to “file
explosion”
❌ It reduces cohesion
instead of improving it
Modern C# encouraging something better.
🚀 The Modern Philosophy
Use one file per concept, not
one file per class.
A “concept” can include multiple related types:
- A command + validator
- A request + response + handler
- A DTO + enum + mapper
- A small set of helpers only used
by one feature
This is how modern C# is intended to be used.
✨ Modern Example: The Vertical Slice Way
Old thinking:
Break a feature into four separate files:
UpdateExpenseGroupBudgetHandler.cs
UpdateExpenseGroupBudgetRequest.cs
UpdateExpenseGroupBudgetResponse.cs
UpdateExpenseGroupBudgetValidator.cs
Sure, it's organized…
but it’s not cohesive.
Modern thinking:
Use one file per feature.
/ExpenseGroup/UpdateExpenseGroupBudget.cs
And inside:
namespace ExpenseGroup;
public static class UpdateExpenseGroupBudget
{
// Request DTO
public record Request(int GroupId, decimal Revenue, decimal Margin);
// Response DTO
public record Response(bool Success);
// Validator
public class Validator: AbstractValidator<Request>
{
public Validator()
{
RuleFor(x =>
x.Revenue).GreaterThanOrEqualTo(0);
RuleFor(x =>
x.Margin).GreaterThanOrEqualTo(0);
}
}
// Handler
public class Handler: IRequestHandler<Request, Response>
{
private readonly IExpenseService _service;
public Handler(IExpenseService service)
{
_service = service;
}
public async Task<Response> Handle(Request request,
CancellationToken ct)
{
await
_service.UpdateGroupBudgetAsync(request.GroupId, request.Revenue,
request.Margin);
return new Response(true);
}
}
}
Why this is better:
- You understand the entire
feature without jumping across files
- Everything required to maintain
this feature is in one place
- Fewer files = less mental load
- AI tools can reason about your
feature more easily
- Visual Studio / VS Code keeps
files relevant and small
This is modern C# the way it’s meant to be written.
🧩 When should you still separate classes?
Modern does NOT mean messy.
You should use separate files when:
✔ A class is large
(>300 lines)
✔ A class is reused
across slices
Domain entities, repository interfaces, mapper profiles, and global helpers.
✔ A type is important
enough to deserve its own location
If it matters independently, give it its own space.
The rule isn’t dead—it’s just no longer universal.
🔥 Example: When NOT to combine
// Don't put all of this in one file:
public class Project {}
public class Employee {}
public class Region {}
public class District {}
These are domain types, not feature types.
They belong in their own files because they are reused everywhere.
💡 The Mental Shift
Modern architecture—especially Vertical Slice—forces a new mindset:
❌ Don’t organize by
technical type
(Models folder, Interface folder, Services folder)
✔ Organize by user
behavior
(Features → Commands → Queries → UI → Integration)
It’s about workflow, not taxonomy.
That’s what modernizing your thinking looks like.
🧠 Why This Matters Even More Today
Two big reasons:
1. Feature-first development
Modern systems minimize coupling and maximize independence.
Feature-level files support this goal.
2. AI-assisted development
Copilot, Cursor, GPT, and Sonnet reason more effectively when:
- The feature lives in one place
- Dependencies are visible in one
file
- Request/Response/Handler/Validator
are co-located
One-file-per-feature actually improves your AI developer experience.
🛠 Modernizing your codebase: a quick checklist
✔ Consolidate related
types into single feature files
✔ Use file-scoped
namespaces
✔ Adopt Vertical
Slice Architecture
✔ Keep domain models
in their own files
✔ Let AI tools help
refactor toward feature cohesion
✔ Treat “one class
per file” as optional, not law
🎯 Final Thoughts
The phrase I keep coming back to is:
“Modernize your code by modernizing your thinking.”
Clinging to old conventions slows down development.
C# has evolved.
Your architecture has evolved.
AI-augmented tooling has evolved.
It's time our coding habits evolve right along with them.
If you're still organizing your codebase like it's 2003…
you’re missing out on the clarity, speed, and elegance that modern C# offers.

Comments
Post a Comment