Skip to main content

Introducing Part 2 — what Fat Controller and Fat Model mean

Part 1 covered PHP class design principles that do not depend on any framework. Part 2, starting with this chapter, applies those principles to real Laravel code.

If you are unsure about the Laravel basics

Part 2 assumes you have used Laravel's basic MVC structure, routing, and basic CRUD with Eloquent. If you are unsure, we recommend first reading Introduction and Laravel API development basics from A Practical Guide to Laravel × DDD × Clean Architecture.

What a Fat Controller is​

Look at the OrderController below.

❌ Bad: a Fat Controller
class OrderController extends Controller
{
public function store(Request $request)
{
$validated = $request->validate([
'items' => 'required|array',
'customer_email' => 'required|email',
]);

$total = 0;
foreach ($validated['items'] as $item) {
$total += $item['price'] * $item['quantity'];
}

foreach ($validated['items'] as $item) {
$stock = DB::table('stocks')->where('name', $item['name'])->value('quantity');
if ($stock < $item['quantity']) {
return response()->json(['error' => 'Not enough stock'], 422);
}
}

$order = Order::create([
'customer_email' => $validated['customer_email'],
'total' => $total,
]);

Mail::raw("Thank you for your order. Your total is {$total} yen.", function ($message) use ($validated) {
$message->to($validated['customer_email'])->subject('Order confirmation');
});

return response()->json($order, 201);
}
}

Validation, calculating the total, checking stock, creating the order, and sending the confirmation email are all packed into the single store method. A controller that has grown fat by accumulating business logic like this is called a Fat Controller1. It is one of the most commonly seen anti-patterns in MVC.

What a Fat Model is​

What happens if you think "let us slim the controller down" and move the processing straight into the model?

❌ Bad: now it is a Fat Model instead
class Order extends Model
{
public static function placeOrder(array $items, string $customerEmail): self
{
$total = 0;
foreach ($items as $item) {
$total += $item['price'] * $item['quantity'];
}

foreach ($items as $item) {
$stock = DB::table('stocks')->where('name', $item['name'])->value('quantity');
if ($stock < $item['quantity']) {
throw new RuntimeException('Not enough stock');
}
}

$order = self::create([
'customer_email' => $customerEmail,
'total' => $total,
]);

Mail::raw("Thank you for your order. Your total is {$total} yen.", function ($message) use ($customerEmail) {
$message->to($customerEmail)->subject('Order confirmation');
});

return $order;
}
}

The processing merely moved from the controller to the model; several responsibilities are still packed into one method. This is the same structural problem as the god class from chapter 1.

Taylor Otwell, the creator of Laravel, has spoken in favor of keeping controllers thin and models fat2. But "fat" here does not mean a high line count; it means a rich public API that calls out to other services and commands. Distinguish it from the Order class above, which packs the distinct concerns of validation, stock checking, and notification into one method.

What Part 2 covers​

Part 2 covers the responsibilities of an Eloquent model in chapter 9 and splitting responsibilities into service classes in chapter 10, working toward a design that avoids both the Fat Controller and the Fat Model.

What it does not go into:

  • DDD tactical patterns (value objects, entities, aggregates, the repository pattern, and so on)
  • Layered Clean Architecture implementations (the use case layer, the presentation layer, managing transaction boundaries, and so on)

That territory belongs to A Practical Guide to Laravel × DDD × Clean Architecture. The service class extraction in chapter 10 also stays at the class design level of splitting up a class that has grown too big, and does not step into use case layer design such as managing transaction boundaries or publishing domain events.

Summary​

  • A controller that has grown fat by accumulating business logic is called a Fat Controller
  • Simply moving the processing into the model does not change the packing-in structure, and can produce a Fat Model instead
  • Part 2 applies the principles so far to Laravel to design around both
  • DDD tactical patterns and layered Clean Architecture implementations belong to an existing series and are left alone

The next chapter covers the responsibilities of an Eloquent model. We look at how to decide which responsibilities belong in a model and which do not.

Exercises​

For each of the following, say whether a controller, a model, or a service class is the right place, and why
  1. Validating the request
  2. The whole sequence of checking stock and creating the order
  3. Saving to the database (an Eloquent query)

Sample answer

  1. Controller (or Form Request): checking the shape of the request is entry-point processing that happens in front of the controller
  2. Service class: a sequence that spans several concerns is the responsibility of a service class, covered in chapter 10
  3. Model: persisting individual pieces of data is the Eloquent model's own responsibility

Chapters 9 and 10 cover these criteria in detail.


Footnotes​

  1. Source: Put your Laravel controllers on a diet. The Fat Controller is known as one of the most common anti-patterns in MVC. ↩

  2. Source: Taylor Otwell: "Thin" Controllers, "Fat" Models Approach. ↩