Senior 4 min · March 06, 2026

Laravel MVC — Fat Controller Payment API Timeout

POST /api/payments timed out after 30 seconds due to a fat controller triggering 50+ N+1 queries.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • MVC separates concerns: Model (data), View (presentation), Controller (orchestrator)
  • Route maps URL to Controller method
  • Controller delegates to Model, then returns a View with data
  • Eloquent ORM is the default Model layer — rich relationships and query scopes
  • Blade templates handle View logic—layouts, components, and inheritance
  • Performance trap: N+1 queries in relationships; use eager loading (with())
  • Production insight: fat controllers are the #1 maintainability killer; move logic to models or services
Plain-English First

Imagine a restaurant. The customer (browser) tells the waiter (Controller) what they want. The waiter goes to the kitchen (Model) to get the data, then brings back a beautifully plated dish (View) to the table. Nobody expects the customer to cook, and nobody expects the chef to serve — everyone has one job. That's MVC: three clear roles so your code never becomes a tangled mess where everything does everything.

Every Laravel app you've ever used — a blog, an e-commerce store, a SaaS dashboard — is built on a pattern called MVC. It's not a Laravel invention; it's been around since the 1970s. But Laravel takes that pattern and makes it feel so natural that most developers follow it without even realising they're applying a decades-old architectural principle. Understanding it deeply is what separates a developer who 'writes Laravel' from one who 'thinks in Laravel'.

Before MVC, web apps were a nightmare to maintain. PHP files mixed SQL queries, HTML markup, and business logic all in one place. Change the database table? You'd hunt through fifty files. Redesign the frontend? You'd break the data layer. MVC solves this by enforcing a strict separation of concerns — each layer has one job and only one job, which means changes in one layer rarely ripple into the others.

By the end of this article you'll be able to trace exactly how a browser request travels through a Laravel application from route to response, understand why fat controllers are an anti-pattern, know when logic belongs in a Model versus a Controller versus a Service class, and write code that a teammate can pick up and understand without a walkthrough.

What is Laravel MVC Pattern?

MVC (Model-View-Controller) is a software architectural pattern that separates an application into three interconnected components. Laravel embraces this pattern as its default architecture, providing a clear structure: routes map URLs to controller methods, controllers delegate to models (often via Eloquent ORM) for data operations, and return rendered views (Blade templates) as HTTP responses. This separation ensures that changes in one layer (e.g., database schema) do not ripple into the presentation layer without explicit handling.

In a typical Laravel application, a request travels: public/index.php → HTTP Kernel → Router → Middleware → Controller → Model → View → Response. Each component has a single responsibility. The controller is a thin orchestrator; the model encapsulates business logic and data access; the view handles only presentation logic.

Understanding this flow is critical because common production issues — slow pages, untestable code, inconsistent data — often stem from violating this separation.

app/Http/Controllers/PostController.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

namespace Io\Thecodeforge\Http\Controllers;

use Io\Thecodeforge\Models\Post;
use Illuminate\Http\Request;

class PostController extends Controller
{
    public function index()
    {
        $posts = Post::with('author')->latest()->paginate(10);
        return view('posts.index', compact('posts'));
    }
}
Output
Fetches posts with eager-loaded author, passes to Blade view.
The MVC Mindset
  • Route is the menu — directs the order to the right chef
  • Controller is the cook — reads the order, fetches ingredients (Model), and plates the meal (View)
  • Model is the pantry — stores and manages the ingredients (data and logic)
  • View is the plate — arranges the ingredients for the customer (browser)
Production Insight
Route caching in production speeds up URL matching by 10x but requires controller routes, not closures. Always use controller routes for endpoints that benefit from caching.
A missing with() on a relationship in a controller can silently degrade performance — add 100ms per request.
Fat controllers are the most common violation; extract logic to services or models early.
Key Takeaway
MVC is about separation of concerns.
Controllers orchestrate but don't own business logic.
Use routes with controllers for cacheability.
Where does this logic belong?
IfThe logic queries the database or performs calculations
UsePut it in a Model (scopes, accessors, relationships) or a Service class
IfThe logic handles HTTP request parsing, validation, or authentication
UsePut it in the Controller or a Form Request
IfThe logic formats data for display (currency, dates, truncation)
UsePut it in Blade or a custom Blade directive
IfThe logic involves third-party API calls or complex workflows
UsePut it in a dedicated Service class (e.g., PaymentService)

