Home

06 Software Architecture Part 1: Foundations and Architectural Views

lecture architecture non-functional-requirements 4+1-views design-decisions trade-offs

1. Introduction: Your Sprints Are Fast, But Can Your System Scale?

In Parts 1 and 2 of Agile Development, we learned to:

Your Road Profile Viewer team has been delivering features rapidly. The Sprint Review demos are impressive. The Product Owner is happy. The CI pipeline is green.

Then the stakeholder asks: “Can we deploy this to 1,000 concurrent users across three European offices?”

Suddenly, you realize:

The Agile process is working. The architecture is not.

This scenario isn’t hypothetical. Twitter famously experienced this in 2008-2010: their monolithic Ruby on Rails application couldn’t handle the explosive growth in users. The “Fail Whale” error page became iconic — a symbol of what happens when a system’s architecture can’t support its success.

1.1 What Is Software Architecture?

Software architecture describes how a system is organized as a set of communicating components and how those components behave and interact.

Architecture is the fundamental organization that shapes everything from how fast the system runs to how easily it can be changed. It answers questions like:

1.2 The Architecture Model

An architecture model enables:

Benefit Description Example
Stakeholder Communication Provides a shared understanding for technical and non-technical stakeholders Block diagrams showing how Road Profile Viewer connects to the database
System Analysis Enables evaluation against critical requirements before implementation "If we add 1,000 users, which component will bottleneck first?"
Reuse Allows leveraging existing solutions and patterns Using the MVC pattern because it's proven in millions of web apps
Documentation Records design decisions and their rationale for future developers "We chose SQLite for simplicity; migrate to PostgreSQL if scaling"

1.3 The Bridge: Agile Meets Architecture

Agile tells us how to organize work. Architecture tells us how to organize the system itself. Both must work together.

Some developers believe Agile means “no upfront design” — just code and refactor. This is a misunderstanding. Even the most Agile teams need architectural decisions early:

The difference is that Agile architecture evolves through iterative refinement rather than being fully specified upfront. We make enough architectural decisions to start, then refine as we learn.


2. Learning Objectives

By the end of this lecture (Part 1), you will be able to:

  1. Name the motivation and goals for architectural design — why architecture matters and what an architecture model enables
  2. Explain the dependency between system structure and non-functional requirements — how performance, security, and maintainability shape architectural choices
  3. Apply the 4+1 View Model to describe a system from multiple perspectives — Logical, Process, Development, Physical, and Scenarios

Part 2 will cover architectural patterns (MVC, layered architecture, distributed systems) and how to apply them to Road Profile Viewer.


3. Architectural Design Decisions

3.1 Architecture as a Creative Process

Architectural design is fundamentally creative. Unlike algorithms with provably optimal solutions, architecture involves trade-offs that depend on:

“There is no formulaic architectural design process. It depends on the type of system being developed, the background and experience of the system architect, and the specific requirements for the system.” — Sommerville, Software Engineering

Because of this, it’s best to think of architectural design as a series of decisions to be made rather than a sequence of steps to follow.

3.2 Key Design Questions

During architectural design, architects must consider these fundamental questions:

?
1 📋 Is there a generic architecture template?
2 🖥️ How will it be distributed across hardware?
3 🧩 What architectural patterns might be used?
4 🏗️ What is the fundamental structuring approach?
5 📦 How will components be decomposed?
6 🎛️ What strategy controls component operation?
7 What organization delivers NFRs best?
8 📝 How should the architecture be documented?
Templates & Patterns
System Structure
Non-Functional Requirements
Documentation

For Road Profile Viewer, consider:

3.3 Non-Functional Requirements Drive Architecture

The most profound insight in architectural design is this:

Architectural decisions depend heavily on non-functional requirements, and conversely, influence the system’s non-functional properties.

Your choice of architecture determines what the system can do, not just what it does.

