A distributed API idempotency middleware built with Go, Redis, Lua, and Next.js to prevent duplicate processing of retryable POST, PUT, and PATCH requests.
The engine coordinates concurrent requests using an atomic Redis-backed state machine. Requests sharing the same tenant + idempotency key are coordinated so that concurrent duplicates do not execute the downstream operation simultaneously. Once a request completes, its response can be replayed for subsequent retries.
Project goal: Explore reliable idempotency, concurrency control, atomic state transitions, and failure handling in distributed REST APIs.
Client
|
| HTTP Request
| Idempotency-Key
v
+-------------------+
| Go API/Proxy |
+---------+---------+
|
| Atomic state transition
v
+-------------------+
| Redis + Lua |
| |
| Started |
| Completed |
| Failed |
| Response Cache |
+---------+---------+
|
| First request only
v
+-------------------+
| Downstream API |
| / Database / |
| Payment Service |
+-------------------+
+-------------------+
| Next.js Dashboard |
| |
| Request replay |
| Race simulation |
| Redis inspection |
+-------------------+
Distributed clients commonly retry requests when they experience:
- Network timeouts
- Connection failures
- HTTP 5xx responses
- Load-balancer retries
- Client-side retry policies
For state-mutating APIs, blindly retrying a request can cause duplicate side effects.
For example:
Client
|
| POST /payment
|
v
Payment Service
|
| Payment succeeds
|
X Network timeout
|
Client does not know whether payment succeeded
|
| Retry
v
Payment Service
Without idempotency protection, the second request may create a duplicate operation.
This project uses an idempotency key to coordinate retries and duplicate requests.
Each request is identified using:
tenant + idempotency key
The request payload is also fingerprinted so that the same idempotency key cannot silently be reused with a different request payload.
A simplified request lifecycle is:
+-----------+
| Missing |
+-----+-----+
|
| First request
v
+-----------+
| Started |
+-----+-----+
|
+--------+--------+
| |
| Success | Failure
v v
+-------------+ +-------------+
| Completed | | Failed |
+-------------+ +-------------+
|
|
v
Replay cached
response
The actual state transitions are implemented using Redis Lua scripts so the coordination logic executes atomically.
Request
|
| tenant=A
| key=abc
v
Redis
|
| key does not exist
v
STARTED
|
v
Downstream operation
|
v
COMPLETED
The resulting response can be stored for subsequent retries.
Request
|
| tenant=A
| key=abc
v
Redis
|
| COMPLETED
v
Cached response
|
v
Client
The downstream operation does not need to execute again.
Request 1 ───────┐
|
Request 2 ───────┤
|
Request 3 ───────┼──> Redis + Lua
|
Request 4 ───────┤
|
Request 5 ───────┘
|
v
One request owns
the operation
|
v
Downstream API
|
v
Cached response
The dashboard includes a race-condition simulation that sends multiple concurrent requests using the same idempotency key.
The same idempotency key should not represent different operations.
For example:
Request 1:
key = payment-123
amount = 100
Request 2:
key = payment-123
amount = 500
The request fingerprint allows the engine to detect this mismatch instead of treating the second request as the same operation.
If the downstream operation fails, the request can be transitioned out of the active state so that a later retry can attempt the operation again.
The critical coordination operations are implemented as Redis Lua scripts.
This avoids a fragile sequence such as:
GET
|
v
check state
|
v
SET
where multiple workers can observe the same state between operations.
Instead, the state transition is performed atomically inside Redis.
Conceptually:
Request
|
v
Lua Script
|
+-- key missing? ------> acquire request
|
+-- request running? --> coordinate duplicate
|
+-- completed? --------> replay response
|
+-- failed? -----------> allow retry
Idempotency state is namespaced using the tenant and idempotency key.
Conceptually:
idempotency:{tenant}:{key}
This prevents unrelated tenants from accidentally sharing idempotency state.
The request payload is fingerprinted before processing.
This allows the engine to distinguish:
Same key + same payload
from:
Same key + different payload
The second case is rejected rather than silently reusing the result of a different request.
The project includes a Next.js dashboard for demonstrating the engine.
The dashboard supports:
- Proxy request execution
- Replaying the same request
- Payload mismatch simulation
- Concurrent race-condition simulation
- Downstream failure simulation
- Redis key inspection
- TTL inspection
- Manual key eviction
.
├── api/
│ ├── proxy.go
│ └── inspect.go
│
├── app/
│ └── page.tsx
│
├── pkg/
│ ├── engine/
│ │ ├── state machine
│ │ ├── Lua scripts
│ │ ├── hashing
│ │ └── key namespacing
│ │
│ └── store/
│ └── Redis client
│
├── public/
├── package.json
├── go.mod
└── README.md
| Component | Technology |
|---|---|
| Backend | Go |
| State Store | Redis / Upstash Redis |
| Atomic Coordination | Redis Lua |
| Dashboard | Next.js / TypeScript |
| Deployment | Vercel + Upstash Redis |
| Testing | Go test |
| Static Analysis | go vet |
- Go version declared in
go.mod - Node.js 20+
- npm
- Upstash Redis database
Create an Upstash Redis database and configure:
UPSTASH_REDIS_URL=rediss://default:<password>@<host>:<port>For local development, place the variable in .env.local or export it in the shell.
Do not commit credentials or .env.local.
Install frontend dependencies:
npm installStart the development server:
npm run devOpen:
http://localhost:3000
Run the Go tests:
go test ./...Run static analysis:
go vet ./...Run frontend linting:
npm run lintRun TypeScript validation:
npx tsc --noEmitBuild the Next.js application:
npm run buildThe project can be deployed as a Next.js application with the Go API handlers deployed as serverless functions.
- Push the repository to GitHub.
- Import the repository into Vercel.
- Configure:
UPSTASH_REDIS_URL=<your-upstash-redis-url>
- Use the repository root as the project root.
- Build using:
npm run buildThe project is designed to explore several distributed failure scenarios:
A client retries the same request after a timeout.
The idempotency key allows the engine to determine whether the request is new, already executing, or already completed.
Multiple clients send the same request concurrently.
Redis Lua-based coordination prevents the requests from independently executing the same operation.
If downstream processing fails, the request can be retried instead of permanently remaining in an active state.
Once a request reaches a terminal successful state, subsequent retries can receive the previously stored response.
This repository is intended as a technical demonstration, not a production-ready public proxy.
The Redis inspection endpoint can read and delete idempotency state and should therefore be protected or removed before public deployment.
The proxy also supports a client-supplied X-Target-URL for demonstration purposes. In a production system, downstream destinations should be restricted using an allowlist or configured server-side rather than accepting arbitrary client-provided URLs.
Redis provides:
- Low-latency key/value operations
- Atomic operations
- TTL support
- Lua scripting
- Shared state across multiple application instances
The idempotency state transition often requires multiple logical checks and updates.
A Lua script allows the coordination logic to execute atomically within Redis rather than relying on multiple client-side commands.
A client may retry after a successful downstream operation but before receiving the response.
Returning the stored response allows the retry to receive the original result without repeating the downstream operation.
- Distributed request coordination
- API idempotency
- Redis state machines
- Redis Lua scripting
- Atomic state transitions
- Race-condition handling
- Retry semantics
- Response caching
- Request fingerprinting
- Multi-tenant key namespacing
- REST API middleware design
- Go backend development
This project is an educational implementation focused on distributed-systems concepts and failure handling. Production systems should additionally consider authentication, authorization, downstream transaction semantics, Redis availability, observability, rate limiting, persistence requirements, and deployment security.