MVP
An evolution of MVC where the View is completely passive — it delegates all presentation logic to the Presenter, which mediates it through an interface, making UI logic fully testable without instantiating the screen.
Intent
Remove all presentation logic from the View and move it to the Presenter, so the View becomes "dumb" enough to be replaced by a mock in tests. The Presenter knows the View only through a narrow interface — and never through references to concrete widgets or UI components.
MVP emerged in the early 1990s in work by Mike Potel (Taligent) and was popularized in the Android and Windows Forms worlds as a solution to a problem classic MVC left open: in web MVC, the Controller was thin and delegated to the Model, but in desktop applications the View tended to accumulate presentation logic — color choices, formatting, button enabling — mixed together with UI events.
MVP solves this directly: the View implements an interface (e.g.,
IUserView) that exposes only display and event-notification
operations. The Presenter receives that interface by injection and
uses it to read data from the Model and push results back to the
View. The View never talks to the Model; the Presenter is the sole
mediator.
Problem
In graphical interfaces (desktop, mobile, complex web forms), classic MVC doesn't clearly define who's responsible for presentation logic — that layer between "raw data" and "pixel on screen": value formatting, conditional enabling of controls, form validation, error-message selection. The practical consequences are:
- View with presentation business code: the View starts containing conditionals like "if the balance is negative, show it in red" — logic that belongs to whoever knows the state, not whoever draws pixels.
- Zero testability of UI logic: to test whether the error message displays correctly, you need to instantiate the whole screen — with a graphical framework, a UI thread, a click event. The feedback loop is slow and fragile.
- Direct View-Model coupling: if the View reads the Model directly (as in classic MVC with Observer), any change in the model can force a change in the View — the two evolve coupled.
MVP solves these three problems: the Presenter is testable with an IView mock; the View doesn't know the Model; and presentation logic has a defined place — in the Presenter.
Structure
┌────────────────────────────────────────────────────────────────┐
│ User │
│ (interacts with the View) │
└───────────────────────────┬────────────────────────────────────┘
│ event (click, input)
▼
┌─────────────────────────────────────────────────────────────┐
│ VIEW │
│ - implements IView (narrow interface) │
│ - displays data pushed by the Presenter │
│ - delegates ALL events to the Presenter │
│ - does NOT know the Model │
└───────────────────────────┬─────────────────────────────────┘
│ notifies events via IView
▼
┌─────────────────────────────────────────────────────────────┐
│ PRESENTER │
│ - receives IView by injection (never the concrete class) │
│ - contains all presentation logic │
│ - queries/updates the Model │
│ - pushes results by calling IView methods │
└───────────────────────────┬─────────────────────────────────┘
│ uses
▼
┌─────────────────────────────────────────────────────────────┐
│ MODEL │
│ - data + business rules │
│ - completely unaware of the View and the Presenter │
└─────────────────────────────────────────────────────────────┘
Dependencies:
View → IView (implements the interface)
Presenter → IView (depends on the abstraction, not the concrete View)
Presenter → Model (reads and updates data)
Model → (nobody) (doesn't know View or Presenter)
Variant: Passive View
In the Passive View variant (the most common and strictest one), the View has absolutely no logic at all: it only exposes properties (text fields, labels, button state) that the Presenter reads and writes. The View doesn't format data, doesn't validate input, doesn't make visual decisions — all of that is the Presenter's responsibility. The payoff is maximum testability: the Presenter can be tested with a trivial IView mock.
Variant: Supervising Controller
In the Supervising Controller variant (or Supervising Presenter), the View is a bit less passive: it performs simple, direct binding with the Model for trivial cases (like displaying a field's name with no transformation), while the Presenter steps in only for more complex presentation logic. This variant is a middle ground between MVP and MVVM — it gains simplicity in the Presenter at the cost of a small residual coupling of the View to the Model.
How it works
The IView interface
The centerpiece of MVP is the interface the View implements. It
must be narrow — only the methods the Presenter actually needs to
call. It shouldn't expose widgets, DOM elements, framework
components or any concrete UI type. It should speak in terms of
data and intent: displayUserName(name: string),
showEmailError(message: string),
enableSaveButton(enabled: boolean).
The Presenter
The Presenter is injected with the IView (and with the Model or repository it needs). It registers itself as a listener for the View's events — or the View calls Presenter methods directly when the user acts. When an event occurs, the Presenter runs the logic, queries the Model if needed, and calls the IView's methods to update the screen. The Presenter never instantiates the View; it receives it ready-made.
Code example
// ── MODEL — data and business rules ──────────────────────────
class UserModel {
findById(id: string): { name: string; email: string } | null {
// hits a repository; the View never calls this directly
return { name: 'Ana', email: 'ana@example.com' };
}
}
// ── IVIEW — narrow interface the View implements ──────────────
// Only display and notification operations — no concrete widgets.
interface IProfileView {
displayName(name: string): void;
displayEmail(email: string): void;
showError(message: string): void;
enableEditing(enabled: boolean): void;
}
// ── PRESENTER — all presentation logic ─────────────────────────
class ProfilePresenter {
constructor(
private readonly view: IProfileView,
private readonly model: UserModel,
) {}
loadProfile(id: string): void {
const user = this.model.findById(id);
if (!user) {
this.view.showError('User not found.');
this.view.enableEditing(false);
return;
}
this.view.displayName(user.name);
this.view.displayEmail(user.email);
this.view.enableEditing(true);
}
}
// ── CONCRETE VIEW — implements IProfileView ────────────────────
// Could be DOM, React, Android Activity, Windows Form, etc.
class ProfileViewDOM implements IProfileView {
displayName(name: string): void {
document.getElementById('name')!.textContent = name;
}
displayEmail(email: string): void {
document.getElementById('email')!.textContent = email;
}
showError(message: string): void {
document.getElementById('error')!.textContent = message;
}
enableEditing(enabled: boolean): void {
(document.getElementById('btn-edit') as HTMLButtonElement)
.disabled = !enabled;
}
}
// ── MOCK VIEW — used in the Presenter's unit tests ─────────────
class MockProfileView implements IProfileView {
displayedName = '';
displayedError = '';
editingEnabled = false;
displayName(name: string): void { this.displayedName = name; }
displayEmail(_email: string): void { /* ignored in the test */ }
showError(msg: string): void { this.displayedError = msg; }
enableEditing(e: boolean): void { this.editingEnabled = e; }
}
<?php
// ── MODEL — data and business rules ──────────────────────────
class UserModel
{
public function findById(string $id): ?array
{
// hits a repository; the View never calls this directly
return ['name' => 'Ana', 'email' => 'ana@example.com'];
}
}
// ── IVIEW — narrow interface the View implements ──────────────
// Only display and notification operations — no concrete widgets.
interface IProfileView
{
public function displayName(string $name): void;
public function displayEmail(string $email): void;
public function showError(string $message): void;
public function enableEditing(bool $enabled): void;
}
// ── PRESENTER — all presentation logic ─────────────────────────
class ProfilePresenter
{
public function __construct(
private readonly IProfileView $view,
private readonly UserModel $model,
) {}
public function loadProfile(string $id): void
{
$user = $this->model->findById($id);
if ($user === null) {
$this->view->showError('User not found.');
$this->view->enableEditing(false);
return;
}
$this->view->displayName($user['name']);
$this->view->displayEmail($user['email']);
$this->view->enableEditing(true);
}
}
// ── CONCRETE VIEW — implements IProfileView ────────────────────
// Could be a Blade template, a Livewire component, etc.
class ProfileViewHtml implements IProfileView
{
private string $name = '';
private string $email = '';
private string $error = '';
private bool $editingEnabled = false;
public function displayName(string $name): void { $this->name = $name; }
public function displayEmail(string $email): void { $this->email = $email; }
public function showError(string $message): void { $this->error = $message; }
public function enableEditing(bool $enabled): void { $this->editingEnabled = $enabled; }
public function render(): string
{
// All HTML output generated here — no presentation logic
$nameEsc = htmlspecialchars($this->name, ENT_QUOTES, 'UTF-8');
$emailEsc = htmlspecialchars($this->email, ENT_QUOTES, 'UTF-8');
$errorEsc = htmlspecialchars($this->error, ENT_QUOTES, 'UTF-8');
$disabled = $this->editingEnabled ? '' : 'disabled';
return "<p>{$nameEsc}</p><p>{$emailEsc}</p>"
. "<p class='error'>{$errorEsc}</p>"
. "<button {$disabled}>Edit</button>";
}
}
Note that ProfilePresenter can be unit tested by
instantiating a MockProfileView and a
UserModel with fake data — no graphical framework, no
DOM, no HTTP. That's MVP's central payoff.
When to use
- Desktop or mobile applications with complex presentation logic: forms with conditional validation, dynamic control enabling, rule-dependent formatting — these are scenarios where the Presenter has real work to do and testability without a physical UI is an immediate win.
- When UI-logic testability is a priority: MVP lets you test all the visual behavior (what shows, when it shows, in what state) without instantiating a single widget or opening a browser. For legacy Android or Windows Forms applications, it was the main testing strategy.
- When the View can be swapped without changing the logic: by defining IView as a contract, you can have a graphical View for production and a console View for tests or scripts — the Presenter doesn't change.
When to avoid
- Modern frameworks with native data binding (Vue, Angular, React + state management): in those environments, declarative binding eliminates the need for the Presenter to explicitly "push" data to the View. The right pattern becomes MVVM or reactive-state-based architectures (Flux, Redux, Signals).
- Views with minimal or no logic: if the View is just a static template that renders data without meaningful conditional logic, the Presenter layer is unnecessary — a simple MVC is more productive.
-
Teams unfamiliar with dependency inversion: the
IView is a contract that needs to stay consistent between View
and Presenter. If the team isn't disciplined, the interface leaks
concrete UI details (parameters of type
HTMLElementorButton), destroying the testability MVP promises.
Pros and cons
Pros
- Presenter fully testable without instantiating the real View — just an IView mock is enough.
- Passive View: can be swapped (DOM, Android, console) without changing the Presenter.
- Clear separation between presentation logic (Presenter) and rendering (View).
- Model fully isolated from the presentation layer — the View never touches it directly.
- Explicit contracts via IView ease parallel work between whoever builds the UI and whoever implements the logic.
Cons
- More infrastructure code: every View needs an IView interface, a Presenter and Presenter tests — MVP multiplies artifacts even for simple screens.
- Risk of a fat Presenter: without discipline, all application logic migrates into the Presenter, which ends up orchestrating use cases, validating business rules and formatting data — mixing responsibilities that belong to the Model.
- IView maintenance: every screen change that requires a new piece of data pushed by the Presenter requires updating the interface, the concrete View and the test mock.
- Less suited to modern reactive UIs: the "Presenter calls view.display()" flow is imperative and doesn't naturally fit observable-state-based architectures.
Common pitfalls
1. Fat Presenter
The most frequent pitfall in MVP. The Presenter starts focused on presentation, but gradually accumulates calls to external services, business validation rules, orchestration of multiple use cases and even data transformations that belong to the Model. The result is a Presenter with hundreds of lines mixing presentation, application and domain concerns. The rule of thumb is clear: the Presenter should delegate to the Model (or to application services) anything that isn't a decision about how to display data on screen.
Rule of thumb: if the Presenter contains business calculations, database queries or direct external API calls (without going through services or repositories), it's fat. Extract those responsibilities to the Model or to an application layer.
2. IView that leaks concrete widgets
If the IView interface defines methods like
getEmailTextField(): TextField or takes parameters of
type Button or HTMLInputElement, the
Presenter ends up depending on the graphical framework — and
testability collapses. IView should speak only in terms of data and
intent: getTypedEmail(): string,
enableConfirmButton(enabled: boolean): void. No
concrete UI type should appear in the interface's parameters or
return values.
3. View that bypasses the Presenter
In applications with easy access to the repository or the Model (via global dependency injection or a singleton), it's tempting for the View to call the Model directly "just to read a quick piece of data." That bypass destroys MVP's central invariant: the View doesn't know the Model. Once introduced, that exception tends to multiply, degrading the pattern into a poorly structured MVC.
4. Confusing MVP with renamed MVC
In classic web MVC (Laravel, Rails, Django), the Controller receives the request, calls the Model and passes data to the View to render. The View can even read data from the Model directly in some frameworks. In MVP, the View never knows the Model — that's the defining distinction. If your "Presenter" still passes the whole Model object to the View to render, you have MVC with a renamed Controller, not MVP.
Related architectures and patterns
- MVC
- MVVM (Model-View-ViewModel)
- Observer
- Humble Object (coming soon)
MVC is MVP's direct predecessor. The key difference is the role of the View: in classic MVC, the View can observe the Model directly (via the Observer pattern) and update itself without going through the Controller; in MVP, the View never touches the Model — the Presenter is the sole link between the two. MVP can be seen as an MVC where the Controller was renamed to Presenter and the View was made completely passive.
Observer is the mechanism MVP uses so the View can notify the Presenter of user events: the View fires events (click, submit, typing) and the Presenter is the registered observer. Alternatively, the View calls Presenter methods directly — but even then the Observer pattern is implicit in the platform's event model (DOM events, Android listeners).
MVVM can be understood as an evolution of MVP for environments with native data binding. The key difference: in MVP the Presenter pushes data to the View by explicitly calling IView methods; in MVVM the ViewModel exposes observable state and the binding syncs the View automatically — the ViewModel doesn't call the View, it doesn't know it. In modern reactive UIs (Vue, Angular, SwiftUI, Compose), MVVM is more natural than MVP.
Humble Object is a testability pattern described by Gerard Meszaros that formalizes exactly what MVP practices: separating hard-to-test code (the View with a graphical framework) from easy-to-test code (the Presenter with pure logic). The View in MVP is literally a Humble Object — it has no logic of its own worth testing.