Non-Functional Requirement Architectural Implication Example
Performance Localize critical operations in few large components; deploy on same machine to minimize network latency Game engines keep physics and rendering in a tight loop on GPU
Security Use layered architecture with most critical assets in innermost layers; apply high security validation to inner layers Banking systems: public web → API gateway → business logic → encrypted database
Safety Co-locate safety-related operations in single component or small set; enables focused validation and protection systems Aircraft control: safety-critical code isolated, can be formally verified
Availability Include redundant components; enable replacement and updates without stopping the system Netflix: multiple instances of each service; one failure doesn't take down the site
Maintainability Use fine-grained, self-contained components; separate data producers from consumers; avoid shared data structures Microservices: each team owns their service, can change without coordinating

3.3.1 Performance-Driven Architecture

When performance is the primary concern, architecture must minimize latency and maximize throughput. This shapes every decision — from how components are grouped to where they physically run.

Core Principles for Performance:

  1. Localize critical operations: Keep performance-sensitive code in as few components as possible to minimize communication overhead
  2. Minimize network hops: Every network call adds latency (typically 1-100ms); local function calls are nanoseconds
  3. Co-locate data and computation: Process data where it lives; don’t ship large datasets across boundaries
  4. Use larger, monolithic components: Fewer, bigger components mean less inter-component communication

Example: Web Application Performance

# Performance-optimized: Keep related operations together
class ProfileAnalyzer:
    """All performance-critical calculations in one place."""

    def __init__(self, profile_data: list[float]):
        # Cache data locally to avoid repeated fetches
        self._data = profile_data
        self._cached_stats = None

    def analyze(self) -> dict:
        """Single method performs all calculations in memory."""
        if self._cached_stats is None:
            # Do all computation in one pass
            self._cached_stats = {
                "mean": sum(self._data) / len(self._data),
                "max": max(self._data),
                "min": min(self._data),
                "range": max(self._data) - min(self._data),
            }
        return self._cached_stats

This keeps all calculations in a single class with cached results — no network calls, no database queries during analysis.

Real-Time and Embedded Systems

Performance requirements become critical in real-time systems where responses must occur within strict time bounds. Missing a deadline isn’t just slow — it’s a failure.

System Type Timing Requirement Architectural Implication
Hard real-time Deadlines must never be missed Predictable execution paths, no garbage collection, dedicated hardware
Soft real-time Occasional deadline misses acceptable Priority scheduling, bounded queues, graceful degradation
Near real-time Fast response expected (< 100ms) Caching, async processing, load balancing

Case Study: Autonomous Vehicle Architecture

Autonomous vehicles are among the most complex performance-critical systems, with dozens of embedded devices that must work together in real-time:

Autonomous Vehicle Architecture
📡 LiDAR 10-20 Hz
📷 Camera 30-60 Hz
📶 Radar 20-77 Hz
🛰️ GPS/GNSS 10 Hz
🔀 Sensor Fusion ECU ⚡ < 50ms
Combines all sensor data into unified world model
🧠 Perception & Planning 🎮 GPU
Object detection • Path planning • Decision making
CAN Bus / Ethernet TSN
🎯 Steering ECU 1 kHz
🛑 Braking ECU 1 kHz
Throttle ECU 100 Hz
Sensors
Processing
Actuators
Safety-Critical (1 kHz)

Note: This diagram presents a simplified, illustrative architecture for educational purposes. It is not derived from any specific production vehicle. Real-world autonomous vehicle architectures vary significantly between manufacturers and are continuously evolving as sensor technology, compute capabilities, and safety standards advance. The architectural landscape in this domain is broad—from Tesla’s vision-only approach to Waymo’s multi-sensor fusion strategy—and remains an active area of research and innovation.

Why this architecture?

Contrast: Microservices Would Fail Here

Imagine if an autonomous vehicle used web-style microservices:

❌ BAD: HTTP request to "Brake Service" → 50-200ms network latency
       Car travels 2.8 meters at 100 km/h during that time!

✓ GOOD: Direct CAN bus message → < 1ms latency
        Car travels 2.8 centimeters

Robotics Example: Industrial Robot Arm

