Event-driven state, undoable commands
Event-driven state and undoable commands.
Pattern: Store state transitions as events with structured metadata instead of just updating state fields.
Implementation (from PR #121):
module Event::Stages
extend ActiveSupport::Concern
included do
store_accessor :particulars, :stage_id
end
def stage
@stage ||= account.stages.find_by_id stage_id
end
end
Why it matters:
Application: Any system with status fields (order status, ticket status, approval workflows) should consider event sourcing instead of direct updates.
Pattern: Use after_create_commit to create default associated records, ensuring they're only created after the transaction succeeds.
Implementation (from PR #413):
class Workflow < ApplicationRecord
DEFAULT_STAGES = [ "Triage", "In progress", "On Hold", "Review" ]
after_create_commit :create_default_stages
private
def create_default_stages
Workflow::Stage.insert_all \
DEFAULT_STAGES.collect { |default_stage_name|
{ workflow_id: id, name: default_stage_name }
}
end
end
Why it matters:
insert_all for performance with multiple recordsApplication: Use for any resource that requires default child records (project templates, user preferences, configuration presets).
Pattern: When a parent's workflow changes, update all children in a single operation.
Implementation (from PR #329):
class Bucket < ApplicationRecord
belongs_to :workflow, optional: true
has_many :bubbles, dependent: :destroy
after_save :update_bubbles_workflow, if: :saved_change_to_workflow_id?
private
def update_bubbles_workflow
bubbles.update_all(stage_id: workflow&.stages&.first&.id)
end
end
Why it matters:
update_all for performance (single SQL UPDATE)&.)Application: Useful for hierarchical data where parent changes should cascade (organization settings, folder permissions, template applications).
Pattern: Derive state from associations rather than storing redundant data.
Implementation (from PR #389):
module Card::Colored
extend ActiveSupport::Concern
def color
color_from_stage || Colorable::DEFAULT_COLOR
end
private
def color_from_stage
stage&.color&.presence if doing?
end
end
Why it matters:
Application: Prefer computed properties over denormalized columns when consistency matters more than read performance.
Pattern: Extend Turbo Streams with custom actions for complex UI state changes.
Implementation (from PR #389):
# JavaScript
Turbo.StreamActions.set_css_variable = function() {
const name = this.getAttribute("name")
const value = this.getAttribute("value")
this.targetElements.forEach(element =>
element.style.setProperty(name, value)
)
}
# Ruby helper
module TurboStreamsActionsHelper
def set_css_variable(target, name:, value:)
tag.turbo_stream target: target, action: "set_css_variable", name:, value:
end
end
Turbo::Streams::TagBuilder.prepend(TurboStreamsActionsHelper)
# Usage in turbo_stream view
<%= turbo_stream.set_css_variable dom_id(@card, :card_container),
name: "--card-color",
value: @card.color %>
Why it matters:
Application: Use for any dynamic UI that needs to reflect server-side state changes (theme updates, progress indicators, status badges).
Pattern: Structure operations as command objects with built-in undo support.
Implementation (from PR #662):
class Command::Stage < Command
include Command::Cards
store_accessor :data, :stage_id, :original_stage_ids_by_card_id
def execute
original_stage_ids_by_card_id = {}
transaction do
cards.find_each do |card|
next unless card_compatible_with_stage?(card)
original_stage_ids_by_card_id[card.id] = card.stage_id
card.change_stage_to stage
end
update! original_stage_ids_by_card_id: original_stage_ids_by_card_id
end
end
def undo
transaction do
affected_cards_by_id = user.accessible_cards
.where(id: original_stage_ids_by_card_id.keys)
.index_by(&:id)
stages_by_id = Workflow::Stage
.where(id: original_stage_ids_by_card_id.values)
.uniq
.index_by(&:id)
original_stage_ids_by_card_id.each do |card_id, original_stage_id|
card = affected_cards_by_id[card_id.to_i]
stage = stages_by_id[original_stage_id.to_i]
next unless card && stage
card.change_stage_to stage
end
end
end
end
Why it matters:
index_by for efficient lookups when undoingApplication: Critical for user-facing bulk operations, data imports, or any destructive action that users might want to reverse.
Pattern: Build flexible filters by chaining scopes with polymorphic relationships.
Implementation (from PR #218):
class Filter < ApplicationRecord
include Params, Resources, Summarized
has_and_belongs_to_many :stages,
class_name: "Workflow::Stage",
join_table: "filters_stages"
def bubbles
result = base_scope
result = result.assigned_to(assignees.ids) if assignees.present?
result = result.in_stage(stages.ids) if stages.present?
result = result.tagged_with(tags.ids) if tags.present?
result
end
end
# In the filterable model
module Bubble::Staged
included do
scope :in_stage, ->(stage) { where stage: stage }
end
end
Why it matters:
Application: Essential for any list view with multiple filter criteria (admin panels, reports, search results).
Pattern: Delegate workflow state to parent but allow local overrides.
Implementation (from PR #121):
module Bubble::Staged
extend ActiveSupport::Concern
included do
belongs_to :stage, class_name: "Workflow::Stage", optional: true
end
def workflow
stage&.workflow
end
def toggle_stage(stage)
if self.stage == stage
update! stage: nil
track_event :unstaged, stage_id: stage.id
else
update! stage: stage
track_event :staged, stage_id: stage.id
end
end
end
Why it matters:
Application: Use for optional categorization systems, toggleable features, or reversible state.
Pattern: Generate human-readable summaries of complex filter/workflow state.
Implementation (from PR #218):
module Filter::Summarized
def summary
[
index_summary,
tag_summary,
assignee_summary,
stage_summary,
terms_summary
].compact.to_sentence + " #{bucket_summary}"
end
private
def stage_summary
if stages.any?
"staged in #{stages.pluck(:name).to_choice_sentence}"
end
end
end
Why it matters:
Application: Any complex query builder, filter system, or workflow state display.
Pattern: Test both the state change AND the event creation.
Implementation (from PR #413):
test "create with default stages" do
workflow = Workflow.create name: "My New Workflow"
assert_equal Workflow::DEFAULT_STAGES.sort,
workflow.stages.collect(&:name).sort
end
Best practices from the PRs:
assert_changes for state verificationPattern: Set initial workflow state before record creation to ensure consistency.
included do
before_create :assign_initial_stage
end
private
def assign_initial_stage
self.stage = collection.initial_workflow_stage
end
Why it matters:
Application: Use when every record must start in a specific workflow state (new orders, draft documents, pending approvals).
Pattern: Validate workflow transitions based on business context.
Implementation (from PR #662):
class Command::Stage < Command
validates_presence_of :stage
private
def card_compatible_with_stage?(card)
stage&.workflow && card.collection.workflow == stage.workflow
end
end
Why it matters:
Application: Critical for multi-tenant systems or contexts where different entities have different workflow rules.
update_all, insert_all for performance with multiple records&. operator for optional associations in workflowsEach pattern is production-tested and battle-hardened from a real-world project management application.