The first thing I do when I inherit a codebase is open the folder tree and look at the top level. Not the README, not the architecture diagram somebody drew two reorganizations ago. The folder tree. It tells me what the team actually believed the software was about, and nine times out of ten it lies. You get a top level that reads Controllers, Services, Models, Repositories, and Validators, and you learn precisely nothing about what the system does. It could be a payments ledger or a pet grooming scheduler. Same skeleton either way.
I have come around, hard, to organizing code by feature instead of by technical layer. This is not a fashionable opinion in every .NET shop, where the layered template has been the default since roughly forever. But I have watched feature-based structure quietly reduce merge pain, shrink the blast radius of changes, and cut the time it takes a new hire to make a safe change from days to hours on the teams I run. Here is the case, caveats included, because it is not a free lunch.
The tyranny of the layer
The layered layout groups files by what kind of thing they are. All controllers live together, all services live together, all EF entities live together. It feels tidy the way a hardware store is tidy: every screw in its bin. The problem is that nobody ships a screw. They ship a shelf, and building the shelf means walking to six different bins.
Take a real change: we needed to add a partial-refund flow to a merchant settlement service. That one feature touched a controller, a request DTO, a validator, a domain service, two repository methods, an EF configuration, and a background job that reconciled the refund against the acquirer file. In the layered structure those eight files sat in eight different directories, sometimes in different projects. To understand the feature you had to hold the whole solution in your head and jump around it like a scavenger hunt. A new engineer took the better part of a week to feel safe touching it.
The deeper issue is that the layer boundary is real to the framework and imaginary to the business. Nobody has ever filed a ticket that says "the validators are wrong." They say "refunds over the original amount are getting through." That bug lives in a feature, and the feature is scattered.
What feature-based actually means
Feature-based structure inverts the grouping. The top level is a list of the things your system does. Payments. Settlements. Refunds. Disputes. Onboarding. Inside each of those folders you keep everything that feature needs, regardless of its technical role. The controller, the handler, the validators, the DTOs, the domain types, and the persistence bits for that slice all sit together.
Here is the shape I reach for on a typical .NET service. Notice that the folder names read like a product roadmap, not a design-patterns glossary.
src/
Features/
Refunds/
IssueRefund/
IssueRefundEndpoint.cs
IssueRefundCommand.cs
IssueRefundHandler.cs
IssueRefundValidator.cs
RefundPolicy.cs
Reconcile/
ReconcileRefundsJob.cs
AcquirerFileParser.cs
RefundsDbContext.cs
Settlements/
...
Shared/
Money.cs
Result.cs
Time/IClock.cs
The unit of organization is the vertical slice: a single use case with all of its collaborators nearby. If you have used MediatR, this pairs naturally with the request-handler pattern, but you do not need MediatR to do it. The point is not the library. The point is that when I open the IssueRefund folder, I can read the entire feature top to bottom without alt-tabbing across the solution.
The blast radius argument
This is the reason I care most, because it is the one that shows up in incident reviews. When code for a feature is colocated, the blast radius of a change is legible. If I touch the IssueRefund folder, I have a strong prior that I have affected refund issuance and nothing else. The diff is contained, the reviewer knows what they are reviewing, and the tests that matter are right there next to the code.
In a layered structure, a change to a shared service class can ripple into features nobody remembered were using it. I once watched a one-line change to a generic AmountFormatter, which lived in a Utilities folder that fourteen features quietly depended on, break the way we rendered fees on statements in a single currency. It shipped on a Thursday. We found it the following Tuesday when a merchant in Warsaw complained. The change looked safe because the file gave no hint of who its customers were.
Code that changes together should live together. If two files are always edited in the same pull request, putting them in different folders is not organization, it is filing your left and right shoes in separate rooms.
Coupling you can see
Feature folders make coupling visible, and visible coupling is coupling you can manage. When something in the Refunds feature needs something from Settlements, that dependency crosses a folder boundary you can actually see in the import statement. It becomes a design conversation: should Refunds really know about Settlements internals, or should there be a published contract between them?
In the layered world those dependencies are invisible because everything already lives in the same Services folder, so reaching across features costs nothing and shows nothing. That is how you end up with a big ball of mud where the "domain services" layer is really one giant graph of mutual dependencies. The structure did not cause the coupling. It hid it. Hidden coupling is the kind that survives to production.
A useful rule I enforce in review: a feature folder may depend on Shared, and it may depend on another feature only through that feature's public contract, never through its internals. C# does not enforce this for free, so we back it up with an architecture test.
[Fact]
public void Features_should_not_reference_each_others_internals()
{
var result = Types.InAssembly(typeof(Program).Assembly)
.That().ResideInNamespace("App.Features.Refunds")
.ShouldNot()
.HaveDependencyOn("App.Features.Settlements.Internal")
.GetResult();
Assert.True(result.IsSuccessful, string.Join(", ",
result.FailingTypeNames ?? Enumerable.Empty<string>()));
}
The onboarding dividend
The clearest signal came from onboarding. On a layered service, a new backend engineer's first ticket usually turned into a two-day archaeology dig. On the feature-organized settlement service that replaced it, I started handing new hires a single folder on day one and saying "own this." They could read the whole slice, run its tests, and make a scoped change without needing the whole system in their head first.
That is not a soft benefit. Faster time-to-first-safe-change is one of the few onboarding metrics that actually correlates with a team's throughput six months later. When the structure of the code mirrors the structure of the product, you can hand people a bounded piece of the product and they can reason about it. The folder tree becomes documentation that cannot go stale, because it is the thing that runs.
Where it hurts
I promised caveats, and there are real ones. Feature-based structure is not a magic organizing principle that makes decisions for you. It forces harder questions earlier, and if you dodge them you get a mess with different-shaped edges.
- Deciding what a feature is. The boundaries are a design act, not a mechanical one. Slice too fine and you get four hundred folders with one file each. Slice too coarse and you have rebuilt the ball of mud with a nicer name. This takes judgment and it takes revisiting.
- The Shared folder wants to eat everything. Every genuinely cross-cutting concern goes there, and if you are not disciplined, Shared quietly becomes the new god layer. I keep Shared boring on purpose: Money, Result, a clock abstraction, and not much else.
- Cross-feature transactions. When a single database transaction must span features, colocation does not save you. You still have to make a deliberate call about who owns the transaction and how the features coordinate.
- Some things really are layers. Auth middleware, logging, the outbound HTTP pipeline. Forcing genuinely horizontal concerns into a feature folder is as silly as forcing features into layers.
A pragmatic migration, not a rewrite
Nobody should stop the roadmap to reorganize folders. That is a great way to spend a quarter delivering nothing and annoying everyone. The way I have done this twice now is opportunistic. New features get born feature-shaped. When you touch an old area for a real ticket, you pull that slice out of the layers as part of the work, tests and all. Over eight or nine months the layered core shrinks to a stubborn residue of genuinely shared plumbing, which is exactly where it belongs.
The trick is to resist the urge to make it a project. It is a habit, enforced in code review, not a migration with a Gantt chart. If a pull request adds a new controller to the old Controllers folder when it could have started a clean feature slice, that is a review comment, not a crisis. Small, constant pressure in one direction beats a heroic reorg every time, because the heroic reorg has no test coverage for the fear it produces.
The honest scorecard
Is any of this scientifically proven to make software better? No, and I distrust anyone who tells you their folder layout is a silver bullet. What I can tell you from running these teams is narrower and, I think, more useful. Feature-based structure did not make my engineers smarter. It made the consequences of their changes easier to see, and it put the things that change together within arm's reach of each other.
Layered structure optimizes for a question almost nobody asks: "show me all the validators." Feature structure optimizes for the question every engineer asks every day: "where does refunds live, and what will I break if I change it?" That trade, for the work my teams actually do, is not close.

Conclusion
If you want to know which way a codebase leans, do not read its docs. Grep its import statements. In a healthy feature-based service most dependencies point down into Shared and only occasionally sideways through a published contract. In a sick one they form a dense sideways web, and no amount of folder naming will save you from that. The structure is a mirror, not a cure. But a good mirror is worth a great deal, because it shows you the coupling while you can still afford to fix it, instead of on a Thursday, in Warsaw, five days too late.