Architecture

MVVM

An architecture where the View binds to the ViewModel through declarative data binding — the ViewModel exposes observable state and commands, the View observes it automatically, and the ViewModel never references the View.

Intent

Eliminate the imperative coupling between presentation logic and the View through declarative data binding: the View declares "my text displays viewModel.userName" and the binding runtime syncs automatically whenever the ViewModel's state changes. The ViewModel never calls the View; it doesn't know it.

MVVM was described by John Gossman in 2005, in the context of WPF (Windows Presentation Foundation) and later Silverlight — both from Microsoft. The original goal was to make full use of XAML's binding system: designers could work on the View in XAML while developers worked on the ViewModel in C#, without a direct dependency between the two.

The pattern migrated to the web with Knockout.js (2010), Angular.js (2010), Vue.js (2014) and Angular (2016). Across all these platforms the underlying mechanism is the same: the ViewModel exposes reactive properties (observables), the View consumes them through declarative binding, and any change in the ViewModel propagates automatically to the View. The Observer pattern is the foundation of every binding system.

Problem

MVP (with a Presenter) solves UI-logic testability, but keeps a maintenance problem: for every piece of data that needs to appear on screen, the Presenter must explicitly call an IView method (view.displayName(name)). As the screen grows, that "push data" code grows proportionally, and every new visual property requires updating the interface, the View and the Presenter.

  • Imperative, fragile synchronization: the Presenter has to remember to call every update method in the right order. If a piece of Presenter state changes and the corresponding update method isn't called, the View silently goes stale.
  • Costly two-way binding in MVP: when the user edits a field and the data needs to flow back to the Presenter, an explicit listener is needed for every field. MVVM solves this with two-way binding: the View updates the ViewModel automatically when the user types, and the ViewModel updates the View when the state changes.
  • Coupling of the View to the IView interface: in MVP, adding a new visual property requires updating the IView, the View and the Presenter. In MVVM, you just add the property to the ViewModel and bind it in the View — no interface contract to change.

MVVM solves these problems by replacing the imperative protocol (Presenter calls methods) with a declarative one (View observes the ViewModel). Synchronization stops being the responsibility of manual code and becomes the responsibility of the binding runtime.

