11.2 MVC, MVP, and MVVM: Who Owns the Interaction State
MVC, MVP, and MVVM all address the same set of questions: Who interprets input, who owns the interface state, who triggers business actions, and how the view gets updated.
Their primary function is to illustrate boundaries. Placing Spring MVC controllers, domain models, and database repositories within a single "MVC triangle" conflates two different scales of architecture.
MVC: Separating Input, State, and Presentation
In server-side Web MVC:
- The Controller translates HTTP requests into application calls;
- Model is the data provided for page rendering and does not equate to a complete domain model;
- View Generate HTML based on Model.
@Controller
final class RankingPageController {
private final LoadRanking loadRanking;
@GetMapping("/rankings/{season}")
String show(@PathVariable String season, Model model) {
RankingViewData ranking = loadRanking.forSeason(season);
model.addAttribute("ranking", ranking);
return "ranking";
}
}The Controller doesn't handle ranking; it selects examples and views. RankingViewData can be specifically tailored to cut pages without exposing domain objects to templates.
The event loop and state synchronization in client-side MVC may differ, so don't judge the pattern solely by class name. Instead, diagram the actual control flow and ownership of state.
MVP: Presenter Actively Drives a Passive View
MVP often abstracts the View into an interface. The Presenter receives user intents, invokes use cases, and explicitly updates the View:
interface RankingView {
void showLoading();
void showRanking(List<RankingRow> rows);
void showError(String message);
}
final class RankingPresenter {
private final RankingView view;
private final LoadRanking loadRanking;
void onSeasonSelected(String season) {
view.showLoading();
try {
view.showRanking(loadRanking.forSeason(season).rows());
} catch (RankingUnavailable ex) {
view.showError("Ranking temporarily unavailable");
}
}
}The Passive View is easy to test with test doubles and suits scenarios where UI frameworks struggle with direct testing or require centralized orchestration of interaction flows. The downside is that the Presenter can easily grow into a "God object" containing excessive view-specific details.
MVVM: View Observes Bindable States
In MVVM, the ViewModel exposes the state and commands needed by the interface, and the View reflects this state through binding or declarative rendering.
type RankingState =
| { kind: 'idle' }
| { kind: 'loading' }
| { kind: 'ready'; rows: RankingRow[] }
| { kind: 'failed'; message: string }
class RankingViewModel {
state: RankingState = { kind: 'idle' }
async load(season: string) {
this.state = { kind: 'loading' }
try {
this.state = { kind: 'ready', rows: await api.loadRanking(season) }
} catch {
this.state = { kind: 'failed', message: 'Ranking temporarily unavailable' }
}
}
}Explicit joint states avoid contradictory combinations of loading=true, error!=null, and rows!=null. The ViewModel should not hold specific DOM nodes nor should it directly propagate remote DTOs to all components.
Selection Criteria
| Question | MVC | MVP | MVVM / Declarative State |
|---|---|---|---|
| Who invokes the input | Controller | Presenter | View invokes commands |
| Who updates the view | Controller/View collaboration | Presenter | Binding or rendering system |
| Where is the state primarily stored | Model or Session | Presenter | ViewModel / Store |
| Main Advantages | Clear request-response flow | Passive View is easy to replace and test | Direct mapping from state to interface |
| Common Risks | Controller Bloating | Presenter Bloating | Implicit Response Chains and Global State Chaos |
A framework name can't replace choice. A React page can follow a unidirectional data flow or be written as hard-to-trace bidirectional synchronization; a Spring MVC project can also shove all business logic into the Controller.
Push side effects to the edge
The hardest part to test in logic isn't conditional branches; it's timers, networking, navigation, and local storage. By encapsulating these side effects as dependencies, state transitions can be tested as pure logic.
Current state + User event + Use case result → New state + Pending effectStress test:
- Loading, empty results, failure, and retry states;
- Repeated clicks and expired responses;
- Handling results when canceling or leaving the page;
- Is the error translated into a message suitable for the user;
- Show that the model is stable without exposing backend internal fields.
Connecting with Application Layering
Regardless of the display mode used, the boundary should be closed at the point where the application use case is invoked:
View / HTTP
↓
Controller / Presenter / ViewModel
↓
Application Use Case
↓
Domain + PortsDisplay modes can evolve with client technology changes; use cases and domain rules should not be rewritten as a result. The next chapter will further decouple external dependencies, making databases, messaging, and web frameworks core components that can be swapped in as adaptable adapters.
References
- Martin Fowler, GUI Architectures
- Martin Fowler, Presentation Model
- Microsoft, The Model-View-ViewModel Pattern