The Request Lifecycle in Laravel MVC

Every request hits public/index.php, then the HTTP kernel. The router matches the URL to a route definition and dispatches it to the appropriate Controller method. That method interacts with Models (often via Eloquent) and returns a View with data. This flow is strict — the Controller never echoes HTML, the Model never handles HTTP input, and the View never writes to the database.

Here's a concrete example of a clean Controller that plays by the rules:

```php <?php

namespace Io\Thecodeforge\Http\Controllers;

use Io\Thecodeforge\Models\Order; use Illuminate\Http\Request;

class OrderController extends Controller { public function show(Order $order, Request $request) { // Implicit route model binding — Laravel fetches the Order by ID $items = $order->items()->with('product')->get(); return view('orders.show', compact('order', 'items')); } } ```

Notice the Controller is thin: it only orchestrates. The Model (Order) contains the relationship definition. The View (orders.show) renders the HTML. No SQL, no raw HTML in the Controller.

app/Http/Controllers/OrderController.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

namespace Io\Thecodeforge\Http\Controllers;

use Io\Thecodeforge\Models\Order;
use Illuminate\Http\Request;

class OrderController extends Controller
{
    public function show(Order $order, Request $request)
    {
        $items = $order->items()->with('product')->get();
        return view('orders.show', compact('order', 'items'));
    }
}
Output
Returns a rendered Blade view with the order and its items.
The Sandwich Pattern
  • Route is the menu — directs the order to the right chef
  • Controller is the cook — reads the order, fetches ingredients (Model), and plates the meal (View)
  • Model is the pantry — stores and manages the ingredients (data and logic)
  • View is the plate — arranges the ingredients for the customer (browser)
Production Insight
Route caching (php artisan route:cache) speeds up route matching by 5-10x but won't work with closures. Use controllers for all routes that need caching.
Always use with() on relationships to avoid N+1 — a single missing with can add 100ms per request in production.
Never access $_GET or $_POST directly — use $request->input() for testability.
Key Takeaway
A request flows: URL → Route → Controller → Model → Controller → View → Response.
Keep the Controller as a thin orchestrator.
Eager load in the Controller, not in the View.
Where does this logic belong?
IfThe logic queries the database or performs calculations
UsePut it in a Model (scopes, accessors, relationships) or a Service class
IfThe logic handles HTTP request parsing, validation, or authentication
UsePut it in the Controller or a Form Request
IfThe logic formats data for display (currency, dates, truncation)
UsePut it in Blade or a custom Blade directive
IfThe logic involves third-party API calls or complex workflows
UsePut it in a dedicated Service class (e.g., PaymentService)

Controllers: The Orchestrators (Not the Brains)

A Controller's job is to accept an HTTP request, coordinate with Models or Services, and return a response. That's it. Fat controllers with 300 lines of business logic are the #1 maintainability problem in Laravel apps.

Use resource controllers for standard CRUD operations. They enforce a clear pattern: index, create, store, show, edit, update, destroy. Each method should be under 15 lines.

Dependency injection via the constructor or method injection ensures your controller is testable. Never new Service() inside a controller method — let Laravel's service container resolve it.

If you find yourself writing a controller method that does more than three things (validate input, call a service/model, return a view), break it down.

app/Http/Controllers/ProductController.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php

namespace Io\Thecodeforge\Http\Controllers;

use Io\Thecodeforge\Services\ProductService;
use Illuminate\Http\Request;

class ProductController extends Controller
{
    public function __construct(
        private ProductService $productService
    ) {}

    public function store(Request $request)
    {
        $validated = $request->validate([
            'name' => 'required|string|max:255',
            'price' => 'required|numeric|min:0',
        ]);

        $product = $this->productService->create($validated);

        return redirect()->route('products.show', $product);
    }
}
Output
Validates, delegates to ProductService, then redirects.
Fat Controller Trap
Every time you add a foreach loop or a complex arithmetic calculation inside a controller, ask yourself: does this really belong here? If not, move it to a model accessor, a service, or a presentation layer.
Production Insight
Inject services via constructor — it makes unit testing trivial. Mock ProductService and assert the controller calls the correct methods.
Resource controllers automatically generate error pages for 404 and 403 — make sure your App\Exceptions\Handler handles ModelNotFoundException gracefully.
If a controller action starts with if chains for different user roles, that logic belongs in policies or form requests.
Key Takeaway
A controller method should read like a recipe — ingredients in, steps delegated, dish served.
Keep methods short: validate → delegate → return.
Use resource controllers and dependency injection.

