# Design Patterns Explained This document explains the key design patterns used in the Bug Bounty Platform, why they were chosen, and how they improve code quality. --- ## Table of Contents 1. [Dependency Injection](#dependency-injection) 2. [Repository Pattern](#repository-pattern) 3. [Layered Architecture](#layered-architecture) 4. [Factory Pattern](#factory-pattern) 5. [Strategy Pattern](#strategy-pattern) 6. [Singleton Pattern](#singleton-pattern) 7. [Mixin Pattern](#mixin-pattern) 8. [Observer Pattern (Pub/Sub)](#observer-pattern-pubsub) --- ## Dependency Injection **What it is:** Instead of creating dependencies inside a class, they are "injected" from outside. **Without Dependency Injection:** ```python class UserService: def __init__(self): self.db = Database() # Hardcoded dependency self.cache = Redis() # Hardcoded dependency def get_user(self, user_id: str): cached = self.cache.get(f"user:{user_id}") if cached: return cached user = self.db.query(f"SELECT * FROM users WHERE id = '{user_id}'") self.cache.set(f"user:{user_id}", user) return user ``` Problems: - Hard to test (can't mock Database or Redis) - Tight coupling (UserService depends on specific implementations) - Can't swap implementations (e.g., use a different cache) **With Dependency Injection:** ```python class UserService: def __init__(self, db: Database, cache: Cache): self.db = db # Injected from outside self.cache = cache # Injected from outside def get_user(self, user_id: str): cached = self.cache.get(f"user:{user_id}") if cached: return cached user = self.db.query(f"SELECT * FROM users WHERE id = '{user_id}'") self.cache.set(f"user:{user_id}", user) return user # Usage db = Database() cache = Redis() user_service = UserService(db=db, cache=cache) ``` Benefits: - Easy to test (inject mocks) - Loose coupling (depends on interfaces, not implementations) - Flexible (can inject different implementations) ### Dependency Injection in FastAPI FastAPI has built-in DI using `Depends()`: ```python from fastapi import Depends from typing import Annotated # Dependency function async def get_db_session() -> AsyncGenerator[AsyncSession, None]: async with DatabaseSessionManager().session() as session: yield session # Type alias for convenience DatabaseSession = Annotated[AsyncSession, Depends(get_db_session)] # Route using dependency @router.get("/users/{user_id}") async def get_user( user_id: UUID, session: DatabaseSession, # ← Automatically injected ) -> UserResponse: repo = UserRepository(session) user = await repo.get_by_id(user_id) return user ``` **How it works:** 1. FastAPI sees `session: DatabaseSession` parameter 2. Recognizes `DatabaseSession` is an `Annotated` type with `Depends()` 3. Calls `get_db_session()` to get a session 4. Injects the session into the route handler 5. Automatically cleans up when request finishes **Real example from the codebase:** ```python # backend/app/core/dependencies.py async def get_current_user( token: str = Header(...), session: DatabaseSession, ) -> User: payload = decode_access_token(token) user_id = payload["sub"] repo = UserRepository(session) user = await repo.get_by_id(user_id) if not user: raise UnauthorizedError() return user # Type alias CurrentUser = Annotated[User, Depends(get_current_user)] # Usage in routes @router.get("/me") async def get_me(user: CurrentUser) -> UserResponse: return user # User is automatically injected and authenticated! ``` **Why this is powerful:** - Route handlers don't need to worry about authentication - DRY principle (don't repeat auth logic in every route) - Easy to test (mock `get_current_user`) - Composable (dependencies can depend on other dependencies) --- ## Repository Pattern **What it is:** An abstraction layer between business logic and data storage. **The problem:** ```python # Business logic mixed with database queries @router.post("/users") async def create_user(data: UserCreate, session: DatabaseSession): # Database query in route handler stmt = select(User).where(User.email == data.email) result = await session.execute(stmt) existing_user = result.scalar_one_or_none() if existing_user: raise HTTPException(status_code=400, detail="Email already exists") user = User( email=data.email, password_hash=hash_password(data.password), ) session.add(user) await session.commit() return user ``` Problems: - Route handler knows about database schema - Hard to test (need to set up database) - Can't switch databases easily - Duplicate query logic across multiple routes **The Repository Pattern solution:** ```python # 1. Create a repository class class UserRepository(BaseRepository[User]): async def find_by_email(self, email: str) -> User | None: stmt = select(User).where(User.email == email) result = await self.session.execute(stmt) return result.scalar_one_or_none() async def create(self, data: UserCreate) -> User: user = User( email=data.email, password_hash=hash_password(data.password), ) self.session.add(user) await self.session.flush() await self.session.refresh(user) return user # 2. Use repository in route handler @router.post("/users") async def create_user(data: UserCreate, session: DatabaseSession): repo = UserRepository(session) existing_user = await repo.find_by_email(data.email) if existing_user: raise HTTPException(status_code=400, detail="Email already exists") user = await repo.create(data) await session.commit() return user ``` Benefits: - Route handler doesn't know about database schema - Query logic is reusable - Easy to test (mock repository) - Can swap database implementation ### Generic Base Repository To avoid repeating CRUD operations, we use a **generic base repository**: ```python from typing import Generic, TypeVar ModelT = TypeVar("ModelT", bound=Base) class BaseRepository(Generic[ModelT]): def __init__(self, session: AsyncSession, model: type[ModelT]): self.session = session self.model = model async def get_by_id(self, id: UUID) -> ModelT | None: stmt = select(self.model).where(self.model.id == id) result = await self.session.execute(stmt) return result.scalar_one_or_none() async def get_all(self, limit: int = 100, offset: int = 0) -> list[ModelT]: stmt = select(self.model).limit(limit).offset(offset) result = await self.session.execute(stmt) return list(result.scalars().all()) async def create(self, **kwargs) -> ModelT: instance = self.model(**kwargs) self.session.add(instance) await self.session.flush() await self.session.refresh(instance) return instance async def delete(self, id: UUID) -> None: stmt = delete(self.model).where(self.model.id == id) await self.session.execute(stmt) ``` **Usage:** ```python # Specific repository extends base class UserRepository(BaseRepository[User]): def __init__(self, session: AsyncSession): super().__init__(session, User) # Add domain-specific methods async def find_by_email(self, email: str) -> User | None: stmt = select(User).where(User.email == email) result = await self.session.execute(stmt) return result.scalar_one_or_none() # Usage repo = UserRepository(session) user = await repo.get_by_id(user_id) # From BaseRepository user = await repo.find_by_email(email) # From UserRepository ``` **Why generics?** - Type safety: `repo.get_by_id()` returns `User`, not `Any` - Reusable: All models get CRUD for free - DRY: Write common operations once --- ## Layered Architecture **What it is:** Organizing code into layers, where each layer has a specific responsibility. ``` ┌─────────────────────────────────────┐ │ Presentation Layer │ ← Routes (HTTP) │ - Handle HTTP requests/responses │ │ - Input validation │ │ - Serialize data to JSON │ └───────────────┬─────────────────────┘ │ ┌───────────────▼─────────────────────┐ │ Business Layer │ ← Services │ - Core business logic │ │ - Orchestrate repositories │ │ - Transaction management │ └───────────────┬─────────────────────┘ │ ┌───────────────▼─────────────────────┐ │ Data Access Layer │ ← Repositories │ - Database queries │ │ - CRUD operations │ │ - No business logic │ └───────────────┬─────────────────────┘ │ ┌───────────────▼─────────────────────┐ │ Database │ ← PostgreSQL └─────────────────────────────────────┘ ``` **Rules:** 1. **Top-down dependency:** Upper layers depend on lower layers, never the reverse 2. **Single responsibility:** Each layer has one job 3. **No layer skipping:** Routes can't directly query the database **Example: Creating a report** ```python # 1. Presentation Layer (routes.py) @router.post("/reports") async def create_report( data: ReportCreate, user: CurrentUser, session: DatabaseSession, ) -> ReportResponse: report = await submit_vulnerability_report( session=session, author_id=user.id, data=data, ) return ReportResponse.from_orm(report) ``` The route: - Validates input (`ReportCreate` Pydantic model) - Authenticates user (`CurrentUser` dependency) - Calls service function - Serializes output (`ReportResponse`) ```python # 2. Business Layer (service.py) async def submit_vulnerability_report( session: AsyncSession, author_id: UUID, data: ReportCreate, ) -> Report: # Validate program exists program_repo = ProgramRepository(session) program = await program_repo.get_by_slug(data.program_slug) if not program: raise ProgramNotFoundError() # Check if program accepts reports if program.status != ProgramStatus.ACTIVE: raise ProgramNotActiveError() # Create report report_repo = ReportRepository(session) report = await report_repo.create( author_id=author_id, program_id=program.id, title=data.title, description=data.description, severity=data.severity, ) # Send notification (in real app) # await notify_program_owner(program.owner_id, report.id) await session.commit() return report ``` The service: - Implements business rules (program must be active) - Coordinates multiple repositories (Program + Report) - Manages transactions (commit) - Throws domain exceptions ```python # 3. Data Access Layer (repository.py) class ReportRepository(BaseRepository[Report]): async def create( self, author_id: UUID, program_id: UUID, title: str, description: str, severity: Severity, ) -> Report: report = Report( author_id=author_id, program_id=program_id, title=title, description=description, severity=severity, status=ReportStatus.NEW, ) self.session.add(report) await self.session.flush() await self.session.refresh(report) return report ``` The repository: - Builds the Report model - Adds to session - Returns the instance - No business logic **Benefits:** - **Testability:** Mock each layer independently - **Maintainability:** Change one layer without affecting others - **Scalability:** Extract layers to separate services if needed - **Clarity:** Easy to find where logic lives --- ## Factory Pattern **What it is:** A function/class that creates objects without specifying their exact class. **Real example from the codebase:** ```python # backend/app/factory.py def create_app() -> FastAPI: app = FastAPI( title="Bug Bounty Platform", version="1.0.0", docs_url="/api/docs", redoc_url="/api/redoc", ) # Add middleware app.add_middleware( CORSMiddleware, allow_origins=settings.CORS_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Add rate limiting app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, rate_limit_handler) # Register routers app.include_router(auth_router, prefix="/api/v1/auth", tags=["auth"]) app.include_router(user_router, prefix="/api/v1/users", tags=["users"]) app.include_router(program_router, prefix="/api/v1/programs", tags=["programs"]) app.include_router(report_router, prefix="/api/v1/reports", tags=["reports"]) app.include_router(admin_router, prefix="/api/v1/admin", tags=["admin"]) return app # Usage app = create_app() ``` **Benefits:** - Centralized configuration - Easy to create multiple app instances (testing, different environments) - Follows Single Responsibility Principle **Another example: Database session factory** ```python class DatabaseSessionManager: def __init__(self): self._engine: AsyncEngine | None = None self._sessionmaker: async_sessionmaker | None = None def init(self, database_url: str): self._engine = create_async_engine( database_url, pool_size=settings.DB_POOL_SIZE, max_overflow=settings.DB_MAX_OVERFLOW, pool_timeout=settings.DB_POOL_TIMEOUT, pool_recycle=settings.DB_POOL_RECYCLE, ) self._sessionmaker = async_sessionmaker( self._engine, expire_on_commit=False, ) async def session(self) -> AsyncGenerator[AsyncSession, None]: if self._sessionmaker is None: raise DatabaseNotInitializedError() async with self._sessionmaker() as session: try: yield session except Exception: await session.rollback() raise finally: await session.close() # Usage db_manager = DatabaseSessionManager() db_manager.init(settings.DATABASE_URL) async with db_manager.session() as session: # Use session pass ``` --- ## Strategy Pattern **What it is:** Define a family of algorithms, encapsulate each one, and make them interchangeable. **Example: Password hashing** ```python from abc import ABC, abstractmethod # Strategy interface class PasswordHasher(ABC): @abstractmethod def hash(self, password: str) -> str: pass @abstractmethod def verify(self, password: str, hash: str) -> bool: pass # Concrete strategies class Argon2Hasher(PasswordHasher): def hash(self, password: str) -> str: return argon2.hash(password) def verify(self, password: str, hash: str) -> bool: try: return argon2.verify(hash, password) except Exception: return False class BcryptHasher(PasswordHasher): def hash(self, password: str) -> str: return bcrypt.hashpw(password.encode(), bcrypt.gensalt()) def verify(self, password: str, hash: str) -> bool: return bcrypt.checkpw(password.encode(), hash.encode()) # Context class PasswordService: def __init__(self, hasher: PasswordHasher): self.hasher = hasher def hash_password(self, password: str) -> str: return self.hasher.hash(password) def verify_password(self, password: str, hash: str) -> bool: return self.hasher.verify(password, hash) # Usage service = PasswordService(Argon2Hasher()) # Can swap to BcryptHasher() hash = service.hash_password("secret123") is_valid = service.verify_password("secret123", hash) ``` **Benefits:** - Easy to add new algorithms (e.g., PBKDF2) - Can switch algorithms without changing code - Each strategy is independently testable --- ## Singleton Pattern **What it is:** Ensure a class has only one instance and provide a global access point. **Example: Database session manager** ```python class DatabaseSessionManager: _instance: "DatabaseSessionManager | None" = None def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) cls._instance._engine = None cls._instance._sessionmaker = None return cls._instance def init(self, database_url: str): if self._engine is not None: return # Already initialized self._engine = create_async_engine(database_url) self._sessionmaker = async_sessionmaker(self._engine) async def session(self): async with self._sessionmaker() as session: yield session # Usage - always returns the same instance db1 = DatabaseSessionManager() db2 = DatabaseSessionManager() assert db1 is db2 # True ``` **When to use:** - Database connections (expensive to create) - Loggers (single logging configuration) - Configuration (load once, use everywhere) **When NOT to use:** - Most other cases (singletons are global state = hard to test) --- ## Mixin Pattern **What it is:** A class that provides methods to other classes but isn't meant to be instantiated itself. **Real example from the codebase:** ```python # backend/app/core/Base.py class UUIDMixin: id: Mapped[UUID] = mapped_column( UUID(as_uuid=True), primary_key=True, default=uuid.uuid7, ) class TimestampMixin: created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=func.now(), ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=func.now(), onupdate=func.now(), ) class SoftDeleteMixin: deleted_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True, default=None, ) # Usage: Compose mixins class User(Base, UUIDMixin, TimestampMixin): __tablename__ = "users" email: Mapped[str] = mapped_column(String(255)) password_hash: Mapped[str] = mapped_column(String(255)) class Report(Base, UUIDMixin, TimestampMixin, SoftDeleteMixin): __tablename__ = "reports" title: Mapped[str] = mapped_column(String(255)) description: Mapped[str] = mapped_column(Text) ``` **Benefits:** - DRY: Don't repeat `id`, `created_at`, `updated_at` in every model - Composable: Mix and match (User doesn't need SoftDeleteMixin) - Maintainable: Change `UUIDMixin` → all models updated --- ## Observer Pattern (Pub/Sub) **What it is:** When an object changes state, all its dependents are notified automatically. **Example: Event system** ```python from typing import Callable class EventBus: def __init__(self): self._subscribers: dict[str, list[Callable]] = {} def subscribe(self, event: str, handler: Callable): if event not in self._subscribers: self._subscribers[event] = [] self._subscribers[event].append(handler) async def publish(self, event: str, data: dict): if event not in self._subscribers: return for handler in self._subscribers[event]: await handler(data) # Event handlers async def send_email_notification(data: dict): print(f"Sending email to {data['email']}") async def log_event(data: dict): print(f"Logging event: {data}") # Setup bus = EventBus() bus.subscribe("user.registered", send_email_notification) bus.subscribe("user.registered", log_event) # Trigger event await bus.publish("user.registered", { "email": "user@example.com", "user_id": "123", }) ``` **Real-world use case:** ```python # When a report is submitted async def submit_report(...): report = await report_repo.create(...) # Publish event await event_bus.publish("report.submitted", { "report_id": report.id, "program_id": report.program_id, "author_id": report.author_id, }) return report # Multiple handlers react to the event bus.subscribe("report.submitted", notify_program_owner) bus.subscribe("report.submitted", update_analytics) bus.subscribe("report.submitted", log_submission) ``` **Benefits:** - Decoupled: Report submission doesn't know about notifications - Extensible: Add new handlers without modifying code - Testable: Can test each handler independently --- ## Combining Patterns Real-world code often combines multiple patterns: ```python # Dependency Injection + Repository + Layered Architecture # 1. Route (Presentation Layer) @router.post("/users") async def create_user( data: UserCreate, session: DatabaseSession, # Dependency Injection ) -> UserResponse: user = await register_user(session, data) # Service Layer return UserResponse.from_orm(user) # 2. Service (Business Layer) async def register_user( session: AsyncSession, data: UserCreate, ) -> User: repo = UserRepository(session) # Repository Pattern existing_user = await repo.find_by_email(data.email) if existing_user: raise UserAlreadyExistsError() password_hash = hash_password(data.password) # Strategy Pattern user = await repo.create( email=data.email, password_hash=password_hash, ) await session.commit() await event_bus.publish("user.registered", { # Observer Pattern "user_id": user.id, "email": user.email, }) return user # 3. Repository (Data Access Layer) class UserRepository(BaseRepository[User]): # Generic Pattern async def find_by_email(self, email: str) -> User | None: ... async def create(self, **kwargs) -> User: ... ``` This combines: - Dependency Injection (session injected) - Repository Pattern (data access abstraction) - Layered Architecture (route → service → repository) - Strategy Pattern (hash_password can use different algorithms) - Observer Pattern (event_bus.publish) - Generic Pattern (BaseRepository[User]) --- ## Conclusion Design patterns are not about memorization, but about recognizing common problems and applying proven solutions. **Key takeaways:** 1. **Dependency Injection** - Inject dependencies, don't hardcode them 2. **Repository Pattern** - Abstract data access from business logic 3. **Layered Architecture** - Separate presentation, business, and data layers 4. **Factory Pattern** - Centralize object creation 5. **Strategy Pattern** - Make algorithms interchangeable 6. **Singleton Pattern** - One instance for expensive resources 7. **Mixin Pattern** - Compose behavior from reusable pieces 8. **Observer Pattern** - Decouple event producers from consumers For more information: - System architecture: [ARCHITECTURE.md](./ARCHITECTURE.md) - Database design: [DATABASE.md](./DATABASE.md) - Security implementation: [SECURITY.md](./SECURITY.md) - Hands-on tutorial: [GETTING-STARTED.md](./GETTING-STARTED.md)