Structure

  ┌────────────────────────────────────────────────────────────────┐
  │                              User                              │
  │                    (interacts with the View)                   │
  └──────────────────────────┬─────────────────────────────────────┘
                             │ events (click, input)
                             ▼
  ┌─────────────────────────────────────────────────────────────────┐
  │                              VIEW                               │
  │  - declarative: defines the binding with the ViewModel          │
  │  - does NOT contain presentation logic                          │
  │  - does NOT know the Model                                      │
  │  - observes the ViewModel via data binding (reactive)           │
  │  - delegates actions to the ViewModel via commands/events       │
  └──────────────────┬──────────────────────────────────────────────┘
                     │ two-way binding
                     │ View observes ViewModel (Observer under the hood)
                     ▼
  ┌─────────────────────────────────────────────────────────────────┐
  │                            VIEWMODEL                            │
  │  - exposes observable state (reactive properties)               │
  │  - exposes commands (actions the View can invoke)               │
  │  - does NOT know the View (no reference, no IView interface)    │
  │  - queries and updates the Model                                │
  │  - data transformations for display live here                   │
  └──────────────────┬──────────────────────────────────────────────┘
                     │ uses
                     ▼
  ┌─────────────────────────────────────────────────────────────────┐
  │                              MODEL                              │
  │  - data + business rules                                        │
  │  - unaware of the View and the ViewModel                        │
  └─────────────────────────────────────────────────────────────────┘

  Dependencies:
    View       → ViewModel      (via declarative binding — observes state)
    ViewModel  → Model          (reads and updates data)
    ViewModel  → (nobody)       (doesn't know the concrete View)
    Model      → (nobody)       (doesn't know ViewModel or View)

  Critical difference from MVP:
    MVP:  Presenter calls view.displayName(name) — imperative
    MVVM: View observes viewModel.name — declarative and automatic

Two-way binding

In two-way binding, synchronization is bidirectional: when the ViewModel changes a property, the View updates; when the user edits a field in the View, the ViewModel receives the new value automatically. In Vue.js this is the v-model directive; in Angular it's [(ngModel)]; in WPF it's Mode=TwoWay in the XAML binding. Two-way binding is especially useful in forms.

How it works

Observable state

The heart of MVVM is the observable property: a ViewModel property that, when its value changes, notifies every registered observer. The underlying mechanism is the Observer pattern: the ViewModel is the Subject and the Views are the observers. In modern frameworks this mechanism is called reactivity, signals, refs or observables — but the principle is the same.

Commands

Besides state, the ViewModel exposes commands: actions the View can invoke when the user acts (clicks a button, submits a form). The command encapsulates the response logic to the event — without the View knowing what the command does internally. The ViewModel can also expose properties indicating whether a command is enabled, removing the need for the View to decide that on its own.

Code example

// ── MODEL — data and business rules ──────────────────────────
// Completely unaware of Vue, of the ViewModel and of the View.
class UserService {
  async findById(id: string): Promise<{ name: string; email: string }> {
    // hits an API or repository
    return { name: 'Ana', email: 'ana@example.com' };
  }
}

// ── VIEWMODEL — Vue 3 Composition API ────────────────────────
// Exposes reactive state and commands. Never references the View.
// In Vue 3, this block goes inside <script setup> or in useProfileViewModel().
import { ref, computed } from 'vue';

function useProfileViewModel(service: UserService) {
  // Observable state — when it changes, the View updates automatically
  const name    = ref('');
  const email   = ref('');
  const loading = ref(false);
  const error   = ref('');

  // Computed — derived from state; recalculates automatically
  const summary = computed(() => name.value ? `Hello, ${name.value}` : '');

  // Command — action the View invokes; returns nothing to the View
  async function loadProfile(id: string): Promise<void> {
    loading.value = true;
    error.value = '';
    try {
      const user = await service.findById(id);
      name.value  = user.name;
      email.value = user.email;
    } catch {
      error.value = 'Could not load the profile.';
    } finally {
      loading.value = false;
    }
  }

  return { name, email, loading, error, summary, loadProfile };
}

// ── VIEW — declarative template (Vue SFC) ─────────────────────
// <template> below (not executable code — illustrative only)
//
// <p v-if="loading">Loading...</p>
// <p v-if="error">{{ error }}</p>
// <p>{{ summary }}</p>
// <input v-model="email" />          <!-- two-way binding -->
// <button @click="loadProfile('1')">Load</button>
//
// The View contains no logic — only binding and events.

The key point: the ViewModel never calls methods on the View. It only changes its observable properties, and any View registered as an observer syncs automatically. This is the inverse of MVP, where the Presenter explicitly calls view.displayName(name).

When to use

  • Frameworks with native data-binding support: Vue, Angular, SwiftUI, Jetpack Compose, WPF, Knockout. In these environments MVVM is the natural pattern — the framework already provides the binding mechanism and the ViewModel fits without friction.
  • UIs with complex state and many simultaneous updates: when many visual properties derive from the same state, declarative binding is safer than imperative calls: you don't need to remember to call view.updateX() for every piece — the binding takes care of it.
  • Forms with two-way binding: data-entry applications where the user edits fields and the ViewModel needs to reflect those edits immediately benefit greatly from two-way binding.
  • When multiple Views need to observe the same ViewModel: for example, a summary panel and an edit form observing the same product ViewModel. The ViewModel doesn't change; the Views register themselves independently.

When to avoid

  • No binding support in the environment: implementing a binding system by hand (Observables, listeners on every property) to adopt MVVM in vanilla code without a framework is rarely justifiable. The result is usually manual Observer with infrastructure overhead.
  • Simple, static Views: if the screen displays data with no complex interactivity, a simple MVC is more direct. Creating a ViewModel with reactive properties for a read-only page is over-engineering.
  • When magic binding makes debugging harder: in interfaces with extensive two-way binding and ViewModels with many derived (computed) properties, tracing the origin of an unexpected UI change can be laborious. If the team lacks experience with the reactive model, consider starting with MVP or a simple MVC.

Pros and cons

Pros

  • ViewModel testable without instantiating the View — just check the observable properties' state after invoking the commands.
  • Clean separation: the ViewModel doesn't know the View; the View doesn't know the Model.
  • Two-way binding simplifies forms and bidirectional synchronization.
  • Multiple Views can observe the same ViewModel with no modification to the ViewModel.
  • Natural integration with modern reactive frameworks (Vue, Angular, SwiftUI, Compose).

Cons

  • Requires environment support: without a framework with native binding, manually implementing Observables is heavy.
  • Risk of a fat ViewModel: without discipline, the ViewModel accumulates business logic, use-case orchestration and transformations that belong to the Model — the same problems as the fat Presenter in MVP.
  • Magic binding makes debugging harder: tracing why a piece of data changed in the View requires understanding the reactive dependency graph, which can be non-obvious in large applications.
  • Learning curve of the reactive model: developers used to imperative programming take time to start thinking in terms of observable state and automatic derivations.

Common pitfalls

1. Fat ViewModel

The same pitfall as the fat Presenter in MVP, now in the ViewModel: business validation rules, direct calls to external APIs, orchestration of multiple use cases and domain calculations end up in the ViewModel because it's "the most convenient place." The ViewModel should contain only presentation logic — transforming Model data for display, aggregating state for the View, exposing commands that delegate to the Model. Business logic belongs to the Model (or to application services).

Rule of thumb: the ViewModel should be testable with a simple mock of the Model. If the ViewModel's tests need mocks of five different services, the ViewModel is doing too much.

2. Domain logic in the ViewModel

Business validations ("the maximum discount is 30%"), price calculations, eligibility rules — none of that belongs in the ViewModel. The ViewModel can expose whether a field is valid for display purposes (e.g., emailValid: boolean), but the rule that defines what's valid should live in the Model. If you delete the ViewModel, the business rules shouldn't disappear with it.

3. Two-way binding creating cycles

With two-way binding, accidental cycles can arise: the View updates the ViewModel, the ViewModel derives a property, which updates the View, which updates the ViewModel again. Modern frameworks have mechanisms to break those cycles (batched change detection, glitch-free propagation), but developers need to know those mechanisms to avoid unexpected behavior in complex forms.

4. Confusing MVVM with MVP

The distinction is precise and matters: in MVP, the Presenter calls methods on the View (view.displayName(name)) — the flow is imperative and the Presenter knows the View through an interface. In MVVM, the ViewModel exposes observable properties and doesn't know the View exists — the View registers itself as an observer and the binding handles synchronization. If your "ViewModel" has an updateView() method or holds a reference to the View, you have MVP with the names swapped.

Related architectures and patterns

MVC is MVVM's ancestor. In classic MVC, the View can observe the Model directly via Observer; in MVVM the View doesn't know the Model — it observes only the ViewModel. The ViewModel takes on the role of mediator and data translator, a role that in classic MVC was performed partly by the Controller and partly by the direct View-Model Observer.

MVP is MVVM's closest sibling — both extract presentation logic into a dedicated component (Presenter or ViewModel) and isolate the Model from the View. The fundamental difference: in MVP the Presenter pushes data to the View imperatively, calling methods on an IView interface; in MVVM the ViewModel exposes observable state and the View syncs automatically via binding. In environments without binding support, MVP is simpler to implement by hand; in environments with native binding, MVVM is more natural.

Observer is the mechanism that makes MVVM possible. Every observable ViewModel property is a Subject-Observer: when its value changes, every binding registered as an observer is notified and updates the corresponding elements in the View. Frameworks like Vue implement this with reactive proxies; Angular with Signals or Zone.js; Knockout with ko.observable(). The abstraction changes, but the Observer pattern is always underneath.

The Command pattern is frequently used to model the actions exposed by the ViewModel: every button in the View is bound to a Command in the ViewModel that encapsulates the action and, optionally, an enabling condition (canExecute(): boolean). This keeps the View fully declarative — it doesn't know what the command does, only whether it can execute it.