Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,11 @@
"HTTPIP",
"Inclusivity",
"Intelli",
"isinstance",
"Intn",
"joda",
"jodatime",
"jsonify",
"Katacode",
"Karpenter",
"kdeardorff",
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,8 @@ node_modules/

## Webpack
dist/
examples/ch11/example1/venv/
examples/ch11/example2/venv/
examples/ch11/example2/data.sqlite
**/__pycache__/
*.pyc
62 changes: 62 additions & 0 deletions docs/11-application-development/11.0-overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
docs/11-application-development/11.0-overview.md:
category: Software Quality
estReadingMinutes: 10
---
# Application Development: An Overview

Building robust, maintainable applications is a core skill in modern software development. As applications grow in size and complexity, the way we structure our code becomes increasingly important. Poor organization leads to tightly coupled code that is difficult to test, maintain, and extend. Good architectural patterns help teams build applications that are easier to understand, modify, and scale.

This chapter introduces fundamental software architecture patterns that every developer should understand. These patterns provide proven solutions to common design problems and help teams build applications that can evolve with changing requirements.

## What You'll Learn

In this chapter, you will explore:

- **Layered Architecture**: How to separate concerns by organizing code into distinct layers (presentation, business logic, data access)
- **Benefits of Separation**: Why isolating different parts of your application makes code more maintainable and testable
- **Practical Examples**: Hands-on experience refactoring tightly coupled code into well-organized layers
- **Real-World Trade-offs**: When to apply these patterns and how to balance simplicity with structure

## Why Application Architecture Matters

When building applications, you'll often face questions like:

- How do I organize my code so it's easy for others to understand?
- How can I change my data storage without rewriting my entire application?
- How do I test business logic without depending on a database or external APIs?
- How can I prevent small changes from rippling through my entire codebase?

Good application architecture provides answers to these questions. By learning and applying proven patterns, you'll build applications that are:

- **Maintainable**: Easy to update and fix
- **Testable**: Simple to verify with automated tests
- **Flexible**: Adaptable to changing requirements
- **Understandable**: Clear for other developers to work with

## From DevOps to Application Design

Throughout this bootcamp, you've learned about infrastructure, CI/CD pipelines, containers, and cloud platforms. These tools help you deploy and operate applications efficiently. However, the quality of what you deploy matters just as much as how you deploy it.

Well-architected applications are easier to:

- Deploy through CI/CD pipelines
- Run in containers and cloud environments
- Monitor and debug in production
- Scale horizontally or vertically
- Maintain over time

This chapter focuses on the internal structure of your applications, complementing everything you've learned about external infrastructure and tooling.

## Getting Started

The following sections provide hands-on exercises with real code examples. You'll start with a simple tightly coupled application and learn how to refactor it into a layered architecture. Each example uses Python and Flask, but the principles apply to any programming language and framework.

Let's begin by exploring layered architecture patterns.

## Deliverables

- Why is application architecture important in modern software development?
- What problems can arise from tightly coupled code?
- How do good architectural patterns complement DevOps practices?
- What are the key characteristics of maintainable applications?
283 changes: 283 additions & 0 deletions docs/11-application-development/11.1-layers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
---
docs/11-application-development/11.1-layers.md:
category: Software Quality
estReadingMinutes: 30
exercises:
-
name: Compare tightly coupled vs layered architecture
description: Run both example applications, examine their structure, and understand the architectural differences.
estMinutes: 60
technologies:
- Python
- Flask
-
name: Refactor storage backend from in-memory to SQLite
description: Switch the data storage in both examples from in-memory to SQLite, comparing the effort required in tightly coupled vs layered implementations.
estMinutes: 90
technologies:
- Python
- Flask
- SQLite
---
# Layered Architecture

Layered architecture is a software design pattern that organizes code into distinct layers, each with a specific responsibility. This separation of concerns makes applications easier to understand, test, and maintain. The pattern was popularized by Martin Fowler and has become a fundamental approach in enterprise application development.

## What is Layered Architecture?

In a layered architecture, an application is divided into horizontal layers:

- **Presentation Layer**: Handles user interaction and displays information (e.g., web routes, API endpoints, UI components)
- **Business Logic Layer**: Contains the core application logic, rules, and workflows (e.g., validation, calculations, business rules)
- **Data Access Layer**: Manages data storage and retrieval (e.g., database queries, file I/O, external API calls)

