Vertical Slices or Controllers: What You Actually Trade
Controller to service to store means a one-line change touches three folders. Vertical slices fix that, at the cost of some duplication and a conventions problem you should solve early.
Most ASP.NET Core APIs in the wild look the same: a controller calls a service layer, the service layer calls a store. It works. It is also the shape where a one-line change touches three folders and the service layer mostly passes arguments through.
Vertical slices flip that. One folder per feature, and each folder owns its endpoint, its contract, and its logic. Minimal APIs make it a natural fit, because there is no controller class asking to be the center of the universe.
I like slices. That does not make them free, so here is the trade as I see it.
What slices get you
- A change usually touches one folder.
- Features don’t depend on each other, so they are easy to add, change, or delete.
- No pass-through service layer.
Program.csstarts to read like a table of contents instead of a junk drawer.
What they cost
- Some duplication between slices. Each one maps its own response, for example.
- Logic shared by several slices needs a deliberate home, or it gets copied.
- Without team agreement on conventions, slices drift into different styles.
- They are unfamiliar at first to teams used to layers.
The second one is the one that bites. Duplication across slices is fine until three of them need the same rule, and then somebody has to decide where it lives. Decide that on purpose, early, or you will find the same check written four slightly different ways.
When controllers still make sense
- Large existing controller codebases where the migration cost outweighs the benefit.
- Teams that lean on MVC features like model binding conventions, filters, and
[ApiController]behaviors across many endpoints. - Mixed MVC apps where views and APIs share a lot of infrastructure.
You don’t have to pick all at once
This is the part people miss. Controllers and minimal API slices run side by side in the same app. You can move one feature, prove callers can’t tell the difference, and move the next one. No big-bang rewrite, no freeze.
Migrate a slice at a time and let the tests tell you when a feature is done. When the last controller goes, retire MVC. If it never goes, that is also a valid answer.
The point
Slices are a better default for new minimal API work, not a mandate. The cost is real but small, and it is mostly a conventions problem. Agree on where shared logic lives before you need it.