The idea
A self-hosted HTTP proxy that sits between a backend and outbound APIs like Stripe, a shipping carrier, or an internal order service. It intercepts POST/PUT requests, computes a stable idempotency key from the request body plus a caller-supplied dedupe scope, attaches it as a header, and caches the first response so retries — from client timeouts, load balancer retries, or queue redelivery — return the cached result instead of re-executing the mutating call.
Why build this
Payment and order APIs are the classic double-charge failure mode: a network blip mid-request leaves the client unsure whether the call succeeded, so it retries, and without idempotency the second call creates a second charge or a second shipment. Stripe, Adyen, and most mature payment APIs support idempotency keys natively, but plenty of teams either forget to generate them correctly — reusing a key across genuinely different requests, or minting a fresh key on every retry, which defeats the point — or they call internal services that have no idempotency support at all. A drop-in proxy fixes this without touching business logic, and it's a natural fit for teams running queue-based retry systems (SQS, Sidekiq, BullMQ) where at-least-once delivery is the default and exactly-once is the team's problem to solve.
Stack sketch
- Proxy: Go, built on
httputil.ReverseProxyfor low-latency passthrough - Key derivation: hash of method + path + normalized body + a caller-provided
Idempotency-Scopeheader, so callers control what counts as "the same request" - Response cache: Redis with a configurable TTL (default 24h, matching Stripe's own window) storing status code, headers, and body
- Concurrency guard: a short-lived lock per key (Redis
SET NX) so two in-flight retries for the same key don't both hit the upstream API - Config: YAML per upstream target — base URL, which methods to intercept, TTL override, headers to strip before hashing
- Observability: structured logs plus a Prometheus counter for cache hits (duplicate calls caught) vs. passthroughs
Scope for v1
- Single upstream target per proxy instance, configured via YAML
- Automatic key derivation from method + path + body hash — no manual key management required by callers
- Redis-backed response cache with TTL
- In-flight lock to prevent concurrent duplicate execution
- A
/statsendpoint showing hit/miss counts - Out: multi-tenant routing to many upstreams, a UI/dashboard, request/response transformation, support for non-JSON bodies
Where it could go
The next step is multi-target routing — one proxy instance in front of Stripe, a shipping carrier, and an internal order service, each with its own cache and TTL policy, so a team runs a single sidecar instead of one per integration. After that, a small dashboard showing recent duplicate calls caught, with the potential double-charge avoided, turns the tool from invisible infrastructure into something a finance or ops team can point to as evidence it's paying for itself. Longer term, extending the same pattern to GraphQL mutations and gRPC unary calls would cover teams that have already moved off plain REST.
Watch out for
Hashing the request body for key derivation breaks if two logically-identical requests serialize differently — key ordering, floating-point formatting, timestamps embedded in the payload. The proxy needs a canonicalization step, and any field that legitimately changes between "retries" of the same logical operation has to be excluded from the hash or the whole scheme silently stops deduplicating.