# Real-time control loop for robot arm (runs at 1 kHz)
class RobotArmController:
    """
    All control logic in single tight loop.
    No network calls, no disk I/O, no dynamic memory allocation.
    """

    def __init__(self):
        # Pre-allocate all memory at startup
        self.joint_positions = [0.0] * 6  # 6-axis robot
        self.target_positions = [0.0] * 6
        self.motor_commands = [0.0] * 6

    def control_cycle(self) -> None:
        """
        Called every 1ms. MUST complete within 1ms.
        Any delay causes jerky motion or safety shutdown.
        """
        # Read sensors (direct hardware access, no abstraction layers)
        self._read_encoders()

        # Compute control signals (pure math, no I/O)
        for i in range(6):
            error = self.target_positions[i] - self.joint_positions[i]
            self.motor_commands[i] = self._pid_control(i, error)

        # Write to motors (direct hardware access)
        self._write_motors()

    def _pid_control(self, joint: int, error: float) -> float:
        """PID controller - must be deterministic and fast."""
        # Simplified PID (actual implementation has more terms)
        return error * self.kp[joint]

Key architectural decisions for real-time:

Performance Architecture Strategies

Strategy When to Use Example
Monolithic core Critical path must be fast Game engines keep physics + rendering together
Caching layers Repeated expensive operations Redis cache in front of database
Async processing Non-critical work can be deferred Queue email sending instead of blocking
Edge computing Latency to cloud is too high Process sensor data locally on IoT device
Hardware acceleration Computation is the bottleneck GPU for ML inference, FPGA for signal processing

Trade-off: Performance-optimized architectures sacrifice modularity and testability. Tightly coupled, monolithic components are fast but hard to change. Choose this approach only when performance requirements demand it.

3.3.2 Security-Driven Architecture

When security is critical, use defense in depth:

🛡️ Defense in Depth Architecture 🔐
🌐
Public Internet
⚠️ Untrusted Zone
1
🔥 Web Application Firewall
🚫 Block malicious requests ⏱️ Rate limiting
2
🚪 API Gateway
🎫 JWT Authentication Request validation
3
⚙️ Business Logic
👤 Role-based authorization 🧹 Input sanitization
4
🔌 Data Access Layer
🔒 Encrypted connections 💉 SQL injection prevention
🗄️
Database
🔐 Encrypted at Rest

Each layer adds protection; an attacker must breach all five to access data.

Interested in Security? This overview only scratches the surface of security architecture. If you find this topic fascinating, Hochschule Aalen offers dedicated security courses in upcoming semesters—or you might even consider shifting your focus toward IT Security as a specialization. We won’t dive deeper into security in this lecture, but the foundations you learn here will serve you well in those advanced courses.

3.3.3 Availability-Driven Architecture

What is availability? Availability measures how often your system is operational and accessible. It’s typically expressed as a percentage—”99.9% availability” (called “three nines”) means your system can be down for at most 8.76 hours per year. “99.99%” (four nines) allows only 52 minutes of downtime per year.

Why does this matter architecturally? A single server will eventually fail—hardware breaks, networks drop, software crashes. If your architecture relies on one instance of anything critical, that single point of failure determines your maximum availability.

The core principle: Redundancy. To achieve high availability, you must design your system so that when one component fails, another can take over. This has profound architectural implications:

Availability Target Max Downtime/Year Architectural Requirements
99% ("two nines") 3.65 days Basic monitoring, manual recovery acceptable
99.9% ("three nines") 8.76 hours Redundant components, automated failover
99.99% ("four nines") 52.6 minutes Multiple data centers, no single points of failure
99.999% ("five nines") 5.26 minutes Active-active replication, geographic distribution

Example: Database Failover Pattern

Let’s examine a common availability pattern—database replication with automatic failover:

Database Replication with Automatic Failover
🖥️ Application
queries
🗄️ Primary DB ✏️ read/write
🗄️ Replica 1 👁️ read-only
🗄️ Replica 2 👁️ read-only
🔄 Synchronous Replication
Primary (handles writes)
Replicas (handle reads)
Real-time sync

The thought process behind this design:

  1. Primary handles writes: All write operations go to one database to avoid conflicts
  2. Replicas handle reads: Read operations can go to any replica, distributing load
  3. Synchronous replication: Changes are copied to replicas in real-time
  4. Failover capability: If primary fails, a replica can be promoted

Here’s how this translates to code:

# Availability pattern: Multiple instances with failover
class HighAvailabilityService:
    """
    A service that maintains connections to multiple database instances.

    Design decisions:
    - Primary database handles all writes (consistency)
    - Replicas handle reads (scalability + availability)
    - Automatic failover when instances become unreachable
    """

    def __init__(self, primary_db: str, replica_dbs: list[str]):
        self.primary = primary_db
        self.replicas = replica_dbs

    def read_data(self, query: str):
        """
        Read from any available database.

        Why iterate through all databases?
        - If primary is slow or down, replicas can serve reads
        - If one replica fails, we try the next
        - Only raise error if ALL instances are unreachable

        This is called the "failover" pattern.
        """
        databases = [self.primary] + self.replicas

        for db in databases:
            try:
                return self._execute_query(db, query)
            except ConnectionError:
                # Log the failure, but don't crash—try next database
                self._log_failure(db)
                continue

        # Only fail if we've exhausted all options
        raise ServiceUnavailableError("All databases unreachable")

    def write_data(self, query: str):
        """
        Write only to primary database.

        Why not write to replicas?
        - Avoids conflicting writes (two users update same record)
        - Replication handles propagating changes
        - Simpler consistency model
        """
        return self._execute_query(self.primary, query)

    def health_check(self) -> dict:
        """
        Report which instances are healthy.

        Why expose this?
        - Load balancers use it to route traffic
        - Monitoring systems use it for alerts
        - Operations team uses it for debugging
        """
        return {
            "primary": self._is_healthy(self.primary),
            "replicas": [self._is_healthy(r) for r in self.replicas],
            "overall": self._is_healthy(self.primary) or any(
                self._is_healthy(r) for r in self.replicas
            ),
        }

Real-world example: Netflix

Netflix requires extremely high availability—millions of users streaming simultaneously. Their internal availability goal is 99.99% (only 52 minutes of downtime per year). Their architectural approach includes:

Further reading: The Netflix Technology Blog documents their engineering practices in detail. Their Chaos Engineering tagged posts are particularly valuable for understanding resilience testing.

Trade-off: High availability architectures are expensive. Each replica costs money, and the complexity of managing failover adds development and operational overhead. Only invest in high availability where the business truly requires it.

3.3.4 Maintainability-Driven Architecture

What is maintainability? Maintainability measures how easily your system can be modified—to fix bugs, add features, or adapt to changing requirements. Unlike performance (which you can measure in milliseconds) or availability (which you can measure in uptime percentage), maintainability is harder to quantify but critically important for long-lived software.

Why does this matter architecturally? Studies consistently show that 80% of software cost is maintenance, not initial development. A system that’s fast to build but hard to change becomes increasingly expensive over time. For student projects that will be handed off to future cohorts, maintainability is arguably the most important quality.

The core principle: Separation of Concerns. A maintainable architecture isolates different responsibilities so that:

  1. Changes are localized: Modifying one feature doesn’t require touching unrelated code
  2. Components are understandable: Each piece is small enough to comprehend fully
  3. Testing is straightforward: You can test each component in isolation
  4. Teams can work independently: Different developers can work on different components without conflicts

Signs of poor maintainability:

# ❌ BAD: Everything mixed together (the "God Class" anti-pattern)
class ProfileManager:
    def load_and_validate_and_display_and_save(self, path: str):
        # 500 lines mixing file I/O, validation, visualization, database access
        # Change to database? Touch this class.
        # Change to chart style? Touch this class.
        # Fix validation bug? Touch this class.
        # Every change risks breaking something unrelated.
        ...

Signs of good maintainability:

The key insight is the Single Responsibility Principle (SRP): each component should have exactly one reason to change. Let’s apply this to the Road Profile Viewer:

# ✓ GOOD: Separated responsibilities

# ──────────────────────────────────────────────────────────────────
# FILE: profiles/repository.py
# RESPONSIBILITY: Data access (how profiles are stored/retrieved)
# CHANGES WHEN: Database technology changes, file format changes
# ──────────────────────────────────────────────────────────────────
class ProfileRepository:
    """
    Only responsibility: Data access for profiles.

    This class knows HOW to store and retrieve profiles,
    but knows nothing about validation rules or visualization.
    """

    def __init__(self, database_path: str):
        self.database_path = database_path

    def get(self, profile_id: str) -> Profile:
        """Load a profile from storage."""
        # Implementation details hidden here
        # Could be SQLite, PostgreSQL, JSON files—callers don't care
        ...

    def save(self, profile: Profile) -> None:
        """Persist a profile to storage."""
        ...

    def list_all(self) -> list[Profile]:
        """List all available profiles."""
        ...


# ──────────────────────────────────────────────────────────────────
# FILE: profiles/service.py
# RESPONSIBILITY: Business logic (rules and workflows)
# CHANGES WHEN: Business rules change, validation requirements change
# ──────────────────────────────────────────────────────────────────
class ProfileService:
    """
    Only responsibility: Business logic for profiles.

    This class knows WHAT operations are valid,
    but delegates HOW to store data to the repository.
    """

    def __init__(self, repository: ProfileRepository):
        # Dependency injection: receive repository, don't create it
        # This makes testing easy—pass a mock repository in tests
        self.repository = repository

    def validate_and_save(self, profile: Profile) -> None:
        """
        Validate a profile according to business rules, then save.

        Why separate from repository.save()?
        - Validation rules are business logic, not data access
        - Repository shouldn't know about business rules
        - We can change validation without touching storage code
        """
        self._validate_measurements(profile)
        self._validate_metadata(profile)
        self.repository.save(profile)

    def _validate_measurements(self, profile: Profile) -> None:
        """Check that measurements are physically plausible."""
        if profile.length_km < 0:
            raise ValidationError("Profile length cannot be negative")
        if profile.max_elevation > 9000:  # Higher than Everest
            raise ValidationError("Elevation seems implausible")

    def _validate_metadata(self, profile: Profile) -> None:
        """Check that required metadata is present."""
        if not profile.name:
            raise ValidationError("Profile must have a name")


# ──────────────────────────────────────────────────────────────────
# FILE: profiles/views.py
# RESPONSIBILITY: Visualization (how data is displayed)
# CHANGES WHEN: UI requirements change, chart library changes
# ──────────────────────────────────────────────────────────────────
def create_profile_chart(profile: Profile) -> Figure:
    """
    Only responsibility: Create a visual representation of a profile.

    This function knows HOW to create charts,
    but knows nothing about validation or storage.
    """
    fig, ax = plt.subplots()
    ax.plot(profile.distances, profile.elevations)
    ax.set_xlabel("Distance (km)")
    ax.set_ylabel("Elevation (m)")
    ax.set_title(profile.name)
    return fig


def create_gradient_chart(profile: Profile) -> Figure:
    """Create a chart showing road gradient (slope)."""
    # Different visualization, same responsibility category
    ...

Why this structure helps maintainability:

Change Request Without Separation With Separation
"Switch from SQLite to PostgreSQL" Hunt through 2000-line class, risk breaking validation Modify only repository.py
"Add elevation limit validation" Hunt through 2000-line class, risk breaking charts Modify only service.py
"Change chart colors to match brand" Hunt through 2000-line class, risk breaking database Modify only views.py
"Write unit tests for validation" Need to set up database and UI mocks Test ProfileService with mock repository

Dependency Injection: The Key to Testability

Notice how ProfileService receives its repository in the constructor rather than creating it internally:

# ❌ Hard to test: Service creates its own dependencies
class ProfileService:
    def __init__(self):
        self.repository = ProfileRepository("production.db")  # Hardcoded!

# ✓ Easy to test: Dependencies are injected
class ProfileService:
    def __init__(self, repository: ProfileRepository):
        self.repository = repository  # Caller decides which repository

# In production:
service = ProfileService(ProfileRepository("production.db"))

# In tests:
mock_repo = MockProfileRepository()  # Fake that doesn't touch real database
service = ProfileService(mock_repo)
service.validate_and_save(test_profile)
assert mock_repo.save_was_called_with(test_profile)