Models: Where the Business Logic Lives

Models are not just database representations. They are the core of your domain logic. Eloquent models can hold relationships, scopes, accessors, mutators, and custom query methods. This is where you define how your data behaves.

For example, an Order model can have a total accessor that sums its items. A paid scope that filters unpaid orders. A sendConfirmation method that triggers a notification. All of this belongs in the model, not the controller.

Use Eloquent's relationship methods (hasMany, belongsTo, morphMany) to define how models connect. Then leverage eager loading (with()) to avoid the N+1 problem.

Validation rules that are tied to the model's state belong in store or update methods in a Form Request, but computed properties belong in the model.

app/Models/Order.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php

namespace Io\Thecodeforge\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Order extends Model
{
    protected $fillable = ['customer_id', 'status'];

    public function items(): HasMany
    {
        return $this->hasMany(OrderItem::class);
    }

    public function scopePaid($query)
    {
        return $query->where('status', 'paid');
    }

    public function getTotalAttribute(): float
    {
        return $this->items->sum(fn ($item) => $item->price * $item->quantity);
    }

    public function sendConfirmation(): void
    {
        // Logic to send email — could delegate to a Notification class
    }
}
Output
Encapsulates logic: total calculation, scoping, and behaviour.
Production Insight
Accessors like getTotalAttribute are cached per model instance — they don't re-query unless you reload the relationship. Use with('items') before accessing $order->total.
Scopes keep your controllers clean. Instead of Order::where('status', 'paid')->get(), write Order::paid()->get().
Mutators (set{Name}Attribute) are great for normalising data, e.g., hashing passwords or trimming whitespace.
Key Takeaway
Fat models are good — encapsulate business logic in models.
Use relationships, scopes, accessors, and mutators.
Eager load relationships to prevent N+1.
Should this code go in the Model?
IfIt queries or manipulates the database
UseModel (scope, relationship, custom method)
IfIt formats a field for display (e.g., date format, currency)
UseModel accessor or a Blade directive
IfIt involves complex multi-step logic across multiple models
UseService class, not model
IfIt handles input validation or authentication
UseForm Request or Controller, not model

Views: Blade Templates, Layouts, and Components

Blade is Laravel's powerful templating engine. It extends PHP with control structures, layouts, components, and partials. Views should only contain presentation logic — loops, if/else, formatting — never raw SQL queries or complex business math.

Use layouts for common page structure. Define sections with @yield or use Blade components for reusable UI pieces. Components can accept data and have their own logic encapsulated in a component class.

Always pass data from the controller to the view using compact() or with(). Avoid using the View::share() globally except for shared navigation data.

Avoid writing complex PHP in Blade. If you need a helper, create a Blade directive or a dedicated view composer.

resources/views/orders/show.blade.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
@extends('layouts.app')

@section('content')
    <h1>Order #{{ $order->id }}</h1>

    <ul>
        @foreach($items as $item)
            <li>{{ $item->product->name }} x {{ $item->quantity }} — ${{ number_format($item->price, 2) }}</li>
        @endforeach
    </ul>

    <p><strong>Total:</strong> ${{ number_format($order->total, 2) }}</p>
@endsection
Output
Renders an order detail page without fancy logic.
Use view composers for shared data
If every view needs the user's cart count, register a View Composer in AppServiceProvider instead of passing it in every controller.
Production Insight
Blade template compilation caches views in storage/framework/views. Run php artisan view:clear after moving or renaming files.
Use @json to pass PHP data to JavaScript inline — it auto-escapes.
Avoid using @php blocks inside Blade for business logic — that's a sign the logic belongs in the controller or model.
Key Takeaway
Views are for presentation: loops, conditionals, formatting.
Use layouts and components for reusable structure.
Never write database queries or business logic in Blade.
Where to place this template piece?
IfUsed across multiple pages (header, footer, sidebar)
UseDefine in a layout file (layouts/app.blade.php)
IfReusable UI element (button, card, modal)
UseBlade component (components/)
IfInline formatting that depends on model data
UseBlade directive or accessor, not a helper function

Advanced MVC: Service Layer and Repositories

