| app | ||
| cmd/converter | ||
| domain | ||
| infrastructure/coinmarketcap | ||
| go.mod | ||
| go.sum | ||
| README.md | ||
| TASK.md | ||
| Taskfile.yml | ||
Currency Converter CLI
A command-line utility for converting currencies using CoinMarketCap API, built with Go following Clean Architecture principles.
Quick Start
Prerequisites
- Go 1.19 or higher
- CoinMarketCap API key (get from https://coinmarketcap.com/api/)
Installation & Setup
- Clone the repository
- Set your API key:
export CMC_API_KEY=your_api_key_here
Usage
Using Taskfile (Recommended)
# Install task runner (if not already installed)
go install github.com/go-task/task/v3/cmd/task@latest
# Build and run with parameters
task run -- 123.45 USD BTC
task run -- 1 BTC USD
task run -- 100 EUR BTC
# Other available tasks
task build # Build the application
task test # Run tests
task test-verbose # Run tests with verbose output
task clean # Clean build artifacts
task all # Full build pipeline
Using Go directly
# Build the application
go build -o bin/converter ./cmd/converter
# Run conversions
./bin/converter 123.45 USD BTC
./bin/converter 1 BTC USD
./bin/converter 100 EUR BTC
Using Go commands directly
# Run tests
go test ./...
# Build manually
mkdir -p bin && go build -o bin/converter ./cmd/converter
# Run with API key
CMC_API_KEY=your_key ./bin/converter 50 USD BTC
Project Structure
converter/
├── cmd/converter/ # CLI application entry point
│ └── main.go # Main function, argument parsing, DI setup
├── app/ # Application layer (Use Cases)
│ ├── convert_currency.go # Core use case implementation
│ ├── convert_currency_test.go # Use case tests with mocks
│ └── mocks/ # Generated mocks (mockery)
│ └── RateProvider.go # Mock for RateProvider interface
├── domain/ # Domain layer (Business Logic)
│ ├── entities.go # Value objects (Currency, Money, Rate)
│ ├── entities_test.go # Domain entity tests
│ ├── services.go # Domain service (CurrencyConverter)
│ └── services_test.go # Domain service tests
├── infrastructure/ # Infrastructure layer (External APIs)
│ └── coinmarketcap/ # CoinMarketCap API client
│ ├── client.go # HTTP client with retry/fallback logic
│ └── models.go # API response DTOs
├── Taskfile.yml # Task runner configuration
├── go.mod # Go module definition
├── go.sum # Go module checksums
├── TASK.md # Original task requirements (Russian)
├── CLAUDE.md # Project configuration for Claude Code
└── README.md # This file
Architecture Overview
This project implements Clean Architecture with clear separation of concerns:
-
Domain Layer: Pure business logic with no external dependencies
- Value objects (Currency, Money, Rate) with validation
- Domain services (CurrencyConverter) for business operations
- Domain errors and business rules
-
Application Layer: Use cases that orchestrate domain logic
- ConvertCurrencyUseCase: Main conversion workflow
- RateProvider interface: Defines contract for rate fetching
- DTO to domain object mapping with reversal detection
-
Infrastructure Layer: External integrations and technical details
- CoinMarketCap API client with automatic retry logic
- HTTP error handling and response mapping
- Rate reversal logic (USD→BTC becomes BTC→USD inverted)
-
CLI Layer: User interface and dependency injection
- Command-line parsing and validation
- Environment variable configuration
- Error formatting for end users
Testing
The project includes comprehensive test coverage using testify and mockery:
# Run all tests
task test
# or
go test ./...
# Run tests with verbose output
task test-verbose
# or
go test -v ./...
# Run tests with coverage
task test-coverage
# or
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
Test Structure
- Unit Tests: Domain entities and services with table-driven tests
- Use Case Tests: Application layer with generated mocks using testify/mockery
- Mock Generation: Use
task generate-mocksto regenerate mocks
Architecture Decisions
Why decimal.Decimal for Money Handling
We chose shopspring/decimal over floating-point numbers and other alternatives for the following critical reasons:
Financial Accuracy Requirements
- Floating point arithmetic cannot represent decimal numbers exactly
- Example:
0.1 + 0.2 ≠ 0.3in floating-point arithmetic - Currency conversion errors compound with multiple operations
- CoinMarketCap API returns exchange rates with high precision (8+ decimal places)
Real-world Impact
// Problematic with float64
Bad: float64(123.45) * float64(0.000016) = 0.001975200000000001
// Accurate with decimal.Decimal
Good: decimal(123.45) * decimal(0.000016) = 0.001975200000000000
Industry Standards
- Banking and financial systems never use floating-point for monetary calculations
- Major payment APIs (Stripe, PayPal) use string representations to avoid precision loss
shopspring/decimalis the Go standard for financial applications (used by Kubernetes billing, trading systems)
Alternative Analysis
- big.Rat: Overkill for this use case, overly complex API for basic currency operations
- Integer scaling: Requires manual precision management, error-prone across currencies with different decimal places (USD=2, BTC=8, ETH=18)
- float64: Unacceptable precision loss for financial data
Performance vs Precision Trade-off
- Using integer scaling would significantly increase development complexity with manual precision management across different currency types
- decimal.Decimal adds only microseconds vs potentially costly conversion errors
- Ensures user trust by avoiding strange rounding artifacts
- Better maintainability with clear intent and JSON-friendly serialization
Conclusion: The cost of precision errors in financial software far exceeds the minimal performance overhead of using decimal.Decimal.
Currency Handling Strategy
Our implementation uses CoinMarketCap's /v1/cryptocurrency/quotes/latest endpoint with intelligent fallback logic:
- Direct request: Try the requested conversion (e.g.,
USD → BTC) - Fallback on empty data: If no data returned, try reverse direction (
BTC → USD) and invert the rate - Error handling: Distinguish between unknown symbols, invalid parameters, and network issues
This approach handles both crypto-to-fiat and fiat-to-crypto conversions using only the cryptocurrency endpoint:
- BTC → USD: Direct API call works
- USD → BTC: API call fails (USD not in crypto DB), retry as BTC → USD, then invert rate
The automatic fallback with rate inversion eliminates the need for separate fiat currency handling while maintaining mathematical precision using decimal arithmetic.
Currency Precision Strategy
To keep the implementation simple, we use a uniform precision rule:
- All currencies: 8 decimal places (covers cryptocurrency precision requirements)
This approach handles high-precision cryptocurrencies properly while being acceptable for fiat currencies (even though they typically use 2 decimal places). In a production environment, we would maintain per-currency precision metadata, but 8 decimals covers all practical use cases without overcomplicating the initial implementation.
Development
This project follows Clean Architecture principles and SOLID design patterns as specified in the requirements.