| Component | Responsibility | Road Profile Viewer |
|---|---|---|
| Model | Data and business logic; knows nothing about UI | RoadProfile class, GeometryCalculator |
| View | Displays data; receives user input | Dash layout, Plotly charts |
| Controller | Handles user input; updates Model and View | Dash callbacks |
Benefits:
Trade-offs:
Note: Django uses MTV (Model-Template-View) terminology, not MVC. Django's "View" acts as the Controller!
# models.py — Model (same as MVC)
class RoadProfile(models.Model):
name = models.CharField(max_length=100)
data = models.JSONField()
# views.py — Django "View" = MVC Controller (handles requests)
def profile_detail(request, profile_id):
profile = RoadProfile.objects.get(id=profile_id)
return render(request, 'profile.html', {'profile': profile})
# templates/profile.html — Django "Template" = MVC View (presentation)
<h1>{{ profile.name }}</h1>
# models.py (Model)
@dataclass
class RoadProfile:
id: str
name: str
coordinates: list[tuple[float, float]]
# routes.py (Controller)
@app.route('/profiles/<profile_id>')
def show_profile(profile_id):
profile = ProfileRepository.get(profile_id)
return render_template('profile.html', profile=profile)
# templates/profile.html (View) - Jinja2 template
2 minutes: Think about web applications you've used.
2 minutes: Discuss with a neighbor.
Share: What patterns do you recognize?
Discussion questions:
Model (models/profile.py):
class RoadProfile(BaseModel):
"""Domain model - knows nothing about UI or storage."""
id: str
name: str
x_coordinates: list[float]
y_coordinates: list[float]
def max_slope(self) -> float:
"""Business logic lives in the Model."""
...
View (presentation/charts.py):
def create_profile_figure(profile: RoadProfile) -> go.Figure:
"""Pure visualization - no business logic."""
return go.Figure(
data=go.Scatter(
x=profile.x_coordinates,
y=profile.y_coordinates,
mode='lines', name=profile.name
),
layout=go.Layout(title=f"Road Profile: {profile.name}")
)
Controller (presentation/callbacks.py):
@callback(
Output('profile-chart', 'figure'),
Input('profile-dropdown', 'value')
)
def update_chart(profile_id: str):
"""Controller: coordinates Model and View."""
if not profile_id:
return go.Figure() # Empty figure
profile = repository.get(profile_id) # Get Model
return create_profile_figure(profile) # Create View
With proper MVC separation:
RoadProfile can be tested without DashThe Model doesn't know about the View.
The View doesn't know about the Controller.
Everyone talks to the Model.
Layered architecture organizes code into horizontal layers, where each layer only depends on the layer below it.
┌─────────────────────────────────┐
│ Presentation Layer │ ← User Interface
└─────────────────────────────────┘
│ depends on
▼
┌─────────────────────────────────┐
│ Business Layer │ ← Domain Logic
└─────────────────────────────────┘
│ depends on
▼
┌─────────────────────────────────┐
│ Data Access Layer │ ← Database Operations
└─────────────────────────────────┘
The Three Golden Rules:
Downward dependencies only: Presentation → Business → Data
No skipping layers: Presentation should NOT call Data directly
Layers are cohesive: All UI code in Presentation, all domain logic in Business
Why? Changes in one layer don't cascade through the entire system.
| Layer | Responsibility | Typical Components |
|---|---|---|
| Presentation | Handle user interaction, display data | Web pages, API endpoints, CLI |
| Business | Domain logic, validation, calculations | Services, domain models, validators |
| Data Access | Persist and retrieve data | Repositories, ORM models, cache |
Netflix demonstrates layered principles at scale:
┌─────────────────────────────────────────────────────────┐
│ PRESENTATION: Web UI, Mobile Apps, TV Apps, API Gateway│
└─────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────┐
│ BUSINESS: User Service, Content Service, Recommendation│
└─────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────┐
│ DATA: Cassandra, MySQL, ElasticSearch, S3 │
└─────────────────────────────────────────────────────────┘
| Component | Description |
|---|---|
| Web UI | React Single Page Application |
| Mobile Apps | iOS & Android native apps |
| TV Apps | Smart TVs, Roku, PlayStation |
| API Gateway (Zuul) | Routes requests to services |
Each presentation client can be updated independently!
| Service | Responsibility |
|---|---|
| User Service | Authentication & profiles |
| Content Service | Movies & shows metadata |
| Recommendation | ML-powered suggestions |
| Playback Service | Streaming logic |
Each service can scale independently based on demand!
| Database | Use Case |
|---|---|
| Cassandra | User data & viewing history |
| MySQL | Billing & accounts |
| ElasticSearch | Search indexing |
| S3 | Video files (petabytes!) |
Key insight: Different data stores for different needs!
Independent scaling:
Technology flexibility:
Each layer can evolve independently.
@app.callback(...)
def update_chart(profile_id):
if not profile_id:
return {} # UI logic
conn = sqlite3.connect('profiles.db') # Data access
cursor = conn.execute('SELECT * FROM profiles WHERE id = ?', (profile_id,))
row = cursor.fetchone()
x_coords = json.loads(row[1]) # Business logic
y_coords = json.loads(row[2])
max_slope = calculate_max_slope(x_coords, y_coords)
return create_figure(x_coords, y_coords, max_slope) # UI again
Problem: All layers mixed in one function!
data_access/repositories.py:
class ProfileRepository:
def __init__(self, db_path: str = 'profiles.db'):
self.db_path = db_path
def get(self, profile_id: str) -> Profile | None:
conn = sqlite3.connect(self.db_path)
cursor = conn.execute(
'SELECT * FROM profiles WHERE id = ?', (profile_id,)
)
row = cursor.fetchone()
return Profile.from_row(row) if row else None
business/services.py:
class ProfileService:
def __init__(self, repository: ProfileRepository):
self.repository = repository
def get_profile_with_analysis(self, profile_id: str) -> dict:
profile = self.repository.get(profile_id)
if not profile:
raise ProfileNotFoundError(profile_id)
return {
'profile': profile,
'max_slope': self._calculate_max_slope(profile),
}
presentation/callbacks.py:
service = ProfileService(ProfileRepository())
@app.callback(...)
def update_chart(profile_id):
if not profile_id:
return empty_figure()
try:
data = service.get_profile_with_analysis(profile_id)
return create_figure(data)
except ProfileNotFoundError:
return error_figure("Profile not found")
Clean! Each layer has one responsibility.
Scenario: Profile calculations are slow. You want to cache results.
Discuss:
2 minutes think, 2 minutes pair, then share!
┌──────────────┐
│ Client 1 │ ──────┐
│ (Web Browser)│ │
└──────────────┘ │
▼
┌──────────────┐ ┌──────────┐ ┌──────────┐
│ Client 2 │ → │ Server │ → │ Database │
│ (Mobile App) │ │ (Web) │ └──────────┘
└──────────────┘ └──────────┘
▲
┌──────────────┐ │
│ Client 3 │ ──────┘
│ (Desktop App)│
└──────────────┘
| Component | Responsibility |
|---|---|
| Clients | Initiate requests, display results, handle user input |
| Servers | Process requests, manage data, enforce business rules |
| Protocol | Usually HTTP/HTTPS for web applications |
Road Profile Viewer is already client-server:
Microservices decompose a system into small, independently deployable services:
┌─────────────────┐
│ API Gateway │
└────────┬────────┘
┌───────────┼───────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ User │ │ Profile │ │ Analysis │
│ Service │ │ Service │ │ Service │
└────┬─────┘ └────┬─────┘ └────┬─────┘
▼ ▼ ▼
[User DB] [Profile DB] [Cache]
Monolith:
Microservices:
In 2002, Jeff Bezos mandated:
"All teams must expose their functionality through service interfaces."
The "two-pizza team" rule: Teams small enough to feed with two pizzas.
Result: Amazon Web Services — originally internal infrastructure, now an $80+ billion business.
DO use when:
DON'T use when:
Road Profile Viewer: Stay with a monolith! You have 3 developers and a simple domain.
Scenario: Your Road Profile Viewer team grows from 3 to 30 developers. You now have 5 feature teams working on different parts.
What architecture would you recommend?
A) Keep the monolith, just organize code better
B) Split into microservices
C) Start with a "modular monolith" and split later
Cloud platforms provide building blocks for distributed architectures:
Users → CloudFront (CDN) → Load Balancer → ECS/Fargate → RDS
(Edge Cache) (Routing) (Containers) (Database)
For Road Profile Viewer (future):
You don't need AWS complexity for a student project!
In Lecture 4 (Refactoring), we split a monolithic main.py into modules:
road-profile-viewer/
├── src/
│ ├── geometry.py # Ray intersection calculations
│ ├── road.py # Profile generation
│ ├── visualization.py # Chart creation
│ └── main.py # Application entry point
└── tests/
Good start! But we can go further with proper layered architecture.
road-profile-viewer/
├── src/
│ ├── domain/ # Core business concepts
│ │ ├── models.py # RoadProfile, Measurement
│ │ └── services.py # GeometryCalculator
│ │
│ ├── infrastructure/ # External systems
│ │ └── repositories.py # ProfileRepository
│ │
│ ├── presentation/ # User interface
│ │ ├── callbacks.py # Controllers
│ │ └── charts.py # Views
│ │
│ └── api/ # Optional: REST API
│ └── routes.py # FastAPI endpoints
Why add an API when Dash already works?
@app.get("/api/profiles/{profile_id}")
def get_profile(profile_id: str) -> ProfileResponse:
profile = repository.get(profile_id)
if not profile:
raise HTTPException(status_code=404)
return ProfileResponse(**profile.dict())
If you ever need to scale Road Profile Viewer:
| Change | What to Modify |
|---|---|
| Database | Replace SQLite with PostgreSQL — change only infrastructure/ |
| Caching | Add Redis cache — new file in infrastructure/ |
| Deployment | Containerize with Docker — no code changes |
| Load balancing | Add multiple instances — infrastructure only |
The layered architecture makes changes localized!
Scenario: The Product Owner wants to add "export profile to PDF".
In pairs, answer:
5 minutes, then we'll discuss!
Some developers believe Agile and architecture are in conflict:
Extreme 1:
"Agile means no upfront design — just start coding and refactor!"
Extreme 2:
"Architecture requires months of planning before writing any code!"
Both extremes are wrong.
The Agile approach: Emergent design with intentional structure.
Make enough decisions to start: Choose language, framework, basic structure
Implement working software: Build real features, not hypothetical infrastructure
Refactor as patterns emerge: When you see duplication, restructure
Revisit decisions regularly: Sprint retros can address architectural debt
A technical spike is a time-boxed investigation to reduce uncertainty.
Example spike story:
As a developer
I want to investigate PostgreSQL vs SQLite for our database
So that we can make an informed decisionTime-box: 2 days maximum
After the spike: Make an informed architectural decision, not a guess.
| Concept | Key Point | Application |
|---|---|---|
| Patterns | Proven solutions to recurring problems | Use MVC for predictable structure |
| MVC | Separate Model, View, Controller | Pydantic models, charts, callbacks |
| Layered | Presentation → Business → Data | presentation/, domain/, infrastructure/ |
| Distributed | Client-Server, Microservices | Stay monolithic for now |
| Agile | Emergent design, technical spikes | Make decisions, implement, refactor |
Patterns are proven solutions — MVC, layered architecture, and client-server solve recurring problems
MVC separates concerns — Model knows data, View knows display, Controller coordinates
Layers create boundaries — Dependencies flow downward only
Microservices aren't always the answer — Small teams benefit from well-structured monoliths
Architecture evolves with Agile — Start simple, refactor as patterns emerge
Pattern identification: Which pattern does your Road Profile Viewer currently follow?
Layer mapping: Which layer does each of your Python files belong to?
Dependency direction: Do all your imports point "downward"?
Scale trigger: What metric would tell you it's time to move from SQLite to PostgreSQL?
Your action items:
Identify your current pattern: Which pattern does your code follow (if any)?
Map your layers: Which code is presentation? Business? Data access?
Refactor one layer: Pick the messiest area and apply separation
Document your architecture: Create a simple ARCHITECTURE.md
Consider an API: Would a REST API benefit your project?
Books:
Online:
Part 1: Why architecture matters, 4+1 View Model
Part 2: Architectural patterns, applying to Road Profile Viewer
The goal isn't perfect architecture — it's intentional architecture.