When controllers get too heavy or models start to hold business logic that spans multiple models, introduce a service layer. Services are plain PHP classes (often without extending Eloquent) that encapsulate complex operations.

For example, an OrderService can handle placeOrder() which creates the order, deducts inventory, charges the customer, and sends confirmation. The controller calls $orderService->placeOrder($request).

Repositories are an additional abstraction if you need to switch between databases or mock the data layer in tests. In most Laravel apps, Eloquent is sufficient, so repositories can add unnecessary complexity. Use them only when you have a clear reason (multiple data sources, heavy caching logic).

app/Services/OrderService.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?php

namespace Io\Thecodeforge\Services;

use Io\Thecodeforge\Models\Order;
use Io\Thecodeforge\Models\Product;
use Illuminate\Support\Facades\DB;

class OrderService
{
    public function placeOrder(array $data, int $customerId): Order
    {
        return DB::transaction(function () use ($data, $customerId) {
            $order = Order::create([
                'customer_id' => $customerId,
                'status' => 'pending',
            ]);

            foreach ($data['items'] as $item) {
                $product = Product::findOrFail($item['product_id']);
                $product->decrement('stock', $item['quantity']);

                $order->items()->create([
                    'product_id' => $product->id,
                    'quantity' => $item['quantity'],
                    'price' => $product->price,
                ]);
            }

            // Charge the customer (third-party API call)
            // $this->paymentService->charge($order, $data['payment_token']);

            $order->update(['status' => 'paid']);

            return $order;
        });
    }
}
Output
Wraps complex multi-model operation in a transaction.
Service as a Sandwich Chef
  • Controller reads the order and hands it to the chef
  • Service chef orchestrates many ingredients (models, APIs)
  • Model pantry stores the raw ingredients
  • View plate presents the final dish
Production Insight
Services should be injected into controllers via constructor injection. Use Laravel's service container to bind interfaces to implementations for test swaps.
Transactions (DB::transaction) are critical for atomic operations — without them, a payment could succeed while inventory fails to update.
Don't over-engineer: if a service has only one method and that method is only called from one controller, consider keeping the logic in the controller (if it's still under 20 lines).
Key Takeaway
Service layer extracts complex business logic from controllers and models.
Use it for multi-model workflows, third-party integrations, and atomic transactions.
Don't add repositories unless you need to swap data sources.
● Production incidentPOST-MORTEMseverity: high

The Fat Controller That Brought Down a Payment API

Symptom
POST /api/payments timed out after 30 seconds for large invoices. CPU spiked on the web server. Eloquent queries were executed one by one inside loops.
Assumption
The developer assumed that putting all logic in the Controller made it easier to debug. They thought moving it to a Service class would 'add complexity'.
Root cause
The Controller method had over 200 lines — it calculated discount rules, fetched related records in N+1 fashion, and formatted the response manually. Each request triggered 50+ separate database queries.
Fix
Extracted discount logic into a DiscountService, used eager loading (with('items','discounts')), and refactored the Controller to three lines: validate, call service, return response. Timeout vanished.
Key lesson
  • Keep Controllers thin — they should only parse input, call a service or model, and return a response.
  • Never do N+1 loops inside a Controller. Use Eloquent relationships with eager loading.
  • If a Controller method exceeds 20 lines, extract the logic into a dedicated class (Service, Action, or Repository).
Production debug guideSymptom → Action guide for common MVC-related problems3 entries
Symptom · 01
View shows no data — variables are null or undefined
Fix
Check the Controller action: ensure you pass data via view('name', compact('var')) or view('name')->with('var', $value). Use dd($var) in the Controller before returning the view to verify the variable exists.
Symptom · 02
404 error for existing routes
Fix
Run php artisan route:list to confirm the route is registered. Check that the route URI matches exactly (including trailing slashes). Verify the Controller method name is correct and the namespace is properly set in RouteServiceProvider.
Symptom · 03
N+1 query problem — page loads slowly with many queries
Fix
Enable Laravel Debugbar or log queries with DB::enableQueryLog(). Identify loops that call relationships without eager loading. Add ->with('relationship') to the initial query. Use $model->load('relationship') if already retrieved.
★ Laravel MVC Quick Debug Cheat SheetImmediate actions and commands for common MVC runtime issues
Request not reaching Controller (500 error before any output)
Immediate action
Check Laravel logs in `storage/logs/laravel.log` for the full stack trace
Commands
tail -n 100 storage/logs/laravel.log
php artisan config:clear && php artisan route:clear
Fix now
Often a cached config or route file is stale; clearing caches resolves most mid-deployment issues.
View not found — Blade compilation error+
Immediate action
Verify the view path and name: `view('folder.name')` maps to `resources/views/folder/name.blade.php`
Commands
php artisan view:clear
Check file permissions: `ls -la resources/views/folder/name.blade.php`
Fix now
Run composer dump-autoload if the error persists after clearing views.
Eloquent relationship returns null unexpectedly+
Immediate action
Dump the SQL query to see what's being executed
Commands
DB::enableQueryLog(); $model->relation; dd(DB::getQueryLog());
Check foreign key column names: the default `model_id` must exist and be populated.
Fix now
Add ->with('relation') to ensure eager loading, or verify the relationship definition in the Model.
Laravel MVC Components
ComponentResponsibilityExample in LaravelCommon Pitfall
RouteMap URL to Controller methodRoute::get('/orders', [OrderController::class, 'index'])Route closures can't be cached for performance
ControllerAccept request, delegate, return responseOrderController with index(), show(), store()Fat controllers (>20 lines per method)
ModelBusiness logic, relationships, data accessOrder extends Model with scopes and accessorsN+1 queries when relationships are not eager loaded
ViewPresentation, HTML renderingorders/index.blade.php using Blade directivesComplex PHP logic inside @php blocks
ServiceOrchestrate complex workflowsPaymentService::charge($order)Over-engineering when not needed

Key takeaways

1
MVC in Laravel enforces strict separation
Controllers orchestrate, Models hold business logic, Views handle presentation.
2
Keep controllers thin—aim for methods under 15 lines. Delegate to Models or Services.
3
Eager load relationships with with() to eliminate the N+1 problem.
4
Blade is for presentation only—never put raw queries or complex logic in templates.
5
Use resource controllers for standard CRUD operations to keep routes and methods predictable.
6
Service classes are your friend for multi-model workflows and third-party integrations—they keep controllers and models clean.
7
Route model binding reduces boilerplate—use it instead of manual findOrFail.

Common mistakes to avoid

4 patterns
×

Putting business logic in controllers

Symptom
Controller methods exceed 50 lines, repeated logic across actions, unit tests become integration tests.
Fix
Extract logic to model scopes/accessors or a Service class. Aim for controller methods under 15 lines.
×

Forgetting to eager load relationships

Symptom
Slow page loads with hundreds of individual queries (N+1). Laravel Debugbar shows many duplicate queries.
Fix
Always use with('relationship') on the initial query. If already retrieved, call $model->load('relationship').
×

Using `@php` or `<?php` in Blade for business logic

Symptom
Views become unreadable, logic is hard to test, and separation of concerns is broken.
Fix
Compute data in the Controller or Model and pass it to the view. Use Blade directives for repeated formatting.
×

Not using route model binding

Symptom
Explicit Order::findOrFail($id) calls in controllers; routes are verbose.
Fix
Type-hint the model in the controller method signature—Laravel auto-resolves it from the route parameter.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Describe the full request lifecycle in Laravel MVC, from URL to response...
Q02JUNIOR
What is the N+1 problem in Eloquent and how do you solve it?
Q03SENIOR
Explain the difference between using a Service class and putting logic i...
Q04SENIOR
Why are 'fat controllers' considered an anti-pattern in Laravel?
Q05SENIOR
How does Laravel's service container support dependency injection in con...
Q01 of 05SENIOR

Describe the full request lifecycle in Laravel MVC, from URL to response.

ANSWER
1. Public/index.php bootstraps the application. 2. The HTTP kernel handles the request. 3. The router matches the URL to a route definition and dispatches to the assigned Controller method. 4. The Controller interacts with Models (Eloquent) or Services to fetch/manipulate data. 5. The Controller returns a View (Blade template) with data. 6. Laravel sends the rendered HTML back to the browser. Middleware can intercept before/after the controller.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is Laravel MVC Pattern in simple terms?
02
Can I use MVC without Eloquent?
03
When should I create a resource controller?
04
What is the difference between `compact()` and `view()->with()`?
05
How do I test a Laravel MVC application?
🔥

That's Laravel. Mark it forged?

4 min read · try the examples if you haven't

Previous
Introduction to Laravel
2 / 15 · Laravel
Next
Laravel Routing