All documentation

Reviews & Audits

Module 15 Performance Review

Follow-up to 0004-final-performance-review.md and 0006-module-14-performance-review.md, scoped to what Module 15 added. Same posture as both prior reviews: static code analysis only — this sandbox has never had a running instance of the application to load-test against (and, as of task #366, cannot even complete a full tsc pass — see that task's disclosure), so every finding below is read from source, not measured.

1. QuotaService.resolveLimit() — N+1 fixed during this pass

QuotaService.check() is documented as "the enforcement entry point every resource-consuming command handler is expected to call" — a hot path by design. resolveLimit() was originally a sequential loop issuing one quotaPolicy.findFirst per candidate scope (USER, then WORKSPACE, then ORGANIZATION — up to 3 round-trips), plus a 4th query for the plan-tier fallback and a 5th for the usage counter itself in check(). Rewritten to a single findMany with an OR across every candidate (scopeType, scopeId) pair, with the most-specific match picked in memory — same result, at most one query instead of up to three. QuotaPolicy already carries @@index([scopeType, scopeId]) (task #325), which supports this query well without a further migration change. See apps/api/src/modules/billing/quota.service.ts.

2. UsageService.record() — fan-out, not a loop, correct as written

Three usageCounter.upsert() calls (DAILY/MONTHLY/TOTAL) via Promise.all — a fixed, bounded fan-out (always exactly 3, never proportional to any table's size), not a loop over data. No finding.

3. FeatureFlagsService.evaluateAll() — acceptable for expected table size

One findAll() over every FeatureFlag row plus one findOverridesForScopes() call per evaluation. Feature-flag tables are operator-curated and stay small (tens, not millions, of rows) in every real-world deployment of this pattern — no pagination or caching was added, and none is warranted at this table's expected cardinality. Flagging for future awareness only: if a deployment ever accumulates thousands of flags (unlikely), this endpoint's cost would grow linearly with total flag count, not active flag count.

4. StripeWebhookController / SubscriptionService.applyStripeWebhookUpdate() — no new query concerns

Single lookups by indexed unique/foreign-key columns (providerCustomerId, id) per webhook event; resolvePlanTierByStripePriceId() does a plan.findMany() (unindexed scan) but the Plan table is operator-configured and expected to hold single-digit-to-low-tens of rows, the same "small, curated table" reasoning as finding #3. No finding.

5. InvoiceService.sync() — pre-existing (task #331), re-confirmed in scope

Loops over the provider's returned invoice list doing one invoice.upsert() per invoice — bounded by a single customer's invoice history (?limit=100 in StripePaymentProvider.listInvoices()), not unbounded. Re-reviewed as part of this pass since StripeWebhookController (task #366) is a new caller of this method; no change needed.

6. Health/diagnostics endpoints — no regression

GET /observability/health/diagnostics (task #353) and the channel field added to GET /observability/health (task #356) both add zero query cost beyond what already existed (channel reads a ConfigService value already loaded at boot; diagnostics' one SELECT 1 was already present before this task).

Summary

One real fix made (QuotaService.resolveLimit()'s N+1 → 1 query), same category and severity as Module 14's Plugin Marketplace fix. Everything else added this module is either bounded fan-out, small-table scans that don't warrant an index/cache, or unchanged pre-existing logic re-checked because a new caller was added. Consistent with the prior reviews' headline: nothing pathological found, nothing load-tested — a static read, not a benchmark result.