PRG 400: Advanced Python – Course Project
Welcome to the PRG 400 course project. Over the next few weeks you'll build a production-grade web application the way professional teams do: a clean architecture, sensible design patterns, tests, containers and a real deployment.
The point isn't just to get something working. You'll apply SOLID principles, the 12-Factor App methodology and modular design so that your app is maintainable and deployable by someone who has never met you.
At a Glance
| What | A layered Python web API (routes → services → repositories) backed by a database |
| Framework | FastAPI (recommended), Flask or Django (see Core Requirements below) |
| Runs in | Docker + Docker Compose (app + PostgreSQL) |
| Timeline | Weeks 2–8, with a deliverable every week |
| Tests | pytest, at least 60% overall coverage |
| Ends with | A live public URL, a 7–10 minute presentation and a 4–6 page final report |
| Biggest marks | SOLID principles in code (20%), repository & service layer (15%), testing (15%) |
Objectives
By the end of this project you will be able to:
- Architect a clean, modular Python web application that follows the SOLID principles
- Apply 12-Factor App practices for configuration, logging and environments
- Build a RESTful API backend with FastAPI or Flask
- Containerise your application with Docker and Docker Compose
- Write unit and integration tests with meaningful coverage
- Deploy your application to a cloud platform
- Document your architecture decisions and reflect on the engineering trade-offs
Core Requirements
Every project must include:
| Requirement | Details |
|---|---|
| Web framework | FastAPI (recommended) or Django or Flask |
| Database | PostgreSQL or SQLite (via SQLAlchemy ORM) |
| Containerisation | Dockerfile + docker-compose.yml |
| Configuration | Environment variables via .env (never hardcoded) |
| Testing | Unit tests with pytest (minimum 60% coverage) |
| Logging | Structured logging (no bare print() statements in production code) |
| Code quality | ruff or flake8 linting, black formatting |
| Documentation | README.md with setup, architecture and API docs |
SOLID Principles in Your Project
Your project must visibly apply all five SOLID principles. You'll be marked on how deliberately and correctly you use each one, not on whether the words appear in your code.
S: Single Responsibility Principle
A class or module should have only one reason to change.
Each module should own one concern. A route handler shouldn't also format data, validate input and write to the database. Split those jobs across layers:
app/
├── api/routes/ # HTTP layer only: receives requests, returns responses
├── services/ # Business logic only: applies the domain rules
├── repositories/ # Data access only: talks to the database
├── models/ # Domain entities: data shapes and validation
└── core/ # App-wide concerns: config, logging, exceptions
O: Open/Closed Principle
Software entities should be open for extension, but closed for modification.
Design your core logic so you can add features without editing existing code. Abstract base classes, dependency injection and the strategy pattern all help here.
Example: a NotificationService that works with any NotificationChannel (email, SMS, webhook). Adding a new channel means writing a new class; the service itself never changes.
from abc import ABC, abstractmethod
class NotificationChannel(ABC):
@abstractmethod
def send(self, recipient: str, message: str) -> None: ...
class EmailChannel(NotificationChannel):
def send(self, recipient: str, message: str) -> None:
print(f"[email] to {recipient}: {message}") # real code would send an email
class SMSChannel(NotificationChannel):
def send(self, recipient: str, message: str) -> None:
print(f"[sms] to {recipient}: {message}") # real code would send an SMS
class NotificationService:
def __init__(self, channel: NotificationChannel):
self._channel = channel
def notify(self, recipient: str, message: str) -> None:
self._channel.send(recipient, message)
NotificationService(EmailChannel()).notify("[email protected]", "Your order has shipped")
NotificationService(SMSChannel()).notify("+977-9800000000", "Your order has shipped")
[email] to [email protected]: Your order has shipped
[sms] to +977-9800000000: Your order has shipped
L: Liskov Substitution Principle
Subtypes must be substitutable for their base types without altering program correctness.
If you use inheritance or abstract classes, every subclass must honour the full contract of its parent. A subclass should never demand more from its callers (stricter preconditions), promise less (weaker postconditions), or raise exceptions the parent wouldn't.
What to watch for: if you find yourself writing if isinstance(obj, SubclassX) inside shared logic, you're probably breaking LSP.
I: Interface Segregation Principle
Clients should not be forced to depend on interfaces they do not use.
Prefer several small, focused interfaces over one large "god" interface. If a class only needs to read from a repository, it should depend on something like Readable, not on a full CRUD repository.
from typing import Protocol
class Readable(Protocol):
def get(self, id: int): ...
class Writable(Protocol):
def save(self, entity): ...
def delete(self, id: int): ...
class ReadWriteRepository(Readable, Writable, Protocol): ...
D: Dependency Inversion Principle
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Your service layer should never import a concrete database class directly. Pass dependencies in through the constructor or FastAPI's Depends() system.
from typing import Protocol
class UserRepository(Protocol): # the abstraction
def get(self, user_id: int) -> dict: ...
class PostgresUserRepository: # a concrete, low-level detail
def get(self, user_id: int) -> dict:
return {"id": user_id, "source": "postgres"}
# Bad: the high-level service creates its own low-level dependency
class UserServiceBad:
def __init__(self):
self.repo = PostgresUserRepository() # tightly coupled
# Good: the service depends on the abstraction, injected from outside
class UserService:
def __init__(self, repo: UserRepository):
self.repo = repo
service = UserService(PostgresUserRepository()) # or a fake repo in your tests
print(service.repo.get(1))
{'id': 1, 'source': 'postgres'}
12-Factor App Methodology
Your application must follow the 12-Factor App principles. Here's what each factor means for your project.
| Factor | What you must do |
|---|---|
| I. Codebase | One Git repo, one app. No sharing code between apps by copy-paste. |
| II. Dependencies | Declare every dependency in requirements.txt or pyproject.toml. Assume nothing is pre-installed. |
| III. Config | Load all config (DB URLs, secrets, ports) from environment variables. Use python-dotenv locally. Never commit .env. |
| IV. Backing Services | Treat your database, cache or email provider as attached resources, swappable through config rather than code changes. |
| V. Build, Release, Run | The Dockerfile separates build from run. docker compose manages the release and run stages. |
| VI. Processes | Your app must be stateless. No in-memory session state; keep state in the database or cache. |
| VII. Port Binding | Your app exposes itself on a port (e.g. 8000) set by an environment variable, not hardcoded. |
| VIII. Concurrency | Design so that several workers can run in parallel. Avoid global mutable state. |
| IX. Disposability | The app starts fast and shuts down gracefully. Handle SIGTERM for a clean shutdown. |
| X. Dev/Prod Parity | Docker Compose keeps local development close to production. No "works on my machine". |
| XI. Logs | Log to stdout/stderr as structured events (JSON preferred). Don't write log files yourself. |
| XII. Admin Processes | Run DB migrations as one-off tasks (e.g. alembic upgrade head), not as part of app startup. |
Required Project Structure
Your repository must follow this structure. You may add to it, but don't remove any of the required parts.
your-project/
│
├── app/
│ ├── __init__.py
│ ├── main.py # App entry point, FastAPI/Flask app factory
│ │
│ ├── api/
│ │ ├── __init__.py
│ │ ├── routes/ # Route handlers (one file per resource)
│ │ │ ├── __init__.py
│ │ │ └── items.py
│ │ └── dependencies.py # FastAPI Depends() injections
│ │
│ ├── core/
│ │ ├── __init__.py
│ │ ├── config.py # Pydantic BaseSettings — loads from env
│ │ ├── logging.py # Structured logger setup
│ │ └── exceptions.py # Custom exception classes
│ │
│ ├── models/
│ │ ├── __init__.py
│ │ ├── domain.py # Pure domain entities (Pydantic / dataclasses)
│ │ └── orm.py # SQLAlchemy ORM models
│ │
│ ├── repositories/
│ │ ├── __init__.py
│ │ ├── base.py # Abstract base repository (interface)
│ │ └── item_repository.py # Concrete implementation
│ │
│ └── services/
│ ├── __init__.py
│ └── item_service.py # Business logic, depends on repository interface
│
├── tests/
│ ├── __init__.py
│ ├── conftest.py # Pytest fixtures, test DB setup
│ ├── unit/
│ │ └── test_item_service.py
│ └── integration/
│ └── test_item_routes.py
│
├── migrations/ # Alembic migration scripts
│ ├── env.py
│ └── versions/
│
├── .env.example # Template for env vars — committed to repo
├── .env # Actual secrets — NEVER committed (in .gitignore)
├── .gitignore
├── Dockerfile
├── docker-compose.yml
├── docker-compose.test.yml # Separate compose for running tests
├── requirements.txt
├── pyproject.toml # Linting, formatting, test config
├── alembic.ini
└── README.md # Your project's own README
Dockerfile Requirements
Your Dockerfile must use a multi-stage build, like this one:
# Stage 1: Builder — install dependencies
FROM python:3.12-slim AS builder
WORKDIR /app
# Install dependencies into a virtual environment
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Stage 2: Runtime — lean final image
FROM python:3.12-slim AS runtime
WORKDIR /app
# Copy only the venv and app code — no build tools in production image
COPY --from=builder /opt/venv /opt/venv
COPY ./app ./app
ENV PATH="/opt/venv/bin:$PATH"
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
# Run as non-root user (security best practice)
RUN adduser --disabled-password --gecos "" appuser
USER appuser
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Your docker-compose.yml must define at least:
- an
appservice (your web application) - a
dbservice (PostgreSQL) - proper use of
environment,depends_on,volumesandhealthcheck
Configuration: The Right Way
Use Pydantic BaseSettings (from the pydantic-settings package) to load all configuration from environment variables:
# app/core/config.py
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env")
app_name: str = "My App"
database_url: str
secret_key: str
debug: bool = False
port: int = 8000
settings = Settings()
Your .env.example (committed) should list every variable with a placeholder value:
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
SECRET_KEY=changeme
DEBUG=false
PORT=8000
Your .env (never committed) holds the real values for local development.
Project Timeline
Milestones at a Glance
| Week | Focus | Deliverables |
|---|---|---|
| 2 | Architecture design & proposal | Architecture proposal (1–2 pages), ERD, endpoint design table |
| 3 | Literature & technical review | Technical review report (2–3 pages), annotated links to reviewed repos |
| 4 | Project scaffold & core models | Running Compose stack (app + db), one Alembic migration applied, GET /health |
| 5 | Repository & service layer | Working service + repository layer with tests, ≥ 50% coverage on the service layer |
| 6 | API layer & integration | All planned endpoints testable in Swagger UI/Postman, integration tests for ≥ 3 endpoints |
| 7 | Hardening, testing & documentation | HTML coverage report, clean linting output, complete README.md |
| 8 | Deployment, demo & submission | Live URL, final report (4–6 pages), slides, final GitHub repo |
The details for each week are below.
Week 2: Architecture Design & Proposal
- Choose your project domain (see Sample Project Ideas below)
- Define your data models, API endpoints and service boundaries
- Sketch your layered architecture (routes → services → repositories)
- Identify which SOLID principles apply where
Deliverables:
- 1–2 page Architecture Proposal
- Entity-Relationship Diagram (ERD)
- Endpoint design table (method, path, description, auth required)
Week 3: Literature & Technical Review
- Review 2–3 open-source Python web applications on GitHub
- Evaluate their architecture: Do they follow SRP? Are the services testable? Is config handled properly?
- Identify what you'll do differently or improve on
Deliverables:
- Technical Review Report (2–3 pages)
- Annotated links to the reviewed repositories, with critique notes
Week 4: Project Scaffold & Core Models
- Initialise your repository with the required folder structure
- Set up
Dockerfile,docker-compose.ymland.env.example - Implement the domain models and ORM models
- Configure Alembic and create your first migration
Deliverables:
- Running Docker Compose stack (app + db)
- At least one Alembic migration applied
GET /healthendpoint returning{"status": "ok"}
Week 5: Repository & Service Layer
- Implement an abstract base repository and at least one concrete repository
- Implement the service layer with dependency injection
- Write unit tests for the service layer using mock repositories
- Demonstrate the Dependency Inversion and Open/Closed principles in code
Deliverables:
- Working service + repository layer with tests
- Test coverage report showing ≥ 50% coverage on the service layer
Week 6: API Layer & Integration
- Implement all API routes using FastAPI/Flask
- Connect routes → services → repositories through dependency injection
- Add request validation (Pydantic schemas)
- Add structured logging to all service operations
Deliverables:
- All planned endpoints implemented and testable by hand in Swagger UI or Postman
- Integration tests for at least 3 endpoints
Week 7: Hardening, Testing & Documentation
- Reach ≥ 60% overall test coverage
- Add error handling (custom exceptions, correct HTTP status codes)
- Write your project's own
README.md: setup guide, environment variables, how to run tests, API reference - Run the linter (
ruff/flake8) and formatter (black) with zero errors
Deliverables:
- Test coverage report (HTML output from
pytest --cov) - Clean linting output
- Complete project
README.md
Week 8: Deployment, Demo & Submission
- Deploy using one of: Railway, Render, Fly.io or a VPS with Docker
- The live application must be reachable at a public URL
- Present your application (7–10 minutes): demo, architecture walkthrough and reflection
- Submit the final report
Deliverables:
- Live deployment URL
- Final Project Report (4–6 pages), see the structure below
- Presentation slides
- Final GitHub repository (clean commit history, no secrets committed)
Final Project Report Structure
Your report must cover:
- Project Overview: What does your app do? Who is it for?
- Architecture Diagram: A layered diagram showing routes, services, repositories and the DB
- SOLID Principles Applied: One section per principle: where it appears, why you used it, what problem it solves
- 12-Factor Compliance: Which factors you implemented and how
- Testing Strategy: What you tested, how, and your coverage results
- Deployment: The platform you chose, the deployment steps, any challenges
- Reflection: What you'd do differently, and what you learned
Sample Project Ideas
Pick a domain that interests you. The technical requirements are the same whichever you choose.
| Domain | Project Idea |
|---|---|
| Healthcare | Patient appointment scheduling API |
| Finance | Personal budget tracker with category analysis |
| Education | Course enrollment and grade management system |
| E-Commerce | Product catalog and order management API |
| Productivity | Task manager with priority queues and deadlines |
| IoT / Monitoring | Sensor data ingestion and threshold alerting API |
| HR | Employee leave request and approval workflow |
| Library | Book lending and reservation management system |
Recommended Tools & Libraries
| Purpose | Recommended |
|---|---|
| Web framework | FastAPI (preferred), Flask |
| ORM | SQLAlchemy 2.x |
| Migrations | Alembic |
| Validation | Pydantic v2 |
| Testing | pytest, pytest-cov, httpx (for FastAPI) |
| Mocking | pytest-mock, unittest.mock |
| Config | pydantic-settings, python-dotenv |
| Logging | Python logging module with a JSON formatter (python-json-logger) |
| Linting | ruff |
| Formatting | black |
| Containerisation | Docker, Docker Compose |
| API docs | Swagger UI (built into FastAPI), Postman |
| Deployment | Railway, Render, Fly.io |
| Diagramming | draw.io, Lucidchart, Mermaid (in Markdown) |
Evaluation Criteria
| Component | Weight | Key Rubric Points |
|---|---|---|
| Architecture Proposal & ERD | 10% | Clarity of design, layer separation, endpoint planning |
| Technical Review Report | 5% | Depth of analysis, actionable takeaways |
| SOLID Principles (in code) | 20% | Correct and intentional application of all 5 principles |
| 12-Factor Compliance | 10% | Config from env, stateless design, Docker parity, structured logs |
| Repository & Service Layer | 15% | Abstraction quality, dependency injection, testability |
| API Layer & Functionality | 10% | Correctness, validation, error handling, HTTP semantics |
| Testing & Coverage | 15% | Coverage ≥ 60%, meaningful tests (not just happy path) |
| Deployment & Presentation | 10% | Live app, clear demo, architecture explanation |
| Final Report | 5% | Completeness, reflection quality, accuracy |
| Total | 100% |
Academic Integrity
- All code must be your own work.
- You may use libraries, documentation and AI tools as references, but you must be able to explain every line of your code.
- Copying code from other students or repositories without attribution is a violation of academic policy.
- If you use an AI coding assistant, say so in your report and describe how you used it.
Frequently Asked Questions
Can I use AI tools like Copilot or ChatGPT? Yes, as references. You must be able to explain every line, and you must mention the AI use in your report (see Academic Integrity above).
Which database should I use?
PostgreSQL or SQLite, through the SQLAlchemy ORM. Your docker-compose.yml must include a PostgreSQL db service.
Do I commit my .env file?
No. Commit .env.example with placeholder values; keep .env out of Git via .gitignore.
What coverage do I need? At least 50% on the service layer by Week 5, and at least 60% overall by Week 7.
Where can I deploy? Railway, Render, Fly.io or a VPS running Docker. The app must be reachable at a public URL.
How long is the presentation? 7–10 minutes: a demo, an architecture walkthrough and your reflection.
Getting Started
Once you accept the GitHub Classroom assignment, clone your repository and get the stack running:
# Clone your repo
git clone https://github.com/your-org/your-repo.git
cd your-repo
# Copy the example env file, then edit .env with your local values
cp .env.example .env
# Build and start the stack
docker compose up --build
# In a separate terminal: run migrations
docker compose exec app alembic upgrade head
# Run tests
docker compose -f docker-compose.test.yml run --rm test
Your app will be at http://localhost:8000, and the API docs at http://localhost:8000/docs.
Submission Checklist
Tick these off before your final submission in Week 8.
Code & repository
- [ ] Repository follows the required project structure
- [ ] All five SOLID principles applied deliberately (and explained in the report)
- [ ] Config loaded from environment variables;
.env.examplecommitted,.envnot committed - [ ] Structured logging, no bare
print()in production code - [ ]
ruff/flake8andblackrun with zero errors - [ ] Clean commit history with no secrets committed
Containers & deployment
- [ ] Multi-stage
Dockerfile - [ ]
docker-compose.ymlwithappanddbservices, usingenvironment,depends_on,volumesandhealthcheck - [ ] Alembic migrations run as a one-off task
- [ ] App deployed and reachable at a public URL
Testing
- [ ] Unit tests for the service layer using mock repositories
- [ ] Integration tests for at least 3 endpoints
- [ ] At least 60% overall coverage, with the HTML coverage report
Documents
- [ ] Project
README.md: setup, environment variables, how to run tests, API reference - [ ] Final report (4–6 pages) covering all seven sections
- [ ] Presentation slides for a 7–10 minute demo
Good engineers don't just write code that works. They write code that is readable, testable, maintainable and deployable by someone who has never met them.