Skip to content

Commit f343225

Browse files
docs: Update documentation for modular architecture
1 parent db3915c commit f343225

2 files changed

Lines changed: 320 additions & 6 deletions

File tree

ARCHITECTURE.md

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
# Architecture Documentation
2+
3+
This document describes the modular architecture of the financial-calculations-api project.
4+
5+
## Overview
6+
7+
The project follows a **layered architecture** pattern with clear separation between:
8+
- **API Layer** (routes): HTTP request/response handling
9+
- **Service Layer**: Pure business logic
10+
- **Model Layer**: Data validation and serialization
11+
- **Core Layer**: Configuration and cross-cutting concerns
12+
13+
## Directory Structure
14+
15+
```
16+
app/
17+
├── main.py # FastAPI app entry point (34 lines)
18+
├── core/ # Core configuration and error handling
19+
│ ├── config.py # Constants, CORS, metadata, scipy imports
20+
│ └── errors.py # Exception handlers and custom exceptions
21+
├── models/ # Pydantic request/response models
22+
│ ├── common.py # Common models (ErrorDetail, ErrorResponse, Echo)
23+
│ ├── tvm.py # Time Value of Money models
24+
│ ├── mortgage.py # Mortgage calculation models
25+
│ ├── bonds.py # Bond calculation models
26+
│ └── xirr.py # XIRR calculation models
27+
├── services/ # Pure business logic (no FastAPI dependencies)
28+
│ ├── tvm.py # TVM calculation services
29+
│ ├── mortgage.py # Mortgage calculation services
30+
│ ├── bonds.py # Bond calculation services
31+
│ └── xirr.py # XIRR calculation services
32+
└── api/
33+
└── routes/ # FastAPI route handlers
34+
├── system.py # Health, echo, info endpoints
35+
├── tvm.py # TVM endpoints
36+
├── mortgage.py # Mortgage endpoints
37+
├── bonds.py # Bond endpoints
38+
└── xirr.py # XIRR endpoints
39+
```
40+
41+
## Layer Responsibilities
42+
43+
### 1. Core Layer (`app/core/`)
44+
45+
**Purpose**: Shared configuration and cross-cutting concerns.
46+
47+
#### `config.py`
48+
- Application constants (MAX_AMOUNT, MAX_AMORTIZATION_MONTHS, etc.)
49+
- CORS configuration (ALLOWED_ORIGINS)
50+
- Build metadata (BUILD_TIMESTAMP, GIT_SHA, ENVIRONMENT)
51+
- Optional scipy imports with fallback
52+
53+
#### `errors.py`
54+
- Exception handlers for unified error responses
55+
- Custom exceptions (NoSolutionError)
56+
- Error response formatting
57+
58+
### 2. Model Layer (`app/models/`)
59+
60+
**Purpose**: Data validation and serialization using Pydantic.
61+
62+
**Rules**:
63+
- Models contain **only** Pydantic `BaseModel` classes
64+
- No business logic
65+
- No FastAPI dependencies
66+
- Field validation and examples for Swagger documentation
67+
68+
**Example**:
69+
```python
70+
class FutureValueRequest(BaseModel):
71+
principal: float = Field(..., ge=0, le=MAX_AMOUNT)
72+
annual_rate: float = Field(..., ge=0, le=1)
73+
years: float = Field(..., gt=0)
74+
compounds_per_year: int = Field(..., gt=0)
75+
```
76+
77+
### 3. Service Layer (`app/services/`)
78+
79+
**Purpose**: Pure business logic calculations.
80+
81+
**Rules**:
82+
- **No FastAPI dependencies** (no `HTTPException`, no `Request`, etc.)
83+
- **No HTTP concerns** (status codes, headers, etc.)
84+
- Functions take primitive types or model instances
85+
- Functions return primitive types or dictionaries
86+
- Raise custom exceptions (e.g., `NoSolutionError`) for business logic errors
87+
- Easily testable without FastAPI
88+
89+
**Example**:
90+
```python
91+
def calculate_future_value(
92+
principal: float,
93+
annual_rate: float,
94+
years: float,
95+
compounds_per_year: int
96+
) -> float:
97+
"""Calculate future value using compound interest."""
98+
rate_per_period = annual_rate / compounds_per_year
99+
total_periods = compounds_per_year * years
100+
future_value = principal * (1 + rate_per_period) ** total_periods
101+
return round(future_value, 2)
102+
```
103+
104+
### 4. API Layer (`app/api/routes/`)
105+
106+
**Purpose**: HTTP request/response handling.
107+
108+
**Rules**:
109+
- FastAPI route decorators (`@router.post`, `@router.get`)
110+
- Request/response models from `app/models/`
111+
- Call service functions from `app/services/`
112+
- Handle HTTP exceptions and convert service exceptions to HTTP responses
113+
- Return properly formatted responses
114+
115+
**Example**:
116+
```python
117+
@router.post("/v1/tvm/future-value", response_model=FutureValueResponse)
118+
def calculate_future_value(payload: FutureValueRequest):
119+
"""Calculate the future value of an investment."""
120+
future_value = calc_fv(
121+
payload.principal,
122+
payload.annual_rate,
123+
payload.years,
124+
payload.compounds_per_year,
125+
)
126+
return {"ok": True, "future_value": future_value}
127+
```
128+
129+
## Request Flow
130+
131+
```
132+
1. HTTP Request
133+
134+
2. FastAPI Router (app/api/routes/*)
135+
- Validates request using Pydantic models
136+
- Extracts parameters
137+
138+
3. Service Layer (app/services/*)
139+
- Performs business logic calculations
140+
- May raise custom exceptions
141+
142+
4. Router (app/api/routes/*)
143+
- Handles exceptions
144+
- Formats response
145+
146+
5. HTTP Response
147+
```
148+
149+
## Error Handling
150+
151+
### Service Layer Errors
152+
153+
Services raise custom exceptions:
154+
- `NoSolutionError`: When a numerical solver cannot find a solution
155+
156+
### Route Layer Error Handling
157+
158+
Routes catch service exceptions and convert them to HTTP responses:
159+
160+
```python
161+
try:
162+
result = calculate_bond_yield(...)
163+
except NoSolutionError as e:
164+
raise HTTPException(
165+
status_code=status.HTTP_400_BAD_REQUEST,
166+
detail={
167+
"ok": False,
168+
"error": {
169+
"code": "NO_SOLUTION",
170+
"message": str(e),
171+
"details": e.details,
172+
},
173+
},
174+
)
175+
```
176+
177+
### Global Exception Handlers
178+
179+
`app/core/errors.py` provides global exception handlers:
180+
- `validation_exception_handler`: Handles Pydantic validation errors
181+
- `http_exception_handler`: Formats HTTPException responses consistently
182+
183+
## Adding New Features
184+
185+
### Adding a New Endpoint
186+
187+
1. **Add models** in `app/models/<domain>.py`:
188+
```python
189+
class NewRequest(BaseModel):
190+
field: float = Field(...)
191+
192+
class NewResponse(BaseModel):
193+
ok: bool
194+
result: float
195+
```
196+
197+
2. **Add service function** in `app/services/<domain>.py`:
198+
```python
199+
def calculate_new(field: float) -> float:
200+
"""Pure calculation logic."""
201+
return field * 2
202+
```
203+
204+
3. **Add route** in `app/api/routes/<domain>.py`:
205+
```python
206+
@router.post("/v1/domain/new", response_model=NewResponse)
207+
def new_endpoint(payload: NewRequest):
208+
result = calculate_new(payload.field)
209+
return {"ok": True, "result": result}
210+
```
211+
212+
4. **Register router** in `app/main.py` (if new domain):
213+
```python
214+
from app.api.routes import new_domain
215+
app.include_router(new_domain.router)
216+
```
217+
218+
## Testing Strategy
219+
220+
### Service Layer Tests
221+
222+
Services can be tested independently:
223+
224+
```python
225+
def test_calculate_future_value():
226+
result = calculate_future_value(10000, 0.07, 10, 12)
227+
assert abs(result - 19671.51) < 0.01
228+
```
229+
230+
### Route Layer Tests
231+
232+
Routes are tested using FastAPI's `TestClient`:
233+
234+
```python
235+
def test_future_value_endpoint():
236+
response = client.post("/v1/tvm/future-value", json={
237+
"principal": 10000,
238+
"annual_rate": 0.07,
239+
"years": 10,
240+
"compounds_per_year": 12
241+
})
242+
assert response.status_code == 200
243+
assert response.json()["ok"] is True
244+
```
245+
246+
## Benefits of This Architecture
247+
248+
1. **Separation of Concerns**: Each layer has a single, well-defined responsibility
249+
2. **Testability**: Services can be tested without FastAPI, routes can be tested with TestClient
250+
3. **Reusability**: Service functions can be reused in different contexts (CLI, background jobs, etc.)
251+
4. **Maintainability**: Changes are isolated to specific layers
252+
5. **Scalability**: Easy to add new endpoints and domains
253+
6. **Type Safety**: Pydantic models provide runtime validation and type hints
254+
255+
## Migration from Monolithic Structure
256+
257+
The project was refactored from a single `app/main.py` file (1721 lines) to this modular structure:
258+
259+
- **Before**: All code in `app/main.py` (models, services, routes, config)
260+
- **After**: Organized into 4 layers across 20+ files
261+
- **Result**: `app/main.py` reduced to 34 lines (only app initialization)
262+
263+
This refactoring improves:
264+
- Code organization
265+
- Developer experience
266+
- Testability
267+
- Maintainability

