Skip to content

Fix N+1 query problem in status() with StatusContext pattern#19

Merged
rameerez merged 5 commits intomainfrom
fix/status-n-plus-1-queries
Feb 15, 2026
Merged

Fix N+1 query problem in status() with StatusContext pattern#19
rameerez merged 5 commits intomainfrom
fix/status-n-plus-1-queries

Conversation

@rameerez
Copy link
Copy Markdown
Owner

Summary

  • Fix severe query redundancy in status() method where helper methods re-called limit_status() internally
  • Introduce StatusContext pattern that caches all computed values within a single status() call
  • Reduce database queries from ~70 to 16 for a typical 3-limit call (77% reduction)

Problem

When calling plan_owner.limits (which calls status()) with 3 limits (products, licenses, activations), the gem was executing:

  • 18+ limit_status() calls instead of 3
  • 72+ effective_plan_for() calls instead of 1
  • ~70 total database queries for a single page load

This happened because helper methods like severity_for(), highest_severity_for(), message_for(), etc. all re-called limit_status() internally, and each limit_status() call independently resolved the plan, counted usage, and checked grace state.

Solution: StatusContext Pattern

Created a request-scoped context object that caches all computed values within a single status() call:

  • Thread-safe by design: each call gets its own context instance
  • No external dependencies: no RequestStore gem needed
  • Explicit lifecycle: created at start, garbage collected after
  • Backwards compatible: existing public API unchanged

The context caches:

  • effective_plan - plan resolution (called once)
  • limit_config_for(key) - limit configuration lookup
  • limit_status(key) - full status hash per limit
  • current_usage_for(key) - usage count
  • grace_active?(key) - grace period check
  • should_block?(key) - block check
  • severity_for(key) - computed from cached data
  • period_window_for(key) - period window calculation

Performance Results

Metric Before After Reduction
Total queries ~70 16 77%
limit_status() calls 18 3 83%
effective_plan_for() calls 72 ~5 93%
enforcement_states queries 6 3 50%

Test plan

  • All 481 existing tests pass
  • Tested in host app (licenseseat) with real data
  • Verified query count reduction with benchmark script

🤖 Generated with Claude Code

The status() method had severe query redundancy. When calling
plan_owner.limits with 3 limits, the gem executed:
- 18+ limit_status() calls instead of 3
- 72+ effective_plan_for() calls instead of 1
- ~70 database queries for a single call

This happened because helper methods like severity_for(), message_for(),
overage_for(), highest_severity_for() all re-called limit_status()
internally, and each limit_status() call independently resolved the plan.

Solution: StatusContext pattern
- Request-scoped context object created at start of status()
- Caches: plan resolution, limit configs, usage, percent used,
  grace state, enforcement states, severity, period windows
- Thread-safe by design (each call gets its own context)
- Implements period window calculation inline to avoid extra queries
- Backwards compatible (existing public API unchanged)

Performance improvement:
- Total queries: ~70 → 16 (77% reduction)
- enforcement_states queries: 6 → 3 (50% reduction)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude bot commented Feb 15, 2026

Code Review: Fix N+1 Query Problem with StatusContext Pattern

Summary

This is an excellent performance optimization that addresses a severe N+1 query problem. The implementation is well-designed, thread-safe, and backwards-compatible. The 77% reduction in queries (from ~70 to 16) is substantial and will have meaningful impact on production performance.

✅ Strengths

Architecture & Design

  • Clean abstraction: The StatusContext pattern is a solid solution - request-scoped, thread-safe by design, and no external dependencies
  • Backwards compatible: Existing public API is unchanged; all changes are internal to status()
  • Well-isolated: New code is confined to a single new file plus minimal changes to the main module
  • Appropriate caching scope: Each status() call gets its own context instance, avoiding stale data issues

