-
Notifications
You must be signed in to change notification settings - Fork 0
feat: scale improvements — O(1) reporting, Redis idempotency cache, centi-cent accrual #12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
df56301
feat: add ledger_totals table and wallets.fractional_balance column
0-sayed 9fb1bcf
docs: document bigint mode:number safety bound in ledger_totals schema
0-sayed 7236077
feat: replace ledger GROUP BY scan with O(1) ledger_totals SELECT
0-sayed 1a02dde
test: assert from() called with ledgerTotals in aggregate test
0-sayed 1c19577
feat: maintain ledger_totals running total on deposit
0-sayed 799609d
fix: use upsert for ledger_totals to handle missing rows; fix mockWal…
0-sayed b0b90e3
feat: add centi-cent accrual to eliminate systematic author under-pay…
0-sayed dc16a65
feat: maintain ledger_totals running totals on purchase
0-sayed 257d07a
feat: add global RedisModule providing ioredis REDIS_CLIENT token
0-sayed 06f7d23
feat: add Redis SETNX edge cache for idempotency in PurchasesService
0-sayed b139d61
fix: delete Redis sentinel when purchase transaction fails
0-sayed 6d2574a
docs: add ADR-008/009/010 and update architecture docs for scale impr…
0-sayed 817a8e3
fix: guard zero-amount ledger_totals upserts and platform wallet update
0-sayed 3a29967
fix: resolve all PR #12 review items
0-sayed d40e5af
refactor: consolidate migration 0002 into 0001
0-sayed f6d8ee4
fix: delete sentinel when post-commit or cold-start cache write fails
0-sayed File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| # ADR-008: Running Totals for Reporting | ||
|
|
||
| **Status:** Accepted | ||
| **Date:** 2026-04-12 | ||
|
|
||
| ## Context | ||
|
|
||
| `ReportsProcessor.aggregate()` ran a `GROUP BY` over the entire `ledger` table inside a `REPEATABLE READ` transaction. At 100M+ rows this causes a full table scan, consumes memory proportional to result size, and holds a long-running transaction on the operational database. | ||
|
|
||
| ## Decision | ||
|
|
||
| A new `ledger_totals` table maintains pre-aggregated running sums per ledger type: | ||
|
|
||
| ```sql | ||
| type ledger_type PRIMARY KEY | ||
| total BIGINT NOT NULL DEFAULT 0 | ||
| ``` | ||
|
|
||
| The table is updated inside the same transaction as every ledger insert using `INSERT ... ON CONFLICT DO UPDATE`. `ReportsProcessor` replaces the `GROUP BY` with a single `SELECT * FROM ledger_totals` (4 rows, no scan). | ||
|
|
||
| ## Reasoning | ||
|
|
||
| - The running total is always consistent with the ledger because it is updated in the same transaction. No eventual consistency. | ||
| - O(1) read path replaces O(N) scan. | ||
| - `BIGINT` is chosen because the sum of 100M+ cent-denominated values can exceed INT4's ~2.1 billion max. | ||
| - Upsert (`INSERT ... ON CONFLICT DO UPDATE`) is used rather than bare `UPDATE` to handle fresh databases where the row may not yet exist. | ||
|
|
||
| ## Consequences | ||
|
|
||
| - Reporting is O(1) regardless of ledger size. | ||
| - Write path has a small additional upsert per transaction (negligible vs. ledger insert cost). | ||
| - Migration includes a backfill from existing ledger data. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| # ADR-009: Redis Idempotency Cache | ||
|
|
||
| **Status:** Accepted | ||
| **Date:** 2026-04-12 | ||
|
|
||
| ## Context | ||
|
|
||
| Every purchase request hit Postgres with a `SELECT WHERE idempotency_key = ?` before the transaction. Under high retry volume this is unnecessary latency and DB load for requests that already have a known result. | ||
|
|
||
| ## Decision | ||
|
|
||
| Redis SETNX edge cache. Before the DB check: | ||
|
|
||
| ``` | ||
| SET idempotency:<key> 'processing' EX 86400 NX | ||
| ``` | ||
|
|
||
| - If NX returns `null` (key exists): read the cached value and return or `409`. | ||
| - If NX returns `'OK'` (new request): proceed to DB. | ||
| - Cache the result after commit. | ||
| - Delete sentinel on transaction failure so clients can retry. | ||
|
|
||
| ## Reasoning | ||
|
|
||
| - Redis is a latency shortcut — the DB remains the correctness backstop. The `UNIQUE` constraint on `purchases.idempotency_key` and the existing DB SELECT are untouched. | ||
| - Any request that slips past Redis (eviction, cold start, Redis unavailable) falls through to the DB path. | ||
| - The `'processing'` sentinel must be deleted on transaction failure — otherwise recoverable failures (e.g., 402 insufficient funds) would block client retries for 24 hours. | ||
| - TTL of 86400s (24 hours) covers any reasonable client retry window. | ||
|
|
||
| ## Consequences | ||
|
|
||
| - Repeated requests with a completed purchase are served from Redis without hitting Postgres. | ||
| - Redis unavailability degrades gracefully (falls through to DB). | ||
| - A separate `RedisModule` is required because BullMQ does not expose its internal ioredis instance via NestJS DI. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| # ADR-010: Centi-cent Fractional Accrual | ||
|
|
||
| **Status:** Accepted | ||
| **Date:** 2026-04-12 | ||
| **Supersedes:** ADR-005 | ||
|
|
||
| ## Context | ||
|
|
||
| `Math.floor(price * 70 / 100)` always floors the author royalty, giving every sub-cent remainder to the platform. Over millions of microtransactions this is a systematic wealth transfer away from authors. | ||
|
|
||
| Example over three 99-cent transactions: author should receive 3 × 69.3 = 207.9 cents but actually receives 3 × 69 = 207 cents; platform over-receives ~1 cent per 3 transactions. | ||
|
|
||
| ## Decision | ||
|
|
||
| Per-author centi-cent accrual. Track each author wallet's fractional royalty remainder at centi-cent precision (1/100 of a cent, stored as integer 0–99 in `wallets.fractional_balance`). When accumulated centi-cents reach 100, sweep one whole cent into the author's balance within the same transaction. | ||
|
|
||
| ```typescript | ||
| const exactNumerator = itemPrice * AUTHOR_ROYALTY_PERCENT; // e.g. 99*70 = 6930 | ||
| const authorFloorCents = Math.floor(exactNumerator / 100); // 69 | ||
| const remainderCentiCents = exactNumerator % 100; // 30 | ||
| const newFractional = authorWallet.fractionalBalance + remainderCentiCents; | ||
| const sweepCents = Math.floor(newFractional / 100); // whole cents to sweep | ||
| const leftoverCenti = newFractional % 100; // stored back | ||
| const totalAuthorCents = authorFloorCents + sweepCents; // actual credit | ||
| const platformCut = itemPrice - totalAuthorCents; // always = itemPrice | ||
| ``` | ||
|
|
||
| ## Reasoning | ||
|
|
||
| - Money conservation holds — `totalAuthorCents + platformCut === itemPrice` always. | ||
| - Stays within ADR-003's integer-only philosophy — centi-cents are a finer-grained integer unit. | ||
| - The `SELECT FOR UPDATE` on the author wallet row means `fractionalBalance` can be set directly (no SQL arithmetic needed — we hold the lock). | ||
| - No background job required. | ||
|
|
||
| ## Consequences | ||
|
|
||
| - Author wallets gain a `fractional_balance` column (0–99). | ||
| - Authors receive the correct economic share over time rather than systematically losing sub-cent remainders. | ||
| - Platform receives slightly less on average (closer to the nominal 30%). | ||
| - Ledger entries record `totalAuthorCents` and `platformCut` (actual money moved, not nominal percentages). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| CREATE TABLE "ledger_totals" ( | ||
| "type" "ledger_type" PRIMARY KEY NOT NULL, | ||
| "total" bigint DEFAULT 0 NOT NULL | ||
| ); | ||
| --> statement-breakpoint | ||
| ALTER TABLE "wallets" ADD COLUMN "fractional_balance" integer DEFAULT 0 NOT NULL;--> statement-breakpoint | ||
| ALTER TABLE "wallets" ADD CONSTRAINT "wallets_fractional_balance_non_negative" CHECK ("wallets"."fractional_balance" >= 0);--> statement-breakpoint | ||
| ALTER TABLE "wallets" ADD CONSTRAINT "wallets_fractional_balance_lt_100" CHECK ("wallets"."fractional_balance" < 100); | ||
|
|
||
| -- Backfill running totals from existing ledger data | ||
| INSERT INTO ledger_totals (type, total) | ||
| SELECT type, COALESCE(SUM(amount), 0) FROM ledger GROUP BY type | ||
| ON CONFLICT (type) DO NOTHING; | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.