Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
218 changes: 218 additions & 0 deletions jobs/Backend/Task/CnbExchangeRateProvider/Architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
# Architecture

## Overview

The Exchange Rate Provider is built using Clean Architecture principles to ensure separation of concerns, testability, and maintainability. The architecture is divided into multiple layers, each with specific responsibilities and dependencies.

## Architectural Diagrams

### Clean Architecture Layers

```mermaid
graph TD
%% Presentation Layer (Outer)
A[Presentation Layer<br/>🌐 Entry Points<br/>├── <b>ExchangeRateProvider.Api</b><br/>│ ├── REST Controllers<br/>│ ├── Swagger/OpenAPI<br/>│ └── Health Monitoring<br/>└── <b>ExchangeRateProvider.Console</b><br/> └── CLI Interface]

%% Application Layer
B[Application Layer<br/>⚙️ Use Cases & Business Logic<br/>├── MediatR CQRS Pipeline<br/>├── GetExchangeRatesQuery<br/>├── Query Handlers<br/>└── Dependency Injection]

%% Domain Layer (Core)
C[Domain Layer<br/>🎯 Core Business Rules<br/>├── <b>Entities</b><br/>│ ├── Currency<br/>│ └── ExchangeRate<br/>├── <b>Interfaces</b><br/>│ ├── IExchangeRateProvider<br/>│ └── IProviderRegistry<br/>└── Business Logic]

%% Infrastructure Layer
D[Infrastructure Layer<br/>🔧 External Concerns<br/>├── CnbExchangeRateProvider<br/>├── Intelligent Caching<br/>├── Distributed Cache<br/>├── Resilience Policies<br/>└── Service Registration]

%% External Systems
E[External Systems<br/>🌍 Third-Party Services<br/>├── CNB API<br/>├── Redis Cache<br/>└── Docker Runtime]

%% Dependencies (Outer layers depend on inner layers)
A -->|depends on| B
B -->|depends on| C
D -->|depends on| C
D -->|integrates with| E

%% Enhanced Styling - Beautiful, eye-friendly colors
style C fill:#e3f2fd,stroke:#1976d2,stroke-width:4px,color:#0d47a1
style B fill:#f3e5f5,stroke:#7b1fa2,stroke-width:3px,color:#4a148c
style A fill:#fff8e1,stroke:#f57c00,stroke-width:2px,color:#e65100
style D fill:#e8f5e8,stroke:#388e3c,stroke-width:2px,color:#1b5e20
style E fill:#f5f5f5,stroke:#757575,stroke-width:1px,color:#424242
```

### Data Flow Diagram

```mermaid
flowchart TD
Client[Client Application] -->|HTTP GET /api/exchange-rates| Controller[ExchangeRatesController]
Controller -->|Validate Request| Controller
Controller -->|Send Query| MediatR[MediatR Pipeline]
MediatR -->|Route to Handler| Handler[GetExchangeRatesQueryHandler]
Handler -->|Request Providers| Registry[ProviderRegistry]
Registry -->|Select Provider| CacheCheck{Cache Check<br/>CnbCacheStrategy}
CacheCheck -->|Cache Hit| CacheReturn[Return Cached Data]
CacheCheck -->|Cache Miss| Provider[CnbExchangeRateProvider]
Provider -->|Fetch Data| CNBAPI[CNB API<br/>https://api.cnb.cz/cnbapi/exrates/daily]
CNBAPI -->|Return Rates| Provider
Provider -->|Apply Caching| DistributedCache[DistributedCachingExchangeRateProvider<br/>Redis]
DistributedCache -->|Store in Cache| DistributedCache
DistributedCache -->|Return Data| Handler
CacheReturn -->|Return Data| Handler
Handler -->|Process Response| Handler
Handler -->|Return Result| MediatR
MediatR -->|Return Result| Controller
Controller -->|Format Response| Controller
Controller -->|HTTP Response| Client

style Client fill:#e3f2fd
style Controller fill:#fff3e0
style MediatR fill:#f3e5f5
style Handler fill:#e8f5e8
style Registry fill:#e1f5fe
style CacheCheck fill:#fff9c4
style Provider fill:#e8f5e8
style CNBAPI fill:#ffebee
style DistributedCache fill:#f3e5f5
```

## Layer Descriptions

### Domain Layer
- **Purpose**: Contains core business entities, value objects, and interfaces
- **Components**:
- `Currency` and `ExchangeRate` entities with validation
- `IExchangeRateProvider` interface for provider abstraction
- `IProviderRegistry` for managing multiple providers
- **Dependencies**: None (innermost layer)

