35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English 35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English
Ruby Pattern Matching: Stop Writing Nested if/elsif Chains

Ruby Pattern Matching: Stop Writing Nested if/elsif Chains

TTB Software
ruby
Ruby pattern matching with case/in replaces nested conditionals for destructuring hashes, validating API responses, and parsing webhooks. Practical examples for Ruby 3.0+.

Ruby’s case/when has been the workhorse of conditional logic since the language’s earliest days. But since Ruby 3.0, pattern matching with case/in has quietly become one of the most powerful features most Rubyists still aren’t using.

If you’re writing nested conditionals to destructure hashes, check array shapes, or validate API responses, pattern matching replaces all of it with something dramatically more readable.

The Basics: case/in vs case/when

Traditional Ruby:

case response
when Hash
  if response[:status] == "ok" && response[:data].is_a?(Array)
    process(response[:data])
  end
end

With pattern matching:

case response
in { status: "ok", data: Array => items }
  process(items)
end

Same result, half the noise. The in keyword destructures and binds in one step.

Destructuring Nested Structures

Pattern matching shines when you’re dealing with deeply nested data — exactly the kind of thing APIs throw at you.

case api_response
in { user: { name: String => name, roles: [*, "admin", *] } }
  grant_admin_access(name)
in { user: { name: String => name, roles: Array } }
  grant_standard_access(name)
in { error: { code: Integer => code, message: String => msg } }
  handle_error(code, msg)
end

The [*, "admin", *] pattern matches any array containing "admin" at any position. Try expressing that cleanly with if/elsif.

Guard Clauses with if

Patterns support inline guards:

case order
in { total: Float | Integer => amount } if amount > 1000
  apply_bulk_discount(order)
in { total: Float | Integer => amount } if amount > 0
  process_standard(order)
in { total: 0 }
  handle_zero_order(order)
end

The Find Pattern

Ruby 3.0 introduced the find pattern for matching elements within arrays:

case users
in [*, { name: "Roger", role: } , *]
  puts "Found Roger with role: #{role}"
end

This searches through the array and binds the first match. No find or detect needed.

Pin Operator for Dynamic Values

When you need to match against a variable rather than bind to one, use the pin operator ^:

expected_status = "active"

case account
in { status: ^expected_status, balance: Integer => bal }
  puts "Active account with balance: #{bal}"
end

Without ^, expected_status would be treated as a new variable binding instead of a comparison.

Pattern Matching in Method Arguments (Ruby 3.1+)

Since Ruby 3.1, you can use pattern matching directly in method signatures for validation:

def process_event(event)
  event => { type: String => type, payload: Hash => data }
  # If pattern doesn't match, raises NoMatchingPatternError

  case type
  in "user.created"
    create_user(data)
  in "user.deleted"
    remove_user(data)
  end
end

The standalone => operator (rightward assignment) destructures and raises if the pattern doesn’t match — a concise alternative to manual validation.

Real-World Example: Parsing Webhook Payloads

Here’s a before/after from a Stripe webhook handler:

Before:

def handle_webhook(payload)
  return unless payload.is_a?(Hash)
  return unless payload[:type].is_a?(String)

  case payload[:type]
  when "invoice.paid"
    return unless payload[:data].is_a?(Hash)
    return unless payload[:data][:object].is_a?(Hash)
    amount = payload.dig(:data, :object, :amount_paid)
    customer = payload.dig(:data, :object, :customer)
    return unless amount && customer
    record_payment(customer, amount)
  when "customer.subscription.deleted"
    customer = payload.dig(:data, :object, :customer)
    return unless customer
    cancel_subscription(customer)
  end
end

After:

def handle_webhook(payload)
  case payload
  in { type: "invoice.paid",
       data: { object: { amount_paid: Integer => amount,
                          customer: String => customer } } }
    record_payment(customer, amount)
  in { type: "customer.subscription.deleted",
       data: { object: { customer: String => customer } } }
    cancel_subscription(customer)
  else
    Rails.logger.debug("Unhandled webhook: #{payload[:type]}")
  end
end

When you pair pattern matching with structured logging, unhandled webhook types get captured automatically in your logs instead of silently passing through.

The pattern matching version is shorter, self-documenting, and impossible to forget a nil check — if the structure doesn’t match, the pattern simply doesn’t execute.

Performance Considerations

Pattern matching isn’t free. For hot paths processing millions of iterations, a direct hash lookup is faster. But for request handling, webhook processing, and configuration parsing — the places where you actually write tangled conditionals — the overhead is negligible and the clarity gain is massive.

When to Reach for It

Use pattern matching when:

  • Destructuring API responses or webhook payloads
  • Validating complex data shapes
  • Replacing chains of dig + nil checks
  • Switching on nested structure, not just type

Stick with case/when when:

  • You’re matching simple values or types
  • Performance is critical (tight loops)
  • Your team hasn’t seen the syntax yet (introduce it gradually)

Getting Started

If you’re on Ruby 3.0+, you already have it. No gems needed. Start by replacing one ugly conditional chain in your codebase. Once you see the difference, you won’t go back.

Pattern matching isn’t syntactic sugar — it’s a different way of thinking about data flow. And in a language built around developer happiness, that’s exactly where it belongs. It works particularly well in background job handlers where you’re routing different event types to different processing paths.

Frequently Asked Questions

Does pattern matching work with ActiveRecord models?

Not directly — pattern matching works with hashes, arrays, and objects that implement deconstruct or deconstruct_keys. You can match against model.attributes (which returns a hash) or implement deconstruct_keys on your models. In practice, pattern matching is most useful for API responses, params hashes, and service object results rather than ActiveRecord objects.

Is Ruby pattern matching slower than if/elsif chains?

For typical use cases like request handling or webhook processing, the difference is negligible. Pattern matching is slightly slower than a direct hash lookup in tight loops processing millions of iterations, but for any code running at request scale, readability matters more than nanoseconds. Benchmark your specific case if performance is critical.

What happens when no pattern matches?

If no in branch matches and there’s no else clause, Ruby raises NoMatchingPatternError. This is actually a feature — it forces you to handle all cases explicitly. Add an else branch to handle unknown cases gracefully, or let it raise if an unmatched pattern represents a genuine error in your data.

T

About the Author

Roger Heykoop is a senior Ruby on Rails developer with 19+ years of Rails experience and 35+ years in software development. He specializes in Rails modernization, performance optimization, and AI-assisted development.

Get in Touch

Share this article

Need Expert Rails Development?

Let's discuss how we can help you build or modernize your Rails application with 19+ years of expertise

Schedule a Free Consultation