My portfolio service for metered API usage, written in Go with PostgreSQL. It accepts usage events, freezes their unit price, and records charges asynchronously. This is an educational project: no real customers, invoices, taxes, or payments.
Author: Vladimir Krauchuk (@krav01).
- Idempotency: an event ID identifies one immutable input, not one HTTP attempt.
- Acceptance transaction: the usage event and its work item commit together.
- A concurrent worker queue built with PostgreSQL row locks and
SKIP LOCKED. - An immutable ledger entry per event, committed with queue removal.
- Integer pricing, overflow checks, and exact decimal-string aggregate totals.
- Strict JSON validation, a trusted-producer bearer token, bounded requests, and graceful shutdown.
- Bounded queue admission, worker retry backoff, and verified database-crash recovery.
- Durable event quarantine after three integrity failures, with generation-checked manual recovery.
- Full-service load measurements and optional local monitoring with tested alerts.
The database effects are idempotent; this is not a claim of exactly-once network delivery. The design notes explain transaction boundaries and deliberate limits. See verification evidence and reproducible benchmarks for measured scope and limitations. For a guided walkthrough, see the demo script and v0.3.0 release notes and the existing-demo upgrade procedure.
Prerequisites: Docker with Compose, make, and OpenSSL. Run from this repository:
export POSTGRES_PASSWORD=$(openssl rand -hex 24)
export BILLING_API_TOKEN=$(openssl rand -hex 32)
make upCompose uses its own database and volume. The HTTP API is bound to 127.0.0.1:8080
and PostgreSQL to 127.0.0.1:54329. Keep the generated password if restarting an
existing volume: PostgreSQL initialization does not change its stored password.
The stack uses a normal Docker bridge so these localhost port mappings work;
it does not block outbound network access from containers.
Never point this demo's migration command at an existing or production database.
curl --fail-with-body http://127.0.0.1:8080/healthz
curl --fail-with-body http://127.0.0.1:8080/readyz
curl --fail-with-body -i http://127.0.0.1:8080/v1/events \
-H "Authorization: Bearer $BILLING_API_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"event_id":"demo_001","customer_id":"demo_customer","meter":"api_calls","units":7}'The first request returns 202. Repeat the exact request for 200 and the
original frozen price; use the same ID with different units for 409.
curl --fail-with-body http://127.0.0.1:8080/v1/events/demo_001 \
-H "Authorization: Bearer $BILLING_API_TOKEN"
curl --fail-with-body http://127.0.0.1:8080/v1/customers/demo_customer/summary \
-H "Authorization: Bearer $BILLING_API_TOKEN"
make downSummary amounts and units include only processed events. Pending, failed, and processed
counts are separate, so a freshly accepted event need not appear in the amount
yet. The default rate is 1000 micro-USD per call: seven calls produce "7000"
micro-USD after processing. Aggregate amounts/units are decimal strings so JSON
clients do not silently round large totals; individual event integers require
an int64-capable JSON decoder for full precision.
make down stops the demo without deleting its database volume. Deleting that
volume or applying a down migration destroys demo data; neither is part of the
normal startup path.
| Method | Path | Purpose |
|---|---|---|
| POST | /v1/events |
Accept new usage or replay an earlier event |
| GET | /v1/events/{event_id} |
Return frozen pricing and processing state |
| POST | /v1/events/{event_id}/retry |
Reactivate a failed generation without repricing |
| GET | /v1/customers/{customer_id}/summary |
Exact processed totals and queue counts |
| GET | /healthz |
Process liveness, public |
| GET | /readyz |
Bounded database reachability check, public |
| GET | /metrics |
Fixed-label HTTP, queue, and worker metrics, authenticated |
Business endpoints and metrics require a bearer token. This token represents
one trusted internal producer, not tenant-level authorization. IDs contain
1–64 ASCII letters, digits, underscores, or hyphens. The only meter is api_calls.
Units must be a positive int64 and the accepted price multiplication must fit in
int64. JSON bodies are limited to 16 KiB, with unknown or duplicate fields rejected.
See OpenAPI for request and response schemas and Operations for metric semantics and example alerts. See Failure and recovery verification for the CI database-crash scenario and its limits. See backup/restore verification for full-row comparisons and processing after restore. See event recovery for quarantine, retry preconditions, and migration limits. An optional local Prometheus/Grafana demo includes a provisioned dashboard and tested alerts.
| Variable | Default | Meaning |
|---|---|---|
DATABASE_URL |
required | New demo database connection, never logged |
BILLING_API_TOKEN |
required | 32–4096 printable ASCII bytes, no whitespace; generate randomly |
BILLING_RATE_MICROS |
1000 |
Positive integer micro-USD per API call |
BILLING_MAX_PENDING_EVENTS |
10000 |
Maximum unfinished events (pending + failed), 1–1000000; shared across API instances |
HTTP_ADDR |
127.0.0.1:8080 |
Listener; Compose binds inside its container |
WORKER_INTERVAL |
100ms |
Polling delay, 10ms–1m |
WORKER_BATCH |
100 |
Batch size, 1–1000 |
The pool is capped at eight connections. Connection setup, database statements, HTTP requests, and graceful shutdown have time bounds. Changing the configured rate affects new events only. Startup does not automatically apply SQL migrations. Use the dedicated one-shot migration service in Compose.
Go 1.27.1 is the pinned toolchain for CI and both Docker builds. pgx is the only direct runtime dependency; HTTP routing, logging, JSON, metrics, and tests use the standard library.
make test
make vet
make lint
make vulnIntegration tests are separate and must target a disposable migrated database:
export TEST_DATABASE_URL="postgres://billing_demo:${POSTGRES_PASSWORD}@127.0.0.1:54329/usage_billing?sslmode=disable"
make integrationThe integration command fails when its database configuration is absent; it does not silently count skipped database tests as a successful integration run. CI provisions its own PostgreSQL service and applies versioned migrations before testing.
Tests cover new/replayed/conflicting events, concurrent acceptance and processing, frozen prices, integer boundaries, authentication, request validation, and cancellation. Passing tests are evidence of the exercised cases, not proof of financial correctness.
This repository does not operate a payment provider, spend funds, or modify Sunday System. No performance figures are claimed without recorded measurements. Production work would include per-tenant authorization, TLS termination, rate limits and per-tenant quotas, separate least-privilege database roles, backups and restore drills, retention, invoice/refund rules, and a security review. The demo token grants access to every demo customer; never expose this setup publicly.