Home

06 Software Architecture Part 2: Architectural Patterns and Application

lecture architecture design-patterns MVC layered-architecture microservices distributed-systems

1. Introduction: From Understanding to Application

In Part 1, we explored:

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:

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:

  1. Explain what architectural patterns are and why they provide value as proven solutions to recurring problems
  2. Apply MVC and layered architecture patterns to structure interactive applications with proper separation of concerns
  3. Evaluate when to use distributed patterns like client-server and microservices based on project requirements
  4. 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?

  1. Proven: Thousands of systems have validated these approaches
  2. Communicable: “We use MVC” instantly conveys structure to other developers
  3. Analyzable: Known patterns have known trade-offs
  4. 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:

interacts user input updates notifies
👤 User
🖼️ View UI Display
🎮 Controller Input Handling
💾 Model Data & Logic
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?

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.py
View (presentation) Template templates/*.html
Controller (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:

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.

🖥️ Presentation Layer User Interface
depends on
⚙️ Business Layer Domain Logic
depends on
📁 Data Access Layer Database Operations
connects to
🗄️ Database Persistent Storage

Key rules:

  1. Downward dependencies only: Presentation depends on Business; Business depends on Data
  2. No skipping layers: Presentation should not call Data directly
  3. 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:

🎬
Presentation Layer
Web UI
React SPA
Mobile Apps
iOS & Android
TV Apps
Smart TVs, Roku
API Gateway
Zuul
API calls
⚙️
Business Layer (Microservices)
User Service
Auth & profiles
Content Service
Movies & shows
Recommendation
ML suggestions
Playback Service
Streaming logic
Data queries
💾
Data Layer
Cassandra
User data & history
MySQL
Billing & accounts
ElasticSearch
Search indexing
S3
Video files

Further reading: The Netflix Technology Blog provides detailed insights into their architecture. Particularly relevant:

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:

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:

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:

Road Profile Viewer is already client-server:

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:

When NOT to use microservices:

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:

👥 Users (Browsers, Apps)
Web browsers, mobile apps, API clients
🌐 CloudFront (CDN) AWS
Caches static content at edge locations worldwide
⚖️ Application Load Balancer AWS
Routes requests to healthy instances
📦 ECS/Fargate
Container 1
📦 ECS/Fargate
Container 2
🗄️ RDS (PostgreSQL) AWS
Managed database with automatic backups

Further reading: AWS provides extensive documentation and architecture guidance:

For students: AWS offers AWS Educate with free credits and learning resources.

For Road Profile Viewer deployment (future):

A simple cloud deployment might use:

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?

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:

  1. Database: Replace SQLite with PostgreSQL (change only infrastructure/database.py)
  2. Caching: Add Redis cache in infrastructure/cache.py (no domain changes)
  3. Deployment: Containerize with Docker, deploy to cloud
  4. 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:

  1. Make enough decisions to start: Choose language, framework, basic structure
  2. Implement working software: Build real features, not hypothetical infrastructure
  3. Refactor as patterns emerge: When you see duplication or complexity, restructure
  4. Revisit decisions regularly: Sprint retrospectives can address architectural debt

In Scrum:

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

  1. Patterns are proven solutions — MVC, layered architecture, and client-server solve recurring problems with known trade-offs
  2. MVC separates concerns — Model knows data, View knows display, Controller coordinates
  3. Layers create boundaries — Presentation depends on Business depends on Data, never the reverse
  4. Microservices aren’t always the answer — Small teams benefit more from well-structured monoliths
  5. Architecture evolves with Agile — Start simple, refactor as patterns emerge, use spikes for big decisions
  6. Structure enables change — Good architecture makes future changes localized and predictable

7. Reflection Questions

  1. 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?

  2. 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?

  3. 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?

  4. 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?

  5. 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”).

  6. 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

8.2 Articles and Resources

8.3 Tools


9. What’s Next

With both parts of this lecture, you now have a complete understanding of software architecture:

Part 1 covered:

Part 2 covered:

Apply to Road Profile Viewer:

  1. Identify your current pattern: Which pattern does your code currently follow (if any)?
  2. Map your layers: Which code is presentation? Business logic? Data access?
  3. Refactor one layer: Pick the messiest area and apply proper separation
  4. Document your architecture: Create a simple ARCHITECTURE.md with the four views
  5. 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.

© 2026 Dominik Mueller   •  Powered by Soopr   •  Theme  Moonwalk