Keep It in the Controller
As long as you keep things tidy, keeping business logic in your controllers is just fine.
In web development, it’s often recommended to keep business logic out of your controllers so you can separate your domain logic from the HTTP request/response cycle. While this principle holds, the insistence that domain logic must always live outside the controller may not always be necessary.
It’s not that controllers should only handle HTTP requests but rather that your domain logic should be unaware of HTTP requests. Decoupling the business logic from the request makes the business logic easier to understand and reuse. However, you can achieve this separation without immediately extracting. You can instead can structure the code so that the business logic doesn’t reference HTTP-specific details.
The decision to extract up front is driven comes from our assumption that we will need to repeat that controller’s logic. However, often that controller will be the only place we use that specific logic. Other usecases often have slightly different circumstances that require changes to the workflow. So, when they pop up, we introduce flags and other options so the extracted code can handle multiple scenarios. What started as a clean, straightforward function becomes more and more complex with each new scneario that’s supported.
By waiting to see how things play out we can avoid this complexity. If we can let the potential duplication linger we give ourselves a chance to see if the duplication is real or coincidental. As long as we keep the busines logic separate from HTTP logic in the controller method, extracing the common logic later will be easy.
Conincidental duplication is often the source of complex, hard-to-follow code. You think you’re making things easier for yourself, but 2 months later you realize that what you thought was a single concept repeated 3 times is actually 2 different but similar concepts. The result is awkward abstractions that don’t clearly model your domain.
Ultimately, this approach is about caution. It’s not just about keeping your domain logic out of your controllers—it’s about avoiding premature abstraction. Keep the separation between HTTP logic and domain logic clear, but give yourself the flexibility to adapt and refactor as you learn more about the actual needs of your application.


Great article! I’m a huge fan of this approach as well. I’ve really come to love integration-style tests