Skip to content
SP StackPractices
intermediate By StackPractices

Model-View-Presenter (MVP) Pattern

Separate presentation logic from the view by introducing a presenter that intermediates between the model and a passive view, enabling testable UI code.

Topics: design

Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.

Model-View-Presenter (MVP) Pattern

Overview

The Model-View-Presenter (MVP) Pattern separates an application into three components: the Model (business logic and data), the View (UI display), and the Presenter (mediator that handles user input and updates both Model and View). The View is passive — it delegates all user actions to the Presenter and is updated by the Presenter in response.

MVP is especially valuable for creating testable UI code. Since the Presenter contains all presentation logic and has no dependency on UI frameworks, it can be unit tested in isolation with mocked Views.

When to Use

Use the MVP Pattern when:

  • You need highly testable UI logic without browser or GUI dependencies
  • The view technology may change (web, desktop, mobile) but business logic remains
  • You want to keep the view as simple and passive as possible
  • Multiple views need to share the same presentation logic

When to Avoid

  • Simple UIs where the overhead of three separate components is not justified
  • Applications where the view needs to be highly reactive (MVVM is better)
  • The presenter becomes a God class that knows too much about both model and view
  • Frameworks that naturally favor other patterns (React favors component-based over MVP)

Solution

Python

from typing import Protocol

# Model
class User:
    def __init__(self, name: str, email: str):
        self.name = name
        self.email = email

class UserRepository:
    def __init__(self):
        self._users = {"1": User("Alice", "alice@example.com")}

    def find_by_id(self, user_id: str) -> User:
        return self._users.get(user_id)


# View Interface
class UserView(Protocol):
    def set_name(self, name: str): ...
    def set_email(self, email: str): ...
    def show_error(self, message: str): ...


# Presenter
class UserPresenter:
    def __init__(self, view: UserView, repository: UserRepository):
        self.view = view
        self.repository = repository

    def load_user(self, user_id: str):
        user = self.repository.find_by_id(user_id)
        if user:
            self.view.set_name(user.name)
            self.view.set_email(user.email)
        else:
            self.view.show_error("User not found")

    def save_user(self, user_id: str, name: str, email: str):
        if not name or not email:
            self.view.show_error("Name and email are required")
            return
        # Save logic...
        self.view.set_name(name)
        self.view.set_email(email)


# Concrete View (Console)
class ConsoleUserView:
    def __init__(self):
        self.name = ""
        self.email = ""
        self.error = ""

    def set_name(self, name: str):
        self.name = name
        print(f"Name: {name}")

    def set_email(self, email: str):
        self.email = email
        print(f"Email: {email}")

    def show_error(self, message: str):
        self.error = message
        print(f"Error: {message}")


# Usage
view = ConsoleUserView()
presenter = UserPresenter(view, UserRepository())
presenter.load_user("1")

Java

public class User {
    private final String name;
    private final String email;
    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }
    public String getName() { return name; }
    public String getEmail() { return email; }
}

class UserRepository {
    private final java.util.Map<String, User> users = new java.util.HashMap<>();
    public UserRepository() {
        users.put("1", new User("Alice", "alice@example.com"));
    }
    public User findById(String id) { return users.get(id); }
}

interface UserView {
    void setName(String name);
    void setEmail(String email);
    void showError(String message);
}

class UserPresenter {
    private final UserView view;
    private final UserRepository repository;

    public UserPresenter(UserView view, UserRepository repository) {
        this.view = view;
        this.repository = repository;
    }

    public void loadUser(String userId) {
        User user = repository.findById(userId);
        if (user != null) {
            view.setName(user.getName());
            view.setEmail(user.getEmail());
        } else {
            view.showError("User not found");
        }
    }

    public void saveUser(String userId, String name, String email) {
        if (name == null || email == null || name.isEmpty() || email.isEmpty()) {
            view.showError("Name and email are required");
            return;
        }
        view.setName(name);
        view.setEmail(email);
    }
}

class ConsoleUserView implements UserView {
    public void setName(String name) { System.out.println("Name: " + name); }
    public void setEmail(String email) { System.out.println("Email: " + email); }
    public void showError(String message) { System.out.println("Error: " + message); }
}

// Usage
UserView view = new ConsoleUserView();
UserPresenter presenter = new UserPresenter(view, new UserRepository());
presenter.loadUser("1");

JavaScript

class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }
}

class UserRepository {
  constructor() {
    this.users = new Map([['1', new User('Alice', 'alice@example.com')]]);
  }
  findById(id) {
    return this.users.get(id);
  }
}

class UserPresenter {
  constructor(view, repository) {
    this.view = view;
    this.repository = repository;
  }

  loadUser(userId) {
    const user = this.repository.findById(userId);
    if (user) {
      this.view.setName(user.name);
      this.view.setEmail(user.email);
    } else {
      this.view.showError('User not found');
    }
  }

  saveUser(userId, name, email) {
    if (!name || !email) {
      this.view.showError('Name and email are required');
      return;
    }
    this.view.setName(name);
    this.view.setEmail(email);
  }
}

class ConsoleUserView {
  setName(name) { console.log('Name:', name); }
  setEmail(email) { console.log('Email:', email); }
  showError(message) { console.log('Error:', message); }
}

// Usage
const view = new ConsoleUserView();
const presenter = new UserPresenter(view, new UserRepository());
presenter.loadUser('1');

Explanation

MVP divides responsibility as follows:

  • Model: Contains data structures and business rules. Has no knowledge of the UI.
  • View: Displays data and forwards user actions to the Presenter. Contains zero logic.
  • Presenter: Acts as the middleman. Receives user input from the View, manipulates the Model, and updates the View with results.

The key characteristic is that the View is passive — it does not pull data from the Model. All data flows through the Presenter.

Variants

VariantView RoleUse Case
Passive ViewView has no logic at allMaximum testability
Supervising ControllerView can bind simple properties directly to ModelReduces presenter boilerplate
MVCController handles input, View observes ModelFrameworks like Rails, Django
MVVMView binds to ViewModel propertiesReactive UIs (WPF, Vue, Angular)

Best Practices

  • Make the View an interface. This enables unit testing the Presenter with a mock View.
  • Keep the View dumb. No business logic, no data transformation, no decision-making.
  • The Presenter should not know the UI framework. It talks to an abstract View interface.
  • One Presenter per screen. Do not reuse a Presenter across unrelated views.
  • Model should be framework-agnostic. It works with or without a UI.

Common Mistakes

  • View knows about the Model. The View should only know about the Presenter.
  • Presenter leaks framework code. Importing UI toolkit classes makes testing hard.
  • The View is not passive. If the View calls Model methods directly, it is MVC, not MVP.
  • Presenter as a God class. If it grows too large, split into use-case-specific presenters.
  • Tight coupling between View and Presenter. Use interfaces so either can be swapped.

Real-World Examples

Android (early)

Before Jetpack Compose, Android developers used MVP to separate Activities (Views) from Presenters, making business logic testable without the Android emulator.

GWT (Google Web Toolkit)

GWT applications commonly used MVP to separate widget code (View) from application logic (Presenter) for testability.

WinForms / WebForms

Microsoft’s early UI frameworks used a variation of MVP where code-behind files acted as Presenters between the declarative view and the data model.

Frequently Asked Questions

Q: What is the difference between MVP and MVC? A: In MVC, the View observes the Model directly. In MVP, all communication goes through the Presenter and the View is passive.

Q: What is the difference between MVP and MVVM? A: MVVM uses two-way data binding between View and ViewModel. MVP uses explicit method calls through an interface.

Q: Can I use MVP with React? A: You can, but React’s component model naturally favors container/presentational components or hooks instead of classical MVP.