{"id":50330264,"url":"https://github.com/tarek-bohdima/exchangerates","last_synced_at":"2026-05-29T09:02:31.335Z","repository":{"id":179742851,"uuid":"491628156","full_name":"Tarek-Bohdima/ExchangeRates","owner":"Tarek-Bohdima","description":"Currency-exchange Android app rebuilt as an interview-prep showcase for graph algorithms (BFS / Dijkstra / Bellman-Ford / Floyd-Warshall + arbitrage detection), Union-Find reachability, and Gang of Four design patterns — Kotlin 2.1, Jetpack Compose Material 3, Hilt.","archived":false,"fork":false,"pushed_at":"2026-05-11T03:21:09.000Z","size":680,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-05-11T04:32:53.191Z","etag":null,"topics":["algorithms","android","bellman-ford","clean-architecture","coroutines","data-structures","design-patterns","dijkstra","exchange-rates","floyd-warshall","graph-algorithms","hilt","interview-preparation","jetpack-compose","kotlin","ksp","material3","mvvm-architecture","retrofit2","union-find"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Tarek-Bohdima.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2022-05-12T18:34:41.000Z","updated_at":"2026-05-11T03:21:12.000Z","dependencies_parsed_at":null,"dependency_job_id":"3733c6bf-0627-4592-8504-02d2c17426e3","html_url":"https://github.com/Tarek-Bohdima/ExchangeRates","commit_stats":null,"previous_names":["tarek-bohdima/exchangerates"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/Tarek-Bohdima/ExchangeRates","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Tarek-Bohdima%2FExchangeRates","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Tarek-Bohdima%2FExchangeRates/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Tarek-Bohdima%2FExchangeRates/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Tarek-Bohdima%2FExchangeRates/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Tarek-Bohdima","download_url":"https://codeload.github.com/Tarek-Bohdima/ExchangeRates/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Tarek-Bohdima%2FExchangeRates/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33644313,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-29T02:00:06.066Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["algorithms","android","bellman-ford","clean-architecture","coroutines","data-structures","design-patterns","dijkstra","exchange-rates","floyd-warshall","graph-algorithms","hilt","interview-preparation","jetpack-compose","kotlin","ksp","material3","mvvm-architecture","retrofit2","union-find"],"created_at":"2026-05-29T09:02:23.662Z","updated_at":"2026-05-29T09:02:31.324Z","avatar_url":"https://github.com/Tarek-Bohdima.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Exchange Rates\n\n\u003e A single-screen Android app rebuilt as a teaching reference for graph algorithms, classic data structures, and Gang of Four design patterns. It happens to convert currencies because the rate table is a natural graph.\n\nNot a production app. No auth, no analytics. A live backend (Frankfurter, https://www.frankfurter.dev/) *is* wired up but isn't the default — the embedded rate dataset is hand-curated and intentionally contains an arbitrage triangle so Bellman-Ford has something to find on first launch.\n\nThe intended audience is a candidate studying for an interview, or an interviewer who wants something concrete to pick apart. Every algorithm carries step-by-step pedagogical comments. Every Gang of Four pattern is wired live; nothing is in the repo \"for show\".\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/screenshots/01-initial.png\" alt=\"Initial state: input card, algorithm picker, arbitrage card, reachable-currencies card\" width=\"320\" /\u003e\n  \u0026nbsp;\u0026nbsp;\n  \u003cimg src=\"docs/screenshots/02-result.png\" alt=\"After Convert: result card with composite rate, hop count, algorithm used, and a USD → AUD path breadcrumb\" width=\"320\" /\u003e\n\u003c/p\u003e\n\nThe first screenshot is the cold-start state — Dijkstra is the default strategy, the rate-graph has already been folded by Bellman-Ford into the arbitrage card (`EUR → GBP → USD → EUR, +1.190%`), and Union-Find tells the UI which currencies are reachable from USD. The second shows the result card after tapping **Convert**: composite rate, hop count, the algorithm used, and a breadcrumb of the actual path the strategy picked.\n\n---\n\n## What's interesting about it\n\n**Five interchangeable path-finding strategies**, hot-swappable from the UI:\n\n| Strategy | Algorithm | Complexity | What it optimises |\n|---|---|---|---|\n| `DirectLookupStrategy` | hash probe | **O(1)** | direct edge if one exists — fastest possible |\n| `BfsPathFindingStrategy` | breadth-first search | **O(V + E)** | fewest conversions (hops) |\n| `DijkstraPathFindingStrategy` | max-product Dijkstra | **O((V + E) log V)** | best composite rate (no-arbitrage graphs) |\n| `BellmanFordPathFindingStrategy` | Bellman-Ford on `−ln(rate)` | **O(V · E)** | best rate + detects arbitrage |\n| `FloydWarshallPathFindingStrategy` | all-pairs DP | **O(V³)** once, then **O(1)** | all-pairs precompute, cached per graph |\n\nSwitching strategies in the UI lets you see how each one picks a different path through the same graph (BFS optimises hops, Dijkstra/Bellman-Ford/Floyd-Warshall agree on the best *rate*).\n\n**Other algorithmic pieces:**\n\n- **`UnionFind\u003cT\u003e`** with union-by-rank + iterative path compression — powers the \"reachable from X\" card. Amortised α(n) per operation.\n- **`DetectArbitrageUseCase`** — full Bellman-Ford from every vertex, traces the negative cycle, canonicalises rotations so duplicates aren't reported, filters FP noise via a configurable epsilon.\n- **`LruCache\u003cK, V\u003e`** — the classic `LinkedHashMap(accessOrder = true)` + `removeEldestEntry` override trick.\n\n---\n\n## Gang of Four patterns, with addresses\n\n| Pattern | Where to look |\n|---|---|\n| **Strategy** | `core/algorithm/PathFindingStrategy.kt` + five implementations |\n| **Factory** | `core/algorithm/StrategyFactory.kt` — interface, `DefaultStrategyFactory` impl wired via Hilt-multibound `Map\u003cStrategyKind, PathFindingStrategy\u003e` |\n| **Template Method** | `core/algorithm/AbstractPathFindingStrategy.kt` — guards `findPath`, subclasses implement `search` |\n| **Chain of Responsibility** | `core/algorithm/StrategyChain.kt` — first non-null wins (Direct → BFS → Dijkstra → Bellman-Ford) |\n| **Builder** | `core/ds/CurrencyGraph.Builder` — fluent, validating, single-shot, returns an immutable graph |\n| **Decorator** | `data/repository/CachingExchangeRatesRepository.kt` — wraps `DefaultExchangeRatesRepository`, wired live in `DataModule` |\n| **Adapter** | `data/remote/dto/ExchangeRatesDto.toDomain()` — DTO ↔ domain bridge, keeps Moshi annotations off domain models |\n| **Facade** | `domain/usecase/ConversionFacade.kt` — interface; `DefaultConversionFacade` is the impl. Single seam between ViewModel and the use-case subsystem |\n| **Singleton** | Hilt `@Singleton` scopes throughout `di/` |\n| **Observer** | Repository → ViewModel via `StateFlow`/`Flow` |\n| **State** | `ui/conversion/ConversionUiState` sealed interface — exhaustive `when` in Compose |\n\n---\n\n## Architecture\n\n```\nui/                  ── Compose Material 3, one screen\n  conversion/        ── ConversionScreen, ConversionViewModel, sealed UiState\n  theme/             ── M3 colors + typography + dynamic colour\n       │\n       ▼\ndomain/              ── Pure Kotlin — no Android, no framework\n  model/             ── Currency (value class), Money (BigDecimal), ExchangeRate, ConversionPath, ConversionResult, ArbitrageOpportunity\n  usecase/           ── ConvertCurrencyUseCase, DetectArbitrageUseCase, GetReachableCurrenciesUseCase, ConversionFacade\n       │\n       ▼\ndata/                ── Repository + data sources\n  repository/        ── ExchangeRatesRepository (interface), Default + Caching (Decorator)\n  source/            ── ExchangeRatesDataSource: Embedded (default) or Remote (Retrofit)\n  remote/            ── Retrofit API + Moshi DTOs (one-line swap to go live)\n       │\n       ▼\ncore/                ── Reusable building blocks\n  algorithm/         ── Path-finding strategies (Strategy + Template Method + Chain of Responsibility)\n  ds/                ── CurrencyGraph, UnionFind, LruCache\n  concurrent/        ── DispatcherProvider indirection (testable coroutines)\n\ndi/                  ── Hilt modules: Algorithm (multi-bind), Data, Network, Coroutine\n```\n\nLaw of Demeter is taken seriously: the ViewModel knows about `ConversionFacade` and nothing else; use cases know about their direct collaborators (`StrategyFactory`, `CurrencyGraph`) and nothing about who owns them.\n\n---\n\n## Stack\n\n| | |\n|---|---|\n| Language | Kotlin 2.1.0 |\n| Build | Gradle 8.10.2, AGP 8.7.3, KSP 2.1.0-1.0.29, version catalog (`gradle/libs.versions.toml`) |\n| JVM | Java 17 |\n| Android | `compileSdk` 35, `minSdk` 24, `targetSdk` 35 |\n| UI | Jetpack Compose (BOM 2024.12) + Material 3, dynamic colour on API 31+ |\n| DI | Hilt 2.53.1 (KSP, no kapt) |\n| Async | Coroutines 1.10.1 + StateFlow |\n| Network | Retrofit 2.11 + Moshi 1.15 codegen + OkHttp logging; backend is [Frankfurter](https://www.frankfurter.dev/) (free, no API key, ECB-backed). Only used when `RemoteExchangeRatesDataSource` is bound. |\n| Test | JUnit 4, Turbine, MockK, kotlinx-coroutines-test |\n\n---\n\n## Build \u0026 run\n\n```bash\n# Debug APK\n./gradlew :app:assembleDebug\n\n# Install on a running emulator/device\n./gradlew :app:installDebug\n\n# Unit tests (algorithm parity, Union-Find, arbitrage detection)\n./gradlew :app:testDebugUnitTest\n\n# One specific test\n./gradlew :app:testDebugUnitTest \\\n  --tests \"com.tarek.exchangerates.core.algorithm.PathFindingStrategyTest.dijkstra*\"\n\n# Lint\n./gradlew :app:lint\n```\n\nTo run against the live [Frankfurter](https://www.frankfurter.dev/) API instead of the embedded dataset, change one binding in `di/DataModule.kt`:\n\n```kotlin\nabstract fun bindDataSource(impl: EmbeddedExchangeRatesDataSource): ExchangeRatesDataSource\n//                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ flip to RemoteExchangeRatesDataSource\n```\n\nRepository, use cases, ViewModel, and UI need no changes — that's the payoff of coding to the interface.\n\nA heads-up about the trade-off: Frankfurter is ECB reference data, so it's strictly arbitrage-free and quotes one base against ~30 currencies, which produces a hub-and-spoke graph (every conversion is 1 or 2 hops via USD). The arbitrage card disappears and Dijkstra/Bellman-Ford/Floyd-Warshall all collapse to roughly the same answer. Useful for proving the swap works; less useful for showcasing what the algorithms can do — which is why embedded is the default.\n\n---\n\n## About the data\n\n`EmbeddedExchangeRatesDataSource` ships with ~30 edges across 12 currencies. Most reciprocal pairs are tuned to round-trip within 0.1% of 1.0 (no arbitrage). **One triangle is deliberately broken:**\n\n```\nUSD → EUR  ×  EUR → GBP  ×  GBP → USD\n0.9250  ×  0.8580  ×  1.2750  ≈  1.0119\n```\n\nThat's a ~1.19% loop — well above the 0.1% epsilon — so `DetectArbitrageUseCase` flags it as `USD → EUR → GBP → USD` on first launch. Touch those numbers and you change what the arbitrage card shows.\n\n---\n\n## What this app is *not*\n\n- Not a real-time FX trading tool. Don't trade on the embedded rates.\n- Not a complete production reference. It deliberately skips analytics, crash reporting, persistence, navigation, and error retries — those would dilute the algorithmic content.\n- Not a perfect Dijkstra. The max-product variant requires an arbitrage-free graph; the file's docblock explains exactly when and why. Bellman-Ford is the safe choice on the general graph.\n\nFor a deeper dive into where each pattern lives and how the modules wire together, see [`CLAUDE.md`](./CLAUDE.md).\n\n---\n\n## License\n\n[MIT](./LICENSE) © 2026 Tarek Bohdima.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftarek-bohdima%2Fexchangerates","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftarek-bohdima%2Fexchangerates","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftarek-bohdima%2Fexchangerates/lists"}