Performance

  • Significant impact: 77% query reduction is excellent
  • Targeted optimization: Eliminates redundant effective_plan_for() calls (from 72 to ~5) and limit_status() calls (from 18 to 3)
  • Well-measured: The PR includes specific benchmark data showing the improvements

Code Quality

  • Comprehensive caching: Caches all necessary values (effective_plan, limit_config, limit_status, current_usage, grace_active, should_block, severity, warning_thresholds, period_window)
  • Clear method organization: Public API methods at top, private helpers at bottom
  • Good comments: Explains why the pattern exists and how it works

🔍 Issues & Recommendations

1. CRITICAL: Side Effect Removed in fresh_enforcement_state

Location: lib/pricing_plans/status_context.rb:282

The context's fresh_enforcement_state method intentionally does NOT destroy stale enforcement states:

if stale
  # State is stale, treat as nil (don't destroy - that's a side effect)
  @fresh_enforcement_state_cache[key] = nil

However, GraceManager.fresh_state_or_nil (line 174-176) DOES destroy stale states:

if stale_for_window?(state, period_start, window_start_epoch, current_epoch)
  state.destroy!
  return nil
end

Impact: Stale enforcement states will accumulate in the database over time. For per-period limits, old states should be cleaned up when periods roll over.

Recommendation:

  • Either add the state.destroy! call back to fresh_enforcement_state in the context (with a comment explaining it's an intentional write operation)
  • OR add a background job to clean up stale enforcement states periodically
  • OR document that this is intentional behavior with justification

2. Potential Issue: Inconsistent Behavior for Grace Ends At

Location: lib/pricing_plans/status_context.rb:472 vs lib/pricing_plans.rb:471

The new compute_summary_message_from_context (line 472) calls:

grace_end = keys.map { |k| ctx.grace_ends_at(k) }.compact.min

But the original summary_message_for (line 471) called:

grace_end = keys.map { |k| GraceManager.grace_ends_at(plan_owner, k) }.compact.min

The context's grace_ends_at uses the cached enforcement_state, while GraceManager.grace_ends_at uses find_state (no caching but also no freshness check).

Question: Should grace_ends_at in the context use fresh_enforcement_state instead of enforcement_state to ensure we're not showing grace end times for stale states?

3. Code Duplication: Period Window Calculation

Location: lib/pricing_plans/status_context.rb:345-423

The context duplicates ~80 lines of period calculation logic from PeriodCalculator. This creates maintenance burden if period logic changes.

Recommendation: Consider extracting a shared helper or passing the resolved plan to PeriodCalculator.window_for to avoid the extra effective_plan_for call, rather than duplicating all the logic.

Alternative: If duplication is intentional for performance, add a comment explaining why and suggesting that both implementations must be kept in sync.

4. Missing Test Coverage for StatusContext

Issue: The PR adds 425 lines of new code in StatusContext but I don't see a corresponding test file test/status_context_test.rb.

Recommendation: Add dedicated unit tests for StatusContext to verify:

  • Caching behavior (methods are called only once)
  • Thread safety (each instance is independent)
  • Correct computation of all cached values
  • Edge cases (nil plans, unconfigured limits, stale states)

The PR description says "All 481 existing tests pass" which is great for regression testing, but the new code should have its own coverage.

5. Minor: Inconsistent Naming

The private method compute_summary_message_from_context (line 660) is oddly specific. Consider renaming to just summary_message_for_context for consistency with the original summary_message_for.

🔒 Security

No security concerns. The changes are performance optimizations with no changes to authorization logic, input validation, or data access patterns.

🧪 Testing

Strengths:

  • All 481 existing tests pass
  • Tested in production app (licenseseat) with real data
  • Includes benchmark script for verification

Gaps:

  • No dedicated unit tests for StatusContext itself
  • No test explicitly verifying query count reduction
  • Consider adding a test that uses query logging/counting to assert the N+1 is fixed

📝 Documentation

Missing:

  • No CHANGELOG entry documenting the performance improvement
  • No inline documentation for the StatusContext class explaining the caching strategy
  • Consider adding a code comment in status() method explaining why context is used

🎯 Recommendations Summary

  1. MUST FIX: Address the missing state.destroy! for stale states or document why it's omitted
  2. SHOULD FIX: Add unit tests for StatusContext
  3. CONSIDER: Reduce duplication of period calculation logic
  4. CONSIDER: Verify grace_ends_at should use fresh state
  5. NICE TO HAVE: Add CHANGELOG entry and inline documentation

Verdict

Approve with minor changes. This is solid work that delivers significant value. The architecture is sound and the performance gains are real. Address the critical issue around stale state cleanup and add test coverage, and this will be production-ready.

Great job on the benchmark data and backwards compatibility! 🚀


Review generated with Claude Code

Adds comprehensive tests for the new StatusContext class:
- Caching behavior (plan, config, status, severity)
- limit_status for configured and unconfigured limits
- severity_for at different usage levels
- highest_severity_for aggregation
- message_for at different severities
- overage_for calculation
- period_window_for per-period limits
- grace and blocking behavior

Branch coverage: 65.4% (above 65% threshold)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude bot commented Feb 15, 2026

Pull Request Review: Fix N+1 Query Problem with StatusContext Pattern

Summary

This PR introduces a StatusContext pattern to eliminate severe query redundancy in the status() method, reducing database queries from ~70 to 16 (77% reduction) for a typical 3-limit call. The implementation is well-designed and addresses a critical performance issue.


✅ Strengths

1. Excellent Problem Identification & Solution Design

  • The N+1 problem was correctly identified: helper methods repeatedly called limit_status(), and each call re-resolved the plan
  • The StatusContext pattern is the right architectural choice here - request-scoped caching that's thread-safe by design
  • No external dependencies (RequestStore) needed - clean implementation

2. Strong Performance Gains

  • 77% reduction in total queries (~70 → 16)
  • 83% reduction in limit_status() calls (18 → 3)
  • 93% reduction in effective_plan_for() calls (72 → ~5)
  • These are substantial, measurable improvements

3. Backwards Compatibility

  • Public API unchanged - existing code continues to work
  • Tests pass (481 existing tests)
  • Drop-in optimization without breaking changes

4. Code Quality

  • Well-structured class with clear separation of concerns
  • Good use of memoization patterns (@cache ||= value)
  • Comprehensive test coverage (195 lines of new tests)
  • Inline documentation explaining the pattern

🔍 Issues & Recommendations

CRITICAL: Logic Duplication & Maintenance Risk

The StatusContext class duplicates significant logic from existing classes:

  1. Period calculation logic (lines 347-425 in status_context.rb) is copied from PeriodCalculator
  2. Grace/blocking logic (lines 64-108) duplicates GraceManager
  3. Severity computation (lines 310-336) duplicates existing severity logic

Problem: When PeriodCalculator or GraceManager get updated, developers must remember to update StatusContext too. This creates a maintenance burden and risk of divergence.

Recommendation: Refactor to eliminate duplication:

# Instead of duplicating calculate_period_window, call PeriodCalculator directly
# but pass cached values to avoid re-resolving the plan
def period_window_for(limit_key)
  key = limit_key.to_sym
  @period_window_cache ||= {}
  return @period_window_cache[key] if @period_window_cache.key?(key)
  
  limit_config = limit_config_for(limit_key)
  return @period_window_cache[key] = [nil, nil] unless limit_config && limit_config[:per]
  
  # Call PeriodCalculator but pass config to avoid re-lookup
  @period_window_cache[key] = PeriodCalculator.calculate_window(
    @plan_owner, 
    limit_config[:per],
    # Pass any other needed cached values
  )
end

Consider adding a PeriodCalculator.calculate_window_with_config(plan_owner, config) method that accepts pre-resolved configuration to avoid re-lookups while keeping the logic in one place.

MEDIUM: Incomplete Replacement in status()

Line 342 in lib/pricing_plans.rb:

msg = compute_summary_message_from_context(ctx, keys, sev)

The PR introduces compute_summary_message_from_context but the old summary_message_for method still exists (lines 446-479) and is used elsewhere in the codebase.

Issues:

  1. Two nearly-identical methods doing the same thing (code duplication)
  2. The new method is private but only used in status() - could this be inlined?
  3. Both methods call GraceManager.grace_ends_at (line 471) and ctx.grace_ends_at (line 679) respectively

Recommendation:

  • Either refactor summary_message_for to use StatusContext internally
  • Or document why both are needed and when to use each

MEDIUM: Test Coverage Gaps

The new tests in status_context_test.rb are good, but missing:

  1. Thread safety testing: The PR claims "thread-safe by design" but there are no concurrent tests
  2. Integration tests: No tests showing the actual query count reduction in a realistic scenario
  3. Edge cases:
    • What happens with stale enforcement states?
    • Multiple limits with mixed per-period and persistent caps?
    • Grace period expiration during a single status() call?

Recommendation: Add:

def test_concurrent_status_calls_are_independent
  org = create_organization
  threads = 5.times.map do
    Thread.new { PricingPlans.status(org, limits: [:projects, :custom_models]) }
  end
  results = threads.map(&:value)
  # Each should have independent context, no shared state
  assert_equal 5, results.size
end

MINOR: Documentation & Comments

  1. Line 270 (status_context.rb): Comment says "don't destroy - that's a side effect" but the method returns nil without cleanup. Consider adding:

    # Note: We return nil for stale states rather than destroying them to avoid 
    # side effects during read operations. Cleanup happens in GraceManager.
  2. Missing class-level documentation: Add examples of usage at the top of StatusContext class:

    # Example:
    #   ctx = StatusContext.new(user)
    #   ctx.limit_status(:projects)  # Queries DB
    #   ctx.limit_status(:projects)  # Returns cached value

MINOR: Potential Memory Concern

With 8 separate cache hashes (@limit_config_cache, @limit_status_cache, etc.), a call with many limits could accumulate significant cached data. For a typical 3-limit call this is fine, but what about 20+ limits?

Recommendation: Add a note in documentation about expected usage patterns, or consider a single unified cache hash.


🔒 Security Considerations

No security concerns identified

  • No SQL injection risks (uses AR properly)
  • No authentication/authorization changes
  • Caching is request-scoped (no cross-user data leakage)
  • Row-level locking preserved from existing implementation

🧪 Testing Recommendations

Beyond the test coverage gaps mentioned above:

  1. Benchmark test: Add a performance test that actually measures query counts:

    def test_query_count_reduction
      org = create_organization
      # Create some test data
      
      count_before = count_queries do
        PricingPlans.status(org, limits: [:projects, :custom_models, :licenses])
      end
      
      assert count_before < 20, "Expected < 20 queries, got #{count_before}"
    end
  2. Regression test: Ensure the refactor doesn't change behavior:

    def test_status_output_unchanged_from_previous_implementation
      org = create_organization
      # Compare output structure to ensure backwards compatibility
    end

📋 Checklist Before Merge

  • Resolve logic duplication (refactor period calculation)
  • Add thread safety tests
  • Add integration test with actual query count assertions
  • Document relationship between summary_message_for and compute_summary_message_from_context
  • Add class-level documentation with usage examples
  • Consider adding a CHANGELOG entry noting the performance improvement

Final Verdict

Recommendation: Approve with changes requested

This PR solves a real performance problem with a solid architectural approach. The 77% query reduction is excellent. However, the logic duplication is a significant maintenance concern that should be addressed before merging.

The StatusContext pattern itself is sound - the implementation just needs refactoring to reuse existing logic rather than duplicating it. Once the duplication is resolved and test coverage is improved, this will be a strong addition to the codebase.

Estimated effort to address: 2-4 hours of refactoring work.

Great work identifying and fixing this performance issue! 🚀

…grace_ends_at fix

1. CRITICAL: Added state.destroy! in fresh_enforcement_state to clean up stale
   enforcement states (consistent with GraceManager behavior)

2. MEDIUM: Removed ~80 lines of duplicated period calculation code from
   StatusContext. Added PeriodCalculator.window_for_period_type() to accept
   pre-resolved period_type, avoiding redundant plan lookups

3. MEDIUM: Changed grace_ends_at to use fresh_enforcement_state instead of
   enforcement_state to avoid returning stale data

4. Added comprehensive tests for new behaviors including stale state cleanup,
   grace period handling, message generation branches, and caching

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude bot commented Feb 15, 2026

PR Review: Fix N+1 Query Problem with StatusContext Pattern

Summary

This PR introduces a well-designed caching pattern to eliminate severe query redundancy in the status() method. The implementation achieves a 77% reduction in database queries (from ~70 to 16) - an impressive performance improvement.


✅ Strengths

1. Excellent Problem Identification

The PR clearly identifies and documents the N+1 query problem where helper methods like severity_for(), message_for(), and overage_for() were internally re-calling limit_status(), causing redundant database queries and plan resolutions.

2. Clean Architecture

  • Thread-safe by design: Each status() call gets its own context instance, avoiding shared state issues
  • No external dependencies: Doesn't require RequestStore or similar gems
  • Explicit lifecycle: Context is created, used, and garbage collected within a single method call
  • Backwards compatible: Public API remains unchanged

3. Comprehensive Caching Strategy

The StatusContext class caches all computed values appropriately including plan resolution, limit configurations, usage counts, grace states, severity calculations, warning thresholds, and period windows.

4. Excellent Test Coverage

The PR includes 459 lines of comprehensive tests covering caching behavior verification, edge cases (stale states, unconfigured limits, unlimited limits), all severity levels, message generation, and proper destruction of stale enforcement states.

5. Performance Results

Documented performance improvements are significant:

  • Total queries: ~70 → 16 (77% reduction)
  • limit_status() calls: 18 → 3 (83% reduction)
  • effective_plan_for() calls: 72 → ~5 (93% reduction)

🔍 Areas for Improvement

1. Potential Memory Overhead (Severity: Low)

Each StatusContext instance maintains 10+ cache hashes. For endpoints that check many limits simultaneously, this could create temporary memory pressure. This is acceptable for the use case, but consider documenting the memory trade-off in comments.

2. Stale State Destruction Side Effect (Severity: Medium)

In fresh_enforcement_state (lib/pricing_plans/status_context.rb:404-435), stale enforcement states are destroyed as a side effect of a read operation. This couples read operations with write operations (database DELETE), which could cause unexpected behavior in read-only contexts, make debugging harder, and potentially trigger issues in read-replica setups.

Recommendation: Consider moving cleanup to a separate maintenance job, adding a configuration flag to control auto-cleanup behavior, or at minimum, document this behavior clearly.

3. Code Duplication (Severity: Low)

The compute_summary_message_from_context method duplicates logic from message_for in StatusContext. Both build similar human-readable messages with sentence joining logic. Consider extracting common message-building logic into a shared helper method.

4. Missing Error Handling (Severity: Low)

In message_for, there's a silent rescue for custom message builders. Silent failures make debugging difficult. Recommendation: Log the error before falling through.

5. Period Window Caching Initialization (Severity: Very Low)

In period_window_for, the cache is conditionally initialized (@period_window_cache ||= {}) which is inconsistent with other caches initialized in the constructor. Initialize @period_window_cache = {} in the constructor for consistency.


🔒 Security Considerations

No security concerns identified

The PR doesn't introduce security vulnerabilities - no user input is directly used in queries, no authentication/authorization changes, caching is request-scoped (no cross-user data leakage), and database queries use existing safe patterns.


🧪 Testing Recommendations

The test coverage is excellent, but consider adding:

  1. Concurrency test: Verify thread safety by running multiple contexts in parallel threads
  2. Memory/performance benchmark: Document the actual memory overhead of the context object
  3. Integration test: Test the full status() flow with real database queries to verify the 77% reduction claim

🎯 Overall Assessment

Recommendation: APPROVE with minor suggestions

This is a high-quality PR that solves a real performance problem with a clean, well-tested solution. The StatusContext pattern is an appropriate choice - it's simple, effective, and maintains backwards compatibility.

What I Like Most

  • Clear problem statement with quantified impact
  • Clean architectural pattern (request-scoped caching)
  • Comprehensive test coverage (459 lines of tests for 343 lines of implementation)
  • Zero breaking changes to the public API
  • Well-documented performance improvements

Priority Actions

  1. Medium Priority: Consider the stale state destruction side effect
  2. Low Priority: Initialize @period_window_cache in constructor
  3. Low Priority: Add error logging for custom message builder failures

The PR is ready to merge as-is, but addressing the stale state destruction concern would make it even more robust for production use.

Great work on this optimization! The 77% query reduction will significantly improve response times.

🤖 Review generated with Claude Code

rameerez and others added 2 commits February 15, 2026 18:52
Admin overrides (manual assignments) should take precedence over
subscription-based plans. The previous order checked Pay subscriptions
first, which meant manual assignments were ignored for users with
active subscriptions - defeating the purpose of admin overrides.

New resolution order:
1. Manual assignment (admin override) - FIRST
2. Pay subscription
3. Default plan

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude bot commented Feb 15, 2026

PR Review: Fix N+1 Query Problem with StatusContext Pattern

Summary

This is an excellent performance optimization that introduces a well-designed caching pattern to eliminate severe N+1 query issues. The implementation is solid, comprehensive, and follows Rails best practices.

✅ Strengths

1. Architecture & Design

  • StatusContext pattern is exemplary: Thread-safe by design (instance-per-call), no global state, explicit lifecycle
  • Clean separation of concerns: Context handles caching, existing code focuses on business logic
  • Backwards compatible: Public API unchanged, existing code continues to work
  • Comprehensive caching: All query-triggering methods are cached (plan, config, usage, grace state, severity, etc.)

2. Performance Impact 🚀

  • 77% query reduction (70 → 16 queries) is outstanding
  • Targeted optimization: Addresses the root cause (redundant limit_status() calls) rather than symptoms
  • Measurable results: Clear benchmarking shows concrete improvement

3. Code Quality

  • Well-documented: Clear comments explaining purpose and design decisions
  • Consistent patterns: All caching methods follow same @cache[key] ||= compute() pattern
  • Good error handling: Safe navigation (&.), rescue blocks in message_for, fallback to defaults
  • Comprehensive tests: 459 lines of tests covering caching, staleness, edge cases, all severity levels

4. Important Bug Fixes

  • Stale state cleanup (lib/pricing_plans/status_context.rb:284-287): Correctly destroys stale enforcement states for per-period limits, preventing database bloat
  • Manual assignment precedence (lib/pricing_plans/plan_resolver.rb:13-26): Admin overrides now correctly take priority over subscriptions - this is semantically correct for administrative control
  • Period calculator optimization (lib/pricing_plans/period_calculator.rb:16-18): New window_for_period_type avoids redundant plan lookups

🔍 Observations & Recommendations

1. Potential Circular Dependency (Minor)

Location: lib/pricing_plans/status_context.rb:310

The compute_limit_status → grace_ends_at → fresh_enforcement_state → period_window_for chain could be optimized. Consider pre-computing fresh_enforcement_state once and passing it to dependent methods to avoid potential cache checking overhead.

Recommendation: Not critical, but could micro-optimize by computing fresh_enforcement_state first in compute_limit_status.

2. Stale State Logic Complexity (Minor)

Location: lib/pricing_plans/status_context.rb:280-282

The third condition window_start_epoch != current_epoch is redundant when combined with the second. If window_start_epoch < current_epoch, it's already !=. The third condition only adds value if window_start_epoch > current_epoch, which would be a future period (shouldn't happen).

Recommendation: Consider simplifying to:
stale = (state.exceeded_at && state.exceeded_at < period_start) || (window_start_epoch && window_start_epoch != current_epoch)

3. Cache Initialization Pattern (Style)

Location: lib/pricing_plans/status_context.rb:124

All other caches are initialized in the constructor, but @period_window_cache is lazy-initialized. For consistency, consider initializing in the constructor.

4. Error Swallowing (Low Risk)

Location: lib/pricing_plans/status_context.rb:190-192

Silent error swallowing could hide bugs in custom message_builder. Consider logging errors (if debug mode enabled) to aid troubleshooting.

5. Test Coverage

  • 459 new tests with comprehensive coverage of all code paths
  • Tests cover: caching behavior, staleness detection, all severity levels, grace periods, edge cases
  • Good use of stubs to test different scenarios
  • Tests verify both positive and negative cases

Excellent test coverage!

🔒 Security Considerations

No security issues identified

  • Proper use of parameterized queries (ActiveRecord find_by)
  • No SQL injection vectors
  • No mass assignment vulnerabilities
  • Destruction of stale records is properly scoped to plan_owner

📊 Performance Considerations

Outstanding performance improvement

  • 77% fewer queries is significant for user-facing endpoints
  • Memory overhead is minimal: Context is short-lived (request-scoped), garbage collected after each status() call
  • No N+1 regressions: All repeated lookups are now cached
  • Database impact reduced: Especially important at scale