### Application Layer
- **Purpose**: Contains business logic and use cases
- **Components**:
- MediatR-based command/query pattern
- `GetExchangeRatesQuery` and handler for rate retrieval
- **Dependencies**: Domain Layer

### Infrastructure Layer
- **Purpose**: Handles external concerns and implementations
- **Components**:
- `CnbExchangeRateProvider`: CNB API integration
- `CnbCacheStrategy`: Intelligent caching based on CNB publication schedule
- `DistributedCachingExchangeRateProvider`: Redis-based caching decorator
- Polly policies for resilience (retry, circuit breaker)
- Provider registration services
- **Dependencies**: Domain Layer

### Presentation Layer
- **Purpose**: Entry points for the application
- **Components**:
- **API Layer** (`ExchangeRateProvider.Api`): ASP.NET Core RESTful web API with controllers, Swagger, health checks
- **Console Layer** (`ExchangeRateProvider.Console`): Command-line interface for testing
- **Dependencies**: Application Layer


## Key Architectural Decisions

### 1. Clean Architecture
**Decision**: Implement Clean Architecture with strict layer separation.
**Rationale**:
- Ensures separation of concerns
- Improves testability by allowing mocking of dependencies
- Facilitates maintainability and evolution of the codebase
- Prevents business logic from being coupled to external frameworks

### 2. Provider Abstraction Pattern
**Decision**: Use `IExchangeRateProvider` interface with priority-based provider registry.
**Rationale**:
- Allows easy addition of new exchange rate providers
- Enables fallback mechanisms if primary provider fails
- Supports different providers for different currencies or regions
- Maintains single responsibility principle

### 3. Intelligent Caching Strategy
**Decision**: Implement time-based caching that adapts to CNB publication schedule.
**Rationale**:
- Reduces unnecessary API calls to CNB
- Optimizes performance during high-frequency publication windows
- Balances freshness of data with system performance
- Reduces load on external API

### 4. CQRS with MediatR
**Decision**: Use MediatR for implementing CQRS pattern in the Application layer.
**Rationale**:
- Separates read and write operations
- Improves code organization and maintainability
- Enables easy testing of handlers
- Supports cross-cutting concerns like logging and validation

### 5. Resilience with Polly
**Decision**: Implement Polly policies for retry, circuit breaker, and timeout.
**Rationale**:
- Handles transient failures gracefully
- Prevents cascading failures
- Improves system reliability and user experience
- Provides configurable resilience strategies

### 6. Distributed Caching with Redis
**Decision**: Use Redis for distributed caching in multi-instance deployments.
**Rationale**:
- Enables cache sharing across multiple application instances
- Improves performance in scaled environments
- Provides persistence and backup capabilities
- Integrates well with cloud deployments

### 7. Docker Containerization
**Decision**: Provide multi-stage Dockerfiles for development and production.
**Rationale**:
- Ensures consistent deployment across environments
- Simplifies scaling and orchestration
- Improves development workflow
- Enables efficient CI/CD pipelines

### 8. Comprehensive Testing Strategy
**Decision**: Maintain high test coverage with unit, integration, and infrastructure tests.
**Rationale**:
- Ensures code quality and prevents regressions
- Enables confident refactoring and feature additions
- Validates integration with external services
- Supports continuous integration practices

## Data Flow

1. **API Request**: Client sends request to `/api/exchange-rates`
2. **Controller**: `ExchangeRatesController` receives request and validates input
3. **Application**: `GetExchangeRatesQuery` is sent via MediatR
4. **Handler**: `GetExchangeRatesQueryHandler` processes the query
5. **Infrastructure**: Provider registry selects appropriate provider
6. **Caching**: Cache strategy checks for cached data
7. **Provider**: If cache miss, fetches data from CNB API
8. **Response**: Data flows back through layers to client

## Production Features

### Implemented Features
- ✅ **Rate Limiting**: ASP.NET Core Rate Limiting
- ✅ **Health Checks**
- ✅ **Prometheus Metrics**: Request counts, response times, cache hit rates
- ✅ **Swagger/OpenAPI**: Interactive API documentation
- ✅ **Circuit Breaker**: Automatic failure detection and recovery using Polly
- ✅ **Retry Policies**: Exponential backoff for transient failures
- ✅ **Distributed Caching**: Redis support for multi-instance deployments
- ✅ **Docker Support**: Production-ready containerization

### Security Considerations
- Input validation for currency codes
- Rate limiting to prevent abuse
- Secure defaults for Redis configuration

### Monitoring and Observability
- Health endpoints for load balancer checks
- Prometheus metrics for alerting
- Structured logging with Serilog
- Request tracing and correlation IDs

## Deployment Architecture

The application supports multiple deployment scenarios:

- **Development**: Local Docker Compose with Redis
- **Production**: Containerized deployment with external Redis
- **Console**: Standalone executable for batch operations
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using ExchangeRateProvider.Application.Queries;
using ExchangeRateProvider.Domain.Entities;
using MediatR;
using Microsoft.AspNetCore.Mvc;

namespace ExchangeRateProvider.Api.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class ExchangeRatesController : ControllerBase
{
private readonly IMediator _mediator;
private readonly ILogger<ExchangeRatesController> _logger;
private readonly IConfiguration _configuration;

public ExchangeRatesController(
IMediator mediator,
ILogger<ExchangeRatesController> logger,
IConfiguration configuration)
{
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
}

/// <summary>
/// Gets exchange rates for a specified list of currencies against CZK.
/// </summary>
/// <param name="currencyCodes">A comma-separated list of currency codes (e.g., USD,EUR,GBP).</param>
/// <returns>A list of exchange rates against CZK.</returns>
[HttpGet]
public async Task<ActionResult<IEnumerable<ExchangeRate>>> GetExchangeRates(
[FromQuery] string currencyCodes)
{
// Parse currency codes
var requestedCodes = currencyCodes.Split(',', StringSplitOptions.RemoveEmptyEntries);

// Validate currency codes
if (string.IsNullOrWhiteSpace(currencyCodes) || !requestedCodes.Any())
{
_logger.LogInformation("No currency codes provided, returning empty result.");
return Ok(new List<ExchangeRate>());
}

// Enforce max currency count
var maxCount = _configuration.GetValue<int>("ExchangeRateProvider:MaxCurrencies", 20);
var requestedCount = requestedCodes.Length;

if (requestedCount > maxCount)
{
_logger.LogWarning("Too many currency codes requested. Maximum allowed is {MaxCount}.", maxCount);
return BadRequest($"Too many currency codes. Maximum allowed is {maxCount}.");
}

// Parse and validate currency codes
var currencies = new List<Currency>();
var invalidCodes = new List<string>();

foreach (var code in requestedCodes.Select(c => c.Trim().ToUpper()).Where(c => !string.IsNullOrWhiteSpace(c)))
{
try
{
currencies.Add(new Currency(code));
}
catch (InvalidCurrencyCodeException)
{
invalidCodes.Add(code);
}
}

if (!currencies.Any())
{
_logger.LogWarning("No valid currency codes provided. Invalid codes: {InvalidCodes}", string.Join(", ", invalidCodes));
return BadRequest($"No valid currency codes provided. Invalid codes: {string.Join(", ", invalidCodes)}");
}

if (invalidCodes.Any())
{
_logger.LogWarning("Some currency codes were invalid and ignored: {InvalidCodes}", string.Join(", ", invalidCodes));
}

// Only CZK is supported as the target currency
var targetCurrency = new Currency("CZK");

try
{
_logger.LogInformation(
"Fetching exchange rates for currency codes: {CurrencyCodes} against CZK.",
currencyCodes);

var exchangeRates = await _mediator.Send(
new GetExchangeRatesQuery(currencies, targetCurrency));

return Ok(exchangeRates);
}
catch (ApplicationException ex)
{
_logger.LogError(ex, "Application error while fetching exchange rates: {Message}", ex.Message);
return StatusCode(500, $"An application error occurred: {ex.Message}");
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error while fetching exchange rates: {Message}", ex.Message);
return StatusCode(500, $"An unexpected error occurred: {ex.Message}");
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Build stage
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore ExchangeRateProvider.sln
RUN dotnet publish ExchangeRateProvider.Api/ExchangeRateProvider.Api.csproj -c Release -o /app/publish

# Runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8080
ENTRYPOINT ["dotnet", "ExchangeRateProvider.Api.dll"]

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>ExchangeRateProvider.Api</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="MediatR" Version="12.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.8" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.0" />
<PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
<PackageReference Include="AspNetCore.HealthChecks.Redis" Version="8.0.2" />
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="8.0.2" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\ExchangeRateProvider.Domain\ExchangeRateProvider.Domain.csproj" />
<ProjectReference Include="..\ExchangeRateProvider.Application\ExchangeRateProvider.Application.csproj" />
</ItemGroup>

</Project>
Loading