class User < ApplicationRecord
has_many :posts
has_many :comments
scope :digest_subscribers, -> {
where(digest_opt_in: true).where.not(confirmed_at: nil)
}
scope :with_unread_since, ->(since) {
where(
"EXISTS (SELECT 1 FROM notifications n " \
"WHERE n.user_id = users.id AND n.read_at IS NULL AND n.created_at >= ?)",
since
)
}
def recent_activity(since)
notifications
.where(read_at: nil)
.where("created_at >= ?", since)
.order(created_at: :desc)
.limit(25)
end
end
class WeeklyDigestJob < ApplicationJob
queue_as :mailers
WINDOW = 7.days
def perform(now = Time.current)
since = now - WINDOW
User.digest_subscribers
.with_unread_since(since)
.find_each(batch_size: 500) do |user|
send_digest(user, since)
end
end
private
def send_digest(user, since)
return if user.digest_sent_at && user.digest_sent_at > since
activity = user.recent_activity(since)
return if activity.empty?
DigestMailer.weekly(user, activity).deliver_later
user.update_column(:digest_sent_at, Time.current)
rescue => e
Rails.logger.error("digest failed user=#{user.id}: #{e.message}")
end
end
class DigestMailer < ApplicationMailer
default from: "digest@example.com"
def weekly(user, activity)
@user = user
@activity = activity
@count = activity.size
mail(
to: user.email,
subject: "Your weekly digest: #{@count} new updates"
)
end
end
weekly_digest:
cron: "0 8 * * 1" # every Monday at 08:00
class: "WeeklyDigestJob"
queue: mailers
timezone: "America/New_York"
description: "Send the opt-in weekly activity digest"
This snippet shows how a recurring weekly digest email is assembled in a Rails app using a scheduled background job, a set of composable ActiveRecord scopes, and a mailer. The unit of work is deliberately split so each piece stays testable in isolation: the scope decides who gets a digest and what they see, the job decides when and how many at a time, and the mailer decides how it looks.
In User digest scopes, the model defines two collaborating scopes. digest_subscribers narrows the population to people who opted in and confirmed their address, avoiding sending to bounced or unverified accounts. with_unread_since uses a correlated EXISTS subquery so users with no recent activity are excluded entirely at the database level rather than being loaded and filtered in Ruby — that keeps the working set small even for large tables. The instance method recent_activity reuses the same window to fetch the actual rows the mailer renders, so the presence check and the render query agree on the same cutoff.
In WeeklyDigestJob, the job is idempotent and batch-friendly. It computes since from a fixed window and iterates with find_each so memory stays flat regardless of subscriber count. The digest_sent_at guard means a re-run (from a retry or an overlapping schedule) will not double-send within the same window; this is the key safety property when a cron trigger and Sidekiq retries can both fire. Delivery uses deliver_later so the SMTP call happens in its own job and one slow send cannot stall the whole batch. Failures per user are rescued and logged so a single bad record doesn't abort the run.
In sidekiq-cron schedule, the recurring trigger is declared as data, not code: a cron expression enqueues WeeklyDigestJob every Monday morning in a fixed timezone. Keeping the schedule in a YAML file loaded at boot means it lives in version control and is visible in the Sidekiq web UI. A subtle trade-off worth noting: the job's own window calculation, not the cron cadence, defines correctness — the cron is only a trigger, so a missed tick can be safely re-run without duplicate emails thanks to the digest_sent_at guard.
Related snips
class CommentsController < ApplicationController
before_action :set_post
def create
@comment = @post.comments.build(comment_params)
System test: asserting Turbo Stream responses
class Post < ApplicationRecord
belongs_to :author, class_name: 'User'
has_many :comments, dependent: :destroy
scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
scope :draft, -> { where(published_at: nil) }
ActiveRecord scopes for reusable query logic
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
class PostsController < ApplicationController
def index
@posts = Post.includes(:author)
.order(created_at: :desc)
.page(params[:page])
.per(10)
Turbo Frames: infinite scroll with lazy-loading frame
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
Share this code
Here's the card — post it anywhere.