Potential optimization: Consider adding database indexes on enforcement_states(plan_owner_type, plan_owner_id, limit_key) if not already present, as fresh_enforcement_state queries by these columns.

🎯 Breaking Changes

Breaking change is justified and well-documented

The manual assignment precedence change (lib/pricing_plans/plan_resolver.rb:13) is technically breaking, but:

  • It's semantically correct: Admin overrides should take precedence
  • Previous behavior was likely a bug (admin couldn't override active subscriptions)
  • Tests updated to reflect new behavior
  • Commit message documents the change clearly

Recommendation: Consider mentioning this in release notes/changelog as it changes resolution order.

📝 Documentation

Suggestions:

  1. Add a comment in StatusContext explaining when contexts are created/destroyed
  2. Consider adding YARD docs for public methods
  3. The PR description is excellent - consider preserving it in commit messages or changelog

✅ Final Verdict

APPROVE ⭐⭐⭐⭐⭐

This is high-quality work that demonstrates:

  • Deep understanding of the performance problem
  • Excellent architectural design (StatusContext pattern)
  • Comprehensive testing
  • Clear documentation
  • Measurable impact (77% query reduction)

The minor observations above are style/optimization suggestions, not blockers. This PR is ready to merge.

Great work! This will significantly improve performance for users checking multiple limits.


Checklist Summary

  • ✅ Code quality: Excellent
  • ✅ Best practices: Follows Rails conventions
  • ✅ Potential bugs: None identified (several bugs fixed!)
  • ✅ Performance: Outstanding improvement (77% reduction)
  • ✅ Security: No concerns
  • ✅ Test coverage: Comprehensive (459 new tests)
  • ⚠️ Breaking changes: Manual assignment precedence (justified)

Recommendation: Merge after considering the minor optimization suggestions above.

@rameerez rameerez merged commit 4f2c8e2 into main Feb 15, 2026
6 of 7 checks passed
@rameerez rameerez deleted the fix/status-n-plus-1-queries branch February 15, 2026 19:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant