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.
- 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
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.
- 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)
with() on a relationship in a controller can silently degrade performance — add 100ms per request.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.
- 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)
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.with() on relationships to avoid N+1 — a single missing with can add 100ms per request in production.$_GET or $_POST directly — use $request->input() for testability.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.
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.ProductService and assert the controller calls the correct methods.App\Exceptions\Handler handles ModelNotFoundException gracefully.if chains for different user roles, that logic belongs in policies or form requests.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 () to avoid the N+1 problem.with()
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.
getTotalAttribute are cached per model instance — they don't re-query unless you reload the relationship. Use with('items') before accessing $order->total.Order::where('status', 'paid')->get(), write Order::paid()->get().set{Name}Attribute) are great for normalising data, e.g., hashing passwords or trimming whitespace.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 or compact(). Avoid using the with()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.
AppServiceProvider instead of passing it in every controller.storage/framework/views. Run php artisan view:clear after moving or renaming files.@json to pass PHP data to JavaScript inline — it auto-escapes.@php blocks inside Blade for business logic — that's a sign the logic belongs in the controller or model.layouts/app.blade.php)components/)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).
- 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
DB::transaction) are critical for atomic operations — without them, a payment could succeed while inventory fails to update.The Fat Controller That Brought Down a Payment API
DiscountService, used eager loading (with('items','discounts')), and refactored the Controller to three lines: validate, call service, return response. Timeout vanished.- 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).
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.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.DB::enableQueryLog(). Identify loops that call relationships without eager loading. Add ->with('relationship') to the initial query. Use $model->load('relationship') if already retrieved.Key takeaways
with() to eliminate the N+1 problem.findOrFail.Common mistakes to avoid
4 patternsPutting business logic in controllers
Forgetting to eager load relationships
with('relationship') on the initial query. If already retrieved, call $model->load('relationship').Using `@php` or `<?php` in Blade for business logic
Not using route model binding
Order::findOrFail($id) calls in controllers; routes are verbose.Interview Questions on This Topic
Describe the full request lifecycle in Laravel MVC, from URL to response.
Frequently Asked Questions
That's Laravel. Mark it forged?
4 min read · try the examples if you haven't