Skip to content

MVC, MVP and MVVM

Separating what the software knows from what it shows — the three presentation patterns, what actually differs between them, and the fat-controller trap.

Intermediate18 minUpdated 8 August 2026

Three patterns, endlessly confused, with one shared purpose: keep the rules of the software separate from how it is displayed, so the rules can be tested without a screen and the display can change without touching the rules.

The differences between them are smaller than the arguments about them suggest. What matters is understanding the axis they vary along, because that is what lets you evaluate a fourth one you have not met.

The shared idea

The model is what the software knows and can do — data and rules, no awareness of any screen. This is the same in all three, and it is the part that matters most.

What differs is only how the display is kept in step with the model.

Conceptual diagramuser inputeventstells the view exactly what to showbindingControllerModelViewView — passivePresenterModelViewViewModelModel
Diagram, as text
  • Controller leads to Model
  • Model leads to View
  • View leads to Controller (user input)
  • View — passive leads to Presenter (events)
  • Presenter leads to Model
  • Presenter leads to View — passive (tells the view exactly what to show)
  • View leads to ViewModel (binding)
  • ViewModel leads to Model

MVC. The controller handles input and updates the model; the view observes the model and redraws. The view knows about the model.

MVP. The view is passive and knows nothing — it exposes "show this text", "disable that button" and raises events. The presenter reads the model and drives the view explicitly.

MVVM. The view model exposes state in a form the view binds to declaratively. Nobody writes the update calls; the binding framework does. It is MVP with the "tell the view what to show" step automated.

The axis is who updates the view and how much the view knows. MVC: the view pulls from the model. MVP: the presenter pushes, explicitly. MVVM: a framework synchronises.

What follows for testing

This is the practical consequence and the reason to care.

MVP is the most testable without a UI framework, because the presenter drives a view interface you can fake. Asserting "when the search returns nothing, the presenter tells the view to show the empty state" needs no rendering at all. That is why MVP survives in codebases with hard-to-test UI toolkits.

MVVM is testable if the view model holds no view references. A view model is a plain object with observable properties, so testing it is testing an object — provided nothing about the widget tree leaked in.

MVC is the least testable in its original form, because view-model coupling means many assertions need a rendered view.

The fat controller

The dominant failure, in every variant, is the middle component absorbing everything.

It happens because the controller is where a request first arrives and the view model is where the screen's data is assembled, so both are the convenient place to put a rule. Convenience compounds: validation, then a calculation, then a permission check, then an exception, and eventually the controller is the application and the model is a bag of fields.

Signals it has happened:

  • Business rules in the controller. Discount arithmetic, eligibility checks, tax logic.
  • The model is data only. No behaviour, only getters and setters — the anaemic domain model, and the fat controller is its inevitable twin.
  • Tests need HTTP. If verifying a business rule requires constructing a request, the rule is in the wrong layer.
  • The same rule appears twice. Once in the web controller and once in the batch job, because there was nowhere shared to put it.

The cure is not a thinner controller, it is a richer model. Rules must have somewhere to go, and a model with no behaviour offers nowhere. Move the rule into the domain object or a domain service, and the controller shrinks as a consequence rather than as a goal.

What modern component frameworks changed

Component-based UI frameworks blurred the distinctions substantially, and pretending otherwise makes the patterns harder to apply than they are.

A component with local state and a render function is MVC and MVVM at once: it observes state and re-renders like a view, and it exposes bound state like a view model. Unidirectional data flow — state flows down, events flow up — is closer to MVP's explicit push than to classic MVC's observation.

The useful part that survives all of it:

  • Keep business rules out of components. A component that computes a price is a fat controller with new syntax.
  • Distinguish view state from domain state. "Which tab is open" belongs to the component. "Whether this order can be cancelled" belongs to the domain.
  • The model should be usable with no UI at all. If the same rules serve an HTTP handler, a background job and a CLI, the separation is real. If they only work inside a component, it is not.

Check your understanding

Common mistakes

Business rules in the controller, presenter or view model. All three patterns fail identically.

An anaemic model. Data with no behaviour guarantees the logic lands somewhere worse.

Tests that need HTTP to verify a rule. The rule is in the wrong layer.

View state and domain state mixed. Which tab is open and whether an order is cancellable are different kinds of fact.

A view model holding references to widgets. Testability is lost for no benefit.

Deriving domain objects only at submission time. The rules cannot run anywhere but the form.

Arguing about which of the three you are using. The distinctions are minor next to what is in the model.

Practice

A single-page application has a checkout component of 1,200 lines containing tax calculation, discount eligibility, inventory checks and the rendering. Design the refactor, and say what stays in the component.

Write your own answer first, on paper or in a note. Then compare.

Show a hint

Sort the 1,200 lines by whether they would still be needed if the checkout were a CLI.

Show a strong answer

Sort by the CLI test, because it settles every case without argument. If checkout were driven from a command line — no rendering at all — which of these lines would still be needed? Tax calculation: yes. Discount eligibility: yes. Inventory availability: yes. Which accordion section is open, whether the submit button is disabled, how a currency is formatted for this locale: no. That single question separates domain from presentation more reliably than any layering discussion.

