ruby 116 lines · 3 tabs

Railway-Oriented Result Monad for Chaining Lead Enrichment Steps in Ruby

Shared by codesnips Aug 2026
3 tabs
module Result
  def self.Success(value)
    Success.new(value)
  end

  def self.Failure(error)
    Failure.new(error)
  end

  class Success
    attr_reader :value

    def initialize(value)
      @value = value
    end

    def success?
      true
    end

    def and_then(callable = nil, &block)
      (callable || block).call(value)
    end

    def map(&block)
      Success.new(block.call(value))
    end

    def value_or(_default)
      value
    end
  end

  class Failure
    attr_reader :error

    def initialize(error)
      @error = error
    end

    def success?
      false
    end

    def and_then(_callable = nil)
      self
    end

    def map
      self
    end

    def value_or(default)
      default
    end
  end
end
3 files · ruby Explain with highlit

This snippet shows how a chain of enrichment steps can be composed with a small hand-rolled Result monad, avoiding the deeply nested conditionals that usually accumulate when each step can fail. The pattern is often called railway-oriented programming: computation runs along a "success track" until any step derails onto a "failure track", after which every subsequent step is skipped and the original failure is carried through untouched.

In Result monad, Success and Failure share a common interface so callers never need to type-check the branch. The key method is and_then, which only invokes the given block on a Success and short-circuits on a Failure. This is the monadic bind: it flattens nested results so that a step returning Result does not produce a Result[Result]. The complementary map transforms a wrapped value without the block having to re-wrap it, and value_or provides a safe unwrap with a default. Because Failure#and_then returns self, once a step fails the block is never run and the error propagates for free.

The individual steps live in EnrichmentSteps. Each is a plain callable that receives an accumulating hash and returns a Result — either Success with an augmented hash or a Failure describing what went wrong. Steps stay tiny and independent: fetch_company guards on a missing domain, verify_email rejects role-based addresses, and score_lead computes a derived field. None of them know about each other or about ordering, which makes them trivial to unit test and reorder.

LeadEnricher wires the pipeline together. Its call method threads the initial context through reduce, applying and_then for each step so the first failure halts the chain. Because and_then expects a callable, the steps are passed by method reference and invoked uniformly. The final Result is either the fully enriched lead or the exact failure from whichever step derailed.

The trade-off is a little upfront machinery versus scattered nil checks and early returns. When steps grow numerous or need to be reordered, the railway approach keeps the happy path linear and the error handling implicit, which is when a developer would reach for it rather than raising exceptions across service boundaries.


Related snips

ruby
class CommentsController < ApplicationController
  before_action :set_post

  def create
    @comment = @post.comments.build(comment_params)

System test: asserting Turbo Stream responses

rails hotwire turbo
by codesnips 4 tabs
ruby
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

rails activerecord patterns
by Alex Kumar 1 tab
ruby
module Api
  module V1
    class UsersController < BaseController
      def show
        user = User.includes(:profile).find(params[:id])

ETags for conditional requests and caching

rails caching http-caching
by Alex Kumar 1 tab
ruby
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

rails turbo hotwire
by codesnips 4 tabs
ruby
require "csv"

class PeopleCsvStream
  include Enumerable

  HEADERS = %w[id full_name email signed_up_at plan].freeze

Resilient CSV Export as a Streamed Response

rails performance streaming
by codesnips 3 tabs
erb
<form data-controller="query-sync" data-action="change->query-sync#apply">
  <select name="status" class="rounded border p-2">
    <option value="">Any</option>
    <option value="open">Open</option>
    <option value="closed">Closed</option>
  </select>

Filter UI that syncs query params via Stimulus (no front-end router)

rails hotwire stimulus
by Henry Kim 2 tabs

Share this code

Here's the card — post it anywhere.

Railway-Oriented Result Monad for Chaining Lead Enrichment Steps in Ruby — share card
Link copied