Global Billing Service
Production-ready billing microservice powering multi-currency payments
The Problem
SaaS platforms need billing systems that handle subscription lifecycle, multi-currency payments, and invoice generation, but most solutions are either too simple or too complex. This microservice hits the sweet spot: a clean, production-ready billing engine that supports 39 currencies, full subscription management, and Stripe integration out of the box.
Architecture
Technical Decisions
Why FastAPI over Flask?
FastAPI gives me automatic request validation via Pydantic, async support out of the box, and auto-generated OpenAPI docs. For a billing service where every request must be validated strictly (you don't want a malformed currency code hitting Stripe), Pydantic models are non-negotiable. Flask would have meant writing all that validation manually.
How do you handle 39 currencies correctly?
Each currency has a minimum charge amount (e.g., $0.50 USD, ¥50 JPY). I built a validation layer that checks every payment against currency-specific minimums before it ever reaches Stripe. Because charging someone in Japanese yen the way you'd charge in US dollars is a good way to lose money.
Why idempotent payment operations?
Network failures happen. If a client retries a payment request, you can't charge them twice. Every payment operation uses an idempotency key. If Stripe sees the same key twice, it returns the original result instead of processing a new charge. This is table-stakes for production payment systems but something most portfolio projects skip entirely.
How is the subscription lifecycle managed?
Subscriptions move through states: created → active → upgraded/downgraded → canceled → reactivated. Each transition has webhook-driven status updates from Stripe, so the system is always in sync with Stripe's state. No polling, no cron jobs. Just real-time webhooks.
Code Spotlight
CURRENCY_MINIMUMS: dict[str, int] = {
"usd": 50, "eur": 50, "gbp": 30,
"jpy": 50, "cad": 50, "aud": 50,
# ... 39 currencies total
}
def validate_charge_amount(
amount: int, currency: str
) -> None:
"""Validate charge meets currency minimum."""
currency = currency.lower()
if currency not in CURRENCY_MINIMUMS:
raise InvalidCurrencyError(
f"Unsupported currency: {currency}"
)
minimum = CURRENCY_MINIMUMS[currency]
if amount < minimum:
raise AmountTooSmallError(
f"Minimum for {currency.upper()}: "
f"{minimum} (got {amount})"
)Every payment hits this validation before touching Stripe. Currency-specific minimums prevent failed charges and protect revenue integrity.
Key Features
Payment Processing
- ▹39 currencies with per-currency minimum validation
- ▹Idempotent payment operations via Stripe idempotency keys
- ▹Stripe Checkout session integration
- ▹Webhook-driven real-time payment status updates
Subscription Management
- ▹Full lifecycle: create → upgrade → downgrade → cancel → reactivate
- ▹Automated invoice generation with sequential numbering (INV-YYYYMM-XXXX)
- ▹Plan comparison and prorated billing support
Infrastructure
- ▹API Key Authentication with JWT
- ▹Health monitoring endpoint
- ▹Structured JSON logging with structlog
- ▹CORS configuration for cross-origin access
Testing
27 tests across 5 categories covering every critical path in the billing system.
27 tests across 5 categories
- ▹SQLite in-memory database for fast, isolated test runs
- ▹Mocked Stripe API for deterministic tests without real API calls
- ▹Separate test categories: Customers (7), Plans (5), Subscriptions (6), Invoices & Payments (6), Currencies & Health (3)
CI/CD Pipeline
Results
39 currencies supported with proper minimum charge validation
27 tests with full mocked Stripe coverage
Automated invoice generation with sequential numbering
Production-ready Docker deployment on AWS ECS Fargate