Trade-off: Maintainable architectures have more files, more classes, and more indirection. A simple script becomes a structured project. This overhead isn’t worth it for throwaway code, but for any software that will live beyond its initial development, the investment pays dividends.

For Road Profile Viewer: As a student project that future cohorts will inherit, prioritize maintainability. Your successors will thank you when they can understand and modify the code without fear of breaking unrelated features.

3.4 Trade-offs: You Cannot Optimize Everything

Here’s the hard truth: architectural goals often conflict.

Goal A Conflicts With Why
Performance Maintainability Large components are fast but hard to change; small components are flexible but add communication overhead
Security Usability More security layers mean more friction for legitimate users
Availability Cost Redundant systems are expensive to build and operate
Flexibility Simplicity Configurable systems have more code paths and potential bugs

How to handle trade-offs:

  1. Prioritize: Decide which non-functional requirements matter most for your system
  2. Partition: Use different architectures for different parts (performance-critical core + maintainable plugins)
  3. Document: Record why you made each trade-off for future developers

For Road Profile Viewer: Maintainability and simplicity should be priorities. Performance optimization can wait until you actually have 1,000 users.


4. Architectural Views: Multiple Perspectives on One System

4.1 Why One Diagram Is Never Enough

A single diagram cannot capture all aspects of a system’s architecture. Consider what different stakeholders need to know:

Each of these questions requires a different view of the same system.

“It is impossible to represent all relevant information about a system’s architecture in a single diagram.” — Sommerville, Software Engineering

4.2 The 4+1 View Model

Philippe Krutchen proposed the 4+1 view model in 1995, which has become the standard way to describe software architecture. It consists of four fundamental views, connected by scenarios (use cases):

🎯 Scenarios Use Cases (+1)
Objects & Classes Runtime Processes Code Organization Hardware Deploy
🧠 Logical View Key abstractions as objects and classes
Process View Runtime interactions and processes
🖥️ Physical View Hardware and deployment topology
📦 Development View Code modules and packages
View Shows Audience Key Question
Logical View Key abstractions as objects or object classes Developers, domain experts "What are the main concepts and how do they relate?"
Process View Runtime composition of interacting processes Performance engineers, integrators "What runs when, and how do components interact at runtime?"
Development View How software is decomposed for development Programmers, project managers "How is the code organized into modules and packages?"
Physical View System hardware and distribution of components System engineers, operations "What hardware runs what software?"
Scenarios (+1) Use cases that exercise the architecture All stakeholders "How does a user request flow through all the layers?"

4.2.1 Logical View

The logical view shows the system’s key abstractions — the concepts that make up the domain.

Road Profile Viewer Logical View:

classDiagram
    class RoadProfile {
        +name: str
        +x_coordinates: list
        +y_coordinates: list
        +metadata: dict
    }

    class ProfileRepository {
        +get(id): RoadProfile
        +save(profile): void
        +list_all(): list
    }

    class GeometryCalculator {
        +find_intersection(ray, surface)
        +calculate_angle(point1, point2)
    }

    class ChartBuilder {
        +build_profile_chart(profile): Figure
        +build_comparison_chart(profiles): Figure
    }

    ProfileRepository --> RoadProfile
    ChartBuilder --> RoadProfile
    GeometryCalculator --> RoadProfile

This view relates directly to requirements: “The system must manage road profiles, perform geometric calculations, and display charts.”

4.2.2 Process View

The process view shows what happens at runtime — processes, threads, and their interactions.

Road Profile Viewer Process View:

sequenceDiagram
    participant User
    participant DashApp
    participant Callbacks
    participant Database

    User->>DashApp: Select profile from dropdown
    DashApp->>Callbacks: Trigger update callback
    Callbacks->>Database: Query profile data
    Database-->>Callbacks: Return profile
    Callbacks->>Callbacks: Generate chart
    Callbacks-->>DashApp: Return updated figure
    DashApp-->>User: Display chart

This view helps analyze performance: “Every user interaction triggers a database query — is that a bottleneck?”

4.2.3 Development View

The development view shows how code is organized into packages and modules.

Road Profile Viewer Development View:

