Numeric Format Choices for Amount Representation
Reexamine numeric format choices for go-luca's amount representation.
go-luca currently uses int64 in smallest currency unit with Account.Exponent (TigerBeetle-inspired). shopspring/decimal is used for display and interest calculations. This research evaluates whether the format choices remain optimal by comparing all major Go decimal/money libraries across performance, precision, database compatibility, and ecosystem support.
Key questions:
- Is shopspring/decimal still the right choice for calculations, or is govalues/decimal better?
- How do the libraries integrate with pgx for PostgreSQL NUMERIC columns?
- How do they round-trip through pglike's SQLite backend (where NUMERIC currently becomes REAL)?
- Should pglike translate NUMERIC to TEXT instead of REAL to preserve precision?
- Does the int64+exponent core model remain sound vs arbitrary-precision alternatives?
Go Decimal/Money Library Comparison
| Library | Stars | Internal Type | Precision | Immutable | pgx Adapter | SQLite/pglike | License | Status |
|---|---|---|---|---|---|---|---|---|
| shopspring/decimal | ~7,300 | big.Int + int32 exp | Arbitrary | Yes | Official (jackc/pgx-shopspring-decimal) | TEXT via Valuer/Scanner | MIT | Slow maintenance |
| cockroachdb/apd | ~780 | BigInt + exp + Context | Arbitrary (configurable) | No | None official | TEXT via Valuer/Scanner | Apache-2.0 | Stable, low-freq |
| alpacahq/alpacadecimal | ~53 | int64 fixed 12dp (fallback: shopspring) | 12 digits / +/-9.2M | Yes | Via shopspring adapter | TEXT via Valuer/Scanner | MIT | Niche, low activity |
| ericlagergren/decimal | ~577 | uint64 / big.Int modes | Arbitrary | No | None | TEXT via string | BSD-3 | Dormant |
| govalues/decimal | ~224 | uint64 + scale | 19 significant digits | Yes | Community (ColeBurch/pgx-govalues-decimal) | TEXT via Valuer/Scanner | MIT | Active |
| govalues/money | ~49 | govalues/decimal + ISO 4217 | 19 digits | Yes | Via govalues/decimal | TEXT via Valuer/Scanner | MIT | Active |
| Rhymond/go-money | ~1,900 | int64 (minor units) | Currency minor units only | Yes | None | INTEGER | MIT | Flagged inactive |
| bojanz/currency | ~627 | apd (wrapped) + currency code | Arbitrary | Yes | Via sql interfaces | TEXT via Valuer/Scanner | MIT | Active (CLDR v48.1) |
Performance ranking (from govalues benchmarks)
govalues/decimal > alpacadecimal > cockroachdb/apd > shopspring/decimal
- govalues: 3-8x faster than shopspring, zero heap allocations for typical ops
- alpacadecimal: 5-100x faster than shopspring within int64 range, falls back to shopspring on overflow
- apd: ~1.5-3.5x faster than shopspring
pgx NUMERIC integration
pgx's built-in pgtype.Numeric can scan to float64 or string. For proper decimal support:
- shopspring: Official
jackc/pgx-shopspring-decimal— register viapgxdecimal.Register(conn.TypeMap()) - govalues: Community adapter exists (v0.1.0, Feb 2026)
- apd: No adapter. Would need custom
NumericCodec. CockroachDB uses apd natively through its own driver path. - ericlagergren: No adapter
pglike NUMERIC to SQLite translation (CRITICAL)
In go-postgres/translate_ddl.go:
case "NUMERIC", "DECIMAL":
out = append(out, Token{Kind: TokKeyword, Value: "REAL", Raw: "REAL"})
This loses precision. SQLite REAL is IEEE 754 double (~15-16 significant digits). PostgreSQL NUMERIC supports up to 131,072 digits before and 16,383 after the decimal.
Fix needed: Translate NUMERIC(p,s) to TEXT instead of REAL. All Go decimal libraries round-trip losslessly through text strings. This matches how pglike already handles TIMESTAMP to TEXT.
SQLite's NUMERIC affinity is also unsafe — it tries to coerce text to INTEGER or REAL. TEXT affinity is the only safe choice for exact decimal storage.
Approach comparison for go-luca
| Approach | Representation | Pros | Cons |
|---|---|---|---|
| int64 + exponent (current) | 10050 with exp=-2 = 100.50 | Fastest. No allocations. Exact. Native SQL INTEGER. | Fixed range (~18 digits). Cross-exponent ops need scaling. |
| shopspring/decimal | big.Int + exp | Arbitrary precision. De facto standard. Official pgx adapter. | Slow (big.Int allocations every op). |
| govalues/decimal | uint64 + scale | Fast (3-8x shopspring). Immutable. 19 digits. Zero alloc. | Smaller community. 19-digit limit. Community pgx adapter only. |
| int64 core + govalues display | int64 storage, govalues for calc/display | Best of both: fast storage, fast calculation | Two representations to bridge |
The "coin" approach (mkobetic/coin)
Uses *big.Int + *Commodity where Commodity defines decimal places. Truncates at every step to commodity precision. Conceptually identical to go-luca's int64 + Account.Exponent but with big.Int (arbitrary range, slower).
Recommendation
- Keep int64 + exponent for storage and core movement recording — it's the fastest, most compact representation and matches TigerBeetle's design
- Consider govalues/decimal to replace shopspring/decimal for interest calculations and display — 3-8x faster, immutable, zero-allocation, 19 digits covers all practical financial amounts
- Fix pglike: change NUMERIC translation from REAL to TEXT to preserve precision
- Benchmark the switchover: measure shopspring vs govalues for go-luca's actual interest calculation workload before committing