MVC
Splits an application into three distinct responsibilities — Model (data and business rules), View (presentation) and Controller (input and flow) — so each layer can evolve independently.
Intent
Separate the concerns of data, presentation and flow control into three distinct components, so that changes to one don't force modifications in the others. The Model doesn't know how the View displays data; the View doesn't know where the data comes from; the Controller coordinates the two without holding business logic.
MVC was created by Trygve Reenskaug at Xerox PARC in the late 1970s (in the context of Smalltalk) as a solution for building interactive graphical interfaces. Since then it migrated to desktop applications, then to server-side web (Rails, Django, Laravel, ASP.NET MVC) and, more recently, influenced variants like MVP and MVVM used in mobile and front-end development.
Problem
Without a separation of responsibilities, presentation code (HTML, formatting), business rules (validation, calculations) and control logic (routing, authentication) end up mixed together — what's known as spaghetti code or, in the web extreme, the "fat controller" or "view with SQL" problem. The practical consequences are:
- Hard to test: you can't test business rules without rendering HTML or making HTTP requests.
- Zero reuse: the same business logic can't be reused by a JSON API and by a web interface.
- High fragility: a database change breaks the template; a layout change breaks the query.
MVC attacks exactly this problem by defining clear boundaries: the Model encapsulates state and rules; the View only knows how to render; the Controller only translates user intent into calls to the Model and selection of a View.
Structure
Classic MVC (Smalltalk / desktop)
In the original MVC, the View observes the Model directly and updates itself when the Model changes — an application of the Observer pattern. The Controller translates user input (keyboard, mouse) into commands to the Model.
┌─────────────────────────────────────────────────────┐
│ User │
│ (interacts with the View) │
└──────────────────────┬──────────────────────────────┘
│ input (click, key)
▼
┌───────────┐
│Controller │ ←── interprets the action
└─────┬─────┘
│ updates
▼
┌───────────┐
│ Model │ ←── data + business rules
└─────┬─────┘
│ notifies (Observer)
▼
┌───────────┐
│ View │ ←── reads the Model and re-renders
└───────────┘
Dependencies:
Controller → Model (updates state)
View → Model (reads state to render)
Model → View (notifies via Observer — without knowing the concrete View)
Web / server-side MVC
On the web, HTTP is stateless: there's no persistent notification channel from the Model to the View. The Observer relationship disappears. The flow is always request-response and unidirectional: the Controller processes the request, queries or modifies the Model, selects a View and renders it with the data.
HTTP Request
│
▼
┌────────────────────────────────────────────────────────┐
│ Controller │
│ - receives and validates the request │
│ - calls the necessary Model(s) │
│ - selects the appropriate View │
│ - passes data to the View to render │
└────────┬───────────────────────────┬───────────────────┘
│ uses │ passes data to
▼ ▼
┌───────────┐ ┌───────────┐
│ Model │ │ View │
│ │ │ │
│- state │ │- template │
│- rules │ │- HTML │
│- queries │ │- JSON │
└───────────┘ └─────┬─────┘
│
▼
HTTP Response
Dependencies in web MVC:
Controller → Model (reads/writes data)
Controller → View (selects and feeds)
View → Model (only data passed by the Controller — no direct reference)
Model (doesn't know Controller or View)
How it works
Model
The Model represents the application's state and holds the business rules. It knows nothing about how data will be displayed or where the request came from. In web applications, the Model usually involves domain entities, data-access repositories and domain services.
A well-designed Model is testable independently — without HTTP,
without templates, without a web framework. Its public interface
exposes behaviors (order.calculateTotal(),
user.canPublish()), not just raw data.
View
The View turns the data provided by the Controller into a representation the user can consume: HTML, JSON, XML, PDF. It contains no business logic — at most, presentation logic (date formatting, pluralization, display conditionals based on flags passed by the Controller).
In modern frameworks, the View is a template (Blade, Twig, ERB, Razor) or a UI component. The principle is the same: the View is passive — it receives data and renders it.
Controller
The Controller is the mediator. It receives input (HTTP request, UI event), superficially validates parameters, delegates the real work to the Model and selects which View to use with which data. The Controller should be thin: if there's business logic in the Controller, it's in the wrong place.
When to use
- Server-side web applications with template rendering: it's the dominant pattern in frameworks like Laravel, Rails, Django, ASP.NET MVC and Spring MVC. The Model/View/Controller separation is native to the framework's structure.
- When the same Model needs to be exposed by multiple Views: a JSON API and an HTML interface can share the same Model — only the Controller and the View change.
- For teams that split responsibilities: back-end handles the Model, front-end handles the templates (View), and the Controller defines the contract between the two.
When to avoid
- Applications with complex domain logic: MVC alone doesn't define where application logic, use cases, or per-use-case authorization rules live. For complex systems, MVC is usually the starting point, but needs to be complemented with Layered Architecture or Hexagonal Architecture so the domain doesn't leak into the Controller.
- SPAs (Single Page Applications): when the front-end is managed by React, Vue or Angular, server-side MVC loses relevance for the presentation layer. The server becomes a REST/GraphQL API — the relevant pattern lives on the client (reactive MVVM, Flux, etc.).
- When the "View" is a plain JSON API with no UI state: the concept of View as a template doesn't apply. A pure API is better modeled with an Application Service layer plus explicit serialization.
Pros and cons
Pros
- Clear separation of responsibilities: Model, View and Controller evolve independently.
- Model fully testable without any presentation infrastructure.
- Enables parallel work: back-end and front-end developers can work in parallel with a defined contract.
- Native support in virtually every modern web framework — a low learning curve for anyone who already knows the framework.
- Multiple Views for the same Model without duplicating logic.
Cons
- Doesn't define where application logic (use cases, orchestration) lives — this gap is often filled poorly, fattening the Controller.
- In large applications, the MVC separation is insufficient: you need to combine it with Layered or Hexagonal Architecture to isolate the domain from infrastructure.
- The concept of "View" is ambiguous in pure REST APIs — the pattern applies with adaptations.
- Can give a false sense of organization: having
models/,views/andcontrollers/folders doesn't guarantee the logic is in the right places.
Common pitfalls
1. Fat Controller
The most common pitfall: all business and orchestration logic ends up in the Controller because it's the "easiest" place to add code. The Controller ends up with hundreds of lines of validation, calculations, database calls, email sending and response formatting. The result is a Controller that's impossible to unit test and impossible to reuse.
Rule of thumb: if the Controller does more than (1) validate input parameters, (2) call a service/use case and (3) select the response, the excess logic belongs in the Model or in an application service. Controllers should be thin enough to be irrelevant to business-rule tests.
2. Business logic in the View
Templates with complex conditionals, total calculations, discount
application and permission validation indicate the View has been
overloaded. The View should receive data already ready for display —
or, at most, presentation flags (canEdit: true)
calculated by the Controller or the Model.
3. Anemic Model
The opposite of the fat controller: the Model becomes just a data container (getters and setters), with no behavior at all. All logic goes into the Controller or service classes. The result is that the "Model" doesn't model anything — it's just a DTO. Prefer rich models that encapsulate invariants and behaviors relevant to the domain.
4. Direct coupling between the View and the database
SQL queries or ORM calls inside templates (the classic N+1 problem in legacy PHP views) are the clearest sign that responsibilities have collapsed. Every query should go through the Model; the View receives only already-resolved collections.
Related architectures and patterns
- Observer
- Layered Architecture
- Hexagonal Architecture (Ports & Adapters)
- MVP (Model-View-Presenter)
- MVVM (Model-View-ViewModel)
Observer is the mechanism that lets the Model notify Views in classic MVC without depending on them concretely — the Model fires events; Views, registered as observers, update themselves. On the server-side web that relationship disappears, since there's no persistent channel.
Layered Architecture complements MVC by defining how the Model is structured internally: Presentation (Controller + View), Application (use cases), Domain (entities and rules) and Infrastructure (database, external APIs). MVC without clear layers inside the Model tends to result in fat controllers.
Hexagonal Architecture goes further and inverts the dependencies: the domain defines ports (interfaces), and the adapters (HTTP Controllers, database adapters) implement those ports. The MVC Controller becomes a primary adapter — and the domain stays completely isolated from the web.
MVP (Model-View-Presenter) and MVVM (Model-View-ViewModel) are evolutions of MVC for reactive UI contexts (Android, iOS, front-end with declarative frameworks). The Presenter and the ViewModel take on the role of mediating the View in a more bidirectional way than the classic MVC Controller allows.