| layout | default |
|---|---|
| title | Aider Tutorial - Chapter 3: Multi-File Projects |
| nav_order | 3 |
| has_children | false |
| parent | Aider Tutorial |
Welcome to Chapter 3: Multi-File Projects. In this part of Aider Tutorial: AI Pair Programming in Your Terminal, you will build an intuitive mental model first, then move into concrete implementation details and practical production tradeoffs.
Work across multiple files, manage complex codebases, and coordinate changes across your entire project.
This chapter covers working with multi-file projects in Aider. You'll learn how to coordinate changes across multiple files, manage project structure, and handle complex refactoring that spans your entire codebase.
Aider uses a "repo-map" to understand your codebase structure:
$ aider
Aider v0.50.0
Models: claude-3-5-sonnet-20241022 with diff edit format
Git repo: .git with 47 files
Repo-map: using 2048 tokens # Shows how much context is usedThe repo-map helps Aider understand:
- File relationships and dependencies
- Project structure and organization
- Import patterns and module structure
- Code organization conventions
# Add specific files
> /add src/main.py src/utils.py src/models.py
# Add entire directories
> /add src/ tests/
# Add with wildcards
> /add **/*.py
# Add files matching patterns
> /add src/**/*.py tests/test_*.pyLet's build a complete authentication system across multiple files:
# Start in a Flask project
cd my-flask-app
aider
# Add all relevant files
> /add app.py models.py routes/auth.py templates/ utils/Request: Implement a complete user authentication system with registration, login, logout, and password reset
Aider will create:
- User model in
models.py - Authentication routes in
routes/auth.py - Session management in
app.py - Password hashing utilities
- Email templates for password reset
- Database migrations if needed
# models.py - User model
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(128))
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)# routes/auth.py - Authentication routes
from flask import Blueprint, render_template, redirect, url_for, flash, request
from flask_login import login_user, logout_user, login_required, current_user
from werkzeug.urls import url_parse
from app import db
from models import User
auth_bp = Blueprint('auth', __name__)
@auth_bp.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
if request.method == 'POST':
username = request.form['username']
email = request.form['email']
password = request.form['password']
user = User(username=username, email=email)
user.set_password(password)
db.session.add(user)
db.session.commit()
flash('Registration successful!')
return redirect(url_for('auth.login'))
return render_template('register.html')
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
if request.method == 'POST':
user = User.query.filter_by(username=request.form['username']).first()
if user and user.check_password(request.form['password']):
login_user(user)
return redirect(url_for('main.index'))
flash('Invalid username or password')
return render_template('login.html')
@auth_bp.route('/logout')
@login_required
def logout():
logout_user()
return redirect(url_for('main.index'))# app.py - Application setup
from flask import Flask
from flask_login import LoginManager
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
db = SQLAlchemy(app)
login_manager = LoginManager(app)
login_manager.login_view = 'auth.login'
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
app.register_blueprint(auth_bp, url_prefix='/auth')
# Import models after db is created
from models import User# Original tightly coupled code
> /add service.py repository.py handlers/
# Request: Refactor the services to use dependency injection pattern
# Result across multiple files:
# service.py
class UserService:
def __init__(self, user_repository, email_service):
self.user_repository = user_repository
self.email_service = email_service
def create_user(self, username, email, password):
user = self.user_repository.create(username, email, password)
self.email_service.send_welcome_email(user.email)
return user
# repository.py
class UserRepository:
def __init__(self, db_connection):
self.db = db_connection
def create(self, username, email, password):
# Database operations
pass
# handlers/user_handler.py
def create_user_handler(request, user_service):
data = request.get_json()
user = user_service.create_user(
data['username'],
data['email'],
data['password']
)
return {'user_id': user.id}, 201# Request: Create a new API module with proper structure
# Aider creates:
# api/
# ├── __init__.py
# ├── routes.py
# ├── schemas.py
# └── middleware.py# Original: Everything in one file
> /add monolithic.py
# Request: Split this monolithic file into proper modules
# Result: Creates multiple files
# models/
# ├── user.py
# ├── product.py
# └── order.py
# services/
# ├── user_service.py
# ├── product_service.py
# └── order_service.py
# api/
# ├── user_routes.py
# ├── product_routes.py
# └── order_routes.py# After implementing a feature
> /add tests/
# Request: Create comprehensive unit tests for the authentication system
# Aider creates:
# tests/
# ├── test_auth.py
# ├── test_models.py
# ├── test_routes.py
# ├── conftest.py
# └── fixtures/
# └── users.py# Request: Implement a shopping cart feature with tests first
# Aider creates:
# tests/test_cart.py
# models/cart.py
# services/cart_service.py
# routes/cart.py# Request: Create environment-specific configuration files
# Aider creates:
# config/
# ├── default.py
# ├── development.py
# ├── testing.py
# └── production.py
#
# .env.example
# docker-compose.yml# Request: Implement secure configuration management
# Aider creates:
# config/secrets.py (with warnings about security)
# .env.local (gitignored)
# docker/secrets/
# ├── db_password.txt
# └── api_key.txt# Request: Create database migration for user authentication tables
# Aider creates:
# migrations/
# ├── 001_create_users_table.sql
# ├── 002_add_user_roles.sql
# └── 003_create_sessions_table
#
# models/user.py (updated)
# models/role.py (new)# Request: Convert raw SQL to SQLAlchemy ORM models
# Result: Proper ORM models with relationships
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
username = Column(String(80), unique=True, nullable=False)
email = Column(String(120), unique=True, nullable=False)
role_id = Column(Integer, ForeignKey('roles.id'))
role = relationship("Role", back_populates="users")
class Role(Base):
__tablename__ = 'roles'
id = Column(Integer, primary_key=True)
name = Column(String(50), unique=True, nullable=False)
users = relationship("User", back_populates="role")# Request: Create a REST API for user management
# Aider creates:
# api/
# ├── users.py
# ├── middleware.py
# └── schemas.py
#
# tests/
# ├── test_users_api.py
# └── test_schemas.py# Request: Implement GraphQL API for the application
# Aider creates:
# graphql/
# ├── schema.py
# ├── types.py
# ├── resolvers.py
# └── mutations.py
#
# api/graphql.py# Request: Create HTML templates for the authentication system
# Aider creates:
# templates/
# ├── base.html
# ├── login.html
# ├── register.html
# ├── profile.html
# └── errors/
# └── 404.html# Request: Add CSS and JavaScript for the user interface
# Aider creates:
# static/
# ├── css/
# │ ├── main.css
# │ └── auth.css
# ├── js/
# │ ├── app.js
# │ └── auth.js
# └── images/
# └── logo.png# Request: Create Docker configuration for the application
# Aider creates:
# Dockerfile
# docker-compose.yml
# .dockerignore
# docker/
# ├── Dockerfile.prod
# └── nginx.conf# Request: Create Kubernetes deployment manifests
# Aider creates:
# k8s/
# ├── deployment.yaml
# ├── service.yaml
# ├── configmap.yaml
# ├── secret.yaml
# └── ingress.yaml# Think about the overall architecture first
> I want to build a blog application. First, help me design the overall structure with models, views, and APIs.
# Then implement incrementally
> Start with the User model and authentication
> Then add the Post model and CRUD operations
> Finally add comments and relationships# Request: Refactor this code to follow clean architecture principles
# Aider will create:
# - Clear separation between business logic, data access, and presentation
# - Dependency injection for testability
# - Interface definitions for loose coupling# Request: Ensure all files follow the same naming conventions and patterns
# Aider will:
# - Standardize variable naming
# - Apply consistent code formatting
# - Use uniform error handling patterns# Request: Add comprehensive documentation for the entire codebase
# Aider creates:
# - README.md files for each module
# - Inline documentation for all functions
# - API documentation
# - Architecture diagrams (in comments)# If Aider suggests circular imports
> Refactor to break the circular dependency between module A and module B
# Aider will suggest:
# - Extract common interfaces
# - Use dependency injection
# - Reorganize module structure# If changes conflict between files
> /diff # Review all changes
> These changes to file A and file B conflict. Please resolve the conflict.
# Aider will help coordinate the changes# For very large projects, be specific about scope
> Only modify the authentication-related files, don't touch the payment system
# Use targeted file selection
> /add auth/ models/user.py # Only these filesIn this chapter, we've covered:
- Multi-File Context: Understanding repository mapping and file relationships
- Coordinated Changes: Implementing features across multiple files
- Project Structure: Creating and managing complex project layouts
- Cross-File Refactoring: Restructuring code across the entire codebase
- Testing: Adding comprehensive tests for multi-file features
- Configuration: Managing environment-specific and secure configurations
- Database Operations: Schema migrations and ORM integration
- API Development: REST and GraphQL API implementation
- Frontend Integration: Templates and static assets
- Deployment: Docker and Kubernetes configuration
- Plan Architecture: Design overall structure before implementation
- Incremental Development: Build features step by step across files
- Consistent Patterns: Maintain uniform conventions across all files
- Dependency Management: Handle imports and relationships carefully
- Testing Coverage: Add tests for all multi-file interactions
- Documentation: Document as you develop for maintainability
Now that you can work across multiple files, let's explore Git integration and how Aider manages version control automatically.
Ready for Chapter 4? Git Integration
Generated for Awesome Code Docs
This chapter is expanded to v1-style depth for production-grade learning and implementation quality.
- tutorial: Aider Tutorial: AI Pair Programming in Your Terminal
- tutorial slug: aider-tutorial
- chapter focus: Chapter 3: Multi-File Projects
- system context: Aider Tutorial
- objective: move from surface-level usage to repeatable engineering operation
- Define the runtime boundary for
Chapter 3: Multi-File Projects. - Separate control-plane decisions from data-plane execution.
- Capture input contracts, transformation points, and output contracts.
- Trace state transitions across request lifecycle stages.
- Identify extension hooks and policy interception points.
- Map ownership boundaries for team and automation workflows.
- Specify rollback and recovery paths for unsafe changes.
- Track observability signals for correctness, latency, and cost.
| Decision Area | Low-Risk Path | High-Control Path | Tradeoff |
|---|---|---|---|
| Runtime mode | managed defaults | explicit policy config | speed vs control |
| State handling | local ephemeral | durable persisted state | simplicity vs auditability |
| Tool integration | direct API use | mediated adapter layer | velocity vs governance |
| Rollout method | manual change | staged + canary rollout | effort vs safety |
| Incident response | best effort logs | runbooks + SLO alerts | cost vs reliability |
| Failure Mode | Early Signal | Root Cause Pattern | Countermeasure |
|---|---|---|---|
| stale context | inconsistent outputs | missing refresh window | enforce context TTL and refresh hooks |
| policy drift | unexpected execution | ad hoc overrides | centralize policy profiles |
| auth mismatch | 401/403 bursts | credential sprawl | rotation schedule + scope minimization |
| schema breakage | parser/validation errors | unmanaged upstream changes | contract tests per release |
| retry storms | queue congestion | no backoff controls | jittered backoff + circuit breakers |
| silent regressions | quality drop without alerts | weak baseline metrics | eval harness with thresholds |
- Establish a reproducible baseline environment.
- Capture chapter-specific success criteria before changes.
- Implement minimal viable path with explicit interfaces.
- Add observability before expanding feature scope.
- Run deterministic tests for happy-path behavior.
- Inject failure scenarios for negative-path validation.
- Compare output quality against baseline snapshots.
- Promote through staged environments with rollback gates.
- Record operational lessons in release notes.
- chapter-level assumptions are explicit and testable
- API/tool boundaries are documented with input/output examples
- failure handling includes retry, timeout, and fallback policy
- security controls include auth scopes and secret rotation plans
- observability includes logs, metrics, traces, and alert thresholds
- deployment guidance includes canary and rollback paths
- docs include links to upstream sources and related tracks
- post-release verification confirms expected behavior under load
- Cline Tutorial
- Roo Code Tutorial
- Continue Tutorial
- Codex Analysis Platform
- Chapter 1: Getting Started
- Build a minimal end-to-end implementation for
Chapter 3: Multi-File Projects. - Add instrumentation and measure baseline latency and error rate.
- Introduce one controlled failure and confirm graceful recovery.
- Add policy constraints and verify they are enforced consistently.
- Run a staged rollout and document rollback decision criteria.
- Which execution boundary matters most for this chapter and why?
- What signal detects regressions earliest in your environment?
- What tradeoff did you make between delivery speed and governance?
- How would you recover from the highest-impact failure mode?
- What must be automated before scaling to team-wide adoption?
Most teams struggle here because the hard part is not writing more code, but deciding clear boundaries for Aider, user, Request so behavior stays predictable as complexity grows.
In practical terms, this chapter helps you avoid three common failures:
- coupling core logic too tightly to one implementation path
- missing the handoff boundaries between setup, execution, and validation
- shipping changes without clear rollback or observability strategy
After working through this chapter, you should be able to reason about Chapter 3: Multi-File Projects as an operating subsystem inside Aider Tutorial: AI Pair Programming in Your Terminal, with explicit contracts for inputs, state transitions, and outputs.
Use the implementation notes around models, username, self as your checklist when adapting these patterns to your own repository.
Under the hood, Chapter 3: Multi-File Projects usually follows a repeatable control path:
- Context bootstrap: initialize runtime config and prerequisites for
Aider. - Input normalization: shape incoming data so
userreceives stable contracts. - Core execution: run the main logic branch and propagate intermediate state through
Request. - Policy and safety checks: enforce limits, auth scopes, and failure boundaries.
- Output composition: return canonical result payloads for downstream consumers.
- Operational telemetry: emit logs/metrics needed for debugging and performance tuning.
When debugging, walk this sequence in order and confirm each stage has explicit success/failure conditions.
Use the following upstream sources to verify implementation details while reading this chapter:
- Aider Repository
Why it matters: authoritative reference on
Aider Repository(github.com). - Aider Releases
Why it matters: authoritative reference on
Aider Releases(github.com). - Aider Docs
Why it matters: authoritative reference on
Aider Docs(aider.chat).
Suggested trace strategy:
- search upstream code for
Aideranduserto map concrete implementation paths - compare docs claims against actual runtime/config code before reusing patterns in production