Project Design — PriceTracker Full-Stack Architecture Blueprint
Project design is like the blueprint before building a building—if you start construction without a blueprint, you might find that the foundation isn't strong enough by the time you reach the third floor, and you'll have no choice but to tear it down and start over. A good blueprint isn't something you draw and then discard; it serves as a guide throughout the entire construction process.
1. What You'll Learn
- Requirements Analysis: Core Use Cases for SaaS Price Tracking (Users, Products, Prices, Subscriptions, Notifications)
- Domain-Driven Design: Identifying Aggregate Roots, Value Objects, and Domain Events
- Technology Selection Decision: Rationale for Choosing FastAPI + PostgreSQL + Redis + Celery + WebSocket
- API Versioning Strategies: The Trade-offs Between URL-Based and Header-Based Versioning
- Architecture Blueprint: Charlie's Deployment Topology—Load Balancing, Database Read-Write Separation, Caching Layers
2. Alice's True Story
(1) Pain Point: The more features are added, the more cluttered it becomes
PriceTracker evolved from a simple API into a service with an ever-increasing number of features, but it lacked a unified design—the boundaries between products and prices were blurred, subscription logic was scattered across 10 endpoints, and the notification system was tightly coupled with business logic. Every time a new feature was added, five files had to be modified, and regression bugs occurred frequently.
(2) Solutions for Domain-Driven Design
Domain-Driven Design (DDD) identifies core domains and boundaries from a business perspective: Product Aggregate, Price Aggregate, User Aggregate, and Subscription Aggregate—each aggregate has clear boundaries and responsibilities, and communication across aggregates occurs via events, eliminating interdependencies.
(3) Revenue
To add new features, you only need to modify the code within the corresponding aggregate; changes no longer affect the entire system. Subscription logic is centralized in the Subscription aggregate, and notifications are triggered via the PriceUpdated event, ensuring complete decoupling.
3. Requirements Analysis
(1) Core Use Cases
| Role | Use Case | Priority |
|---|---|---|
| User (Alice) | Sign Up/Log In | P0 |
| User | Check Product Price | P0 |
| User | Subscription Price Change Notification | P1 |
| Administrator | Bulk Import Prices | P0 |
| Administrator | Manage Product CRUD | P0 |
| Front End (Bob) | Call the API to retrieve data | P0 |
| DevOps (Charlie) | Monitoring System Health | P1 |
| System | Automatic Price Scraping | P2 |
(1) ▶ Example: Script for Sorting Use Cases by Priority
# scripts/prioritize_use_cases.py
use_cases = [
{"role": "User", "action": "register_login", "priority": "P0", "effort": 2},
{"role": "User", "action": "query_price", "priority": "P0", "effort": 1},
{"role": "User", "action": "subscribe_alert", "priority": "P1", "effort": 3},
{"role": "Admin", "action": "bulk_import", "priority": "P0", "effort": 5},
]
# Sort by priority; within the same priority level, sort in ascending order by workload
order = {"P0": 0, "P1": 1, "P2": 2}
sorted_cases = sorted(use_cases, key=lambda x: (order[x["priority"]], x["effort"]))
for uc in sorted_cases:
print(f"[{uc['priority']}] {uc['role']}: {uc['action']} (effort={uc['effort']}d)")
Output:
# Execution Successful
(2) Non-functional Requirements
| Requirements | Objectives | Constraints |
|---|---|---|
| QPS | Millions (cluster) | Nginx LB + K8s elasticity |
| P99 Latency | < 50 ms | Redis Cache + Asynchronous DB |
| Availability | 99.9% | Multi-replica + Automatic Recovery |
| Data Volume | Millions of products + tens of millions of prices | PostgreSQL Partitioning |
4. Domain Model Design
(1) ER Diagram
erDiagram
User ||--o{ Subscription : has
User ||--o{ Product : creates
User ||--o{ Alert : sets
Product ||--o{ Price : has
Product ||--o{ Alert : watched_by
Subscription ||--o{ Feature : includes
User {
int id PK
string email UK
string hashed_password
string role
datetime created_at
}
Product {
int id PK
string name
string category
float base_price
string sku UK
int user_id FK
datetime created_at
}
Price {
int id PK
int product_id FK
float price
string currency
string source
datetime recorded_at
}
Subscription {
int id PK
int user_id FK
string plan
datetime starts_at
datetime expires_at
}
Alert {
int id PK
int user_id FK
int product_id FK
float target_price
string status
}
(2) Aggregation Root Identification
| Aggregate Root | Contains Entities | Boundary Rules |
|---|---|---|
| User | User, Subscription, Alert | The user owns subscriptions and alerts |
| Product | Product, Price | Product has price history |
| PriceImport | Batch Information, Import Status | Bulk import is a separate transaction |
(1) ▶ Example: Domain Event Definitions
# domain/events.py
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
class EventType(Enum):
PRICE_CHANGED = "price_changed"
ALERT_TRIGGERED = "alert_triggered"
USER_SUBSCRIBED = "user_subscribed"
@dataclass
class DomainEvent:
event_type: EventType
aggregate_id: int
occurred_at: datetime = field(default_factory=datetime.utcnow)
payload: dict = field(default_factory=dict)
# Usage: Price Change Events
price_event = DomainEvent(
event_type=EventType.PRICE_CHANGED,
aggregate_id=42,
payload={"old_price": 299.9, "new_price": 249.9, "currency": "CNY"},
)
Output:
# Execution Successful
5. Technology Selection Decisions
(1) Model Comparison
| Need | Option A | Option B | Decision | Reason |
|---|---|---|---|---|
| Web Frameworks | FastAPI | Flask/Django | FastAPI | Asynchronous + Auto-documentation + Type Checking |
| Database | PostgreSQL | MySQL/MongoDB | PostgreSQL | JSONB + Asynchronous Driver Mature |
| Caching | Redis | Memcached | Redis | Rich data structures + persistence |
| Task Queue | Celery+Redis | RQ/Dramatiq | Celery | Mature ecosystem + robust monitoring |
| Real-time Communication | WebSocket | SSE | WebSocket | Bidirectional + Low Latency |
| Container | Docker | Bare Metal/VM | Docker | Consistency + Orchestration |
(2) Overview of the System Architecture
flowchart TD
Client[Client / Bob Frontend] --> Nginx[Nginx Load Balancer]
Nginx --> API1[FastAPI Pod 1]
Nginx --> API2[FastAPI Pod 2]
Nginx --> APIN[FastAPI Pod N]
API1 --> PG_Master[(PostgreSQL Master)]
API2 --> PG_Master
APIN --> PG_Master
PG_Master --> PG_Replica[(PostgreSQL Replica)]
API1 --> Redis[(Redis Cluster)]
API2 --> Redis
APIN --> Redis
Redis --> CW1[Celery Worker 1]
Redis --> CW2[Celery Worker 2]
CW1 --> PG_Master
CW2 --> PG_Master
API1 --> WS[WebSocket Hub]
API2 --> WS
Prometheus[Prometheus] --> API1
Grafana[Grafana] --> Prometheus
6. API Versioning Strategy
(1) URL Version vs. Header Version
| Dimension | URL Version /api/v1/ |
Header Version Accept: v=1 |
|---|---|---|
| Visibility | High (URL is explicit) | Low (hidden in the header) |
| Routing | Simple (prefix-based) | Complex (requires middleware parsing) |
| Cache | Independent URL Cache | Requires Vary Header |
| Swagger | Automatic grouping | Requires manual configuration |
| Recommended | ✅ How to Use PriceTracker | Suitable for Internal APIs |
(1) ▶ Example: URL Versioning Routing Structure
from fastapi import APIRouter
# V1 routes
v1_router = APIRouter(prefix="/api/v1", tags=["v1"])
v1_products = APIRouter(prefix="/products", tags=["products"])
v1_prices = APIRouter(prefix="/prices", tags=["prices"])
v1_auth = APIRouter(prefix="/auth", tags=["auth"])
# V2 routes (future)
v2_router = APIRouter(prefix="/api/v2", tags=["v2"])
# Mount routers
app.include_router(v1_auth)
app.include_router(v1_products, dependencies=[Depends(get_current_user)])
app.include_router(v1_prices, dependencies=[Depends(get_current_user)])
app.include_router(v1_router)
Output:
# Execution Successful
❓ FAQ
Product is the aggregate root, Price can only be modified through Product and cannot be added or deleted independently, thereby ensuring data consistency.publish_price_updated_event.delay(product_id, new_price)), while a more complex approach uses message queues (RabbitMQ/Kafka).📖 Summary
- The requirements analysis distinguishes between functional and non-functional requirements and clarifies priorities and constraints.
- DDD: Identify aggregate roots (User, Product, PriceImport), and define transaction boundaries and consistency rules
- Technology selection based on use case: FastAPI (asynchronous) + PostgreSQL (JSONB) + Redis (data structures) + Celery (task queues)
- API versioning uses the URL prefix
/api/v1/, which is simple, intuitive, and cache-friendly - Deployment topology: Nginx Load Balancer → FastAPI Pods → PostgreSQL Master-Slave → Redis Cluster → Celery Workers
📝 Exercises
- Basic Question (Difficulty: ⭐): Draw a system architecture diagram for PriceTracker (including FastAPI, PostgreSQL, Redis, Celery, and Nginx), and label the responsibilities of each component and the data flow. Hint: Mermaid flowchart
- Advanced Problem (Difficulty ⭐⭐): Design an ER diagram for the PriceTracker domain model, identify three aggregate roots (User, Product, PriceImport), and define the entities included in each aggregate and their boundary rules. Hint: Mermaid erDiagram + aggregate root table
- Challenge (Difficulty: ⭐⭐⭐): Complete the full architectural blueprint for PriceTracker—including the requirements document (core use cases + non-functional requirements), a technology comparison table, code for the API versioning routing structure, and a deployment topology diagram (including master-slave databases, caching tiers, and a Celery cluster). Hint: Incorporate all content covered in this lesson.
---|



