06 Software Architecture Part 2: Architectural Patterns and Application
January 2026 (5966 Words, 34 Minutes)
1. Introduction: From Understanding to Application
In Part 1, we explored:
- Why architecture matters — the bridge between Agile delivery and sustainable systems
- Architectural design decisions — how non-functional requirements shape system structure
- The 4+1 View Model — understanding systems through multiple perspectives (Logical, Process, Development, Physical, and Scenarios)
Now we move from understanding architecture to applying it. This lecture introduces architectural patterns — proven solutions that thousands of systems have validated — and shows how to apply them to your Road Profile Viewer project.
1.1 The Pattern Promise
When a developer says “we use MVC,” other developers instantly understand:
- How responsibilities are divided
- Where to find business logic vs. UI code
- How to add new features without breaking existing ones
Patterns are not just code structures — they are shared vocabulary that enables efficient communication and predictable system behavior.
2. Learning Objectives
By the end of this lecture, you will be able to:
- Explain what architectural patterns are and why they provide value as proven solutions to recurring problems
- Apply MVC and layered architecture patterns to structure interactive applications with proper separation of concerns
- Evaluate when to use distributed patterns like client-server and microservices based on project requirements
- Design an evolved architecture for Road Profile Viewer that balances simplicity with future scalability
3. Architectural Patterns: Proven Solutions to Recurring Problems
3.1 What Is an Architectural Pattern?
An architectural pattern is a description of a system organization that has been successfully used in different software systems.
Patterns are not inventions — they are discoveries. Architects noticed that certain structures keep appearing in successful systems and documented them so others could benefit.
Why use patterns?
- Proven: Thousands of systems have validated these approaches
- Communicable: “We use MVC” instantly conveys structure to other developers
- Analyzable: Known patterns have known trade-offs
- Reusable: Framework support, documentation, and community knowledge
Pattern Categories:
| Category | Purpose | Example Patterns |
|---|---|---|
| Structuring Interactive Systems | Organize human-computer interactions | MVC, MVP, MVVM |
| Organizing System Structure | Organize components and objects | Layered, Pipes and Filters, Repository |
| Supporting Distributed Systems | Enable distributed resources and services | Client-Server, Microservices, Event-Driven |
3.2 Category 1: Structuring Interactive Systems
3.2.1 Model-View-Controller (MVC)
MVC separates an application into three interconnected components:
| Component | Responsibility | Road Profile Viewer Example |
|---|---|---|
| Model | Data and business logic; knows nothing about UI | RoadProfile class, GeometryCalculator |
| View | Displays data to user; receives user input | Dash layout, Plotly charts |
| Controller | Handles user input; updates Model and View | Dash callbacks |
Why MVC?
- Separation of concerns: UI designers can work on Views while developers work on Models
- Testability: Models can be unit tested without UI
- Flexibility: Same Model can have multiple Views (web, mobile, CLI)
3.2.2 MVC in Web Frameworks
Most web frameworks implement MVC (or variations like MVP, MVVM):
Django (Python):
Terminology note: Django uses MTV (Model-Template-View) instead of MVC. This causes confusion because Django’s “View” is actually the Controller in MVC terminology:
MVC Term Django Term Django File Model Model models.pyView (presentation) Template templates/*.htmlController (logic) View views.py
# models.py — Model (same in both MVC and MTV)
class RoadProfile(models.Model):
name = models.CharField(max_length=100)
data = models.JSONField()
# views.py — Django calls this "View", but it acts as MVC Controller
# It handles HTTP requests and coordinates Model ↔ Template
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></h1>
<div id="chart"></div>
Flask (Python):
# models.py
@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
3.2.3 Road Profile Viewer: Applying MVC
Let’s refactor Road Profile Viewer using MVC:
Model (models/profile.py):
from pydantic import BaseModel
class RoadProfile(BaseModel):
"""Domain model - knows nothing about UI or storage."""
id: str
name: str
x_coordinates: list[float]
y_coordinates: list[float]
def get_elevation_at(self, x: float) -> float:
"""Business logic lives in the Model."""
# Interpolate elevation at given x coordinate
...
def max_slope(self) -> float:
"""Calculate maximum slope - pure domain logic."""
...
View (presentation/charts.py):
import plotly.graph_objects as go
from models.profile import RoadProfile
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}",
xaxis_title="Distance (m)",
yaxis_title="Elevation (m)"
)
)
Controller (presentation/callbacks.py):
from dash import callback, Input, Output
from repositories.profile_repository import ProfileRepository
from presentation.charts import create_profile_figure
repository = ProfileRepository()
@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
# Get data from Model
profile = repository.get(profile_id)
# Create View representation
return create_profile_figure(profile)
Benefits achieved:
RoadProfilecan be tested without Dash- Charts can be reused in other contexts (export to PDF, etc.)
- Callbacks are thin — just coordination
3.3 Category 2: Organizing System Structure
3.3.1 Layered Architecture
Layered architecture organizes code into horizontal layers, where each layer only depends on the layer below it.
Key rules:
- Downward dependencies only: Presentation depends on Business; Business depends on Data
- No skipping layers: Presentation should not call Data directly
- Layers are cohesive: All UI code in Presentation, all domain logic in Business
| Layer | Responsibility | Typical Components |
|---|---|---|
| Presentation | Handle user interaction, display data | Web pages, API endpoints, CLI commands |
| Business | Domain logic, validation, calculations | Services, domain models, validators |
| Data Access | Persist and retrieve data | Repositories, ORM models, cache clients |
3.3.2 Netflix: A Layered Architecture Example
Netflix’s architecture demonstrates layered principles at scale:
Further reading: The Netflix Technology Blog provides detailed insights into their architecture. Particularly relevant:
- Optimizing the Netflix API — how their API Gateway (Zuul) evolved
- Netflix Conductor: A Microservices Orchestrator — coordinating services in the business layer
- Evolution of the Netflix Data Pipeline — how their data layer handles petabytes of data
Netflix also open-sources many of their architectural tools at netflix.github.io, including Zuul (API Gateway), Eureka (service discovery), and Hystrix (fault tolerance).
Each layer can scale independently:
- Need more recommendation capacity? Scale just that service
- Database slow? Add read replicas without changing business logic
- New TV platform? Add presentation client without touching backend
3.3.3 Road Profile Viewer: Applying Layers
Current structure (mixed concerns):
# Everything in one file - hard to maintain
app = Dash(__name__)
@app.callback(...)
def update_chart(profile_id):
# UI logic
if not profile_id:
return {}
# Data access
conn = sqlite3.connect('profiles.db')
cursor = conn.execute('SELECT * FROM profiles WHERE id = ?', (profile_id,))
row = cursor.fetchone()
# Business logic
x_coords = json.loads(row[1])
y_coords = json.loads(row[2])
max_slope = calculate_max_slope(x_coords, y_coords)
# Back to UI
return create_figure(x_coords, y_coords, max_slope)
Refactored with layers:
# data_access/repositories.py (Data Layer)
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()
if row:
return Profile.from_row(row)
return None
# business/services.py (Business Layer)
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),
'total_distance': self._calculate_distance(profile),
}
def _calculate_max_slope(self, profile: Profile) -> float:
# Business logic here
...
# presentation/callbacks.py (Presentation Layer)
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")
Benefits:
- Testable: Mock
ProfileRepositoryto testProfileService - Swappable: Replace SQLite with PostgreSQL by changing only
ProfileRepository - Understandable: Each file has one clear purpose
3.4 Category 3: Distributed Systems
3.4.1 Client-Server Architecture
The most fundamental distributed pattern: clients request services from servers.
graph LR
C1[Client 1<br>Web Browser] --> S[Server<br>Web Application]
C2[Client 2<br>Mobile App] --> S
C3[Client 3<br>Desktop App] --> S
S --> DB[(Database)]
Characteristics:
- 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:
- Client: Web browser running JavaScript/Dash frontend
- Server: Python Dash/Flask process handling callbacks
- Protocol: HTTP requests for page loads, WebSockets for live updates
3.4.2 Microservices Architecture
Microservices decompose a system into small, independently deployable services.
graph TB
API[API Gateway]
API --> US[User Service]
API --> PS[Profile Service]
API --> AS[Analysis Service]
US --> UDB[(User DB)]
PS --> PDB[(Profile DB)]
AS --> ADB[(Analysis Cache)]
vs. Monolith:
| Aspect | Monolith | Microservices |
|---|---|---|
| Deployment | All-or-nothing; entire app redeploys | Independent; deploy one service at a time |
| Scaling | Scale everything together | Scale individual services based on demand |
| Technology | One language/framework for all | Each service can use best tool for job |
| Failure | One bug can crash entire system | Failures are isolated to one service |
| Complexity | Simple deployment, complex codebase | Complex deployment, simpler codebases |
Amazon’s transformation:
In 2002, Jeff Bezos mandated that all teams expose their functionality through service interfaces. This “two-pizza team” rule (teams small enough to feed with two pizzas) led to Amazon Web Services — originally internal infrastructure, now a $80+ billion business.
When to use microservices:
- Large teams (can’t coordinate effectively on one codebase)
- Different scaling needs (some features have 100x more traffic)
- Technology diversity (some problems need different languages)
- Organizational independence (teams need autonomy)
When NOT to use microservices:
- Small teams (coordination overhead outweighs benefits)
- Early-stage products (need to iterate quickly)
- Simple domains (added complexity isn’t worth it)
Road Profile Viewer: A monolith is the right choice. You have 3 developers and a simple domain. Microservices would add significant complexity without benefit.
3.4.3 AWS and Cloud Architecture
Cloud platforms like AWS provide building blocks for distributed architectures:
Further reading: AWS provides extensive documentation and architecture guidance:
- AWS Well-Architected Framework — best practices for building secure, high-performing, and cost-efficient systems
- AWS Architecture Center — reference architectures and diagrams for common use cases
- What is Amazon CloudFront? — CDN service documentation
- Elastic Load Balancing — distributing traffic across multiple targets
- Amazon ECS on AWS Fargate — serverless container deployment
For students: AWS offers AWS Educate with free credits and learning resources.
For Road Profile Viewer deployment (future):
A simple cloud deployment might use:
- Render.com or Railway.app (simple PaaS, free tier available)
- Supabase (managed PostgreSQL with free tier)
You don’t need AWS complexity for a student project!
4. Applying Architecture to Road Profile Viewer
4.1 Current State: From Monolith to Modules
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/
This was a good start! But we can go further with proper layered architecture.
4.2 Proposed Evolved Architecture
road-profile-viewer/
├── src/
│ ├── __init__.py
│ │
│ ├── domain/ # Core business concepts
│ │ ├── __init__.py
│ │ ├── models.py # RoadProfile, Measurement
│ │ └── services.py # GeometryCalculator, ProfileAnalyzer
│ │
│ ├── infrastructure/ # External systems
│ │ ├── __init__.py
│ │ ├── database.py # Database connection
│ │ └── repositories.py # ProfileRepository
│ │
│ ├── presentation/ # User interface
│ │ ├── __init__.py
│ │ ├── app.py # Dash application setup
│ │ ├── layouts.py # Page layouts
│ │ ├── callbacks.py # Dash callbacks (Controllers)
│ │ └── charts.py # Chart builders (Views)
│ │
│ ├── api/ # Optional: REST API
│ │ ├── __init__.py
│ │ ├── routes.py # FastAPI endpoints
│ │ └── schemas.py # API request/response models
│ │
│ └── main.py # Entry point
│
├── tests/
│ ├── domain/
│ │ ├── test_models.py
│ │ └── test_services.py
│ ├── infrastructure/
│ │ └── test_repositories.py
│ └── presentation/
│ └── test_callbacks.py
│
├── pyproject.toml
└── README.md
4.3 Adding a REST API Layer
Why add an API when Dash already works?
- Multiple frontends: Same backend for web, mobile, CLI
- Testing: APIs are easier to test than UI callbacks
- Integration: Other systems can access your data
Simple FastAPI addition (api/routes.py):
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from domain.models import RoadProfile
from infrastructure.repositories import ProfileRepository
app = FastAPI(title="Road Profile Viewer API")
repository = ProfileRepository()
class ProfileResponse(BaseModel):
id: str
name: str
x_coordinates: list[float]
y_coordinates: list[float]
max_slope: float | None = None
@app.get("/api/profiles")
def list_profiles() -> list[ProfileResponse]:
"""List all available profiles."""
profiles = repository.list_all()
return [ProfileResponse(**p.dict()) for p in profiles]
@app.get("/api/profiles/{profile_id}")
def get_profile(profile_id: str) -> ProfileResponse:
"""Get a specific profile by ID."""
profile = repository.get(profile_id)
if not profile:
raise HTTPException(status_code=404, detail="Profile not found")
return ProfileResponse(**profile.dict())
@app.post("/api/profiles")
def create_profile(profile: RoadProfile) -> ProfileResponse:
"""Create a new profile."""
saved = repository.save(profile)
return ProfileResponse(**saved.dict())
Now both Dash and the API can use the same ProfileRepository and RoadProfile models.
4.4 Considering Future Scalability
If you ever need to scale Road Profile Viewer:
- Database: Replace SQLite with PostgreSQL (change only
infrastructure/database.py) - Caching: Add Redis cache in
infrastructure/cache.py(no domain changes) - Deployment: Containerize with Docker, deploy to cloud
- Load balancing: Add multiple instances behind a load balancer
The layered architecture makes each of these changes localized. You don’t have to rewrite the whole application.
But remember: Premature optimization is the root of all evil. Build the simple version first. Scale when you have evidence you need to.
5. Architecture and Agile: Working Together
5.1 The Apparent Conflict
Some developers believe Agile and architecture are in conflict:
“Agile means no upfront design — just start coding and refactor!”
“Architecture requires months of planning before writing any code!”
Both extremes are wrong.
5.2 Emergent Architecture in Scrum
The Agile approach to architecture is 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 or complexity, restructure
- Revisit decisions regularly: Sprint retrospectives can address architectural debt
In Scrum:
- Sprint 0: Initial architectural decisions (optional, but common)
- Backlog refinement: Identify stories with architectural implications
- Technical spikes: Time-boxed research for architectural questions
- Definition of Done: Include architectural quality (e.g., “code follows layer conventions”)
5.3 Technical Spikes for Architectural Decisions
A technical spike is a time-boxed investigation to reduce risk or uncertainty.
Example spike story:
As a developer I want to investigate PostgreSQL vs SQLite for our database So that we can make an informed decision about data storage
Acceptance criteria:
- Document pros/cons of each option
- Create proof-of-concept with PostgreSQL
- Measure performance with realistic data volume
- Present recommendation to team
Time-box: 2 days maximum
After the spike, the team can make an informed architectural decision, not a guess.
6. Summary
| Concept | Key Point | Road Profile Viewer Application |
|---|---|---|
| Architectural Patterns | Proven solutions to recurring design problems | Use patterns like MVC for predictable structure |
| MVC Pattern | Separate Model (data), View (display), Controller (coordination) | Pydantic models, chart builders, Dash callbacks |
| Layered Architecture | Presentation → Business → Data Access | presentation/, domain/, infrastructure/ packages |
| Distributed Patterns | Client-Server, Microservices for scale and independence | Already client-server; stay monolithic for now |
| Agile + Architecture | Emergent design with intentional structure | Use technical spikes for uncertain decisions |
| Evolved Structure | domain/, infrastructure/, presentation/ packages | Clear separation enables future growth |
6.1 Key Takeaways
- Patterns are proven solutions — MVC, layered architecture, and client-server solve recurring problems with known trade-offs
- MVC separates concerns — Model knows data, View knows display, Controller coordinates
- Layers create boundaries — Presentation depends on Business depends on Data, never the reverse
- Microservices aren’t always the answer — Small teams benefit more from well-structured monoliths
- Architecture evolves with Agile — Start simple, refactor as patterns emerge, use spikes for big decisions
- Structure enables change — Good architecture makes future changes localized and predictable
7. Reflection Questions
-
Pattern identification: Looking at your current Road Profile Viewer code, can you identify which pattern (if any) it currently follows? What would need to change to make it clearly follow MVC?
-
Layer mapping: List every Python file in your Road Profile Viewer project. Which layer (Presentation, Business, Data Access) does each file belong to? Are there files that mix multiple layers?
-
Dependency direction: Draw a diagram showing how your Python modules import each other. Do all dependencies point “downward” (Presentation → Business → Data)? If not, which imports violate layering?
-
Framework fit: Both Dash and Django implement MVC-like patterns. How does Dash’s callback system map to the Controller role? What about Django’s view functions?
-
Scale trigger: What specific event or metric would tell you it’s time to move Road Profile Viewer from SQLite to PostgreSQL? Write a concrete threshold (e.g., “When daily active users exceed X” or “When database file size exceeds Y MB”).
-
Technical spike: Write a 2-day spike story for investigating one architectural question about Road Profile Viewer (e.g., “Should we add a REST API?” or “Should we use async callbacks?”).
8. Further Reading
8.1 Books
- Ian Sommerville: Software Engineering (10th Edition), Chapter 6 — Source material for this lecture
- Robert C. Martin: Clean Architecture — Practical patterns for maintainable software
- Martin Fowler: Patterns of Enterprise Application Architecture — Comprehensive pattern catalog
- Sam Newman: Building Microservices — When and how to use microservices
8.2 Articles and Resources
- Martin Fowler on MVC — Evolution of UI architectural patterns
- Martin Fowler on Microservices — Definitive introduction to microservices
- The Twelve-Factor App — Best practices for cloud-native applications
- Netflix Tech Blog — Real-world architecture case studies
- FastAPI Documentation — Modern Python API framework
8.3 Tools
- Draw.io / diagrams.net — Free diagramming for architecture diagrams
- Mermaid — Text-based diagrams (used in this course)
- PlantUML — UML diagrams from text
- C4 Model — Hierarchical architecture diagrams (Context, Container, Component, Code)
9. What’s Next
With both parts of this lecture, you now have a complete understanding of software architecture:
Part 1 covered:
- Why architecture matters and what influences architectural decisions
- The 4+1 View Model for understanding systems from multiple perspectives
- How non-functional requirements shape architectural choices
Part 2 covered:
- Architectural patterns (MVC, Layered, Client-Server, Microservices)
- Applying patterns to Road Profile Viewer
- How architecture and Agile development work together
Apply to Road Profile Viewer:
- Identify your current pattern: Which pattern does your code currently follow (if any)?
- Map your layers: Which code is presentation? Business logic? Data access?
- Refactor one layer: Pick the messiest area and apply proper separation
- Document your architecture: Create a simple
ARCHITECTURE.mdwith the four views - Consider an API: Would a REST API benefit your project?
The goal isn’t perfect architecture — it’s intentional architecture. Make conscious decisions about structure, document them, and evolve as you learn.