README.md

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
Stateless JSON API for financial calculations built with FastAPI.
44

5+
> 📖 **Architecture Documentation**: See [ARCHITECTURE.md](ARCHITECTURE.md) for detailed information about the project structure and design patterns.
6+
57
## Quickstart
68

79
### Health Check
@@ -230,7 +232,7 @@ A simple web client is included in the `docs/` directory that can call the API f
230232

231233
3. **Update CORS in Backend**:
232234
- The backend already includes CORS middleware
233-
- Make sure your GitHub Pages URL is in the `ALLOWED_ORIGINS` list in `app/main.py`
235+
- Make sure your GitHub Pages URL is in the `ALLOWED_ORIGINS` list in `app/core/config.py`
234236
- Example: `"https://mytherapy-coding.github.io"`
235237

236238
4. **Verify Deployment**:
@@ -296,7 +298,7 @@ By default, the following origins are allowed:
296298
- GitHub Pages for this project:
297299
- `https://mytherapy-coding.github.io`
298300

299-
If you add another frontend (for example, a different GitHub Pages site or a custom domain), update `ALLOWED_ORIGINS` in `app/main.py`:
301+
If you add another frontend (for example, a different GitHub Pages site or a custom domain), update `ALLOWED_ORIGINS` in `app/core/config.py`:
300302

