06 Software Architecture Part 1: Foundations and Architectural Views
January 2026 (10906 Words, 61 Minutes)
1. Introduction: Your Sprints Are Fast, But Can Your System Scale?
In Parts 1 and 2 of Agile Development, we learned to:
- Embrace change through short iteration cycles
- Deliver working software every 1-4 weeks
- Organize work using Scrum’s roles, events, and artifacts
- Track user stories with GitHub Issues and Projects
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:
- Your Dash app runs on a single laptop
- SQLite can’t handle concurrent writes from multiple locations
- The entire application is one Python process
- There’s no way to scale individual components independently
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:
- How are responsibilities divided among components?
- How do components communicate with each other?
- How is the system deployed across hardware?
- What happens when one component fails?
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:
- Where will data be stored?
- How will users authenticate?
- What programming languages and frameworks will we use?
- How will components communicate?
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:
- Name the motivation and goals for architectural design — why architecture matters and what an architecture model enables
- Explain the dependency between system structure and non-functional requirements — how performance, security, and maintainability shape architectural choices
- 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:
- System type: A real-time embedded system has different needs than a web application
- Architect experience: Knowledge of what has worked (and failed) in similar systems
- Specific requirements: The unique constraints and goals of your project
“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:
For Road Profile Viewer, consider:
- Template: Web dashboard applications have common patterns we can follow
- Distribution: Currently single-machine; could distribute database and UI separately
- Patterns: MVC for UI, layered architecture for separation of concerns
- Structure: Modular Python packages with clear responsibilities
- Decomposition: Split visualization, data access, and business logic
- Control: Request-response for web, callbacks for Dash interactions
- Non-functional: Prioritize maintainability (student project) over performance
- Documentation: README with architecture diagram, docstrings in code
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:
- Localize critical operations: Keep performance-sensitive code in as few components as possible to minimize communication overhead
- Minimize network hops: Every network call adds latency (typically 1-100ms); local function calls are nanoseconds
- Co-locate data and computation: Process data where it lives; don’t ship large datasets across boundaries
- 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:
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?
- Dedicated ECUs per function: Each control unit handles one responsibility with guaranteed timing
- Parallel sensor processing: LiDAR, cameras, and radar are processed simultaneously, not sequentially
- GPU for perception: Neural networks for object detection need massive parallel computation
- High-frequency control loops: Steering and braking run at 1 kHz (1000 times/second) for smooth control
- Deterministic communication: CAN bus or Ethernet with time-sensitive networking (TSN) ensures predictable message delivery
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:
- No dynamic memory allocation:
malloc()has unpredictable timing - No garbage collection: GC pauses would miss deadlines
- Direct hardware access: No abstraction layers that add latency
- Pre-computed lookup tables: Avoid expensive calculations at runtime
- Bounded execution time: Every code path must complete within the deadline
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:
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:
The thought process behind this design:
- Primary handles writes: All write operations go to one database to avoid conflicts
- Replicas handle reads: Read operations can go to any replica, distributing load
- Synchronous replication: Changes are copied to replicas in real-time
- 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:
-
Multiple AWS regions: Netflix deploys services across US-East-1 (Virginia) and US-West-2 (Oregon) in an active-active configuration. Users are geo-DNS routed to the closest region. If one region experiences an outage, all traffic can be redirected to the healthy region.
-
Chaos engineering: Netflix pioneered chaos engineering with Chaos Monkey—a tool that randomly terminates production instances to ensure engineers build resilient services. They also use Chaos Gorilla (kills entire Availability Zones) and Chaos Kong (simulates full region failure). As Netflix explains: “We could align our teams around the notion of infrastructure resilience by isolating the problems created by server neutralization and pushing them to the extreme.”
-
Stateless services: No single service holds critical state; everything can be reconstructed from distributed storage. This enables any instance to handle any request, making failover seamless.
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:
- Changes are localized: Modifying one feature doesn’t require touching unrelated code
- Components are understandable: Each piece is small enough to comprehend fully
- Testing is straightforward: You can test each component in isolation
- 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:
- Prioritize: Decide which non-functional requirements matter most for your system
- Partition: Use different architectures for different parts (performance-critical core + maintainable plugins)
- 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:
- Developers need to know: What classes exist? What packages contain them?
- Operations engineers need to know: What servers run what services? How do they communicate?
- Project managers need to know: What teams own what components?
- Security auditors need to know: Where does sensitive data flow?
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):
| 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):
Road Profile Viewer — Scaled (Future):
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
- Physical: Request arrives at load balancer, routed to App Server 1
- Process: Upload handler validates file, parses JSON, calls ProfileService
- Logical: ProfileService creates RoadProfile object, validates with Pydantic
- Development: Code in
services/profile_service.pycallsrepositories/profile_repository.py - Physical: Database write goes to PostgreSQL server
- 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
- Architecture matters from day one — even small projects benefit from intentional structure
- Non-functional requirements shape architecture — performance, security, and maintainability drive design decisions
- Multiple views are necessary — no single diagram captures the full architecture (4+1 model)
- Trade-offs are inevitable — you cannot optimize all qualities; prioritize based on your system’s needs
- Architecture enables communication — block diagrams and views help stakeholders understand the system
6. Reflection Questions
-
Current architecture: How would you describe the current architecture of your Road Profile Viewer project? What views (logical, process, development, physical) can you identify?
-
Non-functional priorities: What are the top three non-functional requirements for your Road Profile Viewer? How do these influence your architectural choices?
-
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+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:
- 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
Coming Up: Part 2 — Architectural Patterns
In Part 2: Architectural Patterns, we’ll explore:
- MVC (Model-View-Controller) — separating concerns in interactive applications
- Layered architecture — organizing code into Presentation, Business, and Data layers
- Distributed patterns — Client-Server, Microservices, and when to use them
- Applying architecture to Road Profile Viewer — evolving your project structure
You’ve learned why architecture matters. Now learn how to apply proven patterns.