I still see the same bug show up in codebases of every size: UI code quietly becomes the place where everything happens. A button click triggers a network call, massages data into a format the UI can print, decides whether the text should be purple, writes to a database, and somehow also handles navigation. It works—until you need to add one more rule, write one test, or support one more screen.
MVVM fixes that by giving you a clean place to put UI-facing logic without smearing it across components, templates, and event handlers. When I teach MVVM to teams, I frame it as a practical way to structure code so that:
- UI code stays platform-specific and mostly declarative.
- Domain data stays reusable.
- Presentation logic becomes testable, composable, and easier to reason about.
You’ll walk away with a mental model of the three parts (Model, View, ViewModel), how data moves between them (including two-way binding), how MVVM differs from MVC in real projects, and a runnable example that shows where formatting and UI rules belong.
The Core Idea: Separate What Changes for Different Reasons
MVVM is less about diagrams and more about acknowledging a truth: UI changes constantly, data models change slower, and presentation rules sit awkwardly in between.
If you don’t intentionally separate these concerns, your UI layer tends to absorb them by default. Then you get problems like:
- A screen can’t be tested without rendering it.
- A small design tweak forces you to touch business logic.
- State restoration is brittle because state is scattered across widgets/components.
- Multiple screens reimplement the same formatting and validation rules.
MVVM gives you a place to put “UI rules” (formatting, enable/disable logic, derived values, validation hints, presentation state) that is:
- Reusable across platforms/screens
- Isolated from direct UI dependencies
- Friendly to unit tests
A simple analogy I use: think of a restaurant.
- The Model is the pantry and ingredients (raw data).
- The View is the plated dish in front of the customer (what they see).
- The ViewModel is the chef (turns ingredients into something presentable, and reacts to orders).
Model, View, ViewModel: What Each One Owns
I’ll use the definitions you provided as the baseline, but I’ll phrase them the way I explain them in code reviews.
Model (Reusable code – data)
The Model represents the business objects in your domain. It encapsulates data and domain behavior. In many projects, it’s “just data,” but the key point is that it is not UI-specific.
What I put in a Model:
- Domain entities and value objects (for example,
Person,Order,InvoiceLine) - Domain rules that are true regardless of UI (for example, “age can’t be negative”, “an order total is sum of its lines”)
What I avoid putting in a Model:
- UI formatting (colors, labels, strings meant only for a single screen)
- UI state (“isSaveButtonEnabled”)
- View concepts (“selectedRowIndex”)
View (Platform-specific code – user interface)
The View is what the user sees. It is platform-specific: HTML, XAML, SwiftUI, Jetpack Compose, WinForms—whatever your UI technology is.
What I expect the View to do:
- Render formatted values
- Bind to properties exposed by the ViewModel
- Forward user actions (clicks, typing, selections) to the ViewModel
What I try hard to keep out of the View:
- Data-fetching
- Data transformation rules
- Branch-heavy logic deciding how things should look
ViewModel (Reusable code – logic)
The ViewModel is the link between the Model and the View. It retrieves data from the Model (or services/repositories), manipulates it into something the View can present easily, and exposes it through properties and commands.
If you remember only one sentence, make it this:
- The ViewModel is a model designed specifically for a View.
It is not the same as your domain model. It’s shaped by how the UI wants to consume data.
A few classic ViewModel responsibilities:
- Derived properties:
fullName,displayAge,statusLabel - Presentation rules:
nameColor,isAdult,canSubmit - State: loading flags, error messages, selected item
- Actions: commands like
save(),deletePerson(),refresh()
How the links work (the detail that matters)
A practical way to think about the “wiring”:
- Between Model and ViewModel: you manipulate and adapt data.
- Between ViewModel and View: you bind data (often two-way).
That last point—two-way binding—is common in MVVM systems. The View can update ViewModel properties when the user types, and the ViewModel can update the View automatically when state changes.
Data Binding and State: The MVVM “Engine Room”
MVVM lives or dies on how state flows.
One-way vs two-way binding
In MVVM systems, you’ll usually see both:
- One-way binding: View reads from ViewModel and updates automatically.
- Two-way binding: View reads from ViewModel, and also writes changes back (for example, input fields).
Two-way binding is powerful because it reduces glue code, but it also introduces a failure mode: invisible coupling through bindings that are hard to trace when they get complex.
My rule of thumb:
- Use two-way binding for simple form inputs.
- Prefer one-way binding for derived values (computed properties) and display-only fields.
Commands / actions
Most MVVM frameworks pair binding with an action concept:
- WPF/UWP/MAUI:
ICommand - Knockout.js: click bindings calling functions
- SwiftUI/Compose: closures and event handlers feeding a ViewModel
I encourage teams to treat commands as the boundary: the View triggers commands; the ViewModel does the work.
Lifecycle and “returning where the user left off”
One reason MVVM became popular in client apps is that it naturally supports holding state in memory.
When the ViewModel owns the screen state (current input, selection, loading/error status), you can:
- Recreate the View without losing state
- Survive UI re-renders (common in reactive UI frameworks)
- Restore the screen to where the user left it, as long as you persist the right ViewModel state
In practice in 2026, this plays nicely with:
- Android: ViewModel + saved state handles
- iOS: observable state models with persistence
- Web: state containers + serialization for restoration
- Desktop: MVVM frameworks with navigation stacks
A Concrete Example: Color Rules and Formatting Belong in the ViewModel
You gave a perfect example: show a person’s name in purple when it’s not properly formatted, or show purple when age > 18 and pink when age < 18. The important part is not the colors—it’s where that logic goes.
If you put the rule directly in UI templates, it spreads:
- One screen uses purple/pink
- Another screen adds a third color for “unknown age”
- A third screen forgets the rule entirely
In MVVM, the ViewModel becomes the single source of truth for presentation rules.
Runnable browser example (Knockout-style MVVM)
This is a small, complete example you can save as mvvm.html and open in a browser. It shows:
- A Model (
PersonModel) holding raw data - A ViewModel (
PersonViewModel) exposing derived properties and rules - A View binding through two-way bindings (
value) and display bindings (text,style)
MVVM Example
body { font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif; padding: 24px; }
.row { margin: 12px 0; }
label { display: inline-block; width: 120px; }
input { padding: 6px 8px; width: 220px; }
.card { border: 1px solid #ddd; border-radius: 10px; padding: 16px; max-width: 520px; }
.hint { color: #666; font-size: 0.95rem; }
.name { font-weight: 700; font-size: 1.3rem; }
Preview (View reads ViewModel):
// Model: raw domain-ish data.
function PersonModel(initial) {
this.name = initial.name;
this.age = initial.age;
}
// ViewModel: presentation-focused state + rules.
function PersonViewModel(personModel) {
var self = this;
// Two-way bindings write into these observables.
self.name = ko.observable(personModel.name);
self.age = ko.observable(personModel.age);
function normalizeWhitespace(s) {
return String(s || ‘‘).replace(/\s+/g, ‘ ‘).trim();
}
function isProperName(s) {
// A simple rule: two words, each starts with a letter, no digits.
// Real apps usually have more nuanced rules.
var cleaned = normalizeWhitespace(s);
if (!cleaned) return false;
if (/\d/.test(cleaned)) return false;
var parts = cleaned.split(‘ ‘);
if (parts.length < 2) return false;
return parts.every(function (p) { return /^[A-Za-z][A-Za-z\-‘]*$/.test(p); });
}
self.displayName = ko.pureComputed(function () {
var cleaned = normalizeWhitespace(self.name());
if (!cleaned) return ‘Name required‘;
// Title-case for display purposes. Keep raw input intact.
return cleaned
.split(‘ ‘)
.map(function (p) { return p.charAt(0).toUpperCase() + p.slice(1).toLowerCase(); })
.join(‘ ‘);
});
self.isAdult = ko.pureComputed(function () {
var n = Number(self.age());
if (!Number.isFinite(n)) return false;
return n >= 18;
});
self.nameColor = ko.pureComputed(function () {
var cleaned = normalizeWhitespace(self.name());
var formattedOk = isProperName(cleaned);
// Presentation rule:
// - If the name format is questionable, show purple.
// - Else color by age: >= 18 purple, < 18 pink.
if (!formattedOk) return ‘#6a00ff‘;
return self.isAdult() ? ‘#6a00ff‘ : ‘#ff2d8d‘;
});
self.ageLabel = ko.pureComputed(function () {
var n = Number(self.age());
if (!Number.isFinite(n) || n < 0) return 'Age is invalid';
return self.isAdult() ? ‘Adult (18+)‘ : ‘Minor (< 18)';
});
self.reset = function () {
self.name(personModel.name);
self.age(personModel.age);
};
}
var model = new PersonModel({ name: ‘alex chen‘, age: 17 });
var vm = new PersonViewModel(model);
ko.applyBindings(vm);
A few things to notice:
- The View doesn’t know how to decide colors. It just binds to
nameColor. - The ViewModel doesn’t touch the DOM. It just exposes state and rules.
- Formatting happens in a computed property, so updates propagate automatically.
This is the heart of MVVM: data and rules move through the ViewModel, and the View becomes a declarative projection of that state.
MVVM vs MVC: What Changes in Real Code
People often compare MVVM and MVC as if they’re competing religions. I prefer to compare them by asking one question:
- Where does the UI-facing logic live?
Here’s a practical table that matches how these patterns typically feel in day-to-day development.
MVVM
—
ViewModel (presentation logic is centralized)
Data binding (often two-way) and reactive updates
Client apps and rich UIs (web client, desktop, mobile)
View calls deletePerson() on the ViewModel
PersonController.deletePerson() handles the action Often connected state; objects can stick around
Where I’ve seen this matter most:
- If you’re building a server-rendered web app, MVC fits naturally because the controller maps cleanly to requests.
- If you’re building a long-lived UI (desktop/mobile/web SPA), MVVM fits naturally because state lives beyond a single transaction.
In other words, MVVM tends to feel best when the UI is an ongoing conversation, not a single request.
Why Teams Stick With MVVM: Maintainability, Extensibility, Testability
These are the benefits I actually see pay off after the first release.
Maintainability: change the UI without fear
When presentation rules live in ViewModels, you can redesign screens without rewriting core logic. I’ve watched teams ship faster simply because they stop debugging the same rule in three different UI components.
Extensibility: add new screens without cloning logic
Because a ViewModel is reusable code, you can:
- Add a new View that binds to the same ViewModel
- Extend the ViewModel with additional derived properties
- Swap a data source (mock vs real repository) without rewriting the View
Testability: treat UI rules like regular code
The ViewModel is where unit testing becomes straightforward. You can test:
- validation rules
- formatting rules
- enable/disable logic
- error handling
…and you can do it without rendering UI.
Here’s a small JavaScript test sketch you can run with Node (no UI needed). It isn’t tied to Knockout specifically; it demonstrates the idea that the logic is now testable.
import assert from ‘node:assert/strict‘;
function decideNameColor({ isProperName, age }) {
if (!isProperName) return ‘purple‘;
if (age >= 18) return ‘purple‘;
return ‘pink‘;
}
assert.equal(decideNameColor({ isProperName: false, age: 25 }), ‘purple‘);
assert.equal(decideNameColor({ isProperName: true, age: 19 }), ‘purple‘);
assert.equal(decideNameColor({ isProperName: true, age: 10 }), ‘pink‘);
console.log(‘ok‘);
In larger systems (C#, Kotlin, Swift), this becomes even more valuable because ViewModels are typically plain classes with deterministic behavior.
Transparent communication between layers
When done well, the ViewModel becomes a clear interface:
- The View consumes it without knowing the domain details.
- The Model layer can evolve without breaking the UI, as long as the ViewModel contract stays stable.
That “transparent interface” property is why MVVM scales beyond toy apps.
When MVVM Hurts (and How to Avoid the Traps)
MVVM is not free. I’ve seen teams get burned when they treat it as ceremony instead of architecture.
When MVVM is overkill
If your UI is extremely simple (a couple of static screens, minimal state, almost no interaction), MVVM can feel like extra files and extra indirection.
My guidance is concrete:
- If you have 0–2 interactive screens and almost no validation/state, start simpler.
- If you have forms, lists, filtering, async loading, navigation state, or multiple UI variants, MVVM pays off quickly.
The hard part: designing the ViewModel
The most common mistake is building a ViewModel that is either:
- Too thin (it just passes raw models through, and the View ends up doing the real work), or
- Too fat (it becomes a second domain layer with unrelated responsibilities)
A ViewModel should be “fat” in the sense that it owns presentation logic, but it should not become your database/service layer.
A clean split I recommend:
- Services/repositories: data access and side effects
- Domain models: business rules
- ViewModels: presentation rules and UI state
- Views: rendering and input wiring
Debugging complex bindings
Two-way binding can create non-obvious flows:
- An input updates a ViewModel property
- That triggers a computed property
- That toggles validation
- That changes another property
If this becomes hard to trace, you need more structure. Techniques that help in 2026-era stacks:
- Prefer explicit actions for major transitions (for example,
onSubmit(),onAgeChanged()) - Keep computed properties pure (no side effects)
- Use logging hooks in development builds around state changes
- Put validation into a single place (not scattered across multiple bindings)
Performance considerations
MVVM is usually fast enough, but you can still create self-inflicted slowdowns:
- Too many computed values recalculating on every keystroke
- Binding huge lists without virtualization
- Triggering network calls directly from property setters
In many real UIs, the difference between a snappy and sluggish form is avoiding unnecessary recomputation. If your UI updates feel laggy, it’s often because you’re doing expensive derived work on every small input change.
What I do instead:
- Debounce user input (typically 100–250ms) for expensive operations
- Cache derived results when inputs haven’t changed
- Virtualize list rendering for large datasets
Practical Guidance: When I Reach for MVVM in 2026
In modern development, MVVM shows up in different clothing depending on your stack:
- .NET MAUI / WPF / WinUI: MVVM is first-class; bindings and commands are built in.
- Android: ViewModel is a standard concept; UI is often reactive (Compose), and the ViewModel owns state.
- iOS: you may not call it MVVM, but observable state models + views is the same shape.
- Web: MVVM appears as view-state objects, stores, and computed selectors.
Here’s when I actively choose it:
- Forms with validation, error states, and submit flows
- Screens with multiple derived values (filters, totals, formatting)
- Apps that must restore state reliably (returning to where the user left off)
- Teams where test coverage for UI rules matters
And here’s when I do not:
- Tiny prototypes with static pages
- Apps where state is almost entirely server-driven and transactional
- Situations where you can’t support binding complexity and still ship (for example, a short-lived internal tool with a one-week deadline)
If you’re on the fence, a simple heuristic works well:
- If you expect UI rules to grow, create a ViewModel early.
- If you expect UI rules to stay flat, keep it simpler and refactor when the pain appears.
Common Mistakes I See (and How I Fix Them)
These are patterns I routinely correct in code reviews.
Mistake 1: Putting data formatting in the View
Symptom: templates full of string slicing, if chains, date formatting, and color decisions.
Fix: move formatting into computed properties on the ViewModel (displayName, displayDate, statusColor).
Mistake 2: Making the ViewModel call UI APIs
Symptom: ViewModel imports UI modules, touches DOM elements, shows dialogs directly.
Fix: push UI work back to the View or a UI service interface. Keep the ViewModel UI-framework-agnostic.
Mistake 3: Mixing database/network code into the ViewModel
Symptom: ViewModel builds SQL queries, manages HTTP clients, parses raw responses.
Fix: create a repository/service layer. The ViewModel should request data, not know how it’s fetched.
Mistake 4: Two-way binding everywhere
Symptom: derived values can also be edited, leading to confusing feedback loops.
Fix: keep editable fields as the minimum set of state; compute everything else one-way.
Mistake 5: Treating the domain model as the ViewModel
Symptom: UI depends on domain fields directly and spreads UI rules across multiple views.
Fix: create a true ViewModel shaped for the screen. It’s okay if it duplicates a subset of domain fields.
Next Steps You Can Apply Immediately
The most useful way to adopt MVVM is to start with one screen that’s currently messy—usually a form or a details page with conditionals.
Here’s what I recommend you do this week:
- Pick a screen where UI code is doing formatting, validation, or complex enable/disable logic.
- Create a ViewModel that exposes exactly what the View needs: display strings, colors, flags, and commands.
- Keep your Model clean: store raw data and domain rules, not UI decisions.
- Use two-way binding only for the fields the user edits; derive the rest.
- Add a few unit tests against the ViewModel rules (color decisions, validation outcomes, computed labels). That’s where you’ll feel the payoff quickly.
MVVM won’t magically make your architecture perfect, but it does give you a stable place to put logic that would otherwise end up scattered across UI code. When the View becomes a projection of state instead of a tangle of event handlers, you ship changes with less risk—and you can finally test the parts of the UI that actually break in production: the rules.


