MVC Pattern — Model-View-Controller Architecture
What You'll Learn
You will learn how the MVC pattern separates concerns in user-facing applications by isolating data, presentation, and input handling into three distinct components. You will also understand how Observer connects Model changes to View updates.
Why It Matters
Monolithic UI applications mix database queries, business logic, and HTML rendering in the same file — impossible to maintain or test. MVC enforces a clean boundary: the Model knows nothing about the UI, the View is a passive template, and the Controller mediates between them. This separation means you can change your database without touching your templates, and redesign your UI without rewriting your business logic.
Real-World Use
Every major web framework — Rails, Django, Spring MVC, ASP.NET MVC — implements this pattern. DodaTech's reporting dashboard uses MVC where a Report model holds data, dashboard HTML templates render the View, and Controller classes handle HTTP requests and user interactions. Adding a new chart type requires only View changes; adding a new data source requires only Model changes.
The Pattern
Model manages data and business rules. View renders the Model into a UI. Controller handles user input and updates the Model. The Observer Pattern often connects Model changes to View updates.
class TaskModel:
def __init__(self):
self._tasks = []
self._observers = []
def add_observer(self, observer):
self._observers.append(observer)
def _notify(self):
for observer in self._observers:
observer.update()
def add_task(self, title: str):
self._tasks.append({"title": title, "done": False})
self._notify()
def toggle_task(self, index: int):
self._tasks[index]["done"] = not self._tasks[index]["done"]
self._notify()
def get_tasks(self):
return self._tasks
class TaskView:
@staticmethod
def render(tasks: list) -> str:
lines = ["=== Task List ==="]
for i, task in enumerate(tasks):
status = "[x]" if task["done"] else "[ ]"
lines.append(f"{i}. {status} {task['title']}")
return "\n".join(lines)
class TaskController:
def __init__(self, model: TaskModel, view: TaskView):
self._model = model
self._view = view
self._model.add_observer(self)
def update(self):
print(self._view.render(self._model.get_tasks()))
print()
def add_task(self, title: str):
self._model.add_task(title)
def toggle_task(self, index: int):
self._model.toggle_task(index)
model = TaskModel()
view = TaskView()
controller = TaskController(model, view)
controller.add_task("Learn MVC")
controller.add_task("Write tutorial")
controller.toggle_task(0)
=== Task List ===
0. [x] Learn MVC
1. [ ] Write tutorial
=== Task List ===
0. [x] Learn MVC
1. [ ] Write tutorial
Structure
classDiagram
class Model {
-data
+update()
+notifyObservers()
}
class View {
+render(model): String
}
class Controller {
+handleInput()
+updateModel()
}
View --> Model : reads
Controller --> Model : updates
Controller --> View : updates
Model --> View : notifies changes
Real-World Usage
- Ruby on Rails — ActiveRecord (Model), ActionView (View), ActionController (Controller) — the canonical web MVC.
- Django — models.py, templates/, views.py — views act as controllers in Django's interpretation.
- iOS UIKit —
UIViewControllermanages the View; separate Model classes hold data. - JavaScript frontend frameworks — React (View + Controller), Redux (Model), Vue.js (ViewModel in MVVM variant).
Related Patterns
- Observer connects Model changes to View updates in MVC.
- Strategy controllers can use different strategies for input handling.
- Composite is used to build the View hierarchy (widget tree).
- Facade the Controller acts as a facade between the user and the Model.
Pros and Cons
| Pros | Cons |
|---|---|
| Clear Separation Of Concerns | Complexity overhead for simple applications |
| Model can be tested independently of UI | Controller can become bloated with too much logic |
| Multiple views can share the same model | Tight coupling between Controller and View in some implementations |
| Parallel development — designers work on View, developers on Model | State synchronisation between Model and View requires careful design |
| Each component is independently replaceable | Not well-suited for real-time or bidirectional communication scenarios |
The code shows a task list application. The TaskModel stores tasks and notifies observers on changes. The TaskView is a passive renderer — it has no logic beyond formatting. The TaskController handles user actions (add_task, toggle_task), updates the model, and the model's notification triggers the view to re-render. The Controller also acts as an Observer.
Practice Questions
- How would you implement two-way data binding in MVC (View changes update the Model automatically)?
- Refactor a simple CRUD application from monolithic code into MVC components.
- Compare MVC with MVVM — what additional benefits does ViewModel provide?
- How do modern frontend frameworks (React, Vue) relate to the classic MVC pattern?
Challenge
Add a "filter" feature to the task list where the View can show only completed or only pending tasks. Where does filtering logic belong — Model, View, or Controller?
Real-World Task
Use DodaTech's architecture mapper to visualise your current application. Identify layers where concerns are mixed and refactor into clear Model, View, and Controller boundaries.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro