BETTER SOFTWARE DESIGN
Your Controllers Are Doing Too Much — This Is How to Simplify Them
No business logic or data access allowed. Refactor to very thin controllers for better maintainability and flexibility
I’m sure you’ve at least once before accessed a database directly from inside a controller. Perhaps you’ve also snuck some business logic in as well.
Chances are you’ve learned this practice from the many tutorials you have watched and all the documentation you’ve skimmed thru. Even Microsoft is guilty of this.
This practice doesn’t seem too bad at first as it gets you out of the gates almost immediately. However, it has some massive drawbacks such as encouraging duplicate logic and entangled responsibilities.
This is the kind of bad practice we need to eliminate.
Let me tell you this, controllers shouldn’t do anything remotely related to business logic, and directly access data stores.
The controller’s only purpose is to receive a request and return a response. Everything that goes in between is not its responsibility.
So, with regards to the statement above, you should now be aware that code like the snippet below is a complete no-go.
Accessing a data store, or even a repository, from a controller, is bound to introduce trouble down the road.
This controller is so far an incredibly simple one. It first starts to get exponentially crazier when you’re also inserting data into your database from the controller.
Your controllers should be super thin
In modern days, we’ve left the Model-View-Controller (MVC) pattern on the shelf. It’s no longer a pattern any new web application uses. Now, we’re mostly building APIs that’ll expose data to the world. We can think of our controllers as the modern-day (data-)presentation layer.
It’s simply well outside the responsibility of the controller to know how data is retrieved — such as remembering to include the user’s list of articles.
Placing data retrieval and business logic in controllers nowadays is the equivalent of placing it inside old-school views, in the context of MVC.
I’m sure you wouldn’t want to do that… so why should you do it in modern controllers?
Let’s refactor that poorly designed controller
Refactoring the messy controller requires a tad more leg work from us. I propose three simple refactoring steps.
- Create a user repository class that’ll deal with data access
- Use a poor man’s version of CQS to encapsulate business logic
- Inject a specialized query class into the controller
We’ll end up with a controller looking like this snippet below. Neat, clean, and easy to understand.
Now, the controller has no idea of how data is accessed. All it knows is it must use this
GetUsers query class.
I’ve drawn an overall solution structure to the absolute best of my abilities… I hope this will help you to understand the different parts and code snippets I’m going to show you.
So, let me give you a brief guided tour thru our system.
1 First off, some user makes a GET request to our web client’s controller at
api/users that invokes the controller action
Get(). The controller takes a dependency on a
GetUsers` class which resides in a different module — or project in .NET terms.
GetUsers class is a special query class that has a dependency on a repository interface defined inside the same module. At runtime, we’re getting a concrete implementation of the
IUserRepository` thru a dependency container.
UserRepository` implements the required interface used by
GetUsers. To retrieve the actual user objects, we need access to a specialized data store class. In the case of .NET, we’ll be using EntityFramework Core for this. It comes with a
DbContext class that much like an in-memory database. I’m sure your framework of choice has something similar to this.
Notice how the
UserRepository` now has all the knowledge required to query the database for the full user object graph — i.e. including every users’ articles.
We’ve completed pull out this knowledge from our controller, which now, only deals with accepting a request and replying with a response.
“It’s a lot more complex now”, you might say
Well, we increased the number of classes.
Some equal this to increasing complexity. However, each class now has a very narrow responsibility. Whenever we’re about to go beyond a class’ responsibility, we make a new one that’ll care only about one single functionality.
Following the separation of concerns principle, you’ll quickly realize how enjoyable it’ll be to work with your solution.
Resources for the curios
-------------------------C# Design Patterns: Command by Filip EkbergCommand in C# by refactoring.guruWhy I No Longer Use MVC Frameworks by Jean-Jacques DubrayIs Model-View-Controller Dead On The Front-End by Alex Moldovan
Nicklas Millard is a software development engineer in one of the fastest-growing banks, building mission-critical financial services infrastructure.
Previously, he was a Big4 Senior Tech Consultant developing software for commercial clients and government institutions.
Connect on LinkedIn