Each layer should only interact with the layer directly below it. This creates clear boundaries and dependencies.

### Key Benefits

**Maintainability**: Changes to one layer typically don't require changes to other layers. For example, switching from one database to another only affects the data access layer.

**Testability**: Each layer can be tested independently. Business logic can be tested without a real database by using mock data access layers.

**Reusability**: Business logic can be reused by different presentation layers (web UI, mobile app, API).

**Clarity**: Developers can quickly understand what each part of the code does and where to make changes.

## The Problem: Tightly Coupled Code

Let's start by understanding the problem that layered architecture solves. Consider a simple user management application where all functionality lives in a single file:

```python
# Everything in one place
@app.route("/users", methods=["GET"])
def list_users():
# Presentation, business logic, and data access all mixed together
users = USERS # Direct access to data structure
return jsonify(users)

@app.route("/users", methods=["POST"])
def create_user():
# Route handler contains validation, business rules, and data manipulation
payload = request.get_json() or {}
name = payload.get("name")
if not name: # Validation in route
return jsonify({"error": "name required"}), 400
user = {"id": NEXT_ID, "name": name} # Direct data manipulation
USERS.append(user) # Direct data access
return jsonify(user), 201
```

### Problems with This Approach

1. **Hard to Test**: You can't test validation logic without involving the web framework
2. **Hard to Change**: Switching from in-memory storage to a database requires editing route handlers
3. **Hard to Reuse**: Business logic is embedded in routes and can't be used by other interfaces
4. **Hard to Understand**: Everything is mixed together, making it difficult to see what the code does

## The Solution: Layered Architecture

Now let's see how layered architecture solves these problems:

### Presentation Layer (Routes)

```python
# presentation/routes.py
@bp.route("/users", methods=["GET"])
def list_users():
return jsonify(get_users()) # Delegates to business layer

@bp.route("/users", methods=["POST"])
def add_user():
payload = request.get_json() or {}
try:
user = create_user(payload) # Delegates to business layer
except ValueError as e:
return jsonify({"error": str(e)}), 400
return jsonify(user), 201
```

The presentation layer is thin—it only handles HTTP concerns (parsing requests, formatting responses) and delegates to the business layer.

### Business Logic Layer (Services)

```python
# services/user_service.py
def get_users():
return repo.get_all() # Delegates to data layer

def create_user(payload):
name = payload.get("name")
if not name or not isinstance(name, str):
raise ValueError("name required") # Business validation
return repo.add({"name": name}) # Delegates to data layer
```

The business layer contains all validation and business rules. It knows nothing about HTTP or databases.

### Data Access Layer (Repository)

```python
# data/in_memory_repo.py
USERS = [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]
NEXT_ID = 3

def get_all():
return USERS.copy()

def add(user):
global NEXT_ID
u = {"id": NEXT_ID, "name": user["name"]}
NEXT_ID += 1
USERS.append(u)
return u
```

The data layer handles storage. It could be in-memory, a database, or an external API—the business and presentation layers don't need to know.

## Hands-On Exercise

We've provided two working examples to demonstrate these concepts:

### Example 1: Tightly Coupled Application

A single-file Flask application where routes, business logic, and data access are all mixed together. This represents a typical quick prototype or small script.

**Location**: `examples/ch11/example1/`

### Example 2: Layered Application

The same application refactored into a layered architecture with clear separation between presentation, business logic, and data access.

**Location**: `examples/ch11/example2/`

### Running the Examples

Both examples are fully functional Flask applications with the same API behavior. Follow the README files in each example directory to:

1. Set up a Python virtual environment
2. Install dependencies
3. Run the application
4. Run the tests

Try making requests to both applications and verify they behave identically:

```bash
# List users
curl http://127.0.0.1:5000/users

# Create a user
curl -X POST http://127.0.0.1:5000/users \
-H "Content-Type: application/json" \
-d '{"name": "Charlie"}'
```

### Exercise: Change the Storage Backend

Now comes the key learning moment. Both examples currently use in-memory storage. Your task is to switch both applications to use SQLite for persistent storage.

**In Example 1 (Tightly Coupled)**:

