Skip to content

Instantly share code, notes, and snippets.

@RafaelChefe
Created January 8, 2026 06:23
Show Gist options
  • Select an option

  • Save RafaelChefe/a43b24928012d4b566170de2ca5bf295 to your computer and use it in GitHub Desktop.

Select an option

Save RafaelChefe/a43b24928012d4b566170de2ca5bf295 to your computer and use it in GitHub Desktop.

Reduce Code Duplication in Pricing PageData Classes via Shared Concern

Request for comment (RFC)

Status: Drafting & seeking input from key stakeholders Author: Rafael Santos Last updated: January 2026


What's this about?

Elements Backend has 20+ PageData classes containing identical pricing display methods that have been copy-pasted across the codebase. This creates:

  • Maintenance burden — The same bug must be fixed in multiple places
  • Inconsistency risk — Implementations can drift apart over time
  • Onboarding friction — Developers must learn patterns scattered across many files

Scope of Duplication

Method # of Classes Example Implementation
currency 11 DetermineCurrency.from_country_code(country_code)
flash_sale_active? 14 FlashSale.enabled?
marketable_item_counts_service 11 MarketableItemCounts::Service.new
pricing_clarity_supported? 2 Feature flag check
pricing_opacity_supported? 2 Feature flag check
billed_annual_price 5 BilledAnnualPriceCalculator.calculate(...)
flash_sale_discount 6 pricing_presenter(:standard_yearly, ...)

Affected Classes

The following PageData classes have 3+ duplicated pricing methods:

  • ItemDetailNeuePageData (6 methods)
  • HomeNeuePageData (6 methods)
  • PricingNeuePageData (4 methods)
  • NeueDownloadModalData (4 methods)
  • StudentsPricingPageData (3 methods)
  • TeamsPricingPageData (3 methods)

Plus 14 additional classes with 1-2 duplicated methods each.


Summary of work proposed

Extract common pricing display methods into a shared Ruby concern that can be included in all pricing-related PageData classes.

The Solution

Create app/models/concerns/pricing_display_helpers.rb:

module PricingDisplayHelpers
  extend ActiveSupport::Concern

  def currency
    @currency ||= DetermineCurrency.from_country_code(country_code)
  end

  def pricing_clarity_supported?
    country_in_feature_flag?("pricing_clarity_supported_countries")
  end

  def pricing_opacity_supported?
    country_in_feature_flag?("pricing_opacity_supported_countries")
  end

  def flash_sale_active?
    FlashSale.enabled?
  end

  private

  def marketable_item_counts_service
    @marketable_item_counts_service ||= MarketableItemCounts::Service.new
  end

  def country_in_feature_flag?(flag_name)
    supported_countries = FeatureManagement.get_value(flag_name, "*").split(",")
    supported_countries.first == "*" || supported_countries.include?(country_code)
  end
end

Usage

Classes include the concern and remove their duplicated methods:

class StudentsPricingPageData
  include ActiveModel::Model
  include ActiveModel::Serialization
  include PricingPresenterFactory
  include PricingDisplayHelpers  # ← Add this

  # REMOVED: def currency
  # REMOVED: def marketable_item_counts_service
  # These now come from the concern
end

Proof of Concept

This approach has been validated on 3 classes:

Class Lines Before Lines After Reduction
StudentsPricingPageData 102 95 -7
PricingPageData 60 53 -7
EnterprisePricingPageData 36 29 -7
Total 198 177 -21

All existing tests pass without modification.

Rollout Plan

Phase Classes Est. Lines Saved
1 ✅ StudentsPricingPageData, PricingPageData, EnterprisePricingPageData -21
2 TeamsPricingPageData, HomeNeuePageData, HomePageData ~-25
3 PricingNeuePageData, ItemDetailNeuePageData ~-30
4 Remaining 14 classes (1-2 methods each) ~-30
Total 20+ classes ~-100+ lines

Each phase can be deployed independently. The concern (51 lines) pays for itself after Phase 2.

Design Decisions

  1. Keep the concern small — Only extract methods that are 100% identical across classes
  2. Don't force it — Classes with custom implementations (e.g., flash_sale_active? checking user-specific state) keep their own versions
  3. Memoize where appropriatecurrency and marketable_item_counts_service are memoized to match existing behavior

Alternatives considered

Alternative 1: Do Nothing

Keep the duplicated code as-is.

Pros Cons
No effort required Same bug must be fixed in 10+ places
No risk of regression Implementations will continue to drift
Onboarding remains difficult

Decision: Rejected — maintenance burden is already causing issues.

Alternative 2: Composition Instead of Inheritance

Use a helper object instead of a concern:

class HomeNeuePageData
  def pricing_helper
    @pricing_helper ||= PricingDisplayHelper.new(country_code:, enrollments:)
  end

  delegate :currency, :flash_sale_active?, to: :pricing_helper
end
Pros Cons
Explicit dependencies More verbose (delegate for each method)
Easier to test in isolation Not consistent with existing codebase patterns
No method name collision risk More objects created

Decision: Rejected — the codebase already uses concerns extensively (PricingPresenterFactory, ActiveModel::Model, etc.). Staying consistent with existing patterns reduces cognitive load.

Alternative 3: Extract to Pricing Engine

Move pricing display logic into the engines/pricing engine as part of its Public API.

Pros Cons
Stronger boundary enforcement Overkill for simple display helpers
Aligns with modular monolith Pricing engine is for calculation, not presentation
Would require API changes

Decision: Rejected — these are presentation/display helpers, not business logic. They belong in the main app, not an engine.

Alternative 4: Create a Base Class

Create PricingPageDataBase that all pricing PageData classes inherit from.

Pros Cons
Clear inheritance hierarchy Ruby single inheritance limits flexibility
All shared behavior in one place Classes already inherit from other bases
Tight coupling

Decision: Rejected — many classes already include multiple concerns; a mixin approach is more flexible than inheritance.


Recommendation

Proceed with the shared concern approach (implemented solution). It:

  • Follows existing Rails/codebase conventions
  • Has been validated with passing tests
  • Can be rolled out incrementally with zero risk
  • Removes ~100+ lines of duplicated code when complete
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment