
MVP & MVVM: Making Views Smarter
MVC's Controller got too fat. Split into Presenter/ViewModel, auto-update with Data Binding. Core pattern of modern frontend.

MVC's Controller got too fat. Split into Presenter/ViewModel, auto-update with Data Binding. Core pattern of modern frontend.
Why does my server crash? OS's desperate struggle to manage limited memory. War against Fragmentation.

Two ways to escape a maze. Spread out wide (BFS) or dig deep (DFS)? Who finds the shortest path?

Fast by name. Partitioning around a Pivot. Why is it the standard library choice despite O(N²) worst case?

Establishing TCP connection is expensive. Reuse it for multiple requests.

When I first learned React, useState blew my mind.
const [count, setCount] = useState(0);
setCount(count + 1); // DOM auto-updates
I'd been coding with jQuery for 3+ years. I learned that updating the UI meant this:
$('#counter').text(count);
$('#status').html('Updated!');
$('#timestamp').text(new Date());
For every button click, I manually updated 10+ DOM elements. But React said "just change the variable, I'll handle the rest." It felt like magic.
A senior dev in my community said "that's MVVM. All modern frontend works this way."
So I dug in. MVC, MVP, MVVM... they all have Model-View, but what's actually different? And why did jQuery become "old-school"? Why did everyone switch to React/Vue? I wanted to understand exactly why.
MVC, MVP, MVVM... they all start with Model-View and differ by one letter. Initially I thought "are these just different names for the same pattern?" They all have Model and View, right?
ViewModel confused me the most. "Model for the View" - but how's that different from a regular Model? They're both just data, aren't they?
// Model
const user = { id: 1, name: 'John' };
// ViewModel
const userViewModel = { name: 'John' };
// Wait, what's the difference...?
"Auto-sync with Data Binding" sounded abstract. What does "sync" mean here? How's it different from just assigning a variable?
let name = 'Alice';
// Is this Data Binding? Or not?
Presenter vs ViewModel... both sit between View and Model. Diagrams showed:
MVP: View ←→ Presenter ←→ Model
MVVM: View ←→ ViewModel ←→ Model
They look identical! Just different names?
A senior engineer explained it with a metaphor that clicked instantly.
MVC: TV (Model) and remote control (Controller) are completely separate.
- Press button → Controller sends signal to Model → TV screen changes
- You can't tell the current channel by looking at the TV. Remote shows nothing either.
MVP: The remote (Presenter) got smarter.
- Press button → Presenter tells View "display 9"
- Then Presenter tells Model (TV) "switch to channel 9"
- Still manual. Presenter has to command everything step-by-step.
MVVM: Remote has a tiny screen (ViewModel) built-in.
- Press 9 on remote screen → TV automatically switches to channel 9
- This is Data Binding. Remote screen and TV are "connected."
- Change one side → other side auto-updates!
That metaphor made me go "OH, so that's why React's setCount auto-updates the screen!"
I also thought about restaurants.
MVC (Traditional Restaurant)This metaphor clarified what "auto-sync" really means.
There's a famous joke among iOS developers.
"Know what MVC stands for?" "Model-View-Controller?" "No, Massive View Controller."
It was real. When I first took over an iOS project, the ViewController.swift file was 2,800 lines.
// ViewController.swift (2,800 lines of nightmare)
class ViewController: UIViewController {
// ===== View stuff =====
@IBOutlet var nameLabel: UILabel!
@IBOutlet var emailTextField: UITextField!
@IBOutlet var profileImageView: UIImageView!
@IBOutlet var saveButton: UIButton!
@IBOutlet var cancelButton: UIButton!
@IBOutlet var loadingSpinner: UIActivityIndicatorView!
// ===== Business logic =====
func validateEmail(_ email: String) -> Bool {
let regex = "^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}$"
// ... 50 lines of validation
}
func validatePassword(_ password: String) -> Bool {
// ... 30 lines
}
func hashPassword(_ password: String) -> String {
// ... 20 lines
}
// ===== Network calls =====
func fetchUser(userId: Int) {
let url = URL(string: "https://api.example.com/users/\(userId)")!
URLSession.shared.dataTask(with: url) { data, response, error in
// ... 100 lines of network handling
}.resume()
}
func uploadProfileImage(_ image: UIImage) {
// ... 150 lines
}
// ===== UI updates =====
func updateUI() {
nameLabel.text = user.name
emailTextField.text = user.email
profileImageView.image = user.profileImage
saveButton.isEnabled = isFormValid
// ... manually updating 50+ UI elements
}
// ===== Delegate methods =====
func textFieldDidChange(_ textField: UITextField) { ... }
func textFieldDidBeginEditing(_ textField: UITextField) { ... }
func textFieldShouldReturn(_ textField: UITextField) -> Bool { ... }
// ... 30 delegate methods
}
Everything in one file.
Looking at MVC's structure:
View → Controller ← Model
Controller knows both View and Model. So naturally all logic gravitates to Controller.
Eventually Controller becomes the project's trash can.
// How do you test this function?
func saveUser() {
let name = nameTextField.text! // depends on UI
let email = emailTextField.text!
if validateEmail(email) { // business logic
let user = User(name: name, email: email)
api.save(user) { result in // network
self.showAlert("Saved!") // back to UI
}
}
}
To test this you need:
Unit testing impossible. UI, business logic, and network are all tangled together.
2. UnreusableWant to show User info on different screens?
// UserProfileViewController.swift
func displayUser() {
nameLabel.text = user.name
}
// UserDetailViewController.swift
func displayUser() {
nameLabel.text = user.name // copy-pasted same code
}
No way to reuse logic. Controller inherits from UIViewController, tightly coupled to a specific screen.
3. Collaboration HellTeammate A: editing ViewController.swift (lines 1-1000)
Teammate B: editing ViewController.swift (lines 1500-2000)
Me: editing ViewController.swift (lines 800-1200)
→ Git Merge Conflict
→ 3 hours resolving conflicts...
Everything in one file meant daily Git conflicts.
These problems led to MVP.
Completely separate View and Model. All communication only through Presenter.
View ←→ Presenter ←→ Model
Critical rules:
// Model
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
save() {
return fetch('/api/users', {
method: 'POST',
body: JSON.stringify({ name: this.name, email: this.email })
});
}
}
// View Interface (methods View must implement)
// Presenter only communicates through this interface
interface UserView {
getNameInput(): string;
getEmailInput(): string;
displayUser(name: string, email: string): void;
showLoading(): void;
hideLoading(): void;
showError(message: string): void;
}
// Presenter
class UserPresenter {
constructor(view, model) {
this.view = view; // only knows View interface
this.model = model;
}
onSaveButtonClick() {
// Get data from View
const name = this.view.getNameInput();
const email = this.view.getEmailInput();
// Validation (business logic)
if (!this.validateEmail(email)) {
this.view.showError('Invalid email');
return;
}
// Update Model
this.model.name = name;
this.model.email = email;
this.view.showLoading();
// Save
this.model.save()
.then(() => {
this.view.hideLoading();
this.view.displayUser(name, email);
})
.catch(err => {
this.view.hideLoading();
this.view.showError(err.message);
});
}
validateEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
}
// View Implementation (HTML/DOM)
class UserViewImpl {
constructor(presenter) {
this.presenter = presenter;
this.nameInput = document.getElementById('name');
this.emailInput = document.getElementById('email');
this.saveBtn = document.getElementById('save');
this.loadingSpinner = document.getElementById('loading');
this.errorDiv = document.getElementById('error');
// Wire up events
this.saveBtn.onclick = () => {
this.presenter.onSaveButtonClick();
};
}
getNameInput() {
return this.nameInput.value;
}
getEmailInput() {
return this.emailInput.value;
}
displayUser(name, email) {
this.nameInput.value = name;
this.emailInput.value = email;
}
showLoading() {
this.loadingSpinner.style.display = 'block';
}
hideLoading() {
this.loadingSpinner.style.display = 'none';
}
showError(message) {
this.errorDiv.textContent = message;
this.errorDiv.style.display = 'block';
}
}
// Usage
const model = new User('John', 'john@example.com');
const presenter = new UserPresenter(null, model);
const view = new UserViewImpl(presenter);
presenter.view = view;
// Mock View
class MockUserView {
constructor() {
this.displayedName = null;
this.displayedEmail = null;
this.errorMessage = null;
}
getNameInput() { return 'Alice'; }
getEmailInput() { return 'alice@example.com'; }
displayUser(name, email) {
this.displayedName = name;
this.displayedEmail = email;
}
showError(msg) { this.errorMessage = msg; }
showLoading() {}
hideLoading() {}
}
// Test
const mockView = new MockUserView();
const model = new User();
const presenter = new UserPresenter(mockView, model);
presenter.onSaveButtonClick();
// Verify
assert(mockView.displayedName === 'Alice');
assert(mockView.displayedEmail === 'alice@example.com');
Test Presenter logic without real UI. Just create a Mock View.
2. Swappable ViewsReuse same Presenter for web and mobile:
// Web View
class WebUserView { ... }
// Mobile View
class MobileUserView { ... }
// Same Presenter
const presenter = new UserPresenter(new WebUserView(), model);
// or
const presenter = new UserPresenter(new MobileUserView(), model);
Presenter code unchanged. Just needs Views to match the interface.
3. Separation of ConcernsView : "Button pressed", "Display this text" (UI)
Presenter : "Validate email", "Save logic" (Business logic)
Model : "Save data", "API calls" (Data)
Clear responsibilities for each.
// In Presenter
this.view.displayUser(name, email);
this.view.updateTitle(`Hello, ${name}`);
this.view.updateBadge(name[0]);
this.view.updateStatus('Online');
Every time data changes, manually call View methods. If User info displays in 10 places? Call 10 methods.
"Can't this be automatic?" I wondered. That's MVVM.
MVVM's game-changer is Data Binding.
View ⇄ ViewModel ⇄ Model
(auto-sync)
ViewModel value changes → View auto-updates View input changes → ViewModel auto-updates
Two-way auto-sync.
"Model for the View". Real meaning:
// Model (raw data from server)
const user = {
id: 1,
first_name: 'John',
last_name: 'Doe',
email: 'john.doe@example.com',
created_at: '2023-01-15T08:30:00Z',
role: 'admin',
is_active: true
};
// ViewModel (transformed for View)
const userViewModel = {
fullName: `${user.first_name} ${user.last_name}`, // 'John Doe'
email: user.email,
joinDate: new Date(user.created_at).toLocaleDateString(), // '1/15/2023'
isAdmin: user.role === 'admin', // true
statusText: user.is_active ? 'Active' : 'Inactive',
badgeColor: user.is_active ? 'green' : 'gray'
};
Model is server/DB format, ViewModel is display format.
Vue.js is most intuitive, so let me show that first.
<template>
<div>
<input v-model="name" />
<p>Hello, {{ name }}!</p>
<button @click="changeName">Change</button>
</div>
</template>
<script>
export default {
data() {
return {
name: '' // ViewModel
};
},
methods: {
changeName() {
this.name = 'Alice'; // just change this
}
}
};
</script>
What happens:
v-model="name": Two-way bind input and name variable
{{ name }}: Display name variable on screen
<p> content auto-updatesWhen this.name = 'Alice' executes:
<input> value becomes 'Alice'<p> content becomes 'Hello, Alice!'I was curious "how does it auto-update?" Simplified Vue 2 implementation:
function makeReactive(obj, key, value) {
const listeners = []; // functions subscribed to this value
Object.defineProperty(obj, key, {
get() {
// Track who's reading this value
if (currentListener) {
listeners.push(currentListener);
}
return value;
},
set(newValue) {
value = newValue;
// Value changed → notify all subscribers
listeners.forEach(listener => listener());
}
});
}
// Usage example
const viewModel = {};
makeReactive(viewModel, 'name', '');
// Connect to View
let currentListener = () => {
document.getElementById('display').textContent = viewModel.name;
};
currentListener(); // initial render
// Now change value
viewModel.name = 'Alice'; // calls setter → runs listeners → DOM auto-updates
Observer Pattern. When value changes, automatically notify subscribers.
React uses a different approach. Not Data Binding but Re-rendering.
function UserProfile() {
const [user, setUser] = useState({
name: '',
email: ''
});
return (
<div>
<input
value={user.name}
onChange={e => setUser({ ...user, name: e.target.value })}
/>
<p>Hello, {user.name}!</p>
<button onClick={() => setUser({ ...user, name: 'Alice' })}>
Change
</button>
</div>
);
}
React's approach:
setUser calledUnlike Vue, doesn't track "exactly which value changed". Instead re-renders entire component (fast thanks to Virtual DOM).
const [count, setCount] = useState(0); // count = ViewModel
count: ViewModel (data to display)setCount: Update ViewModel → View auto-updates{count}: Bind ViewModel to View// Before (jQuery)
$('#count').text(count);
$('#double').text(count * 2);
$('#status').text(count > 0 ? 'Positive' : 'Zero');
// After (React)
return (
<>
<div>{count}</div>
<div>{count * 2}</div>
<div>{count > 0 ? 'Positive' : 'Zero'}</div>
</>
);
Just change ViewModel (count), React handles the rest.
iOS's SwiftUI supports MVVM at language level.
class UserViewModel: ObservableObject {
@Published var name: String = "" // @Published = "update View when this changes"
@Published var email: String = ""
}
struct UserView: View {
@ObservedObject var viewModel: UserViewModel
var body: some View {
VStack {
TextField("Name", text: $viewModel.name) // $ = two-way binding
Text("Hello, \(viewModel.name)!")
}
}
}
@Published and @ObservedObject handle Data Binding automatically. When viewModel.name changes, View auto-redraws.
@Composable
fun UserScreen(viewModel: UserViewModel) {
val name by viewModel.name.observeAsState("")
Column {
TextField(
value = name,
onValueChange = { viewModel.updateName(it) }
)
Text("Hello, $name!")
}
}
class UserViewModel : ViewModel() {
private val _name = MutableLiveData("")
val name: LiveData<String> = _name
fun updateName(newName: String) {
_name.value = newName // change just this → View auto-updates
}
}
LiveData is Observable. Subscribe with observeAsState, value changes trigger auto-recomposition (re-rendering).
View and Model completely unaware of each other. Only Presenter knows both.
2. Testable// Test Presenter (without UI)
const mockView = new MockView();
const presenter = new Presenter(mockView);
presenter.onButtonClick();
assert(mockView.displayedData === 'expected');
3. Clear Control Flow
"Which methods get called" is explicit.
presenter.onSaveClick();
→ this.view.showLoading();
→ this.model.save();
→ this.view.hideLoading();
→ this.view.displaySuccess();
// View interface
interface UserView {
showName(name: string): void;
showEmail(email: string): void;
showAge(age: number): void;
showAddress(address: string): void;
// ... method for every field (20 fields = 20 methods)
}
// Presenter
this.view.showName(user.name);
this.view.showEmail(user.email);
this.view.showAge(user.age);
this.view.showAddress(user.address);
// ... call 20 times
2. Presenter Gets Fat Too
Eventually Presenter accumulates all logic like MVC's Controller.
3. Manual Sync CostEvery value change requires manually calling View methods.
// MVP
presenter.updateUser(newUser);
view.showName(newUser.name);
view.showEmail(newUser.email);
view.showStatus(newUser.status);
// MVVM
viewModel.user = newUser; // Done!
2. Concise Code
// MVP (100 lines)
class UserPresenter {
updateName(name) {
this.model.name = name;
this.view.showName(name);
this.view.showGreeting(`Hello, ${name}`);
this.view.showInitial(name[0]);
this.view.showNameLength(name.length);
}
}
// MVVM (10 lines)
class UserViewModel {
name = ''; // change just this → everywhere auto-updates
}
3. Declarative UI
// Imperative (MVP)
if (user.isAdmin) {
view.showAdminPanel();
view.hideUserPanel();
} else {
view.hideAdminPanel();
view.showUserPanel();
}
// Declarative (MVVM)
<div>{user.isAdmin ? <AdminPanel /> : <UserPanel />}</div>
Declare "what" to display, not "how" to display it.
// Who changed this value?
console.log(viewModel.count); // Expected: 0, Actual: 42
Auto-binding makes tracking hard. Suffer without Vue Devtools or React Devtools.
2. Performance Overhead// Cost of observing every value
const viewModel = reactive({
field1: '',
field2: '',
// ... 100 fields
});
Creating and maintaining Observables costs memory and CPU. Especially Vue 2's Object.defineProperty had performance issues (improved with Proxy in Vue 3).
// Beginners: "Why does it auto-change? Scary..."
const [count, setCount] = useState(0);
setCount(count + 1); // feels like magic
"Auto-updates" is convenient but initially hard to understand.
4. Memory Leak Risk// If you don't clean up observers properly
useEffect(() => {
const subscription = observable.subscribe(data => {
setData(data);
});
// ⚠️ memory leak without cleanup
return () => subscription.unsubscribe();
}, []);
| Item | MVC | MVP | MVVM |
|---|---|---|---|
| View-Model Relation | View can directly reference Model | Presenter mediates (complete separation) | Data Binding for auto-sync |
| UI Update | Controller manually updates | Presenter manually updates | ViewModel changes → auto-update |
| Testing | Hard (View-Model coupled) | Easy (Presenter independent) | Easy (ViewModel independent) |
| Boilerplate | Low | Very high | Medium |
| Learning Curve | Low | Medium | High (need Reactivity understanding) |
| Debugging | Easy (explicit flow) | Easy (explicit flow) | Hard (auto-binding) |
| Representative Frameworks | Ruby on Rails, Django | Android (old), .NET (WPF) | React, Vue, Angular, SwiftUI, Compose |
| Current Trend | Mainly backend | Found in legacy code | Frontend standard |
The Android project I took over 2 years ago was MVP.
Before (MVP)// UserPresenter.kt
class UserPresenter(private val view: UserView) {
fun loadUser(userId: Int) {
view.showLoading()
repository.getUser(userId) { user ->
view.hideLoading()
view.showName(user.name)
view.showEmail(user.email)
view.showProfileImage(user.imageUrl)
view.showJoinDate(formatDate(user.createdAt))
view.showBadge(if (user.isPremium) "Premium" else "Free")
// ... 20 fields
}
}
}
// UserActivity.kt
class UserActivity : AppCompatActivity(), UserView {
override fun showName(name: String) {
nameTextView.text = name
}
override fun showEmail(email: String) {
emailTextView.text = email
}
// ... 20 method implementations
}
Problem: Adding one field required
// UserViewModel.kt
class UserViewModel : ViewModel() {
private val _user = MutableLiveData<User>()
val user: LiveData<User> = _user
private val _loading = MutableLiveData<Boolean>()
val loading: LiveData<Boolean> = _loading
fun loadUser(userId: Int) {
_loading.value = true
repository.getUser(userId) { user ->
_loading.value = false
_user.value = user // Just this!
}
}
}
// UserActivity.kt
class UserActivity : AppCompatActivity() {
private val viewModel: UserViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.user.observe(this) { user ->
nameTextView.text = user.name
emailTextView.text = user.email
profileImageView.load(user.imageUrl)
// use user.xxx wherever needed
}
viewModel.loading.observe(this) { isLoading ->
progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE
}
}
}
Changes:
Code reduced 40%.
Even more dramatic was jQuery → React.
Before (jQuery)// 800-line user.js
let currentUser = null;
$('#loadBtn').click(() => {
$('#loading').show();
$.get('/api/user/123', (user) => {
currentUser = user;
// Manual DOM updates (50 lines)
$('#name').text(user.name);
$('#email').text(user.email);
$('#greeting').text(`Hello, ${user.name}!`);
$('#initials').text(getInitials(user.name));
$('#memberSince').text(formatDate(user.createdAt));
$('#profileImage').attr('src', user.imageUrl);
$('#status').text(user.isOnline ? 'Online' : 'Offline');
$('#statusDot').css('background-color', user.isOnline ? 'green' : 'gray');
$('#postCount').text(user.postCount);
$('#followerCount').text(user.followerCount);
// ... continues
$('#loading').hide();
});
});
$('#editBtn').click(() => {
$('#editModal').show();
$('#editName').val(currentUser.name);
$('#editEmail').val(currentUser.email);
});
$('#saveBtn').click(() => {
const newName = $('#editName').val();
const newEmail = $('#editEmail').val();
$.post('/api/user/123', { name: newName, email: newEmail }, () => {
currentUser.name = newName;
currentUser.email = newEmail;
// Manual updates again (20 lines)
$('#name').text(newName);
$('#email').text(newEmail);
$('#greeting').text(`Hello, ${newName}!`);
$('#initials').text(getInitials(newName));
// ...
$('#editModal').hide();
});
});
After (React)
function UserProfile() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const [editMode, setEditMode] = useState(false);
const loadUser = async () => {
setLoading(true);
const data = await fetch('/api/user/123').then(r => r.json());
setUser(data);
setLoading(false);
};
const saveUser = async (updatedUser) => {
await fetch('/api/user/123', {
method: 'POST',
body: JSON.stringify(updatedUser)
});
setUser(updatedUser); // Just this!
setEditMode(false);
};
if (loading) return <Spinner />;
if (!user) return <button onClick={loadUser}>Load</button>;
return (
<div>
<img src={user.imageUrl} />
<h1>{user.name}</h1>
<p>{user.email}</p>
<p>Hello, {user.name}!</p>
<p>{getInitials(user.name)}</p>
<p>Member since {formatDate(user.createdAt)}</p>
<StatusBadge isOnline={user.isOnline} />
<Stats posts={user.postCount} followers={user.followerCount} />
<button onClick={() => setEditMode(true)}>Edit</button>
{editMode && (
<EditModal user={user} onSave={saveUser} onCancel={() => setEditMode(false)} />
)}
</div>
);
}
Changes:
Initially thought "will this really work?" It did. Way more stable too.
In jQuery days, bugs like this were common:
// Bug: forgot to update one place
$('#saveBtn').click(() => {
currentUser.name = newName;
$('#name').text(newName); // ✅ updated
$('#greeting').text(`Hello, ${newName}!`); // ✅ updated
// ❌ forgot $('#headerName') → BUG!
});
Same data displayed in 10 places, update only 9 → bug.
React makes this bug structurally impossible:
// Change just one state
setUser({ ...user, name: newName });
// Everywhere with {user.name} auto-updates
// Can't forget
This is MVVM's real value I realized. Structure that prevents mistakes.
Honestly rarely used in new projects. MVVM is better.
2025 frontend standard.
What I clearly understood from this study:
View and Model completely unaware of each other. Presenter mediates. Great for testing but lots of code.
Data Binding means change ViewModel → View auto-updates. Manual DOM manipulation gone.
const [state, setState] = useState(initialValue);
state: ViewModelsetState: Update ViewModel → auto re-renderReact, Vue, Angular, Svelte, SwiftUI, Compose... all MVVM-based.
This is MVVM's essence. Initially felt like magic, now feels natural.
If someone asked me to go back to jQuery, I'd refuse. Don't want to do $('#element').text(value) manually everywhere.
As a non-CS founder, understanding MVP/MVVM made framework docs click.
"Why is React designed this way?" → "Oh, MVVM" "Why does Vue have v-model?" → "Oh, Data Binding" "Why does SwiftUI use @Published?" → "Oh, Observer pattern"
I accepted that design patterns are about "understanding" not "memorization". Understanding them helps learn new frameworks faster.