A production-ready currency conversion platform built with Clean Architecture, DDD, and AI-powered chat.
Bonus: Beyond the core API requirement, this submission includes two additional projects — an AI chatbot API and a Next.js chat UI — demonstrating the ability to build advanced, AI-powered applications on top of the base service.
| Service | Stack | Purpose |
|---|---|---|
| Currency API | .NET 10, C# 13 | Core exchange-rate REST API (Frankfurter-backed) |
| AI Agent API (bonus) | .NET 10, Microsoft.Extensions.AI | LLM-powered agent that calls the Currency API as a tool |
| Chat UI (bonus) | Next.js 14, TypeScript | Streaming chat interface with Keycloak login |
| Keycloak | Keycloak 25 | Identity provider (pre-configured realm, runs locally via Docker) |
# Build & start all services (including UI, AI agent, observability stack)
./Start-FullStack.ps1 # uses docker-compose.full-stack.yml
# Or with local Ollama LLM (no external AI provider needed)
./Start-FullStack-Ollama.ps1 # uses docker-compose.ollama.ymlNote on local AI model: The Ollama stack uses
lfm2.5-thinkingby default — a small, lightweight model chosen for easy local demo setup. Because of its limited size it can occasionally produce inaccurate or incomplete responses. For production-quality answers, configure an external provider (OpenAIorGemini) via theAI__Providerenvironment variable.
./Start-Deps.ps1 # uses docker-compose.deps.yml
# Then run each service individually with dotnet run / yarn dev| Service | URL |
|---|---|
| Chat UI | http://localhost:3000 |
| Currency API (Swagger) | http://localhost:9081/swagger |
| AI Agent API (Swagger) | http://localhost:9082/swagger |
| Keycloak Admin | http://localhost:8900 — admin / admin |
| Grafana | http://localhost:3001 |
| Jaeger | http://localhost:16686 |
| Prometheus | http://localhost:9090 |
| OpenSearch Dashboards | http://localhost:5601 |
Authentication is handled by Keycloak (running locally via Docker). The realm is auto-imported on container start — no manual setup required. In production, Keycloak can be swapped for any OIDC-compliant provider (Azure AD, Auth0, Okta, etc.) by updating the authority URL in configuration.
| User | Password | Roles |
|---|---|---|
admin@currencyconverter.com |
Admin1234! |
currency:read, currency:admin, ai:chat |
guest@currencyconverter.com |
Guest1234! |
currency:read, ai:chat |
After logging in via the Chat UI, click the "Copy Token" button to copy your JWT access token. Paste it as a Bearer token in Swagger, Postman, or any HTTP client to interact with the Currency API directly.
| Area | Technology |
|---|---|
| Runtime | .NET 10, C# 13 |
| Web framework | ASP.NET Core 10 |
| AI orchestration | Microsoft.Extensions.AI |
| Frontend | Next.js 14, TypeScript, Tailwind CSS |
| Auth | Keycloak 25, NextAuth, JWT / RBAC |
| Messaging | MediatR (CQRS) |
| Caching | Redis |
| Resilience | Microsoft.Extensions.Http.Resilience (retry + circuit breaker) |
| Validation | FluentValidation |
| Logging | Serilog → OpenSearch |
| Tracing | OpenTelemetry → Jaeger |
| Metrics | Prometheus → Grafana |
| Containerisation | Docker, Docker Compose |
The Currency API is built around DDD principles:
- Value objects —
Currency,Amount,ExchangeDateare immutable, self-validating, and encapsulate domain rules. - Domain policies —
CurrencyPolicyBehavior(MediatR pipeline) enforces forbidden currencies (TRY, PLN, THB, MXN) at the application boundary. - Trading calendar —
TradingCalendarencapsulates business-day logic.
| Pattern | Where |
|---|---|
| Clean Architecture | All .NET services |
| CQRS | Application layer via MediatR |
| Factory | IExchangeRateProviderFactory — selects provider per request |
| Decorator | Redis caching wraps the exchange-rate provider (Scrutor) |
| Pipeline Behaviour | Validation and currency policy via MediatR behaviours |
| Result type | ErrorOr<T> — no exception-driven control flow |
| Action Result Builder | Per-error-type HTTP response mapping |
WebApi → Application → Domain
↓
Infrastructure → Frankfurter.ApiClient
↓
Redis (cache)
Each layer has its own src/ and tests/ project. Infrastructure depends only on abstractions defined in Domain, keeping the core fully testable without external dependencies.
WebApi → Application → Domain
↓
Infrastructure → Microsoft.Extensions.AI (LLM)
↓ ↓
Redis CurrencyConverterPlugin
(chat history) (AI tool → Currency API)
The AI Agent API follows the same Clean Architecture layering as the Currency API. Its Infrastructure layer wires up two AI tools that the LLM can invoke during a conversation:
CurrencyConverterPlugin— calls the Currency API using the publishedClientNuGet package (ICurrencyConverterClient), giving the LLM access to latest rates, conversion, and historical data without duplicating any HTTP logic.DatePlugin— supplies the LLM with the current date, enabling accurate reasoning about historical queries.
Conversation history is persisted per-session in Redis so the agent maintains context across multiple turns. The LLM provider is swappable at runtime via AI__Provider (supports Ollama, OpenAI, and Gemini).
- Multi-environment config —
appsettings.{Environment}.json+ environment variable overrides; works across Dev, Test, and Prod. - Horizontal scaling — stateless services; shared Redis for cache and chat history.
- API versioning — routes follow
/api/v{version}/exchange-rate/.... - Rate limiting — built-in ASP.NET Core rate limiter on all endpoints.
- Observability — structured logs (Serilog/OpenSearch), distributed traces (OpenTelemetry/Jaeger), and metrics (Prometheus/Grafana) wired out of the box.
- Request logging — client IP, JWT
ClientId, HTTP method, endpoint, response code, and response time logged per request. - Resilience — exponential-backoff retry + circuit breaker on all outbound HTTP calls to Frankfurter.
- 90%+ test coverage — unit tests per layer plus integration tests (requires Redis).
Four workflows run on pull requests and pushes to develop / main:
| Workflow | Triggers on | Jobs |
|---|---|---|
| Backend · Currency Converter | Changes under Practice.Backend.CurrencyConverter/ |
Build → Unit tests (per layer, with coverage) → Integration tests (Redis service) → Publish Client + Messages NuGet packages (pre-release on develop, stable on main + GitHub Release) |
| Chatbot · Currency Converter | Changes under Practice.Chatbot.CurrencyConverter/ |
Build → Unit tests → Integration tests |
| Web · Currency Converter | Changes under Practice.Web.CurrencyConverter/ |
Install → Lint → Build |
| Root · Infra | Changes to root *.md, *.ps1, docker-compose*.yml, keycloak/, docs/, or the workflow file itself |
No-op acknowledgement (infra-only change, no service build needed) |
Coverage summaries are posted to the GitHub Actions step summary on every run — open any workflow run, select the relevant job, and scroll to the bottom of the summary to see per-layer coverage reports. Concurrent runs on the same branch are cancelled automatically.
# Currency API — all tests
cd Practice.Backend.CurrencyConverter
dotnet test
# Integration tests (requires RunDepsOnly.ps1 first)
dotnet test tests/Integration.Tests
# With coverage report
dotnet test --collect:"XPlat Code Coverage"Test coverage reports are also available in CI without any local setup — open the relevant GitHub Actions workflow run, go to the job summary, and the per-layer coverage breakdown is listed at the bottom of the page.
- Frankfurter is the single exchange-rate source; the factory pattern allows adding providers without changing existing code.
- Forbidden currencies (TRY, PLN, THB, MXN) are enforced for both conversion and latest-rate requests.
- Historical rate pagination is computed server-side: business days for the full requested range are calculated locally, then only the date window for the requested page is forwarded to Frankfurter — avoiding over-fetching.
- Keycloak is the identity provider for demo purposes only and is fully replaceable.
| Service | Method | Route | Required Role |
|---|---|---|---|
| Currency API | GET |
/api/v1/exchange-rate/latest |
currency:read |
| Currency API | GET |
/api/v1/exchange-rate/conversion |
currency:read |
| Currency API | GET |
/api/v1/exchange-rate/historical |
currency:admin |
| AI Agent API | POST |
/api/v1/chat/message |
ai:chat |
| AI Agent API | GET |
/api/v1/chat/{conversationId}/history |
ai:chat |
The currency:admin role is intentionally restricted to historical rates as this endpoint exposes broader data access (paginated multi-day ranges). Both demo users have currency:read and ai:chat; only admin@currencyconverter.com has currency:admin.
- Plug in additional exchange-rate providers (e.g., Open Exchange Rates) via the existing factory.
- AI-powered exchange rate insights — extend the AI agent with a forecasting tool that analyses historical rate trends and provides conversion timing recommendations (e.g. "EUR/USD has trended down 2% this week — you may get a better rate by waiting").



