Understanding MVC for real: stop bloating your controllers
What each layer of MVC is actually responsible for, and how to keep your controllers thin.
After a while I would open a controller file and ask myself, “when did this get so big?” Database queries, business logic, format conversions, email sending — all tangled together inside a single method. I thought I was using the MVC (Model-View-Controller) pattern, but in practice I had turned the controller into the one place that did everything.
In this post I want to explain what the three layers of MVC actually mean, and where responsibilities should live. It took me several passes and the pain of a real project to internalize this; hopefully it can save you some of that time.
What is MVC and what does each layer do?
MVC (Model-View-Controller) is a software architecture pattern that divides an application into three distinct layers.
- Model: The application’s data and business rules. It talks to the database, validates data, and performs calculations.
- View: Produces the output shown to the user. HTML, JSON, XML — whatever the format, its only job is presentation.
- Controller: The bridge between request and response. It receives the incoming request, calls the appropriate model method, and passes the result to the view.
On paper this looks clean. The problem is how easy it is for the controller to become the place that “knows everything.”
What should the controller NOT do?
When an HTTP request arrives, the controller should not:
- Execute raw SQL queries directly
- Make business-logic decisions (“if the user is a premium member, do this”)
- Send emails or notifications
- Perform complex data transformations
It is perfectly possible to write all of this inside the controller — PHP will not stop you. But a few months later when you open that method again, 150 lines of code will greet you and you will spend real time figuring out what each part does.
A bloated controller method looks something like this:
public function store(Request $request)
{
// Validation
if (empty($request->input('email'))) {
return redirect()->back()->with('error', 'E-posta zorunlu.');
}
// Write to the database
$user = new User();
$user->name = $request->input('name');
$user->email = $request->input('email');
$user->save();
// Send email
mail($user->email, 'Hoş geldiniz', 'Kayıt oldunuz.');
return redirect('/dashboard');
}
Three different responsibilities inside a single method. If validation changes, you open this method. If the email template changes, you come back here. If the user registration logic changes, you are here again.
What should move to the Model layer?
All business logic related to a user should live on the model side. A simple example:
class User extends Model
{
public static function kaydet(array $veri): self
{
$user = new self();
$user->name = $veri['name'];
$user->email = $veri['email'];
$user->save();
return $user;
}
public function hosGeldinGonder(): void
{
mail($this->email, 'Hoş geldiniz', 'Kayıt oldunuz.');
}
}
Now the controller does nothing but coordinate:
public function store(Request $request)
{
$user = User::kaydet($request->only('name', 'email'));
$user->hosGeldinGonder();
return redirect('/dashboard');
}
The controller no longer shows what is being done — it shows in what order calls are made. It is much easier to read.
Keeping the View layer clean
The same kind of mess can happen on the view side. Making business-logic decisions inside the view — templates full of growing conditionals like “if the user is an admin, show this button” — quickly becomes hard to manage.
The view should only ask: “How do I display the data I was given?” Deciding which data to send is the controller’s job. How that data was calculated is the model’s.
Small methods, single responsibility
A practical way to keep controller methods small is to ensure each one does exactly one thing. Registration, update, deletion — each gets its own method, and each method is a handful of lines. When a method starts growing in line count, that is a signal that something is off.
When I first started applying this, I struggled with “where do I put this?” The answer was almost always the model, or a separate helper class. Over time I memorized where to look, and following the code became much easier.
MVC is not a rule — it is a guideline. But understanding the purpose of the guideline is far more valuable than following it blindly.
Comments
Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.