Layer 1: API layer
Location:api/Purpose: HTTP interface and request/response handling The API layer is the entry point for all HTTP requests. It performs no business logic — it only validates input, calls a service, and shapes the response.
Components
| File | Responsibility |
|---|---|
main.py | FastAPI app creation, lifespan startup/shutdown |
routes/currency.py | HTTP endpoints, path parameter parsing |
schemas/requests.py | Pydantic input validation |
schemas/responses.py | Pydantic response shaping |
dependencies.py | Dependency injection wiring |
error_handlers.py | Maps domain exceptions to HTTP status codes |
Example: Route definition
Fromapi/routes/currency.py:21:
Notice how the route delegates all logic to
ConversionService. The route only normalizes input (uppercase) and shapes the response.Example: Application startup
Fromapi/main.py:22:
The lifespan context manager ensures resources are properly initialized at startup and cleaned up at shutdown.
Layer 2: Application layer
Location:application/services/Purpose: Business logic orchestration and use case implementation The application layer contains services that orchestrate domain objects and infrastructure components to fulfill business requirements.
Services
| Service | Responsibility |
|---|---|
CurrencyService | Managing supported currencies, validating currency codes |
RateService | Fetching, aggregating, and caching exchange rates |
ConversionService | Orchestrating end-to-end currency conversion |
Example: Currency validation
Fromapplication/services/currency_service.py:45:
Example: Rate aggregation with retry logic
Fromapplication/services/rate_service.py:30:
application/services/rate_service.py:55:
Understanding the retry strategy
Understanding the retry strategy
The service uses tenacity for resilient external API calls:
- 3 attempts maximum
- Exponential backoff: 1s → 2s → 4s (max 10s)
- Only retries
ConnectionError/TimeoutError - Does not retry
ProviderError(API-level errors like invalid keys)
Example: Conversion orchestration
Fromapplication/services/conversion_service.py:12:
Layer 3: Domain layer
Location:domain/Purpose: Core business entities and rules, framework-agnostic The domain layer is completely framework-free. No FastAPI, SQLAlchemy, or Redis — just plain Python dataclasses and exceptions. This makes it trivially testable.
Components
| Module | Responsibility |
|---|---|
models/currency.py | Frozen dataclasses: ExchangeRate, SupportedCurrency, AggregatedRate |
exceptions/currency.py | Typed exceptions: InvalidCurrencyError, ProviderError, CacheError |
Example: Domain models
Fromdomain/models/currency.py:6:
Models are frozen to ensure immutability, preventing accidental state changes throughout the application lifecycle.
Layer 4: Infrastructure layer
Location:infrastructure/Purpose: External systems integration (databases, caches, APIs) The infrastructure layer handles all interaction with external systems. It implements protocols defined by the application layer and translates between external formats and domain models.
Components
| Module | Responsibility |
|---|---|
providers/ | HTTP clients for each exchange rate API |
cache/redis_cache.py | Redis read/write with TTL management |
persistence/database.py | SQLAlchemy async engine and session factory |
persistence/models/ | ORM table definitions |
persistence/repositories/ | All database and cache queries |
Example: Provider protocol
Frominfrastructure/providers/base.py:5:
Example: Provider implementation
Frominfrastructure/providers/fixerio.py:43:
Each provider owns its own
httpx.AsyncClient and translates its API’s error format into ProviderError, isolating implementation details from the rest of the system.Example: Redis cache service
Frominfrastructure/cache/redis_cache.py:20:
Dependency flow
The dependency injection system wires everything together. Fromapi/dependencies.py:82:
Singleton vs per-request dependencies
Singleton vs per-request dependencies
Singletons (created at startup):
deps.db→ Database engine + session factorydeps.redis_cache→ RedisCacheServicedeps.providers→ Provider instances
AsyncSession→ Database sessionCurrencyRepository→ Repository with session and cache- All service instances → Fresh for each request
Benefits of this architecture
Testability
Each layer can be tested independently with minimal mocking
Flexibility
Swap implementations without touching other layers
Clarity
Every component has a single, well-defined responsibility
Maintainability
Changes are predictable and localized to specific layers
