docker compose up --build
To ensure that duplicate requests with the same transaction_id do not create multiple records, idempotency is implemented using:
- Code Level
- Before processing, the service checks for an existing payment by
transaction_id. - If found, it returns the same response, ensuring consistency.
- Before processing, the service checks for an existing payment by
- Database Level
- A unique constraint is added to the
transaction_idcolumn to prevent duplicate records at the database level.
- A unique constraint is added to the
- Enter API Endpoint
- Attempt to Lock by Idempotency Key (
transaction_id)- Lock Failed:
- Another request with the same
transaction_idis currently processing. - Return
201 Createdwith the existing transaction record (if found).
- Another request with the same
- Lock Failed:
- Check Existing Payment
- If a record with the same
transaction_idexists, return it immediately.
- If a record with the same
- Start Processing
- Simulate processing with a
1sdelay and create the record. - Trigger goroutine for simulate processing
- update payment status to
completedorfailed - update wallet balance if completed
- update payment status to
- Simulate processing with a
- Return Response
- Return
201 Createdwith the newly created payment record.
- Return
-
Open the Postman Collection I created for this project: Postman Collection.
This collection includes all the API endpoints with example requests and can be used directly for testing.
-
Switch to the DEV environment.
-
Set the following environment variable:
| Variable | Value |
|---|---|
base-url |
http://localhost:8080/api/v1 |
You can directly connect to the postgreSQL database to view the data through the following config:
host:localhostport:9999username:postgrespassword:postgres
We implemented unit tests for the Payment Service using Testcontainers-Go and service-level testing. This approach allows us to spin up a PostgreSQL instance during tests, providing:
- Realistic database testing environment
- Isolation per test session
- Automatic cleanup after tests
To test concurrency and ensure the idempotency logic works as expected, we use the hey library to do concurrency testing
Run the following command under the project root to simulate 100 requests with a concurrency level of 10:
hey -n 100 -c 10 -m POST \
-H "Content-Type: application/json" \
-d '{"transaction_id":"unique123","amount":100,"user_id":"1"}' \
http://localhost:8080/api/v1/pay
n 100→ Total number of requests to send.c 10→ Number of concurrent workers.m POST→ HTTP method used for the request.H→ Add request headers.d→ Request body data.
My Concurrent Testing Result
Based on the test results:
-
100 requests were sent concurrently with 10 workers.
-
Only 1 request successfully acquired the lock and proceeded with full processing (indicated by the 1-second latency due to the simulated
time.Sleep(1 * time.Second)). -
The remaining 99 requests returned immediately after detecting that the same
transaction_idwas already being processed, which aligns with the idempotency design. -
All requests returned HTTP 201 Created, confirming that the system consistently returns the same response for duplicate requests with the same
transaction_id.$ hey -n 100 -c 10 -m POST -H "Content-Type: application/json" -d '{"transaction_id":"unique6","amount":100,"user_id":"1"}' http://localhost:8080/api/v1/pay # Send 100 requests Summary: Total: 1.9285 secs Slowest: 1.0136 secs Fastest: 0.1010 secs Average: 0.1129 secs Requests/sec: 51.8549 Total data: 10203 bytes Size/request: 102 bytes Response time histogram: 0.101 [1] | 0.192 [98] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 0.283 [0] | 0.375 [0] | 0.466 [0] | 0.557 [0] | 0.649 [0] | 0.740 [0] | 0.831 [0] | 0.922 [0] | 1.014 [1] | # only 1 request process 1s (sleep 1s) Latency distribution: 10% in 0.1014 secs 25% in 0.1016 secs 50% in 0.1020 secs 75% in 0.1027 secs 90% in 0.1141 secs 95% in 0.1197 secs 99% in 1.0136 secs Details (average, fastest, slowest): DNS+dialup: 0.0009 secs, 0.1010 secs, 1.0136 secs DNS-lookup: 0.0008 secs, 0.0000 secs, 0.0083 secs req write: 0.0000 secs, 0.0000 secs, 0.0003 secs resp wait: 0.1118 secs, 0.1009 secs, 1.0047 secs resp read: 0.0001 secs, 0.0000 secs, 0.0005 secs Status code distribution: [201] 100 responses # all responses return 201
Application Logging
# Examples
emb-payment-backend | [GIN] 2025/09/13 - 09:10:02 | 201 | 100.881567ms | 172.19.0.1 | POST "/api/v1/pay"
emb-payment-backend | [Info] message=Payment processing, failed to acquired the lock...
emb-payment-backend | [GIN] 2025/09/13 - 09:10:02 | 201 | 100.781749ms | 172.19.0.1 | POST "/api/v1/pay"
emb-payment-backend | [Info] message=Payment processing, failed to acquired the lock...
emb-payment-backend | [GIN] 2025/09/13 - 09:10:02 | 201 | 100.782061ms | 172.19.0.1 | POST "/api/v1/pay"
emb-payment-backend | [Info] message=Payment processing, failed to acquired the lock...
emb-payment-backend | [GIN] 2025/09/13 - 09:10:02 | 201 | 100.804661ms | 172.19.0.1 | POST "/api/v1/pay"