You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Choose one of the two active profiles to run the application:
42
+
43
+
***Default Mode (PostgreSQL + In-Memory Cache)**:
44
+
Uses PostgreSQL for durable ledger entry persistence, but keeps idempotency state in a local JVM-memory store. Ideal for fast local development without external cache dependencies.
Uses PostgreSQL for durable ledger entry persistence and Redis for distributed locking (`SETNX`) and idempotency caching. Matches production-scale deployments.
@@ -79,7 +95,7 @@ suite; these tests fail fast rather than falling back to H2.
79
95
80
96
## Production Gaps
81
97
82
-
- The in-memory adapter remains for fast unit-level semantics tests; production-like configuration defaults to Redis for idempotency coordination and JPA/PostgreSQL for ledger durability.
98
+
- The in-memory adapter remains for fast unit-level semantics tests; production-like hybrid configuration uses Redis (jpa,redis profile)for idempotency coordination and JPA/PostgreSQL for ledger durability.
83
99
- No transactional outbox exists yet.
84
100
- No auth or tenant model exist yet.
85
101
- Observability features domain metrics for accepted, replayed, and rejected requests, but does not yet emit structured tracing spans.
Copy file name to clipboardExpand all lines: system-design/idempotent-payment-ledger/docs/ARCHITECT_NOTES.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -19,7 +19,7 @@ This document outlines the key architectural decisions, design trade-offs, and p
19
19
## 2. Concurrency Control & Database-Level Defense-in-Depth
20
20
21
21
### 2.1. Uniqueness Guarantee (V3 Migration)
22
-
To guarantee absolute data integrity under concurrent attempts, a database-level unique constraint `uq_payments_key UNIQUE (tenant_id, idempotency_key)` is enforced on the `payments` table. This serves as the ultimate safety net if the distributed cache layer (Redis) fails or experiences key eviction.
22
+
To enforce transaction consistency under concurrent attempts, a database-level unique constraint `uq_payments_key UNIQUE (tenant_id, idempotency_key)` is enforced on the `payments` table. This serves as the safety net if the distributed cache layer (Redis) fails or experiences key eviction.
When concurrent requests with the same idempotency key bypass the cache layer (e.g., during lock thrashing or cache eviction) and execute the write path simultaneously:
@@ -46,4 +46,4 @@ If the database transaction commits successfully but the subsequent cache comple
46
46
Applying the V3 unique constraint to an existing production database with historical transaction data requires pre-flight audits to identify and resolve any duplicate keys. Refer to the [Operations Runbook](OPERATIONS_RUNBOOK.md) for details on pre-migration cleanup queries.
47
47
48
48
### 4.2. Load Testing & Capacity Estimation
49
-
While the core concurrency invariants are mathematically verified, future work should include running synthetic load simulations (e.g., 5,000+ concurrent requests) to benchmark Redis lock performance and compute production RAM allocation requirements based on anticipated throughput.
49
+
While the core concurrency invariants are functionally verified under concurrent thread simulations, future work should include running synthetic load simulations (e.g., 5,000+ concurrent requests) to benchmark Redis lock performance and compute production RAM allocation requirements based on anticipated throughput.
WHERE state !='idle'AND query NOT LIKE'%pg_stat_activity%';
29
29
```
30
30
3. Restart database or scale connection pool size if required by adjusting `spring.datasource.hikari.maximum-pool-size` configuration.
31
31
4. Note: Redis keys marked `PROCESSING` will naturally expire after 120 seconds. Clients attempting retry during DB outage will receive HTTP `425 Too Early`or HTTP `500`, then automatically transition to clean retries once Postgres recovers.
32
32
33
33
### 2. Redis Cluster Outage / Cache Eviction (P1)
34
34
35
-
***Symptoms**:
35
+
***Symptoms**:
36
36
* API returns HTTP `500`or fails to connect to Redis (`RedisConnectionFailureException`).
37
37
* Prometheus alert triggers for Redis memory exhaustion or service down.
38
38
***Impact**: External boundary locking is disabled. Concurrency race protection falls back entirely to PostgreSQL unique constraints.
@@ -71,7 +71,7 @@ If a Spring Boot container crashes abruptly *after* reserving a key in Redis but
If a P0 merchant transaction is blocked and cannot wait 120 seconds:
73
73
1. Connect to the Redis instance: `redis-cli`.
74
-
2. Locate the stuck key safely using `SCAN` to avoid blocking production:
74
+
2. Locate the stuck key safely using `SCAN` to avoid blocking production:
75
75
`SCAN 0 MATCH idempotency:* COUNT 1000`
76
76
3. Delete the key: `DEL idempotency:<stuck_key>`.
77
77
4. Ask the client to retry immediately.
@@ -82,8 +82,8 @@ If a Spring Boot container crashes abruptly *after* reserving a key in Redis but
82
82
***Cause**: The client reused an existing key but changed parameters (e.g. payer, amount, currency), which is an API violation.
83
83
***Action**: Trace client request parameters in logs. Inspect database payments to see what was originally recorded:
84
84
```sql
85
-
SELECT payment_id, payer_account_id, merchant_account_id, amount, currency, status
86
-
FROM payments
85
+
SELECT payment_id, payer_account_id, merchant_account_id, amount, currency, status
86
+
FROM payments
87
87
WHERE idempotency_key = 'offending-key';
88
88
```
89
89
Confirm that the client needs to generate a fresh, unique `Idempotency-Key` for the new transaction.
@@ -110,23 +110,23 @@ HAVING SUM(CASE WHEN entry_type = 'CREDIT' THEN amount ELSE -amount END) != 0.00
110
110
111
111
Verify that the stored account balance matches the sum of its credit and debit ledger entries, taking into account the initial seeded balances for test accounts (1000.00 for `acct-payer`and`acct-payer-http`).
112
112
```sql
113
-
SELECT
113
+
SELECT
114
114
a.account_id,
115
115
a.balance AS current_stored_balance,
116
116
(
117
-
CASE
118
-
WHEN a.account_id IN ('acct-payer', 'acct-payer-http') THEN 1000.0000
119
-
ELSE 0.0000
117
+
CASE
118
+
WHEN a.account_id IN ('acct-payer', 'acct-payer-http') THEN 1000.0000
119
+
ELSE 0.0000
120
120
END
121
121
+ COALESCE(SUM(CASE WHEN le.entry_type = 'CREDIT' THEN le.amount ELSE -le.amount END), 0.0000)
122
122
) AS calculated_ledger_balance
123
123
FROM accounts a
124
124
LEFT JOIN ledger_entries le ON a.account_id = le.account_id
125
125
GROUP BY a.account_id, a.balance
126
126
HAVING a.balance != (
127
-
CASE
128
-
WHEN a.account_id IN ('acct-payer', 'acct-payer-http') THEN 1000.0000
129
-
ELSE 0.0000
127
+
CASE
128
+
WHEN a.account_id IN ('acct-payer', 'acct-payer-http') THEN 1000.0000
129
+
ELSE 0.0000
130
130
END
131
131
+ COALESCE(SUM(CASE WHEN le.entry_type = 'CREDIT' THEN le.amount ELSE -le.amount END), 0.0000)
132
132
);
@@ -150,15 +150,17 @@ Before executing the **V3 unique constraint migration** (`V3__add_unique_constra
150
150
GROUP BY tenant_id, idempotency_key
151
151
HAVING COUNT(*) > 1;
152
152
```
153
-
2. **Mitigation / Deduplication Action**:
154
-
If duplicate payments are found, the Flyway migration will fail. You must deduplicate or rename the conflicting keys out-of-band before deploying the V3 application code:
155
-
```sql
156
-
-- Example deduplication script (keeps the oldest payment row per tenant/key)
157
-
DELETE FROM payments a
158
-
WHERE a.ctid <> (
159
-
SELECT min(b.ctid)
160
-
FROM payments b
161
-
WHERE a.tenant_id = b.tenant_id
162
-
AND a.idempotency_key = b.idempotency_key
163
-
);
164
-
```
153
+
2. **Mitigation & Incident Response Protocol**:
154
+
If duplicate combinations are detected, applying the `V3` unique constraint will fail, halting the release pipeline. **DO NOT attempt to delete transaction history directly from the database.** Deleting historical rows will violate audit logs and database foreign key integrity (e.g., from ledger entries).
155
+
156
+
Instead, execute the following Incident Response protocol:
157
+
-**Halt Rollout**: Immediately abort the database migration and notify the on-call and release manager teams.
158
+
-**Analyze & Locate Winner**: Identify the duplicate entries and determine which database row is the canonical payment (e.g., matching the success status returned to the customer or payment gateway).
159
+
-**Rekey Conflicting Entries**: For the duplicates that are non-canonical, update the `idempotency_key` column to append a suffix (e.g., `_duplicate_v3_migration_incident_<incident_id>`). This preserves audit logs, honors foreign keys, and satisfies the physical uniqueness constraint:
160
+
```sql
161
+
-- Example suffix rekeying query (safe; preserves historical data for audits)
162
+
UPDATE payments
163
+
SET idempotency_key = idempotency_key || '_dup_incident_12345'
164
+
WHERE payment_id = '<non_canonical_payment_id>';
165
+
```
166
+
-**Audit & Reconcile**: Verify that the associated ledger transactions and entries remain balanced. Execute compensating accounting entries if any discrepancy is found.
Copy file name to clipboardExpand all lines: system-design/idempotent-payment-ledger/src/main/java/infra/systemdesign/paymentledger/application/PaymentIntakeService.java
-4Lines changed: 0 additions & 4 deletions
Original file line number
Diff line number
Diff line change
@@ -12,7 +12,6 @@
12
12
importjava.nio.charset.StandardCharsets;
13
13
importjava.security.MessageDigest;
14
14
importjava.security.NoSuchAlgorithmException;
15
-
importjava.time.Clock;
16
15
importjava.util.HexFormat;
17
16
18
17
@@ -21,17 +20,14 @@ public class PaymentIntakeService {
Copy file name to clipboardExpand all lines: system-design/idempotent-payment-ledger/src/main/java/infra/systemdesign/paymentledger/infrastructure/persistence/JpaLedgerStore.java
Copy file name to clipboardExpand all lines: system-design/idempotent-payment-ledger/src/main/java/infra/systemdesign/paymentledger/infrastructure/persistence/repository/AccountJpaRepository.java
+4-1Lines changed: 4 additions & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -11,5 +11,8 @@ public interface AccountJpaRepository extends JpaRepository<AccountEntity, Strin
0 commit comments