Request for comment (RFC)
Status: Drafting & seeking input from key stakeholders Author: Rafael Santos Last updated: January 2026
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
| 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, ...) |
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.
Extract common pricing display methods into a shared Ruby concern that can be included in all pricing-related PageData classes.
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
endClasses 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
endThis 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.
| 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.
- Keep the concern small — Only extract methods that are 100% identical across classes
- Don't force it — Classes with custom implementations (e.g.,
flash_sale_active?checking user-specific state) keep their own versions - Memoize where appropriate —
currencyandmarketable_item_counts_serviceare memoized to match existing behavior
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.
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.
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.
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.
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