road-profile-viewer/
├── src/
│   ├── __init__.py
│   ├── models/           # Domain models
│   │   ├── __init__.py
│   │   └── profile.py    # RoadProfile class
│   ├── repositories/     # Data access
│   │   ├── __init__.py
│   │   └── profile_repository.py
│   ├── services/         # Business logic
│   │   ├── __init__.py
│   │   ├── geometry.py   # Calculations
│   │   └── analysis.py   # Profile analysis
│   ├── presentation/     # UI components
│   │   ├── __init__.py
│   │   ├── app.py        # Dash application
│   │   └── charts.py     # Chart builders
│   └── main.py           # Entry point
├── tests/
│   ├── test_models.py
│   ├── test_services.py
│   └── test_repositories.py
└── pyproject.toml

This view helps coordinate development: “Anna works on services/, Ben works on presentation/.”

4.2.4 Physical View

The physical view shows hardware and deployment.

Road Profile Viewer — Current (Simple):

Developer Laptop
Python Process
Dash App :8050
SQLite file

Road Profile Viewer — Scaled (Future):

🌐
Web Browser
User 1
🌐
Web Browser
User 2
⚖️
Load Balancer
🖥️
App Server 1
Dash / Flask
🖥️
App Server 2
Dash / Flask
🐘
PostgreSQL Database
Managed Service

4.2.5 Scenarios (The +1)

Scenarios are use cases that exercise the architecture. They connect all four views by tracing a user action through the system.

Scenario: User uploads a new road profile

  1. Physical: Request arrives at load balancer, routed to App Server 1
  2. Process: Upload handler validates file, parses JSON, calls ProfileService
  3. Logical: ProfileService creates RoadProfile object, validates with Pydantic
  4. Development: Code in services/profile_service.py calls repositories/profile_repository.py
  5. Physical: Database write goes to PostgreSQL server
  6. Process: Response flows back through layers to user’s browser

4.3 Applying Views to Road Profile Viewer

You don’t need formal UML diagrams for a student project. Simple sketches and markdown descriptions work well:

Quick Architecture Doc (Markdown):

ROAD PROFILE VIEWER ARCHITECTURE
================================

LOGICAL VIEW
- RoadProfile: Domain entity with coordinates and metadata
- ProfileRepository: Handles database operations
- GeometryCalculator: Ray-surface intersection math
- DashApp: Interactive web visualization

DEVELOPMENT VIEW
- src/models/ - Pydantic data models
- src/services/ - Business logic
- src/presentation/ - Dash UI components

PROCESS VIEW
- Single-threaded Dash app
- Callbacks triggered by user interactions
- Synchronous database queries

PHYSICAL VIEW (Current)
- Runs on single machine
- SQLite file database
- Development server on port 8050

KEY SCENARIO: View Profile
User selects profile → Dropdown callback →
Database query → Chart generation → UI update

4.3.1 A Glimpse at Formal UML Notation

While simple sketches work well for student projects, professional software engineering uses UML (Unified Modeling Language) — a standardized visual notation for software architecture and design.

UML diagrams provide a common language that architects, developers, and stakeholders worldwide understand. Each diagram type maps to specific architectural views.

Here’s how the 4+1 views would look using UML-style diagrams:

Logical View — Class Diagram:

classDiagram
    class RoadProfile {
        +str id
        +str name
        +list~float~ x_coordinates
        +list~float~ y_coordinates
        +get_elevation_at(x: float) float
        +max_slope() float
    }

    class ProfileRepository {
        -str db_path
        +get(profile_id: str) RoadProfile
        +save(profile: RoadProfile) RoadProfile
        +list_all() list~RoadProfile~
    }

    class GeometryCalculator {
        +calculate_intersection(ray: Ray, surface: Surface) Point
        +interpolate(x: float, profile: RoadProfile) float
    }

    ProfileRepository --> RoadProfile : creates/retrieves
    GeometryCalculator --> RoadProfile : uses

Process View — Sequence Diagram:

