Clean Architecture Overview
The Currency Exchange API follows clean architecture principles to ensure maintainability, testability, and separation of concerns. The project is structured in four distinct layers, each with specific responsibilities and dependencies flowing inward toward the core domain.Dependencies flow inward: outer layers depend on inner layers, but inner layers never depend on outer layers. This creates a stable, testable architecture.
Layer Descriptions
API Layer (CurrencyExchange)
The outermost layer handles HTTP requests and responses. It contains no business logic, only coordination. Location:CurrencyExchange/
Key Components:
Controllers
Controllers
REST API endpoints that handle HTTP requests and return responses.CurrencyController (
Controllers/CurrencyController.cs:10)GET /api/Currency- Get all currenciesGET /api/Currency/{searchString}- Search currencies by code or namePOST /api/Currency- Add a new currency
Currency.Validate() with regex patterns for codes (3-5 uppercase letters), names (3-60 characters), and symbols.ExchangeRateController (Controllers/ExchangeRateController.cs:10)GET /api/ExchangeRate- Get all exchange ratesGET /api/ExchangeRate/{searchString}- Find rates by currencyPOST /api/ExchangeRate- Create new exchange ratePATCH /api/ExchangeRate/{baseCurrencyCode}&{targetCurrencyCode}- Update rate
ExchangeRate.cs:13-14).ExchangeController (Controllers/ExchangeController.cs:10)GET /api/Exchange/from={baseCurrencyCode}&to={targetCurrencyCode}&amount={amount}- Convert currency
- Checks for direct rate (USD→RUB)
- Falls back to reverse rate (RUB→USD inverted)
- Calculates cross-rate through intermediate currency
- Throws exception if no rate can be determined
DTOs (Data Transfer Objects)
DTOs (Data Transfer Objects)
Contracts that define the shape of API requests and responses.CurrencyDTO (ExchangeRateDTO (ExchangeDTO (AddExchangeRateDTO (
Contracts/CurrencyDTO.cs)Contracts/ExchangeRateDTO.cs)Contracts/ExchangeDTO.cs)Contracts/AddExchangeRateDTO.cs)Middleware
Middleware
ExceptionHandlerMiddleware (
Extensions/ExceptionHandlerMiddleware.cs)Global exception handling that catches all unhandled exceptions and returns appropriate HTTP status codes:ArgumentException,InvalidOperationException→ 400 Bad RequestArgumentNullException,NullReferenceException→ 404 Not Found- All others → 500 Internal Server Error
app.UseExceptionHandlerMiddleware() in Program.cs:48.Program.cs - Application Startup
Program.cs - Application Startup
Configuration (
Program.cs)Key registrations:Application Layer (CurrencyExchange.Application)
Contains business logic and orchestrates operations between controllers and repositories. Location:CurrencyExchange.Application/
Key Component:
ExchangeService
Location: This service is consumed by
Application/ExchangeService.cs:6Interface: ICurrencyExchangeService<ExchangeRate> (from Core layer)Responsibilities:- Validates conversion amounts (must be ≥ 0)
- Calculates converted amounts using exchange rates
- Rounds results to 2 decimal places using
MidpointRounding.ToZero
ExchangeController to perform the actual currency conversion after the rate is determined.Core Layer (CurrencyExchange.Core)
The heart of the application containing domain models, business rules, and abstractions. Location:CurrencyExchange.Core/
Components:
- Domain Models
- Abstractions (Interfaces)
Currency (ExchangeRate (Both models use factory methods (
Models/Currency.cs:5)Immutable domain model with built-in validation:Models/ExchangeRate.cs:3)Represents the exchange rate between two currencies:Create) to ensure all instances are valid.Data Layer (CurrencyExchange.Data)
Implements data access and external service integrations. Location:CurrencyExchange.Data/
Components:
Repositories
Repositories
CurrenciesRepository (
Repositories/CurrenciesRepository.cs:8)Implements ICurrencyRepository<Currency> with Entity Framework Core:- GetAll(): Returns all currencies from database
- Get(string code): Finds currency by code (case-insensitive)
- Find(string findText): Searches currencies using
EF.Functions.ILikefor case-insensitive partial matching on code or full name - Insert(Currency currency): Adds new currency after checking for duplicates
- CheckExist(Currency currency): Verifies if currency code already exists
CurrencyEntity (database) and Currency (domain model).ExchangeRatesRepository (Repositories/ExchangeRatesRepository.cs:8)Implements IExchangeRateRepository<ExchangeRate> with advanced rate logic:- Get(Currency baseCurrency, Currency targetCurrency): Direct rate lookup
- GetAndSaveRevers(): Calculates inverse rate (if USD→RUB = 82.25, then RUB→USD = 1/82.25 ≈ 0.012) and saves it
- GetAndSaveCross(): Finds intermediate currency and calculates cross-rate:
This method iterates through all existing rates to find a valid path (lines 210-244).
Database Context
Database Context
CurrencyExchangeDBContext (
CurrencyExchangeDBContext.cs)Entity Framework Core context with two DbSets:DbSet<CurrencyEntity> CurrenciesDbSet<ExchangeRateEntity> ExchangeRates
CurrencyConfiguration and ExchangeRateConfiguration classes using Fluent API.Database Schema:External Services
External Services
CBRExchangeRate (Configuration:
ExternalServices/Clients/CBRExchangeRate.cs:13)Background hosted service implementing IHostedService that:- Fetches rates from Central Bank of Russia using SOAP client (
DailyInfoSoapClient) - Parses XML response to extract currency codes, names, and rates
- Updates database with new rates on a timer (configurable interval)
- Timer interval set via
CurrencyReceiptPeriod:Periodinappsettings.json(default: 300 seconds) - Registered in
Program.cs:29-33as a hosted service - Uses
IServiceScopeFactoryto create scopes for database access
The service only updates existing currency pairs. New currencies from CBR must be manually added to the database first.
Entities
Entities
CurrencyEntity (ExchangeRateEntity (These entities are mapped to domain models in repository implementations.
Entities/CurrencyEntity.cs)Entities/ExchangeRateEntity.cs)Data Flow
Let’s trace a typical currency conversion request through all layers:Controller Validates and Fetches Data
The controller:
- Fetches base currency (USD) via
ICurrencyRepository.Get("USD") - Fetches target currency (EUR) via
ICurrencyRepository.Get("EUR") - Attempts to find exchange rate using intelligent fallback:
- Direct:
IExchangeRateRepository.Get(USD, EUR) - Reverse:
IExchangeRateRepository.GetAndSaveRevers(USD, EUR) - Cross:
IExchangeRateRepository.GetAndSaveCross(USD, EUR)
- Direct:
ExchangeController.cs:20-32.Repository Queries Database
ExchangeRatesRepository executes optimized LINQ query with joins:Application Service Calculates Conversion
ExchangeService.Calculate() is called:ExchangeService.cs:16-26.Design Patterns
Repository Pattern
All data access goes through repository interfaces (ICurrencyRepository, IExchangeRateRepository) defined in the Core layer and implemented in the Data layer. This abstraction:
- Decouples business logic from data access technology
- Enables easy unit testing with mock repositories
- Allows switching databases without affecting upper layers
Factory Method Pattern
Domain models use static factory methods (Currency.Create(), ExchangeRate.Create()) instead of public constructors to:
- Enforce validation rules at creation time
- Prevent invalid objects from existing
- Provide clear creation semantics
Dependency Injection
All dependencies are injected through constructors:- Loose coupling between components
- Easy unit testing with mocks
- Centralized configuration in
Program.cs
Hosted Service Pattern
CBRExchangeRate implements IHostedService for background rate updates:
- Starts automatically with the application
- Runs independently of HTTP requests
- Uses dependency injection scopes for database access
Key Architectural Decisions
Immutable Domain Models
Currency and ExchangeRate have private constructors and readonly properties, preventing modification after creation. This ensures data consistency and thread safety.Intelligent Rate Discovery
The
ExchangeRatesRepository implements three fallback strategies (direct, reverse, cross) to maximize rate availability without manual data entry.Generic Repositories
Using
ICurrencyRepository<T> enables type-safe repository operations while allowing specialized implementations like IExchangeRateRepository<T>.Automatic Code Normalization
Currency codes are automatically converted to uppercase in
Currency.Create() (line 23), ensuring consistent querying and avoiding duplicates.Testing Considerations
The architecture supports testing at multiple levels:Unit Testing
- Test domain models in isolation (validation logic)
- Mock repository interfaces to test controllers
- Test
ExchangeServicebusiness logic without database
Integration Testing
- Test repositories against real database using test container
- Verify EF Core queries and mappings
- Test
CBRExchangeRatewith mock SOAP client
End-to-End Testing
- Test complete request flow through all layers
- Verify error handling middleware
- Test rate fallback strategies with various data scenarios
Performance Optimizations
Eager Loading with Joins
Eager Loading with Joins
Repository queries use explicit joins to load related currencies with rates in a single query, avoiding N+1 problems:
Database Indexes
Database Indexes
Unique indexes on
currencies.code and exchangerates.(basecurrencyid, targetcurrencyid) ensure fast lookups and prevent duplicates.Computed Rate Caching
Computed Rate Caching
Reverse and cross rates are calculated once and saved to the database for reuse, avoiding repeated calculations.
Connection Pooling
Connection Pooling
EF Core automatically uses connection pooling for PostgreSQL, configured with
CommandTimeout=1200 in connection string.Summary
The Currency Exchange API demonstrates clean architecture principles with:- Clear separation of concerns across four distinct layers
- Dependency inversion with interfaces in Core layer
- Domain-driven design with rich models and validation
- Repository pattern for data access abstraction
- Intelligent rate discovery with automatic fallbacks
- Background services for automatic external updates
- Comprehensive error handling via middleware