ruby
50 lines · 7 steps
Memoized report metrics in Ruby in Rails
A report object computes each metric once and caches it, so a single summary call reuses shared queries.
Explained by
highlit
1class ReportGenerator
2 def initialize(account, period)
3 @account = account
4 @period = period
5 end
6
7 def summary
8 {
9 revenue: total_revenue,
10 churn_rate: churn_rate,
11 active_customers: active_customers.size,
12 top_products: top_products
13 }
14 end
15
16 def total_revenue
17 @total_revenue ||= paid_invoices.sum(&:amount_cents) / 100.0
18 end
19
20 def churn_rate
21 return @churn_rate if defined?(@churn_rate)
22
23 starting = @account.customers.active_at(@period.begin).count
24 @churn_rate = starting.zero? ? 0.0 : (cancelled_count.to_f / starting).round(4)
25 end
26
27 def active_customers
28 @active_customers ||= @account.customers.active_during(@period).to_a
29 end
30
31 private
32
33 def paid_invoices
34 @paid_invoices ||= @account.invoices.paid.where(issued_at: @period)
35 end
36
37 def cancelled_count
38 @cancelled_count ||= @account.subscriptions.cancelled_during(@period).count
39 end
40
41 def top_products
42 @top_products ||= paid_invoices
43 .flat_map(&:line_items)
44 .group_by(&:product_id)
45 .transform_values { |items| items.sum(&:amount_cents) }
46 .sort_by { |_, total| -total }
47 .first(5)
48 .map(&:first)
49 end
50end
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Memoizing each metric method turns repeated calls into cheap lookups after the first computation.
- 2Use defined? instead of ||= when a legitimate result can be falsey like 0.0.
- 3Grouping metric logic into one object lets several methods share the same underlying query cheaply.
Related explainers
ruby
require "shellwords" require "open3" module Backup
Building safe shell commands in Ruby
shell-out
subprocess
command-injection
Intermediate
7 steps
ruby
class UserAgentParser BROWSERS = [ [/Edg\/([\d.]+)/, "Edge"], [/OPR\/([\d.]+)/, "Opera"],
Parsing user-agent strings in Ruby
regex
pattern-matching
lookup-tables
Intermediate
8 steps
ruby
class LogAggregator BUCKET_FORMAT = "%Y-%m-%dT%H:%M" def initialize(entries)
Bucketing log entries by the minute in Ruby
aggregation
hashing
enumerable
Intermediate
5 steps
javascript
import { useState, useEffect, useCallback, useRef } from 'react'; const cache = new Map(); const inflight = new Map();
Building a stale-while-revalidate hook in React
caching
request-deduplication
custom-hooks
Advanced
10 steps
ruby
class WeeklySignupsReport DEFAULT_WEEKS = 12 def initialize(weeks: DEFAULT_WEEKS, source: User.all)
Building a weekly signups report in Rails
service object
aggregation
group by
Intermediate
7 steps
php
<?php namespace App\Services;
Building a cached daily leaderboard in Laravel
caching
aggregation
eager-loading
Intermediate
9 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/memoized-report-metrics-in-ruby-in-rails-explained-ruby-c941/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.