You'll need to:
1. Add SQLite imports and connection logic
2. Modify the `list_users()` route to query the database
3. Modify the `create_user()` route to insert into the database
4. Add table creation logic
5. Handle database errors

This requires editing the core route handlers and thoroughly retesting.

**In Example 2 (Layered)**:

You'll need to:
1. Edit `app/data/__init__.py` to change one import statement:
```python
# from .in_memory_repo import get_all, add
from .sqlite_repo import get_all, add
```

That's it! The presentation and business layers remain completely unchanged. The SQLite repository implementation is already provided.

## Comparing the Results

After completing both changes:

- **Example 1** required modifying route handlers, mixing database code with HTTP code
- **Example 2** required changing a single import statement

This demonstrates the power of layered architecture: changes are isolated to the appropriate layer, and other layers don't need to know about the implementation details.

### What Makes This Work?

The key is the **repository interface**. Both `in_memory_repo.py` and `sqlite_repo.py` provide the same functions:

- `get_all()` - returns a list of user dictionaries
- `add(user)` - adds a user and returns the created user dictionary

The business and presentation layers depend on this interface, not on the specific implementation. This is an example of **dependency inversion** - depending on abstractions rather than concrete implementations.

## When to Use Layered Architecture

Layered architecture is excellent for:

- **Business applications** with clear separation between UI, logic, and data
- **APIs** that need to support multiple clients
- **Applications that will grow** in size and complexity
- **Team projects** where different developers work on different concerns

You might skip layered architecture for:

- **Quick prototypes** or scripts that won't be maintained
- **Very simple applications** with minimal logic (though even simple apps can benefit)
- **Performance-critical code** where the overhead of layer boundaries matters (rare)

## Best Practices

1. **Keep layers thin**: Each layer should do one thing well
2. **Avoid leaking abstractions**: Don't pass database objects through to the presentation layer
3. **Use consistent interfaces**: Repository functions should have clear, predictable contracts
4. **Test each layer independently**: Unit test business logic without databases; integration test with real data stores
5. **Document layer boundaries**: Make it clear what each layer is responsible for

## Additional Patterns

Layered architecture is foundational, but there are related patterns to explore:

- **Hexagonal Architecture (Ports and Adapters)**: Similar to layers but emphasizes the business logic at the core
- **Clean Architecture**: Robert Martin's approach emphasizing dependency inversion
- **Domain-Driven Design**: Organizing code around business domains rather than technical layers
- **MVC (Model-View-Controller)**: A specific type of layered architecture for UI applications

Each pattern has trade-offs, but they all share the goal of separating concerns and making code more maintainable.

## Real-World Considerations

### Performance

Layer boundaries do add some overhead (function calls, data transformations). In practice, this overhead is negligible for most applications. Profile before optimizing.

### Complexity

Layered architecture adds structure, which some might consider "complexity." However, this structure pays dividends as applications grow. Even small applications benefit from clear organization.

### Over-Engineering

Not every application needs multiple layers. Use judgment: a 50-line script doesn't need layers, but a team application that will be maintained for years probably does.

## Conclusion

Layered architecture is a proven pattern for building maintainable applications. By separating presentation, business logic, and data access into distinct layers, you create code that is easier to understand, test, and modify.

The hands-on examples demonstrate this concretely: changing storage backends in a layered application is trivial, while the same change in tightly coupled code requires significant refactoring.

As you build applications in your DevOps journey, consider how layered architecture can help you create software that's easier to deploy, maintain, and evolve over time.

## Deliverables

- What are the three primary layers in a layered architecture, and what is each responsible for?
- Why is it easier to change the data storage in a layered application compared to a tightly coupled application?
- What is a repository interface, and why is it important?
- Run both example applications and verify they work identically
- Complete the exercise: switch both examples from in-memory to SQLite storage
- Compare your experience refactoring Example 1 vs Example 2
- When would you choose not to use layered architecture?
- How does layered architecture improve testability?
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ docs/6-software-development-practices/6.4-pairprogramming.md:
estReadingMinutes: 20
exercises:
-
name: Pair Programing
name: Pair Programming
description: Using 'Live Share' or some equivillant try pair programming a 'Hello World' app in the language of your choice
estMinutes: 30
technologies:
Expand Down
Loading