What moves out:

  • Tax and discount rules into domain modules, as plain functions or objects over domain values. They become testable with no rendering, and — the part that usually matters more — reusable by the server, which almost certainly needs to recompute them anyway. A price computed only in the browser is a price the server must trust or duplicate.
  • Inventory checks into a service the component calls, returning a domain result rather than a rendering decision. The component decides how to show "out of stock"; it does not decide what out of stock means.
  • Validation rules into the domain, with the component responsible only for displaying the resulting errors. Validation is the most commonly duplicated logic in this kind of file, and it is duplicated because it was written where it was displayed.

What stays in the component, legitimately:

  • View state: which step is active, which fields are touched, whether a spinner is showing.
  • Formatting and localisation. How money is displayed is presentation; how it is calculated is not.
  • Orchestration of user interaction — what happens on click, in what order, with what loading state.
  • Accessibility and focus management. Genuinely view concerns with nowhere else to go.

The structural outcome I would aim for: the component reads as a sequence of "ask the domain, show the answer". If a reader can follow it without learning any business rules, the split worked.

Sequencing: extract one rule at a time, each with a test written before the move, so the behaviour is pinned. Tax first, because it is the most self-contained and the most valuable to share with the server. Do not attempt to restructure the rendering at the same time — two kinds of change in one diff makes a regression impossible to attribute.

What I would flag to the team: 1,200 lines usually means more than one screen's worth of responsibility. After the rules leave, the remaining view code may still want splitting by step, and that is a separate, easier change to make once the logic is gone.

Two engineers disagree about whether their component framework 'is MVVM' and whether they should add a formal view model layer. Resolve it usefully.

Write your own answer first, on paper or in a note. Then compare.

Show a hint

The label question is not worth resolving. Find the question underneath it.

Show a strong answer

The label question has no useful answer and I would say so directly. A component with local state, a render function and props flowing down genuinely has elements of all three: it re-renders from observed state like an MVC view, it exposes bound state like a view model, and its unidirectional event flow resembles MVP's explicit push. Whichever label wins the argument, no code changes. This is a discussion that consumes review time and produces nothing.

The question underneath, which is worth answering: can the business rules run without the UI?

I would resolve the disagreement by asking for a demonstration rather than an argument: pick the most complex rule in the application and write a test for it with no rendering.

  • If that is straightforward, the separation is already adequate and adding a formal view model layer would add files without addressing a problem. The engineer arguing against the layer is right, and now has evidence.
  • If it requires mounting a component, faking a framework, or copying logic into the test, the rules are in the presentation layer. That is a real problem, and the fix is to move them into the domain — which is not the same as adding a view model layer, and this distinction is the useful thing to establish.

What a view model layer would and would not fix:

  • It would help if components are genuinely doing heavy presentation assembly — combining several sources, deriving display state, managing complex interaction — and that assembly is hard to test. Extracting it is a real improvement.
  • It would not help with misplaced business rules. Moving pricing logic from a component into a view model leaves it in the presentation layer; it is still untestable from a background job and still duplicated on the server.

What I would propose concretely:

  • Agree the boundary rule instead of the pattern name: business rules live in modules importable with no framework; components hold view state and rendering. That rule is checkable in review and in a lint rule on imports.
  • Add view models only where a component is demonstrably hard to test for presentation reasons — not uniformly, and not as an architecture decision applied everywhere.

The general point I would make to both of them: all three presentation patterns fail identically when the rules are in the middle component, and all three succeed when the model is rich. Optimise for what is in the model, and the naming question stops mattering.

Key takeaways

  • All three separate what the software knows from how it is shown; the model's role is identical in each.
  • The axis is who updates the view: MVC the view observes, MVP the presenter pushes to a passive view, MVVM a framework binds.
  • MVP is the most testable without a UI framework; MVVM is close if no view references leak into the view model.
  • The fat controller is the dominant failure in all three, and it is the twin of an anaemic model — rules land there because nowhere better exists.
  • The cure is a richer model, not a thinner controller.
  • Distinguish view state from domain state: which tab is open versus whether an order can be cancelled.
  • The CLI test settles most cases: would this logic still be needed with no screen at all?
  • Component frameworks blur the labels; the surviving rule is that the model must be usable with no UI.

Further reading

  • Trygve Reenskaug, "Models–Views–Controllers" (Xerox PARC technical note, 1979) — the original description.
  • Martin Fowler, "GUI Architectures" (2006) — the clearest comparison of the variants, including passive view and supervising controller.
  • Martin Fowler, Patterns of Enterprise Application Architecture (Addison-Wesley, 2002) — model–view–controller and the anaemic domain model.
  • Michael Feathers, Working Effectively with Legacy Code (Prentice Hall, 2004) — extracting logic from untestable UI code.
  • Eric Evans, Domain-Driven Design (Addison-Wesley, 2003).

Check yourself

3 questions in this lesson. Answer each once to complete it.

No problem practices this lesson yet. Browse the practice problems

Next lessonData Access Patterns
Lessons in this track