Go to file
2025-09-06 17:16:03 +07:00
app Add app layer tests 2025-09-06 17:16:00 +07:00
cmd/converter Add executable 2025-09-06 17:16:01 +07:00
domain Add service test 2025-09-06 16:09:10 +07:00
infrastructure/coinmarketcap Implement RateProvider adapter 2025-09-06 17:13:48 +07:00
go.mod Implement RateProvider adapter 2025-09-06 17:13:48 +07:00
go.sum Implement RateProvider adapter 2025-09-06 17:13:48 +07:00
README.md Add README.md 2025-09-06 17:16:03 +07:00
TASK.md Add task definition 2025-09-06 11:34:07 +07:00
Taskfile.yml Add taskfile 2025-09-06 17:16:02 +07:00

Currency Converter CLI

A command-line utility for converting currencies using CoinMarketCap API, built with Go following Clean Architecture principles.

Quick Start

Prerequisites

Installation & Setup

  1. Clone the repository
  2. Set your API key:
    export CMC_API_KEY=your_api_key_here
    

Usage

# 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-mocks to 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.3 in 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/decimal is 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.