301303
```python
302304
ALLOWED_ORIGINS = [
@@ -329,13 +331,42 @@ ALLOWED_ORIGINS = [
329331
financial-calculations-api/
330332
├── app/
331333
│ ├── __init__.py
332-
│ └── main.py
333-
├── docs/ # Web client (served by GitHub Pages)
334+
│ ├── main.py # FastAPI app entry point
335+
│ ├── core/ # Core configuration and error handling
336+
│ │ ├── config.py # Constants, CORS, metadata
337+
│ │ └── errors.py # Exception handlers
338+
│ ├── models/ # Pydantic request/response models
339+
│ │ ├── common.py # Common models (Error, Echo)
340+
│ │ ├── tvm.py # Time Value of Money models
341+
│ │ ├── mortgage.py # Mortgage calculation models
342+
│ │ ├── bonds.py # Bond calculation models
343+
│ │ └── xirr.py # XIRR calculation models
344+
│ ├── services/ # Pure business logic (no FastAPI dependencies)
345+
│ │ ├── tvm.py # TVM calculation services
346+
│ │ ├── mortgage.py # Mortgage calculation services
347+
│ │ ├── bonds.py # Bond calculation services
348+
│ │ └── xirr.py # XIRR calculation services
349+
│ └── api/
350+
│ └── routes/ # FastAPI route handlers
351+
│ ├── system.py # Health, echo, info endpoints
352+
│ ├── tvm.py # TVM endpoints
353+
│ ├── mortgage.py # Mortgage endpoints
354+
│ ├── bonds.py # Bond endpoints
355+
│ └── xirr.py # XIRR endpoints
356+
├── docs/ # Web client (served by GitHub Pages)
334357
│ ├── index.html
335358
│ ├── app.js
336359
│ └── style.css
337-
├── tests/ # Pytest test suite
338-
├── .github/workflows/ # CI workflow
360+
├── tests/ # Pytest test suite
361+
│ ├── test_health.py
362+
│ ├── test_echo.py
363+
│ ├── test_future_value.py
364+
│ ├── test_tvm.py
365+
│ ├── test_mortgage.py
366+
│ ├── test_bond.py
367+
│ ├── test_xirr.py
368+
│ └── test_info.py
369+
├── .github/workflows/ # CI workflow
339370
├── CONCEPTS.md
340371
├── DEPLOY.md
341372
├── LICENSE
@@ -344,3 +375,19 @@ financial-calculations-api/
344375
├── render.yaml
345376
└── requirements.txt
346377
```
378+
379+
### Architecture
380+
381+
The project follows a **modular architecture** with clear separation of concerns:
382+
383+
- **`app/main.py`**: FastAPI application initialization, middleware, exception handlers, and router registration (34 lines)
384+
- **`app/core/`**: Shared configuration and error handling
385+
- **`app/models/`**: Pydantic models for request/response validation
386+
- **`app/services/`**: Pure business logic functions (no FastAPI dependencies, easily testable)
387+
- **`app/api/routes/`**: FastAPI route handlers that call services and return HTTP responses
388+
389+
This architecture provides:
390+
- **Separation of concerns**: Routes → Services → Models
391+
- **Testability**: Services can be tested independently without FastAPI
392+
- **Scalability**: Easy to add new endpoints and domains
393+
- **Maintainability**: Changes are isolated to specific modules

0 commit comments

Comments
 (0)