sequenceDiagram
    participant U as User
    participant V as Dash UI
    participant C as Callback
    participant S as ProfileService
    participant R as Repository
    participant DB as SQLite

    U->>V: Select profile from dropdown
    V->>C: Trigger callback(profile_id)
    C->>S: get_profile(profile_id)
    S->>R: get(profile_id)
    R->>DB: SELECT * FROM profiles
    DB-->>R: Row data
    R-->>S: RoadProfile object
    S-->>C: Profile with analysis
    C-->>V: Plotly Figure
    V-->>U: Display chart

Development View — Package Diagram:

graph LR
    subgraph presentation["📦 presentation"]
        app[app.py]
        callbacks[callbacks.py]
        charts[charts.py]
    end

    subgraph domain["📦 domain"]
        models[models.py]
        services[services.py]
    end

    subgraph infrastructure["📦 infrastructure"]
        repositories[repositories.py]
        database[database.py]
    end

    presentation --> domain
    domain --> infrastructure

    style presentation fill:#dbeafe,stroke:#3b82f6
    style domain fill:#dcfce7,stroke:#22c55e
    style infrastructure fill:#fef3c7,stroke:#f59e0b

Physical View — Deployment Diagram:

graph TB
    subgraph client["🖥️ Client Machine"]
        browser[Web Browser]
    end

    subgraph server["💻 Developer Laptop"]
        subgraph python["🐍 Python Process"]
            dash[Dash App :8050]
            sqlite[(SQLite DB)]
        end
    end

    browser -->|HTTP| dash
    dash -->|File I/O| sqlite

    style client fill:#dbeafe,stroke:#3b82f6
    style server fill:#1e293b,stroke:#475569,color:#fff
    style python fill:#166534,stroke:#22c55e,color:#fff
UML Diagram Type 4+1 View Shows
Class Diagram Logical View Classes, attributes, methods, relationships
Sequence Diagram Process View Object interactions over time
Package Diagram Development View Code organization and dependencies
Deployment Diagram Physical View Hardware nodes and software distribution
Use Case Diagram Scenarios (+1) Actor-system interactions

Looking Ahead: UML diagrams and formal modeling techniques are covered in depth in later courses on Software Design and System Modeling. For now, understanding that these standardized notations exist — and how they map to architectural views — gives you a foundation to build on.

For Road Profile Viewer: Simple markdown documentation and Mermaid diagrams are sufficient. As your projects grow in complexity and team size, formal UML becomes more valuable for communication and documentation.


5. Summary

Concept Key Point Road Profile Viewer Application
Software Architecture The fundamental organization of a system as communicating components Defines how Dash UI, business logic, and database interact
Architectural Decisions Creative process driven by non-functional requirements Prioritize maintainability over performance for student project
Non-Functional Requirements Performance, security, availability, maintainability shape architecture Focus on maintainability and simplicity for learning
4+1 Views Logical, Process, Development, Physical + Scenarios Document each view in README for team coordination
Trade-offs Cannot optimize all qualities; must prioritize Simple and maintainable over scalable and complex

5.1 Key Takeaways

  1. Architecture matters from day one — even small projects benefit from intentional structure
  2. Non-functional requirements shape architecture — performance, security, and maintainability drive design decisions
  3. Multiple views are necessary — no single diagram captures the full architecture (4+1 model)
  4. Trade-offs are inevitable — you cannot optimize all qualities; prioritize based on your system’s needs
  5. Architecture enables communication — block diagrams and views help stakeholders understand the system

6. Reflection Questions

  1. Current architecture: How would you describe the current architecture of your Road Profile Viewer project? What views (logical, process, development, physical) can you identify?

  2. Non-functional priorities: What are the top three non-functional requirements for your Road Profile Viewer? How do these influence your architectural choices?

  3. Trade-off scenario: If you had to choose between making Road Profile Viewer easier to maintain OR faster to run, which would you choose? Why?

  4. 4+1 exercise: Draw a simple diagram for each of the four views of your Road Profile Viewer. What insights does this multi-view approach reveal?


7. What’s Next

In Part 1, you learned:

Coming Up: Part 2 — Architectural Patterns

In Part 2: Architectural Patterns, we’ll explore:

You’ve learned why architecture matters. Now learn how to apply proven patterns.

© 2026 Dominik Mueller   •  Powered by Soopr   •  Theme  Moonwalk