A low-latency market-data gateway in Go. It fronts an upstream price API with a single-flight, in-memory TTL cache: repeat reads are served from memory in ~92 ns, and a burst of concurrent misses for the same key collapses into exactly one upstream call instead of stampeding it.
Zero third-party dependencies — standard library only. Static ~5.6 MB binary, distroless non-root container.
Public market-data APIs are rate-limited and slow (100–800 ms), and naive callers
hammer them with identical requests. marketpulse sits in front and gives you:
- Sub-microsecond repeat reads — cached responses never touch the network.
- Single-flight — 50 goroutines missing the same key trigger one upstream call; the rest wait for and share its result. No thundering herd.
- A stable cache key regardless of argument order —
?ids=btc,ethand?ids=eth,btchit the same entry. - Operational honesty —
/statsexposes live hit ratio; every response carries anX-Response-Timeheader.
flowchart LR
C[client] -->|/price?ids=| S[marketpulse]
S -->|hit ~92ns| C
S -->|miss, single-flight| U[(upstream API)]
U --> S
subgraph S[marketpulse]
K[TTL cache] --- F[single-flight]
end
# with Docker
docker compose up --build
# or bare
go run ./cmd/marketpulse
curl 'http://localhost:8080/price?ids=bitcoin,ethereum&vs=usd'
# {"bitcoin":64213.0,"ethereum":3345.7} (X-Response-Time: 214ms first call)
curl 'http://localhost:8080/price?ids=bitcoin,ethereum&vs=usd'
# same body, X-Response-Time: 41µs (served from cache)
curl http://localhost:8080/stats
# {"hits":1,"misses":1,"hit_ratio":0.5}| Endpoint | Description |
|---|---|
GET /price?ids=<csv>&vs=<ccy> |
Cached spot prices. vs defaults to usd. |
GET /stats |
Cumulative cache hits / misses / hit ratio. |
GET /healthz |
Liveness + active provider name. |
| Env var | Default | Meaning |
|---|---|---|
MARKETPULSE_ADDR |
:8080 |
Listen address. |
MARKETPULSE_TTL |
10s |
Cache entry lifetime. |
- Cache (
internal/cache) — RWMutex-guarded map + per-key single-flight. The loader for a key runs once even under concurrent misses; callers block on async.WaitGroupand share the result. - Provider (
internal/provider) — the server depends on a smallProviderinterface, so the real CoinGecko client and the test mock are interchangeable. Adding a source (DeFiLlama, an exchange WS) is one file. - Server (
internal/server) — thin HTTP layer; caches the marshalled JSON, not the map, so cache hits skip re-serialization too. - main — graceful shutdown on SIGINT/SIGTERM, request logging, timeouts.
BenchmarkGetOrLoadHit-2 11,633,913 91.98 ns/op 0 B/op 0 allocs/op
go test -race ./... is clean; the single-flight guarantee is asserted by test
(25 concurrent requests → 1 upstream call).
make test # go test -race ./...
make bench # cache benchmarksPart of a small polyglot portfolio — right tool per layer: Go for networking (this), Python for high-level AI, TypeScript for test/QA automation, Rust for the from-scratch numerical core. — Nicholas Martins · github.com/nickmartins-lambda